Skip to content
On this page

Button和Spin实现

ts
import type { ComputedRef, InjectionKey, Ref } from 'vue'
import { computed, inject } from 'vue'
import { TinyColor } from '@ctrl/tinycolor'
import type { CSSObject, Theme } from '../../../src'
import { createTheme, useCacheToken } from '../../../src'
import type { MaybeComputedRef } from '../../../src/util'

export type GetStyle = (prefixCls: string, token: DerivativeToken) => CSSObject

export interface DesignToken {
  primaryColor: string
  textColor: string
  reverseTextColor: string

  componentBackgroundColor: string

  borderRadius: number
  borderColor: string
  borderWidth: number
}

export interface DerivativeToken extends DesignToken {
  primaryColorDisabled: string
}

export const defaultDesignToken: DesignToken = {
  primaryColor: '#1890ff',
  textColor: '#333333',
  reverseTextColor: '#FFFFFF',

  componentBackgroundColor: '#FFFFFF',

  borderRadius: 2,
  borderColor: 'black',
  borderWidth: 1,
}

// 模拟推导过程
function derivative(designToken: DesignToken): DerivativeToken {
  return {
    ...designToken,
    primaryColorDisabled: new TinyColor(designToken.primaryColor)
      .setAlpha(0.5).toString(),
  }
}

const defaultTheme = createTheme(derivative)

export const DesignTokenContextKey: InjectionKey<{
  token?: Partial<DesignToken>
  hashed?: string | boolean | undefined
  theme?: Theme<any, any>
}> = Symbol('DesignTokenContext')
export const defaultConfig = {
  token: defaultDesignToken,
  hashed: true,
}

export function useToken(): [MaybeComputedRef<Theme<any, any>>, ComputedRef<DerivativeToken>, ComputedRef<string>] {
  const designTokenContext = inject(DesignTokenContextKey, defaultConfig)
  const salt = computed(() => `${designTokenContext.hashed || ''}`)

  const mergedTheme = computed(() => designTokenContext.theme || defaultTheme)
  const cacheToken = useCacheToken<DesignToken, DerivativeToken>(
    mergedTheme,
    computed(() => {
      return [defaultDesignToken, designTokenContext.token]
    }) as Ref<DesignToken[]>,
    computed(() => ({
      salt: salt.value,
    })),
  )
  return [
    mergedTheme,
    computed(() => cacheToken.value[0]) as any,
    computed(() => (designTokenContext.hashed ? cacheToken.value[1] : '')),
  ]
}
vue
<script lang="ts" setup>
import type { PropType } from 'vue'
import { provide } from 'vue'
import type { DesignToken } from '../theme'
import { DesignTokenContextKey, defaultDesignToken } from '../theme'

const props = defineProps({
  token: {
    type: Object as PropType<Partial<DesignToken>>,
    default: () => defaultDesignToken,
  },
  hashed: {
    type: [Boolean, String] as PropType<boolean | string>,
    default: undefined,
  },
})

provide(DesignTokenContextKey, props)
</script>

<template>
  <slot />
</template>
ts
// 通用框架
import type { DerivativeToken } from '../theme'
import type { CSSInterpolation, CSSObject } from '../../../src'

export const genSharedButtonStyle = (
  prefixCls: string,
  token: DerivativeToken,
): CSSInterpolation => ({
  [`.${prefixCls}`]: {
    borderColor: token.borderColor,
    borderWidth: token.borderWidth,
    borderRadius: token.borderRadius,

    cursor: 'pointer',

    transition: 'background 0.3s',
  },
})

// 实心底色样式
export const genSolidButtonStyle = (
  prefixCls: string,
  token: DerivativeToken,
  postFn: () => CSSObject,
): CSSInterpolation => [
  genSharedButtonStyle(prefixCls, token),
  {
    [`.${prefixCls}`]: {
      ...postFn(),
    },
  },
]

// 默认样式
export const genDefaultButtonStyle = (
  prefixCls: string,
  token: DerivativeToken,
): CSSInterpolation =>
  genSolidButtonStyle(prefixCls, token, () => ({
    'backgroundColor': token.componentBackgroundColor,
    'color': token.textColor,

    '&:hover': {
      borderColor: token.primaryColor,
      color: token.primaryColor,
    },
  }))

// 主色样式
export const genPrimaryButtonStyle = (
  prefixCls: string,
  token: DerivativeToken,
): CSSInterpolation =>
  genSolidButtonStyle(prefixCls, token, () => ({
    'backgroundColor': token.primaryColor,
    'border': `${token.borderWidth}px solid ${token.primaryColor}`,
    'color': token.reverseTextColor,

    '&:hover': {
      backgroundColor: token.primaryColorDisabled,
    },
  }))

// 透明按钮
export const genGhostButtonStyle = (
  prefixCls: string,
  token: DerivativeToken,
): CSSInterpolation => [
  genSharedButtonStyle(prefixCls, token),
  {
    [`.${prefixCls}`]: {
      'backgroundColor': 'transparent',
      'color': token.primaryColor,
      'border': `${token.borderWidth}px solid ${token.primaryColor}`,

      '&:hover': {
        borderColor: token.primaryColor,
        color: token.primaryColor,
      },
    },
  },
]
tsx
import { computed, defineComponent } from 'vue'
import classNames from 'classnames'
import { useStyleRegister } from '../../../src'
import { useToken } from '../theme'
import { genDefaultButtonStyle, genGhostButtonStyle, genPrimaryButtonStyle } from './button.style'

export default defineComponent({
  name: 'AButton',
  props: {
    type: {
      type: String,
      default: 'default',
    },
  },
  setup(props, { slots, attrs }) {
    const prefixCls = 'ant-btn'
    const [theme, token, hashId] = useToken()
    const registerParam = computed(() => {
      return {
        theme: theme.value,
        token: token.value,
        hashId: hashId.value,
        path: [prefixCls],
      }
    })
    const defaultCls = `${prefixCls}-default`
    const primaryCls = `${prefixCls}-primary`
    const ghostCls = `${prefixCls}-ghost`
    const wrapSSR = useStyleRegister(registerParam, () => {
      return [
        genDefaultButtonStyle(defaultCls, token.value),
        genPrimaryButtonStyle(primaryCls, token.value),
        genGhostButtonStyle(ghostCls, token.value),
      ]
    })
    return () => {
      const typeCls: any = {
        [defaultCls]: props.type === 'default',
        [primaryCls]: props.type === 'primary',
        [ghostCls]: props.type === 'ghost',
      }
      const className = slots?.class
      return (wrapSSR(<button class={classNames(prefixCls, typeCls, hashId.value, className)} {...attrs}>
        {slots?.default?.()}
      </button>))
    }
  },
})
tsx
import { computed, defineComponent } from 'vue'
import classNames from 'classnames'
import type { CSSInterpolation } from '../../../src'
import { Keyframes, useStyleRegister } from '../../../src'
import { useToken } from '../theme'
import type { DerivativeToken } from '../theme'

const animation = new Keyframes('loadingCircle', {
  to: {
    transform: 'rotate(360deg)',
  },
})

// 通用框架
const genSpinStyle = (
  prefixCls: string,
  token: DerivativeToken,
  hashId: string,
): CSSInterpolation => [
  {
    [`.${prefixCls}`]: {
      width: 20,
      height: 20,
      backgroundColor: token.primaryColor,
      animationName: animation,
      animationDuration: '1.2s',
      animationIterationCount: 'infinite',
      animationTimingFunction: 'linear',
      // animation: `${animation.getName(hashId)} 1s infinite linear`,
    },
  },
  // animation,
]

const Spin = defineComponent({
  name: 'ASpin',
  setup(props, { attrs }) {
    const prefixCls = 'ant-spin'
    const [theme, token, hashId] = useToken()
    const registerInfo = computed(() => {
      return { theme: theme.value, token: token.value, hashId: hashId.value, path: [prefixCls] }
    })
    const wrapSSR = useStyleRegister(
      registerInfo,
      () => [genSpinStyle(prefixCls, token.value, hashId.value)],
    )
    return () => {
      return wrapSSR(
        <div {...attrs} class={classNames(prefixCls, hashId.value)} />,
      )
    }
  },
})
export default Spin