看我如何将vue-color 插件变成支持vue3+ts的

1,394 阅读1分钟

描述

color-picker 在属于常见的表单组件,尤其在更换背景颜色这些地方用的多。ant-design-vue 没有自带的 color-picker 拾色器。社区有用的比较多的 vue-color,但是目前还不支持 vue3,这里将源码经过改造,让其可以在 vue3 环境中使用。

技术栈

  • vue3
  • typescript
  • lodash
  • tinycolor2

正式开始

先开发几个基础组件,后面的不同类型的拾色器都是在基础组件组合来的。

基础组件

组件的划分,见下图

checkboard

一个单纯的显示颜色的面板,默认为棋盘格面板

<template>
  <div class="vc-checkerboard" :style="bgStyle" />
</template>

<script lang="ts">
import { defineComponent } from 'vue'

const checkboardCache: Record<string, string> = {}
/**
 * get base 64 data by canvas
 *
 * @param {String} c1 hex color
 * @param {String} c2 hex color
 * @param {Number} size
 */
function renderCheckboard(c1: string, c2: string, size: number) {
  // Dont Render On Server
  if (typeof document === 'undefined') {
    return null
  }
  const canvas = document.createElement('canvas')
  canvas.width = canvas.height = size * 2
  const ctx = canvas.getContext('2d')
  // If no context can be found, return early.
  if (!ctx) {
    return null
  }
  ctx.fillStyle = c1
  ctx.fillRect(0, 0, canvas.width, canvas.height)
  ctx.fillStyle = c2
  ctx.fillRect(0, 0, size, size)
  ctx.translate(size, size)
  ctx.fillRect(0, 0, size, size)
  return canvas.toDataURL()
}
/**
 * get checkboard base data and cache
 *
 * @param {String} c1 hex color
 * @param {String} c2 hex color
 * @param {Number} size
 */
function getCheckboard(c1: string, c2: string, size: number) {
  const key = c1 + ',' + c2 + ',' + size
  if (checkboardCache[key]) {
    return checkboardCache[key]
  }
  const checkboard = renderCheckboard(c1, c2, size)
  if (checkboard === null) {
    return null
  }
  checkboardCache[key] = checkboard
  return checkboard
}

export default defineComponent({
  props: {
    size: {
      type: Number,
      default: 8
    },
    white: {
      type: String,
      default: '#fff'
    },
    grey: {
      type: String,
      default: '#e6e6e6'
    }
  },
  computed: {
    bgStyle() {
      const checkboard = getCheckboard(this.white, this.grey, this.size)
      if (checkboard === null) {
        return {}
      }
      return {
        'background-image': `url(${checkboard})`
      }
    }
  }
})
</script>

<style>
.vc-checkerboard {
  position: absolute;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
  background-size: contain;
}
</style>

alpha

透明度,主要功能根据传入的颜色生成渐变色背景,当鼠标移动滑块时,计算出透明度

<template>
  <div class="vc-alpha">
    <div class="vc-alpha-checkboard-wrap">
      <Checkboard />
    </div>
    <div class="vc-alpha-gradient" :style="{ background: gradientColor }" />
    <div
      ref="container"
      class="vc-alpha-container"
      @mousedown="handleMouseDown"
      @touchmove="handleChange"
      @touchstart="handleChange"
    >
      <div class="vc-alpha-pointer" :style="{ left: colors.a * 100 + '%' }">
        <div class="vc-alpha-picker" />
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import Checkboard from './checkboard.vue'
import { ColorResult, AlphaChangeData } from '../../types'

export default defineComponent({
  emits: ['change'],
  components: {
    Checkboard
  },
  props: {
    value: {
      type: Object as PropType<ColorResult>,
      default: () => ({})
    }
  },
  computed: {
    colors(): ColorResult {
      return this.value
    },
    gradientColor(): string {
      const rgba = this.colors.rgba
      const rgbStr = [rgba.r, rgba.g, rgba.b].join(',')
      return 'linear-gradient(to right, rgba(' + rgbStr + ', 0) 0%, rgba(' + rgbStr + ', 1) 100%)'
    }
  },
  methods: {
    handleChange(e: MouseEvent | TouchEvent, skip?: boolean) {
      !skip && e.preventDefault()
      const container = this.$refs.container as HTMLElement
      if (!container) {
        // for some edge cases, container may not exist. see #220
        return
      }
      const containerWidth = container.clientWidth

      const xOffset = container.getBoundingClientRect().left + window.pageXOffset
      const pageX = (e as MouseEvent).pageX || ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageX : 0)
      const left = pageX - xOffset

      let a = 1
      if (left < 0) {
        a = 0
      } else if (left > containerWidth) {
        a = 1
      } else {
        a = Math.round((left * 100) / containerWidth) / 100
      }

      if (this.colors.a !== a) {
        const alphaData: AlphaChangeData = {
          h: this.colors.hsl.h,
          s: this.colors.hsl.s,
          l: this.colors.hsl.l,
          a,
          source: 'rgba'
        }
        this.$emit('change', alphaData)
      }
    },
    handleMouseDown(e: MouseEvent | TouchEvent) {
      this.handleChange(e, true)
      window.addEventListener('mousemove', this.handleChange)
      window.addEventListener('mouseup', this.handleMouseUp)
    },
    handleMouseUp() {
      this.unbindEventListeners()
    },
    unbindEventListeners() {
      window.removeEventListener('mousemove', this.handleChange)
      window.removeEventListener('mouseup', this.handleMouseUp)
    }
  }
})
</script>

<style>
.vc-alpha {
  position: absolute;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
}
.vc-alpha-checkboard-wrap {
  position: absolute;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
  overflow: hidden;
}
.vc-alpha-gradient {
  position: absolute;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
}
.vc-alpha-container {
  cursor: pointer;
  position: relative;
  z-index: 2;
  height: 100%;
  margin: 0 3px;
  -webkit-tap-highlight-color: transparent; /* for removing the highlight */
}
.vc-alpha-pointer {
  z-index: 2;
  position: absolute;
}
.vc-alpha-picker {
  cursor: pointer;
  width: 4px;
  border-radius: 1px;
  height: 8px;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
  background: #fff;
  margin-top: 1px;
  transform: translateX(-2px);
}
</style>

hue

色调,显示默认色板,根据传入颜色计算滑块的位置,可以直接移动滑块改变当前色调

<template>
  <div :class="['vc-hue', directionClass]">
    <div
      class="vc-hue-container"
      role="slider"
      :aria-valuenow="colors.hsl.h"
      aria-valuemin="0"
      aria-valuemax="360"
      ref="container"
      @mousedown="handleMouseDown"
      @touchmove="handleChange"
      @touchstart="handleChange"
    >
      <div class="vc-hue-pointer" :style="{ top: pointerTop, left: pointerLeft }" role="presentation">
        <div class="vc-hue-picker"></div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { ColorResult, HueChangeData } from '../../types'

export default defineComponent({
  name: 'Hue',
  emits: ['change'],
  props: {
    value: {
      type: Object as PropType<ColorResult>,
      default: () => ({})
    },
    direction: {
      type: String as PropType<'horizontal' | 'vertical'>,
      default: 'horizontal'
    }
  },
  data() {
    return {
      oldHue: 0,
      pullDirection: ''
    }
  },
  watch: {
    value(newVal: ColorResult) {
      const h = newVal.hsl.h
      if (h !== 0 && h - this.oldHue > 0) {
        this.pullDirection = 'right'
      }
      if (h !== 0 && h - this.oldHue < 0) {
        this.pullDirection = 'left'
      }
      this.oldHue = h
    }
  },
  computed: {
    colors(): ColorResult {
      return this.value
    },
    directionClass(): Record<string, boolean> {
      return {
        'vc-hue--horizontal': this.direction === 'horizontal',
        'vc-hue--vertical': this.direction === 'vertical'
      }
    },
    pointerTop(): number | string {
      if (this.direction === 'vertical') {
        if (this.colors.hsl.h === 0 && this.pullDirection === 'right') {
          return 0
        }
        return -((this.colors.hsl.h * 100) / 360) + 100 + '%'
      }
      return 0
    },
    pointerLeft(): number | string {
      if (this.direction === 'vertical') {
        return 0
      }
      if (this.colors.hsl.h === 0 && this.pullDirection === 'right') {
        return '100%'
      }
      return (this.colors.hsl.h * 100) / 360 + '%'
    }
  },
  methods: {
    handleChange(e: MouseEvent | TouchEvent, skip?: boolean) {
      !skip && e.preventDefault()

      const container = this.$refs.container as HTMLElement
      if (!container) {
        // for some edge cases, container may not exist. see #220
        return
      }
      const containerWidth = container.clientWidth
      const containerHeight = container.clientHeight

      const xOffset = container.getBoundingClientRect().left + window.pageXOffset
      const yOffset = container.getBoundingClientRect().top + window.pageYOffset
      const pageX = (e as MouseEvent).pageX || ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageX : 0)
      const pageY = (e as MouseEvent).pageY || ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageY : 0)
      const left = pageX - xOffset
      const top = pageY - yOffset

      let h = 0
      let percent = 0

      if (this.direction === 'vertical') {
        if (top < 0) {
          h = 360
        } else if (top > containerHeight) {
          h = 0
        } else {
          percent = -((top * 100) / containerHeight) + 100
          h = (360 * percent) / 100
        }

        if (this.colors.hsl.h !== h) {
          const hueData: HueChangeData = {
            h,
            s: this.colors.hsl.s,
            l: this.colors.hsl.l,
            a: this.colors.hsl.a,
            source: 'hsl'
          }
          this.$emit('change', hueData)
        }
      } else {
        if (left < 0) {
          h = 0
        } else if (left > containerWidth) {
          h = 360
        } else {
          percent = (left * 100) / containerWidth
          h = (360 * percent) / 100
        }

        if (this.colors.hsl.h !== h) {
          const hueData: HueChangeData = {
            h,
            s: this.colors.hsl.s,
            l: this.colors.hsl.l,
            a: this.colors.hsl.a,
            source: 'hsl'
          }
          this.$emit('change', hueData)
        }
      }
    },
    handleMouseDown(e: MouseEvent | TouchEvent) {
      this.handleChange(e, true)
      window.addEventListener('mousemove', this.handleChange)
      window.addEventListener('mouseup', this.handleMouseUp)
    },
    handleMouseUp() {
      this.unbindEventListeners()
    },
    unbindEventListeners() {
      window.removeEventListener('mousemove', this.handleChange)
      window.removeEventListener('mouseup', this.handleMouseUp)
    }
  }
})
</script>

<style>
.vc-hue {
  position: absolute;
  top: 0px;
  right: 0px;
  bottom: 0px;
  left: 0px;
  border-radius: 2px;
}
.vc-hue--horizontal {
  background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.vc-hue--vertical {
  background: linear-gradient(to top, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.vc-hue-container {
  cursor: pointer;
  margin: 0 2px;
  position: relative;
  height: 100%;
}
.vc-hue-pointer {
  z-index: 2;
  position: absolute;
}
.vc-hue-picker {
  cursor: pointer;
  margin-top: 1px;
  width: 4px;
  border-radius: 1px;
  height: 8px;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
  background: #fff;
  transform: translateX(-2px);
}
</style>

saturation

饱和度,主要功能调节颜色的亮暗

<template>
  <div
    class="vc-saturation"
    :style="{ background: bgColor }"
    ref="container"
    @mousedown="handleMouseDown"
    @touchmove="handleChange"
    @touchstart="handleChange"
  >
    <div class="vc-saturation--white"></div>
    <div class="vc-saturation--black"></div>
    <div class="vc-saturation-pointer" :style="{ top: pointerTop, left: pointerLeft }">
      <div class="vc-saturation-circle"></div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { clamp } from '../../helpers/utils'
import { throttle } from 'lodash'
import { ColorResult, SaturationChangeData } from '../../types'

export default defineComponent({
  name: 'Saturation',
  emits: ['change'],
  props: {
    value: {
      type: Object as PropType<ColorResult>,
      default: () => ({})
    }
  },
  computed: {
    colors(): ColorResult {
      return this.value
    },
    bgColor(): string {
      return `hsl(${this.colors.hsv.h}, 100%, 50%)`
    },
    pointerTop(): string {
      return -(this.colors.hsv.v * 100) + 1 + 100 + '%'
    },
    pointerLeft(): string {
      return this.colors.hsv.s * 100 + '%'
    }
  },
  methods: {
    throttle: throttle(
      (fn, data) => {
        fn(data)
      },
      20,
      {
        leading: true,
        trailing: false
      }
    ),
    handleChange(e: MouseEvent | TouchEvent, skip?: boolean) {
      !skip && e.preventDefault()
      const container = this.$refs.container as HTMLElement
      if (!container) {
        // for some edge cases, container may not exist. see #220
        return
      }
      const containerWidth = container.clientWidth
      const containerHeight = container.clientHeight

      const xOffset = container.getBoundingClientRect().left + window.pageXOffset
      const yOffset = container.getBoundingClientRect().top + window.pageYOffset
      const pageX = (e as MouseEvent).pageX || ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageX : 0)
      const pageY = (e as MouseEvent).pageY || ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageY : 0)
      const left = clamp(pageX - xOffset, 0, containerWidth)
      const top = clamp(pageY - yOffset, 0, containerHeight)
      const saturation = left / containerWidth
      const bright = clamp(-(top / containerHeight) + 1, 0, 1)

      const saturationData: SaturationChangeData = {
        h: this.colors.hsv.h,
        s: saturation,
        v: bright,
        a: this.colors.hsv.a,
        source: 'hsva'
      }

      this.throttle(this.onChange, saturationData)
    },
    onChange(param: SaturationChangeData) {
      this.$emit('change', param)
    },
    handleMouseDown() {
      window.addEventListener('mousemove', this.handleChange)
      window.addEventListener('mouseup', this.handleChange)
      window.addEventListener('mouseup', this.handleMouseUp)
    },
    handleMouseUp() {
      this.unbindEventListeners()
    },
    unbindEventListeners() {
      window.removeEventListener('mousemove', this.handleChange)
      window.removeEventListener('mouseup', this.handleChange)
      window.removeEventListener('mouseup', this.handleMouseUp)
    }
  }
})
</script>

<style>
.vc-saturation,
.vc-saturation--white,
.vc-saturation--black {
  cursor: pointer;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

.vc-saturation--white {
  background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.vc-saturation--black {
  background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.vc-saturation-pointer {
  cursor: pointer;
  position: absolute;
}
.vc-saturation-circle {
  cursor: head;
  width: 4px;
  height: 4px;
  box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px rgba(0, 0, 0, 0.3), 0 0 1px 2px rgba(0, 0, 0, 0.4);
  border-radius: 50%;
  transform: translate(-2px, -2px);
}
</style>

editable-input

输入框,显示和改变输入值,用来手动输入精确的色值

<template>
  <div class="vc-editable-input">
    <input
      :aria-labelledby="labelId"
      class="vc-input__input"
      v-model="val"
      @keydown="handleKeyDown"
      @input="update"
      ref="input"
    />
    <span :for="label" class="vc-input__label" :id="labelId">{{ labelSpanText }}</span>
    <span class="vc-input__desc">{{ desc }}</span>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'editableInput',
  emits: ['change'],
  props: {
    label: String,
    labelText: String,
    desc: String,
    value: {
      type: [String, Number],
      required: true
    },
    max: Number,
    min: Number,
    arrowOffset: {
      type: Number,
      default: 1
    }
  },
  computed: {
    val: {
      get(): string | number {
        return this.value
      },
      set(v: any) {
        // TODO: min
        if (!(this.max === undefined) && +v > this.max) {
          ;(this.$refs.input as HTMLInputElement).value = String(this.max)
          return
        }
        return v
      }
    },
    labelId(): string {
      return `input__label__${this.label}__${Math.random().toString().slice(2, 5)}`
    },
    labelSpanText(): string {
      return this.labelText || this.label || ''
    }
  },
  methods: {
    update(e: MouseEvent) {
      this.handleChange((e.target as HTMLInputElement).value)
    },
    handleChange(newVal: string) {
      const data: Record<string, string> = {}
      if (this.label) {
        data[this.label] = newVal
      }
      if (data.hex === undefined && data['#'] === undefined) {
        this.$emit('change', data)
      } else if (newVal.length > 5) {
        this.$emit('change', data)
      }
    },
    handleKeyDown(e: KeyboardEvent) {
      let val = this.val
      const number = Number(val)

      if (number) {
        const amount = this.arrowOffset || 1

        // Up
        if (e.keyCode === 38) {
          val = number + amount
          this.handleChange(String(val))
          e.preventDefault()
        }

        // Down
        if (e.keyCode === 40) {
          val = number - amount
          this.handleChange(String(val))
          e.preventDefault()
        }
      }
    }
  }
})
</script>

<style>
.vc-editable-input {
  position: relative;
}
.vc-input__input {
  padding: 0;
  border: 0;
  outline: none;
}
.vc-input__label {
  text-transform: capitalize;
}
</style>

chrome

chrome 的拾色器是平时接触的比较多的,在 Chrome 浏览器的调试面板可以看到。chrome 也是比较全的组件,用上了上面的全部基础组件。

<template>
  <div
    role="application"
    aria-label="Chrome color picker"
    :class="['vc-chrome', disableAlpha ? 'vc-chrome__disable-alpha' : '']"
  >
    <div class="vc-chrome-saturation-wrap">
      <saturation v-model:value="colors" @change="childChange"></saturation>
    </div>
    <div class="vc-chrome-body">
      <div class="vc-chrome-controls">
        <div class="vc-chrome-color-wrap">
          <div
            :aria-label="`current color is ${colors.hex}`"
            class="vc-chrome-active-color"
            :style="{ background: activeColor }"
          ></div>
          <checkboard v-if="!disableAlpha"></checkboard>
        </div>

        <div class="vc-chrome-sliders">
          <div class="vc-chrome-hue-wrap">
            <hue v-model:value="colors" @change="childChange"></hue>
          </div>
          <div class="vc-chrome-alpha-wrap" v-if="!disableAlpha">
            <alpha v-model:value="colors" @change="childChange"></alpha>
          </div>
        </div>
      </div>

      <div class="vc-chrome-fields-wrap" v-if="!disableFields">
        <div class="vc-chrome-fields" v-show="fieldsIndex === 0">
          <!-- hex -->
          <div class="vc-chrome-field">
            <ed-in v-if="!hasAlpha" label="hex" :value="colors.hex" @change="inputChange"></ed-in>
            <ed-in v-if="hasAlpha" label="hex" :value="colors.hex8" @change="inputChange"></ed-in>
          </div>
        </div>
        <div class="vc-chrome-fields" v-show="fieldsIndex === 1">
          <!-- rgba -->
          <div class="vc-chrome-field">
            <ed-in label="r" :value="colors.rgba.r" @change="inputChange"></ed-in>
          </div>
          <div class="vc-chrome-field">
            <ed-in label="g" :value="colors.rgba.g" @change="inputChange"></ed-in>
          </div>
          <div class="vc-chrome-field">
            <ed-in label="b" :value="colors.rgba.b" @change="inputChange"></ed-in>
          </div>
          <div class="vc-chrome-field" v-if="!disableAlpha">
            <ed-in label="a" :value="colors.a" :arrow-offset="0.01" :max="1" @change="inputChange"></ed-in>
          </div>
        </div>
        <div class="vc-chrome-fields" v-show="fieldsIndex === 2">
          <!-- hsla -->
          <div class="vc-chrome-field">
            <ed-in label="h" :value="hsl.h" @change="inputChange"></ed-in>
          </div>
          <div class="vc-chrome-field">
            <ed-in label="s" :value="hsl.s" @change="inputChange"></ed-in>
          </div>
          <div class="vc-chrome-field">
            <ed-in label="l" :value="hsl.l" @change="inputChange"></ed-in>
          </div>
          <div class="vc-chrome-field" v-if="!disableAlpha">
            <ed-in label="a" :value="colors.a" :arrow-offset="0.01" :max="1" @change="inputChange"></ed-in>
          </div>
        </div>
        <!-- btn -->
        <div
          class="vc-chrome-toggle-btn"
          role="button"
          aria-label="Change another color definition"
          @click="toggleViews"
        >
          <div class="vc-chrome-toggle-icon">
            <svg
              style="width: 24px; height: 24px"
              viewBox="0 0 24 24"
              @mouseover="showHighlight"
              @mouseenter="showHighlight"
              @mouseout="hideHighlight"
            >
              <path
                fill="#333"
                d="M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z"
              />
            </svg>
          </div>
          <div class="vc-chrome-toggle-icon-highlight" v-show="highlight"></div>
        </div>
        <!-- btn -->
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import EditableInput from './common/editable-input.vue'
import Saturation from './common/saturation.vue'
import Hue from './common/hue.vue'
import Alpha from './common/alpha.vue'
import Checkboard from './common/checkboard.vue'
import {
  ColorResult,
  SaturationChangeData,
  HueChangeData,
  AlphaChangeData,
  HEXChangeData,
  RGBAChangeData,
  HSLChangeData,
  ColorChangeValue
} from '../types'
import { getChangeColor, isValidHex } from '../helpers/utils'

export default defineComponent({
  name: 'Chrome',
  emits: ['update:value'],
  components: {
    Saturation,
    Hue,
    Alpha,
    'ed-in': EditableInput,
    Checkboard
  },
  props: {
    disableAlpha: {
      type: Boolean,
      default: false
    },
    disableFields: {
      type: Boolean,
      default: false
    },
    value: {
      type: [Object, String],
      default: () => ({})
    }
  },
  data() {
    return {
      fieldsIndex: 0,
      highlight: false,
      val: getChangeColor(this.value),
      oldHue: 0
    }
  },
  watch: {
    value(newVal) {
      this.val = getChangeColor(newVal)
    }
  },
  computed: {
    colors: {
      get(): ColorResult {
        return this.val
      },
      set(newVal: any) {
        this.val = newVal
        this.$emit('update:value', newVal)
      }
    },
    hsl(): Record<string, string> {
      const { h, s, l } = this.colors.hsl
      return {
        h: Number(h).toFixed(),
        s: `${(s * 100).toFixed()}%`,
        l: `${(l * 100).toFixed()}%`
      }
    },
    activeColor(): string {
      const rgba = this.colors.rgba
      return 'rgba(' + [rgba.r, rgba.g, rgba.b, rgba.a].join(',') + ')'
    },
    hasAlpha(): boolean {
      return this.colors.a < 1
    }
  },
  methods: {
    colorChange(data: ColorChangeValue, oldHue?: number) {
      this.oldHue = this.colors.hsl.h
      this.colors = getChangeColor(data, oldHue || this.oldHue)
      this.$emit('update:value', this.colors.hex)
    },
    simpleCheckForValidColor(data: any) {
      const keysToCheck = ['r', 'g', 'b', 'a', 'h', 's', 'l', 'v']
      let checked = 0
      let passed = 0

      for (let i = 0; i < keysToCheck.length; i++) {
        const letter = keysToCheck[i]
        if (data[letter]) {
          checked++
          if (!isNaN(data[letter])) {
            passed++
          }
        }
      }

      if (checked === passed) {
        return data
      }
    },
    childChange(data: SaturationChangeData | HueChangeData | AlphaChangeData) {
      this.colorChange(data)
    },
    inputChange(data: Record<string, string>) {
      if (!data) {
        return
      }
      if (data.hex) {
        if (isValidHex(data.hex)) {
          const hexData: HEXChangeData = {
            hex: data.hex,
            source: 'hex'
          }
          this.colorChange(hexData)
        }
      } else if (data.r || data.g || data.b || data.a) {
        const rgbaData: RGBAChangeData = {
          r: Number(data.r) || this.colors.rgba.r,
          g: Number(data.g) || this.colors.rgba.g,
          b: Number(data.b) || this.colors.rgba.b,
          a: Number(data.a) || this.colors.rgba.a || 1,
          source: 'rgba'
        }
        this.colorChange(rgbaData)
      } else if (data.h || data.s || data.l) {
        const s = data.s ? Number(data.s.replace('%', '')) / 100 : this.colors.hsl.s
        const l = data.l ? Number(data.l.replace('%', '')) / 100 : this.colors.hsl.l
        const hslData: HSLChangeData = {
          h: Number(data.h) || this.colors.hsl.h,
          s,
          l,
          source: 'hsl'
        }
        this.colorChange(hslData)
      }
    },
    toggleViews() {
      if (this.fieldsIndex >= 2) {
        this.fieldsIndex = 0
        return
      }
      this.fieldsIndex++
    },
    showHighlight() {
      this.highlight = true
    },
    hideHighlight() {
      this.highlight = false
    }
  }
})
</script>

<style>
.vc-chrome {
  background: #fff;
  border-radius: 2px;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.3), 0 4px 8px rgba(0, 0, 0, 0.3);
  box-sizing: initial;
  width: 225px;
  font-family: Menlo;
  background-color: #fff;
}
.vc-chrome-controls {
  display: flex;
}
.vc-chrome-color-wrap {
  position: relative;
  width: 36px;
}
.vc-chrome-active-color {
  position: relative;
  width: 30px;
  height: 30px;
  border-radius: 15px;
  overflow: hidden;
  z-index: 1;
}
.vc-chrome-color-wrap .vc-checkerboard {
  width: 30px;
  height: 30px;
  border-radius: 15px;
  background-size: auto;
}
.vc-chrome-sliders {
  flex: 1;
}
.vc-chrome-fields-wrap {
  display: flex;
  padding-top: 16px;
}
.vc-chrome-fields {
  display: flex;
  margin-left: -6px;
  flex: 1;
}
.vc-chrome-field {
  padding-left: 6px;
  width: 100%;
}
.vc-chrome-toggle-btn {
  width: 32px;
  text-align: right;
  position: relative;
}
.vc-chrome-toggle-icon {
  margin-right: -4px;
  margin-top: 12px;
  cursor: pointer;
  position: relative;
  z-index: 2;
}
.vc-chrome-toggle-icon-highlight {
  position: absolute;
  width: 24px;
  height: 28px;
  background: #eee;
  border-radius: 4px;
  top: 10px;
  left: 12px;
}
.vc-chrome-hue-wrap {
  position: relative;
  height: 10px;
  margin-bottom: 8px;
}
.vc-chrome-alpha-wrap {
  position: relative;
  height: 10px;
}
.vc-chrome-hue-wrap .vc-hue {
  border-radius: 2px;
}
.vc-chrome-alpha-wrap .vc-alpha-gradient {
  border-radius: 2px;
}
.vc-chrome-hue-wrap .vc-hue-picker,
.vc-chrome-alpha-wrap .vc-alpha-picker {
  width: 12px;
  height: 12px;
  border-radius: 6px;
  transform: translate(-6px, -2px);
  background-color: rgb(248, 248, 248);
  box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
}
.vc-chrome-body {
  padding: 16px 16px 12px;
  background-color: #fff;
}
.vc-chrome-saturation-wrap {
  width: 100%;
  padding-bottom: 55%;
  position: relative;
  border-radius: 2px 2px 0 0;
  overflow: hidden;
}
.vc-chrome-saturation-wrap .vc-saturation-circle {
  width: 12px;
  height: 12px;
}

.vc-chrome-fields .vc-input__input {
  font-size: 11px;
  color: #333;
  width: 100%;
  border-radius: 2px;
  border: none;
  box-shadow: inset 0 0 0 1px #dadada;
  height: 21px;
  text-align: center;
}
.vc-chrome-fields .vc-input__label {
  text-transform: uppercase;
  font-size: 11px;
  line-height: 11px;
  color: #969696;
  text-align: center;
  display: block;
  margin-top: 12px;
}

.vc-chrome__disable-alpha .vc-chrome-active-color {
  width: 18px;
  height: 18px;
}
.vc-chrome__disable-alpha .vc-chrome-color-wrap {
  width: 30px;
}
.vc-chrome__disable-alpha .vc-chrome-hue-wrap {
  margin-top: 4px;
  margin-bottom: 4px;
}
</style>

color.gif tinycolor 用于 js 的颜色控制和转换

总结

预览地址,账号:lgf@163.com,密码:123456。

ts版源码地址

js版源码地址

color-picker使用文档

color-picker插件来自ant-simple-pro里面,ant-simple-pro有很多用vue3+ts开发的插件。ant-simple-pro简洁,美观,快速上手,支持3大框架