├── .nvmrc
├── docs
├── navs
│ ├── en.mts
│ └── zh.ts
├── docs.ts
├── sidebars
│ ├── zh.ts
│ └── en.ts
├── head.ts
├── package.json
├── index.md
├── en
│ ├── index.md
│ ├── examples.md
│ └── options.md
├── examples.md
├── .vitepress
│ └── config.mts
├── configs
│ ├── en.ts
│ └── zh.ts
├── theme.ts
└── options.md
├── lib
├── dts
│ ├── vite-env.d.ts
│ └── shims-vue.d.ts
├── src
│ ├── components
│ │ └── ColorPicker
│ │ │ ├── index.ts
│ │ │ ├── components
│ │ │ ├── Inputs
│ │ │ │ ├── components
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── InputHex.vue
│ │ │ │ │ ├── RGBInputs.vue
│ │ │ │ │ ├── InputItem.vue
│ │ │ │ │ ├── HSLInputs.vue
│ │ │ │ │ ├── HSVInputs.vue
│ │ │ │ │ └── CMYKInputs.vue
│ │ │ │ └── index.vue
│ │ │ ├── AdvancedControls
│ │ │ │ ├── components
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── AdvBrightnessBar.vue
│ │ │ │ │ ├── AdvSaturationBar.vue
│ │ │ │ │ └── AdvLightnessBar.vue
│ │ │ │ └── index.vue
│ │ │ ├── index.ts
│ │ │ ├── Preview.vue
│ │ │ ├── Opacity.vue
│ │ │ ├── GradientBar.vue
│ │ │ ├── Hue.vue
│ │ │ ├── OperationGradient.vue
│ │ │ ├── PickerArea.vue
│ │ │ └── Operation.vue
│ │ │ └── index.vue
│ ├── main.ts
│ ├── enums
│ │ └── index.ts
│ ├── constants
│ │ └── index.ts
│ ├── utils
│ │ ├── convert.ts
│ │ ├── format.ts
│ │ ├── color.ts
│ │ ├── utils.ts
│ │ └── gradientParser.ts
│ ├── App.vue
│ ├── interfaces
│ │ └── index.ts
│ └── styles
│ │ ├── iconfont.css
│ │ └── index.scss
├── tsconfig.json
├── uno.config.ts
├── index.html
├── vitest.config.ts
├── tests
│ ├── index.test.ts
│ ├── setup.ts
│ ├── enums.test.ts
│ ├── README.md
│ ├── components
│ │ └── ColorPicker.test.ts
│ ├── interfaces.test.ts
│ └── utils
│ │ ├── format.test.ts
│ │ ├── color.test.ts
│ │ └── utils.test.ts
├── index.ts
├── .eslintrc-auto-import.json
├── package.json
├── vite.config.ts
├── auto-imports.d.ts
├── index.d.ts
└── README.md
├── types
├── vite-env.d.ts
└── shims-vue.d.ts
├── pnpm-workspace.yaml
├── .husky
├── pre-commit
└── commit-msg
├── commitlint.config.cjs
├── imgs
└── introduct.png
├── .browserslistrc
├── .prettierignore
├── .eslintignore
├── .npmrc
├── .vscode
└── extensions.json
├── packages
└── vuets
│ ├── package.json
│ ├── src
│ ├── main.ts
│ └── App.vue
│ ├── vite.config.ts
│ └── index.html
├── .gitignore
├── tsconfig.json
├── .github
├── PULL_REQUEST_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── feature_request.yml
│ └── bug_report.yml
├── workflows
│ └── build.yml
└── CONTRIBUTING.md
├── README.md
├── .eslintrc.cjs
├── .prettierrc.cjs
├── license
├── .cz-config.cjs
├── scripts
└── dev.js
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | v21.1.0
2 |
--------------------------------------------------------------------------------
/docs/navs/en.mts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/dts/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/types/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 | - 'lib'
4 | - 'docs'
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run prettier
5 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional', 'cz'],
3 | }
4 |
--------------------------------------------------------------------------------
/imgs/introduct.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiu-Jun/color-gradient-picker-vue3/HEAD/imgs/introduct.png
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # Browsers that we support
2 |
3 | last 2 versions
4 | > 1%
5 | iOS 7
6 | last 3 iOS versions
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | index.html
4 | auto-imports.d.ts
5 | .eslintrc-auto-import.json
6 | .nuxi
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/docs/docs.ts:
--------------------------------------------------------------------------------
1 | export const docsConfig = {
2 | title: 'color-gradient-picker-vue3',
3 | description: 'document',
4 | lang: 'zh-CN',
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | index.html
4 | auto-imports.d.ts
5 | .eslintrc-auto-import.json
6 | .nuxi
7 | docs/.vitepress/dist
8 | docs/.vitepress/cache
--------------------------------------------------------------------------------
/docs/sidebars/zh.ts:
--------------------------------------------------------------------------------
1 | export const sidebar = [
2 | {
3 | text: '目录',
4 | items: [
5 | { text: '配置', link: '/options' },
6 | { text: '例子', link: '/examples' },
7 | ],
8 | },
9 | ]
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry = https://registry.npmmirror.com
2 |
3 | ignore-workspace-root-check = true
4 | shamefully-hoist = true
5 | strict-peer-dependencies = false
6 | auto-install-peers = true
7 | link-workspace-packages = true
--------------------------------------------------------------------------------
/docs/sidebars/en.ts:
--------------------------------------------------------------------------------
1 | export const sidebar = [
2 | {
3 | text: 'Category',
4 | items: [
5 | { text: 'Options', link: '/options' },
6 | { text: 'Demo', link: '/examples' },
7 | ],
8 | },
9 | ]
10 |
--------------------------------------------------------------------------------
/docs/head.ts:
--------------------------------------------------------------------------------
1 | import type { HeadConfig } from 'vitepress'
2 |
3 | export const head: HeadConfig[] = [
4 | [
5 | 'link',
6 | {
7 | rel: 'icon',
8 | //开发时识别
9 | href: '/assets/favicon.ico',
10 | },
11 | ],
12 | ]
13 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "vscode-icons-team.vscode-icons",
4 | "stylelint.vscode-stylelint",
5 | "dbaeumer.vscode-eslint",
6 | "esbenp.prettier-vscode",
7 | "vue.volar",
8 | "eamodio.gitlens",
9 | "OBKoro1.korofileheader"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/vuets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3ts",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "vite"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "dependencies": {
12 | "color-gradient-picker-vue3": "workspace:^"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description
4 | * @Date: 2024-11-30 21:19:38
5 | * @LastEditTime: 2024-12-07 13:09:37
6 | * @LastEditors: June
7 | */
8 | export { default as ColorPicker } from './index.vue'
9 |
10 | export type { GradientProps, ColorPickerProps } from '@/interfaces'
11 |
--------------------------------------------------------------------------------
/types/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description:
4 | * @Date: 2023-04-11 13:30:14
5 | * @LastEditors: June
6 | * @LastEditTime: 2023-09-27 19:20:00
7 | */
8 | declare module '*.vue' {
9 | import { ComponentOptions } from 'vue'
10 | const componentOptions: ComponentOptions
11 | export default componentOptions
12 | }
13 |
--------------------------------------------------------------------------------
/lib/dts/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description:
4 | * @Date: 2023-04-11 13:30:14
5 | * @LastEditors: June
6 | * @LastEditTime: 2023-09-27 19:20:14
7 | */
8 | declare module '*.vue' {
9 | import { ComponentOptions } from 'vue'
10 | const componentOptions: ComponentOptions
11 | export default componentOptions
12 | }
13 |
--------------------------------------------------------------------------------
/packages/vuets/src/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description:
4 | * @Date: 2023-10-03 23:29:44
5 | * @LastEditors: June
6 | * @LastEditTime: 2023-10-03 23:30:42
7 | */
8 | import { createApp } from 'vue'
9 | import App from './App.vue'
10 |
11 | function start() {
12 | const app = createApp(App)
13 | app.mount('#app')
14 | }
15 | start()
16 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "docs:dev": "vitepress dev",
7 | "docs:build": "vitepress build",
8 | "docs:preview": "vitepress preview"
9 | },
10 | "author": "June",
11 | "license": "ISC",
12 | "description": "",
13 | "devDependencies": {
14 | "vitepress": "^1.5.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as InputItem } from './InputItem.vue'
2 | export { default as InputHex } from './InputHex.vue'
3 | export { default as HSLInputs } from './HSLInputs.vue'
4 | export { default as RGBInputs } from './RGBInputs.vue'
5 | export { default as HSVInputs } from './HSVInputs.vue'
6 | export { default as CMYKInputs } from './CMYKInputs.vue'
7 |
--------------------------------------------------------------------------------
/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "paths": {
6 | "@/*": ["./src/*"]
7 | }
8 | },
9 | "include": [
10 | "./**/*.ts",
11 | "./**/*.d.ts",
12 | "./**/*.vue",
13 | "./**/*.tsx",
14 | "./dts/*.d.ts",
15 | "./index.d.ts",
16 | "./types/*.ts",
17 | "./auto-imports.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/AdvancedControls/components/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description
4 | * @Date: 2024-12-05 14:38:56
5 | * @LastEditTime: 2024-12-05 14:39:21
6 | * @LastEditors: June
7 | */
8 | export { default as AdvBrightnessBar } from './AdvBrightnessBar.vue'
9 | export { default as AdvLightnessBar } from './AdvLightnessBar.vue'
10 | export { default as AdvSaturationBar } from './AdvSaturationBar.vue'
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 | coverage
10 | node_modules
11 | # dist
12 | dist
13 | dist-ssr
14 | *.local
15 | docs/.vitepress/dist
16 | docs/.vitepress/cache
17 |
18 | # Editor directories and files
19 | .vscode/*
20 | !.vscode/extensions.json
21 | .idea
22 | .DS_Store
23 | *.suo
24 | *.ntvs*
25 | *.njsproj
26 | *.sln
27 | *.sw?
28 |
29 | # html
30 | stats.html
--------------------------------------------------------------------------------
/packages/vuets/vite.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description:
4 | * @Date: 2023-10-03 23:27:30
5 | * @LastEditors: June
6 | * @LastEditTime: 2023-10-03 23:27:33
7 | */
8 | import type { ConfigEnv, UserConfigExport } from 'vite'
9 | import vueJsx from '@vitejs/plugin-vue-jsx'
10 | import vue from '@vitejs/plugin-vue'
11 |
12 | export default ({ command }: ConfigEnv): UserConfigExport => {
13 | return {
14 | plugins: [vue(), vueJsx()],
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/src/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Descripttion:
3 | * @version:
4 | * @Author: June
5 | * @Date: 2023-03-17 23:47:54
6 | * @LastEditors: June
7 | * @LastEditTime: 2024-12-10 12:58:18
8 | */
9 | import { createApp } from 'vue'
10 | import App from './App.vue'
11 | import 'virtual:uno.css'
12 | import ElementPlus from 'element-plus'
13 | import 'element-plus/dist/index.css'
14 | import '@/styles/index.scss'
15 |
16 | function start() {
17 | const app = createApp(App)
18 | app.use(ElementPlus)
19 | app.mount('#app')
20 | }
21 | start()
22 |
--------------------------------------------------------------------------------
/packages/vuets/index.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 | color-gradient-picker-vue3
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/AdvancedControls/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
14 |
15 |
16 |
23 |
--------------------------------------------------------------------------------
/lib/uno.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description,
4 | * @Date: 2024-11-30 20:08:48
5 | * @LastEditTime: 2024-12-02 11:19:58
6 | * @LastEditors: June
7 | */
8 | import {
9 | defineConfig,
10 | presetAttributify,
11 | presetUno,
12 | transformerDirectives,
13 | } from 'unocss'
14 |
15 | export default defineConfig({
16 | transformers: [transformerDirectives()],
17 | presets: [presetUno(), presetAttributify()],
18 | shortcuts: [
19 | ['wh-full', 'w-full h-full'],
20 | ['f-center', 'flex justify-center items-center'],
21 | ],
22 | })
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "lib": ["esnext", "dom"],
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "strict": true,
9 | "noImplicitAny": false,
10 | "strictNullChecks": true,
11 | "resolveJsonModule": true,
12 | "skipDefaultLibCheck": true,
13 | "skipLibCheck": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 | "types": ["node", "vite/client"]
17 | },
18 | "exclude": ["**/dist/**", "**/node_modules/**"],
19 | "include": ["./types/*.d.ts"]
20 | }
21 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'color-gradient-picker-vue3'
7 | # text: "color-gradient-picker-vue3-docs"
8 | tagline: Color and gradient picker for vue3.js. It supports RBG, HSL, HSV, CMYK.
9 | actions:
10 | - theme: brand
11 | text: 配置项
12 | link: /options
13 | - theme: alt
14 | text: 使用例子
15 | link: /examples
16 |
17 | features:
18 | - title: 纯色
19 | details: 支持纯色
20 | - title: 渐变色
21 | details: 支持渐变,linear和radial
22 | - title: 各种颜色转换
23 | details: 支持hsv,hsl,cmyk
24 | ---
25 |
--------------------------------------------------------------------------------
/docs/en/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'color-gradient-picker-vue3'
7 | # text: "color-gradient-picker-vue3-docs"
8 | tagline: Color and gradient picker for vue3.js. It supports RBG, HSL, HSV, CMYK.
9 | actions:
10 | - theme: brand
11 | text: 配置项
12 | link: /options
13 | - theme: alt
14 | text: 使用例子
15 | link: /examples
16 |
17 | features:
18 | - title: 纯色
19 | details: 支持纯色
20 | - title: 渐变色
21 | details: 支持渐变,linear和radial
22 | - title: 各种颜色转换
23 | details: 支持hsv,hsl,cmyk
24 | ---
25 |
--------------------------------------------------------------------------------
/lib/index.html:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | color-gradient-picker-vue3
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/lib/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 | import vue from '@vitejs/plugin-vue'
3 | import * as path from 'path'
4 |
5 | export default defineConfig({
6 | plugins: [vue()],
7 | test: {
8 | environment: 'jsdom',
9 | globals: true,
10 | setupFiles: ['./tests/setup.ts'],
11 | coverage: {
12 | provider: 'v8',
13 | reporter: ['text', 'json', 'html'],
14 | exclude: [
15 | 'node_modules/',
16 | 'dist/',
17 | 'tests/',
18 | '**/*.d.ts',
19 | '**/*.config.*',
20 | '**/coverage/**',
21 | ],
22 | },
23 | },
24 | resolve: {
25 | alias: {
26 | '@': path.resolve(__dirname, './src'),
27 | },
28 | },
29 | })
30 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # 例子
6 |
7 | ```HTML
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
31 | ```
32 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description
4 | * @Date: 2024-12-02 12:31:39
5 | * @LastEditTime: 2024-12-05 13:50:52
6 | * @LastEditors: June
7 | */
8 | export { default as Opacity } from './Opacity.vue'
9 | export { default as PickerArea } from './PickerArea.vue'
10 | export { default as Operation } from './Operation.vue'
11 | export { default as OperationGradient } from './OperationGradient.vue'
12 | export { default as Preview } from './Preview.vue'
13 | export { default as Inputs } from './Inputs/index.vue'
14 | export { default as GradientBar } from './GradientBar.vue'
15 | export { default as Hue } from './Hue.vue'
16 | export { default as AdvancedControls } from './AdvancedControls/index.vue'
17 |
--------------------------------------------------------------------------------
/docs/en/examples.md:
--------------------------------------------------------------------------------
1 | ---
2 | outline: deep
3 | ---
4 |
5 | # 例子
6 |
7 | ```HTML
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/navs/zh.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description
4 | * @Date: 2024-12-21 14:31:49
5 | * @LastEditTime: 2024-12-21 15:19:24
6 | * @LastEditors: June
7 | */
8 | export default function getNavs() {
9 | return [
10 | { text: '主页', link: '/' },
11 | // {
12 | // text: '关于',
13 | // items: [
14 | // {
15 | // text: '团队',
16 |
17 | // link: '/zh/examples/about/team',
18 |
19 | // activeMatch: '/about/team',
20 | // },
21 |
22 | // {
23 | // text: '常见问题',
24 |
25 | // link: '/zh/examples/about/problem',
26 |
27 | // activeMatch: '/about/problem',
28 | // },
29 | // ],
30 |
31 | // activeMatch: '/zh/examples/about/', // // 当前页面处于匹配路径下时, 对应导航菜单将突出显示
32 | // },
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/packages/vuets/src/App.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
31 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | ### 🤔 这个 PR 的性质是?(至少选择一个)
8 |
9 | - [ ] 日常 bug 修复
10 | - [ ] 新特性提交
11 | - [ ] 站点、文档改进
12 | - [ ] 演示代码改进
13 | - [ ] 组件样式/交互改进
14 | - [ ] TypeScript 定义更新
15 | - [ ] CI/CD 改进
16 | - [ ] 包体积优化
17 | - [ ] 性能优化
18 | - [ ] 功能增强
19 | - [ ] 国际化改进
20 | - [ ] 代码重构
21 | - [ ] 代码风格优化
22 | - [ ] 测试用例
23 | - [ ] 分支合并
24 | - [ ] 其他改动(是关于什么的改动?)
25 |
26 | ### 🔗 相关 Issue
27 |
28 |
31 |
32 | ### 💡 需求背景和解决方案
33 |
34 |
39 |
40 | ### ☑️ 请求合并前的自查清单
41 |
42 | ⚠️ 请自检并全部**勾选全部选项**。⚠️
43 |
44 | - [ ] 文档已补充或无须补充
45 | - [ ] 代码演示已提供或无须提供
46 | - [ ] TypeScript 定义已补充或无须补充
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 向 color-gradient-picker-vue3 提出新功能需求
2 | description: 创建一个 Issue 描述一下你的功能需求。
3 | title: '[新功能需求] 请在此填写标题'
4 | labels: ['feature: need confirm']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 在提交功能需求前,请注意:
10 |
11 | - 确认这是一个通用功能,并且无法通过现有的 API 或 Slot 实现。
12 | - 尝试在 [Issue](https://github.com/Qiu-Jun/color-gradient-picker-vue3/issues)列表中搜索,并且没有发现同样的需求。
13 | - 请确保描述清楚你的需求,以便其他开发者更好地理解你的需求。
14 |
15 | - type: textarea
16 | id: description
17 | attributes:
18 | label: 这个功能解决了什么问题?
19 | description: 请尽可能详细地说明这个功能的使用场景。
20 | validations:
21 | required: true
22 |
23 | - type: textarea
24 | id: api
25 | attributes:
26 | label: 你期望的 API 是什么样子的?
27 | description: 描述一下这个新功能的 API,并提供一些代码示例。
28 | validations:
29 | required: true
30 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: 部署VitePress
2 |
3 | on:
4 | push:
5 | branches: ['main']
6 |
7 | jobs:
8 | build-and-deploy:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v3
14 | with:
15 | ref: main # 这一步检查 main 代码
16 |
17 | - name: Setup Node.js and pnpm
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: '20.10.0' # 设置 nodejs 的版本
21 |
22 | - name: Build
23 | run: |
24 | npm i -g pnpm
25 | pnpm install
26 | pnpm run build:docs
27 |
28 | - name: Deploy to GitHub Pages
29 | uses: peaceiris/actions-gh-pages@v3
30 | with:
31 | github_token: ${{ secrets.DEPLOY }}
32 | publish_dir: docs/.vitepress/dist # 指定该文件夹中的 dist
33 | publish_branch: gh-pages # 推送到关联仓库的 gh-pages 分支
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
9 |
10 | ## color-gradient-picker-vue3
11 |
12 | Color and gradient picker for vue3.js.
13 |
14 | [](https://www.npmjs.com/package/color-gradient-picker-vue3)
15 | [](https://www.npmjs.com/package/color-gradient-picker-vue3)
16 |
17 |
18 |
19 |
20 | ## color-gradient-picker-vue3
21 |
22 | Color and gradient picker for vue3.js. It supports `RBG`, `HSL`, `HSV`, `CMYK`.
23 |
24 | #### Usage
25 |
26 | [查看文档](https://qiu-jun.github.io/color-gradient-picker-vue3/index.html)
27 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.mts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description
4 | * @Date: 2024-12-21 12:52:42
5 | * @LastEditTime: 2024-12-21 16:30:20
6 | * @LastEditors: June
7 | */
8 | import { defineConfig } from 'vitepress'
9 | import { docsConfig } from '../docs'
10 | import { themeConfig } from '../theme'
11 | import { head } from '../head'
12 | //配置的英文文档设置
13 | import { enConfig } from '../configs/en'
14 | //配置的中文文档设置
15 | import { zhConfig } from '../configs/zh'
16 | // https://vitepress.dev/reference/site-config
17 | export default defineConfig({
18 | base: '/color-gradient-picker-vue3/',
19 | /* 文档配置 */
20 | ...docsConfig,
21 | /* 标头配置 */
22 | head,
23 | /* 主题配置 */
24 | themeConfig,
25 | /* 语言配置 */
26 | locales: {
27 | root: {
28 | label: '简体中文',
29 | lang: 'zh-C',
30 | link: '/index',
31 | ...zhConfig,
32 | },
33 | en: { label: 'English', lang: 'en-US', link: '/en/', ...enConfig },
34 | },
35 | })
36 |
--------------------------------------------------------------------------------
/docs/configs/en.ts:
--------------------------------------------------------------------------------
1 | import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
2 |
3 | //引入以上配置 是英文界面需要修改zh为en
4 | import getNavs from '../navs/zh'
5 | import { sidebar } from '../sidebars/en'
6 |
7 | export const enConfig: LocaleSpecificConfig = {
8 | themeConfig: {
9 | lastUpdatedText: '上次更新',
10 | returnToTopLabel: '返回顶部',
11 |
12 | // 文档页脚文本配置
13 |
14 | docFooter: {
15 | prev: '上一页',
16 | next: '下一页',
17 | },
18 |
19 | // editLink: {
20 |
21 | // pattern: '路径地址',
22 |
23 | // text: '对本页提出修改建议',
24 |
25 | // },
26 |
27 | // logo: '/img/alemon.jpg',
28 |
29 | nav: getNavs(),
30 |
31 | sidebar,
32 | outline: {
33 | level: 'deep', // 右侧大纲标题层级
34 | label: '目录', // 右侧大纲标题文本配置
35 | },
36 |
37 | socialLinks: [
38 | {
39 | icon: 'github',
40 | link: 'https://github.com/Qiu-Jun/color-gradient-picker-vue3',
41 | },
42 | ],
43 | },
44 | }
45 |
--------------------------------------------------------------------------------
/docs/configs/zh.ts:
--------------------------------------------------------------------------------
1 | import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
2 |
3 | //引入以上配置 是英文界面需要修改zh为en
4 | import getNavs from '../navs/zh'
5 | import { sidebar } from '../sidebars/zh'
6 |
7 | export const zhConfig: LocaleSpecificConfig = {
8 | themeConfig: {
9 | lastUpdatedText: '上次更新',
10 | returnToTopLabel: '返回顶部',
11 |
12 | // 文档页脚文本配置
13 |
14 | docFooter: {
15 | prev: '上一页',
16 | next: '下一页',
17 | },
18 |
19 | // editLink: {
20 |
21 | // pattern: '路径地址',
22 |
23 | // text: '对本页提出修改建议',
24 |
25 | // },
26 |
27 | // logo: '/img/alemon.jpg',
28 |
29 | nav: getNavs(),
30 |
31 | sidebar,
32 | outline: {
33 | level: 'deep', // 右侧大纲标题层级
34 | label: '目录', // 右侧大纲标题文本配置
35 | },
36 |
37 | socialLinks: [
38 | {
39 | icon: 'github',
40 | link: 'https://github.com/Qiu-Jun/color-gradient-picker-vue3',
41 | },
42 | ],
43 | },
44 | }
45 |
--------------------------------------------------------------------------------
/lib/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 |
3 | // 这是一个测试入口文件,用于确保所有测试都能正常运行
4 | describe('测试套件', () => {
5 | it('应该能够运行基本测试', () => {
6 | expect(true).toBe(true)
7 | })
8 |
9 | it('应该能够进行数学运算', () => {
10 | expect(1 + 1).toBe(2)
11 | expect(2 * 3).toBe(6)
12 | expect(10 / 2).toBe(5)
13 | })
14 |
15 | it('应该能够处理字符串', () => {
16 | expect('hello').toBe('hello')
17 | expect('hello'.toUpperCase()).toBe('HELLO')
18 | expect('hello world'.split(' ')).toEqual(['hello', 'world'])
19 | })
20 |
21 | it('应该能够处理数组', () => {
22 | const arr = [1, 2, 3, 4, 5]
23 | expect(arr.length).toBe(5)
24 | expect(arr.map((x) => x * 2)).toEqual([2, 4, 6, 8, 10])
25 | expect(arr.filter((x) => x > 3)).toEqual([4, 5])
26 | })
27 |
28 | it('应该能够处理对象', () => {
29 | const obj = { name: 'test', value: 123 }
30 | expect(obj.name).toBe('test')
31 | expect(obj.value).toBe(123)
32 | expect(Object.keys(obj)).toEqual(['name', 'value'])
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description:
4 | * @Date: 2023-02-21 23:42:31
5 | * @LastEditors: June
6 | * @LastEditTime: 2023-10-02 18:39:45
7 | */
8 | module.exports = {
9 | root: true,
10 | parser: 'vue-eslint-parser',
11 |
12 | parserOptions: {
13 | parser: '@typescript-eslint/parser',
14 | ecmaVersion: 2020,
15 | sourceType: 'module',
16 | ecmaFeatures: {
17 | jsx: true,
18 | tsx: true,
19 | },
20 | },
21 |
22 | extends: [
23 | 'plugin:vue/vue3-recommended',
24 | 'plugin:@typescript-eslint/recommended',
25 | 'prettier',
26 | 'plugin:prettier/recommended',
27 | './lib/.eslintrc-auto-import.json',
28 | ],
29 |
30 | rules: {
31 | // override/add rules settings here, such as:
32 | 'vue/multi-word-component-names': 'off',
33 | 'vue/prefer-import-from-vue': 'off',
34 | 'vue/require-default-prop': 'off',
35 | '@typescript-eslint/no-empty-function': 'off',
36 | '@typescript-eslint/ban-ts-comment': 'off',
37 | '@typescript-eslint/no-non-null-assertion': 'off',
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Descripttion: 颜色选择器组件主入口文件
3 | * @version: 3.1.0
4 | * @Author: June
5 | * @Date: 2023-03-18 00:33:21
6 | * @LastEditors: June
7 | * @LastEditTime: 2024-12-10 12:50:24
8 | */
9 |
10 | // 导出主组件
11 | export { ColorPicker } from './src/components/ColorPicker'
12 |
13 | // 导出类型定义
14 | export type {
15 | ColorPickerProps,
16 | GradientProps,
17 | IColor,
18 | IColorValue,
19 | IColorPicker,
20 | IMode,
21 | IProvide,
22 | } from '@/interfaces'
23 |
24 | // 导出枚举
25 | export {
26 | InputType,
27 | GradientType,
28 | Modes,
29 | EventType,
30 | DEFAULT_VALUES,
31 | } from '@/enums'
32 |
33 | // 导出工具函数
34 | export {
35 | createGradientStr,
36 | isValidColor,
37 | formatColor,
38 | getColorContrast,
39 | } from '@/utils/color'
40 |
41 | export {
42 | getColors,
43 | formatInputValues,
44 | round,
45 | clamp,
46 | percentToDecimal,
47 | decimalToPercent,
48 | } from '@/utils/format'
49 |
50 | // 导入样式
51 | import './src/styles/index.scss'
52 | import 'virtual:uno.css'
53 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/InputHex.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
15 |
38 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description:
4 | * @Date: 2023-07-09 23:15:46
5 | * @LastEditors: June
6 | * @LastEditTime: 2023-10-04 10:49:56
7 | */
8 | module.exports = {
9 | // 一行最多 80 字符
10 | printWidth: 80,
11 | // 使用 4 个空格缩进
12 | tabWidth: 2,
13 | // 不使用 tab 缩进,而使用空格
14 | useTabs: false,
15 | // 行尾需要有分号
16 | semi: false,
17 | // 使用单引号代替双引号
18 | singleQuote: true,
19 | // 对象的 key 仅在必要时用引号
20 | quoteProps: 'as-needed',
21 | // jsx 不使用单引号,而使用双引号
22 | jsxSingleQuote: false,
23 | // 末尾使用逗号
24 | trailingComma: 'all',
25 | // 大括号内的首尾需要空格 { foo: bar }
26 | bracketSpacing: true,
27 | // jsx 标签的反尖括号需要换行
28 | jsxBracketSameLine: false,
29 | // 箭头函数,只有一个参数的时候,也需要括号
30 | arrowParens: 'always',
31 | // 每个文件格式化的范围是文件的全部内容
32 | rangeStart: 0,
33 | rangeEnd: Infinity,
34 | // 不需要写文件开头的 @prettier
35 | requirePragma: false,
36 | // 不需要自动在文件开头插入 @prettier
37 | insertPragma: false,
38 | // 使用默认的折行标准
39 | proseWrap: 'preserve',
40 | // 根据显示样式决定 html 要不要折行
41 | htmlWhitespaceSensitivity: 'css',
42 | // 换行符使用 lf
43 | endOfLine: 'auto',
44 | }
45 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 June
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/AdvancedControls/components/AdvBrightnessBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Brightness
5 |
6 |
7 |
8 |
9 |
35 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
22 |
23 |
24 |
37 |
--------------------------------------------------------------------------------
/lib/src/enums/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: 颜色选择器枚举定义
4 | * @Date: 2024-12-04 11:58:12
5 | * @LastEditTime: 2024-12-10 13:15:02
6 | * @LastEditors: June
7 | */
8 |
9 | /**
10 | * 输入类型枚举
11 | */
12 | export enum InputType {
13 | /** HSL颜色模式 */
14 | hsl = 'HSL',
15 | /** RGB颜色模式 */
16 | rgb = 'RGB',
17 | /** HSV颜色模式 */
18 | hsv = 'HSV',
19 | /** CMYK颜色模式 */
20 | cmyk = 'CMYK',
21 | }
22 |
23 | /**
24 | * 渐变类型枚举
25 | */
26 | export enum GradientType {
27 | /** 线性渐变 */
28 | linear = 'linear',
29 | /** 径向渐变 */
30 | radial = 'radial',
31 | }
32 |
33 | /**
34 | * 颜色模式枚举
35 | */
36 | export enum Modes {
37 | /** 纯色模式 */
38 | solid = 'solid',
39 | /** 渐变模式 */
40 | gradient = 'gradient',
41 | }
42 |
43 | /**
44 | * 事件类型枚举
45 | */
46 | export enum EventType {
47 | /** 颜色变化 */
48 | change = 'change',
49 | /** 值更新 */
50 | update = 'update:value',
51 | }
52 |
53 | /**
54 | * 默认值常量
55 | */
56 | export const DEFAULT_VALUES = {
57 | /** 默认颜色 */
58 | DEFAULT_COLOR: 'rgba(175, 51, 242, 1)',
59 | /** 默认宽度 */
60 | DEFAULT_WIDTH: 300,
61 | /** 默认角度 */
62 | DEFAULT_DEGREES: 90,
63 | /** 最大预设颜色数量 */
64 | MAX_PRESET_COLORS: 18,
65 | /** 最小渐变点数量 */
66 | MIN_GRADIENT_POINTS: 2,
67 | } as const
68 |
--------------------------------------------------------------------------------
/lib/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest'
2 |
3 | // Mock CSS modules
4 | vi.mock('*.css', () => ({}))
5 | vi.mock('*.scss', () => ({}))
6 |
7 | // Mock Vue auto-imports
8 | vi.mock('vue', async () => {
9 | const actual = await vi.importActual('vue')
10 | return {
11 | ...actual,
12 | reactive: vi.fn((obj) => obj),
13 | ref: vi.fn((value) => ({ value })),
14 | computed: vi.fn((fn) => ({ value: fn() })),
15 | provide: vi.fn(),
16 | inject: vi.fn(),
17 | nextTick: vi.fn(() => Promise.resolve()),
18 | }
19 | })
20 |
21 | // Mock UnoCSS
22 | vi.mock('virtual:uno.css', () => ({}))
23 |
24 | // Mock tinycolor2
25 | vi.mock('tinycolor2', () => ({
26 | default: vi.fn((color) => ({
27 | toRgb: () => ({ r: 255, g: 0, b: 0, a: 1 }),
28 | toHsv: () => ({ h: 0, s: 100, v: 100 }),
29 | toHex: () => '#ff0000',
30 | toHsl: () => ({ h: 0, s: 100, l: 50 }),
31 | isValid: () => true,
32 | })),
33 | }))
34 |
35 | // Mock lodash-es
36 | vi.mock('lodash-es', () => ({
37 | cloneDeep: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
38 | debounce: vi.fn((fn) => fn),
39 | throttle: vi.fn((fn) => fn),
40 | }))
41 |
42 | // Global test utilities
43 | global.console = {
44 | ...console,
45 | warn: vi.fn(),
46 | error: vi.fn(),
47 | log: vi.fn(),
48 | }
49 |
--------------------------------------------------------------------------------
/lib/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Description
4 | * @Date: 2024-11-30 21:13:34
5 | * @LastEditTime: 2024-12-22 00:03:52
6 | * @LastEditors: June
7 | */
8 | import { InputType } from '@/enums'
9 |
10 | export const inputTypes: InputType[] = [
11 | InputType.rgb,
12 | InputType.hsl,
13 | // InputType.hsv,
14 | // InputType.cmyk,
15 | ]
16 |
17 | export const config = {
18 | barSize: 18,
19 | crossSize: 18,
20 | delay: 150,
21 | defaultColor: 'rgba(175, 51, 242, 1)',
22 | defaultGradient:
23 | 'linear-gradient(90deg, rgb(245, 66, 245) 0%, rgb(0, 0, 255) 100%)',
24 | }
25 |
26 | export const defaultLocales = {
27 | CONTROLS: {
28 | SOLID: 'Solid',
29 | GRADIENT: 'Gradient',
30 | },
31 | }
32 |
33 | export const presetColors = [
34 | 'rgba(0,0,0,1)',
35 | 'rgba(128,128,128, 1)',
36 | 'rgba(192,192,192, 1)',
37 | 'rgba(255,255,255, 1)',
38 | 'rgba(0,0,128,1)',
39 | 'rgba(0,0,255,1)',
40 | 'rgba(0,255,255, 1)',
41 | 'rgba(0,128,0,1)',
42 | 'rgba(128,128,0, 1)',
43 | 'rgba(0,128,128,1)',
44 | 'rgba(0,255,0, 1)',
45 | 'rgba(128,0,0, 1)',
46 | 'rgba(128,0,128, 1)',
47 | 'rgba(175, 51, 242, 1)',
48 | 'rgba(255,0,255, 1)',
49 | 'rgba(255,0,0, 1)',
50 | 'rgba(240, 103, 46, 1)',
51 | 'rgba(255,255,0, 1)',
52 | ]
53 |
--------------------------------------------------------------------------------
/.cz-config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | types: [
3 | { value: 'feature', name: 'feature: 增加新功能' },
4 | { value: 'bug', name: 'bug: 测试反馈bug列表中的bug号' },
5 | { value: 'fix', name: 'fix: 修复bug' },
6 | { value: 'ui', name: 'ui: 更新UI' },
7 | { value: 'docs', name: 'docs: 文档变更' },
8 | { value: 'style', name: 'style: 代码格式(不影响代码运行的变动)' },
9 | { value: 'perf', name: 'perf: 性能优化' },
10 | {
11 | value: 'refactor',
12 | name: 'refactor: 重构(既不是增加feature,也不是修复bug)',
13 | },
14 | { value: 'release', name: 'release: 发布' },
15 | { value: 'deploy', name: 'deploy: 部署' },
16 | { value: 'test', name: 'test: 增加测试' },
17 | {
18 | value: 'chore',
19 | name: 'chore: 构建过程或辅助工具的变动(更改配置文件)',
20 | },
21 | { value: 'revert', name: 'revert: 回退' },
22 | { value: 'build', name: 'build: 打包' },
23 | ],
24 | // override the messages, defaults are as follows
25 | messages: {
26 | type: '请选择提交类型:',
27 | customScope: '请输入您修改的范围(可选):',
28 | subject: '请简要描述提交 message (必填):',
29 | body: '请输入详细描述(可选,待优化去除,跳过即可):',
30 | footer: '请输入要关闭的issue(待优化去除,跳过即可):',
31 | confirmCommit: '确认使用以上信息提交?(y/n/e/h)',
32 | },
33 | allowCustomScopes: true,
34 | skipQuestions: ['body', 'footer'],
35 | subjectLimit: 100,
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/AdvancedControls/components/AdvSaturationBar.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
Saturation
12 |
13 |
14 |
15 |
16 |
42 |
--------------------------------------------------------------------------------
/docs/en/options.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | # color-gradient-picker-vue3
10 |
11 | 这里主要介绍`color-gradient-picker-vue3`的使用以及一些常用配置,以及主要注意事项
12 |
13 | ## 注意
14 |
15 | 由于 ui 设计问题,`color-gradient-picker-vue3`的宽度最小为`320px`, 主要为了保证底部预设色的美观, 如果设置的`width`小于`320`, 那么初始化时会默认为`320`
16 |
17 | ## 使用
18 |
19 | #### pnpm
20 |
21 | ```bash
22 | pnpm add color-gradient-picker-vue3
23 | ```
24 |
25 | #### npm
26 |
27 | ```bash
28 | npm install color-gradient-picker-vue3
29 | ```
30 |
31 | #### yarn
32 |
33 | ```bash
34 | yarn add color-gradient-picker-vue3
35 | ```
36 |
37 | ## 配置说明
38 |
39 | | 参数 | 类型 | 默认值 | 描述 |
40 | | ------------ | ------- | --------------------- | ---------------------------------------- |
41 | | value | String | rgba(175, 51, 242, 1) | 默认颜色 |
42 | | width | Number | 320 | 宽度(注意,颜色选择区域的高度会等于宽度) |
43 | | hideInputs | Boolean | false | 隐藏输入 |
44 | | hideOpacity | Boolean | false | 隐藏透明度设置滑块 |
45 | | hideGradient | Boolean | false | 隐藏渐变 |
46 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/RGBInputs.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
20 |
21 |
22 |
46 |
--------------------------------------------------------------------------------
/lib/src/utils/convert.ts:
--------------------------------------------------------------------------------
1 | export function rgb2cmyk(r: number, g: number, b: number) {
2 | let computedC = 0
3 | let computedM = 0
4 | let computedY = 0
5 | let computedK = 0
6 |
7 | if (
8 | r === null ||
9 | g === null ||
10 | b === null ||
11 | isNaN(r) ||
12 | isNaN(g) ||
13 | isNaN(b)
14 | ) {
15 | return { c: 0, m: 0, k: 0, y: 1 }
16 | }
17 | if (r < 0 || g < 0 || b < 0 || r > 255 || g > 255 || b > 255) {
18 | return { c: 0, m: 0, k: 0, y: 1 }
19 | }
20 |
21 | if (r === 0 && g === 0 && b === 0) {
22 | computedK = 1
23 | return { c: 0, m: 0, k: 0, y: 1 }
24 | }
25 |
26 | computedC = 1 - r / 255
27 | computedM = 1 - g / 255
28 | computedY = 1 - b / 255
29 |
30 | const minCMY = Math.min(computedC, Math.min(computedM, computedY))
31 | computedC = (computedC - minCMY) / (1 - minCMY)
32 | computedM = (computedM - minCMY) / (1 - minCMY)
33 | computedY = (computedY - minCMY) / (1 - minCMY)
34 | computedK = minCMY
35 |
36 | return { c: computedC, m: computedM, y: computedY, k: computedK }
37 | }
38 |
39 | export const cmykToRgb = ({
40 | c,
41 | m,
42 | y,
43 | k,
44 | }: {
45 | c: number
46 | m: number
47 | y: number
48 | k: number
49 | }) => {
50 | const r = 255 * (1 - c) * (1 - k)
51 | const g = 255 * (1 - m) * (1 - k)
52 | const b = 255 * (1 - y) * (1 - k)
53 |
54 | return { r: r, g: g, b: b }
55 | }
56 |
--------------------------------------------------------------------------------
/docs/theme.ts:
--------------------------------------------------------------------------------
1 | import type { DefaultTheme } from 'vitepress'
2 |
3 | export const themeConfig: DefaultTheme.Config = {
4 | // logo: '/img/alemon.jpg',
5 | // i18n路由
6 | i18nRouting: false,
7 | // 搜索配置(二选一)
8 | search: {
9 | // 本地离线搜索
10 | provider: 'local',
11 | // 多语言搜索配置
12 | options: {
13 | locales: {
14 | /* 默认语言 */
15 | zh: {
16 | translations: {
17 | button: {
18 | buttonText: '搜索',
19 | buttonAriaLabel: '搜索文档',
20 | },
21 | modal: {
22 | noResultsText: '无法找到相关结果',
23 | resetButtonTitle: '清除查询结果',
24 | footer: {
25 | selectText: '选择',
26 | navigateText: '切换',
27 | },
28 | },
29 | },
30 | },
31 |
32 | en: {
33 | translations: {
34 | button: {
35 | buttonText: 'Search',
36 | buttonAriaLabel: 'Search for Documents',
37 | },
38 |
39 | modal: {
40 | noResultsText: 'Unable to find relevant results',
41 | resetButtonTitle: 'Clear Query Results',
42 | footer: {
43 | selectText: 'select',
44 | navigateText: 'switch',
45 | },
46 | },
47 | },
48 | },
49 | },
50 | },
51 | },
52 | }
53 |
--------------------------------------------------------------------------------
/docs/options.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | # color-gradient-picker-vue3
10 |
11 | 这里主要介绍`color-gradient-picker-vue3`的使用以及一些常用配置,以及主要注意事项
12 |
13 | ## 注意
14 |
15 | 由于 ui 设计问题,`color-gradient-picker-vue3`的宽度最小为`320px`, 主要为了保证底部预设色的美观, 如果设置的`width`小于`320`, 那么初始化时会默认为`320`
16 |
17 | ## 使用
18 |
19 | #### pnpm
20 |
21 | ```bash
22 | pnpm add color-gradient-picker-vue3
23 | ```
24 |
25 | #### npm
26 |
27 | ```bash
28 | npm install color-gradient-picker-vue3
29 | ```
30 |
31 | #### yarn
32 |
33 | ```bash
34 | yarn add color-gradient-picker-vue3
35 | ```
36 |
37 | ## 配置说明
38 |
39 | | 参数 | 类型 | 默认值 | 描述 |
40 | | ------------ | -------- | --------------------- | ---------------------------------------- |
41 | | value | String | rgba(175, 51, 242, 1) | 默认颜色 |
42 | | width | Number | 320 | 宽度(注意,颜色选择区域的高度会等于宽度) |
43 | | hideInputs | Boolean | false | 隐藏输入 |
44 | | hideOpacity | Boolean | false | 隐藏透明度设置滑块 |
45 | | hideGradient | Boolean | false | 隐藏渐变 |
46 | | presetColors | String[] | String[] | 预设颜色 |
47 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/InputItem.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
15 |
57 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/HSLInputs.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
20 |
21 |
22 |
50 |
--------------------------------------------------------------------------------
/lib/src/App.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
17 |
18 |
22 |
23 |
34 |
35 |
36 |
37 |
57 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Preview.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
33 |
34 |
35 |
60 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/HSVInputs.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
21 |
27 |
28 |
29 |
56 |
--------------------------------------------------------------------------------
/scripts/dev.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Descripttion:
3 | * @version:
4 | * @Author: June
5 | * @Date: 2023-04-11 15:05:52
6 | * @LastEditors: June
7 | * @LastEditTime: 2023-04-11 15:33:42
8 | */
9 | import { execa } from 'execa'
10 | import inquirer from 'inquirer'
11 | import { resolve } from 'path'
12 |
13 | const CWD = process.cwd()
14 | const PKG_DEV = resolve(CWD, './lib')
15 | const PKG_VUE3JS = resolve(CWD, './packages/vue3js')
16 | const PKG_VUE3TS = resolve(CWD, './packages/vue3ts')
17 | const PKG_NUXI = resolve(CWD, './packages/nuxi')
18 |
19 | const run = (bin, args, opts = {}) => {
20 | execa(bin, args, { stdio: 'inherit', ...opts })
21 | }
22 |
23 | async function create() {
24 | const { result } = await inquirer.prompt([
25 | {
26 | type: 'list',
27 | message: '请选择您要启动的子项目:',
28 | name: 'result',
29 | choices: [
30 | {
31 | key: '0',
32 | name: '开发',
33 | value: 'dev',
34 | },
35 | {
36 | key: '1',
37 | name: 'vue3js 例子',
38 | value: 'vue3js',
39 | },
40 | {
41 | key: '2',
42 | name: 'vue3ts 例子',
43 | value: 'vue3ts',
44 | },
45 | {
46 | key: '3',
47 | name: 'nuxi 例子',
48 | value: 'nuxi',
49 | },
50 | ],
51 | },
52 | ])
53 |
54 | switch (result) {
55 | case 'dev':
56 | run('pnpm', ['dev'], { cwd: PKG_DEV })
57 | break
58 | case 'vue3js':
59 | run('pnpm', ['dev'], { cwd: PKG_VUE3JS })
60 | break
61 | case 'vue3ts':
62 | run('pnpm', ['dev'], { cwd: PKG_VUE3TS })
63 | break
64 | case 'nuxi':
65 | run('pnpm', ['dev'], { cwd: PKG_NUXI })
66 | break
67 | default:
68 | break
69 | }
70 | }
71 |
72 | create()
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Color and gradient picker",
3 | "version": "1.0.0",
4 | "description": "Color and gradient picker for vue3.js",
5 | "private": true,
6 | "type": "module",
7 | "scripts": {
8 | "preinstall": "npx only-allow pnpm",
9 | "build": "pnpm run -C lib build",
10 | "dev": "pnpm run -C lib dev",
11 | "predev": "node scripts/dev.js",
12 | "build:docs": "pnpm run -C docs docs:build",
13 | "eslint": "eslint --ext .ts,.tsx,.jsx,.js,.vue --ignore-path .gitignore --fix src",
14 | "prettier": "prettier --write .",
15 | "prepare": "husky install",
16 | "commit": "git-cz"
17 | },
18 | "config": {
19 | "commitizen": {
20 | "path": "node_modules/cz-conventional-changelog"
21 | }
22 | },
23 | "keywords": [],
24 | "author": "June",
25 | "license": "MIT",
26 | "devDependencies": {
27 | "@commitlint/cli": "^17.4.2",
28 | "@commitlint/config-conventional": "^17.4.2",
29 | "@types/node": "^18.15.11",
30 | "@typescript-eslint/eslint-plugin": "^5.48.2",
31 | "@typescript-eslint/parser": "^5.48.2",
32 | "@vitejs/plugin-vue": "^5.2.1",
33 | "@vitejs/plugin-vue-jsx": "^4.1.1",
34 | "autoprefixer": "^9.0.0",
35 | "commitizen": "^4.2.6",
36 | "commitlint-config-cz": "^0.13.3",
37 | "cz-conventional-changelog": "^3.3.0",
38 | "element-plus": "^2.3.9",
39 | "eslint": "^8.32.0",
40 | "eslint-config-prettier": "^8.6.0",
41 | "eslint-plugin-prettier": "^4.2.1",
42 | "eslint-plugin-vue": "^9.9.0",
43 | "execa": "^7.1.1",
44 | "husky": "^8.0.3",
45 | "inquirer": "^9.1.5",
46 | "prettier": "^2.8.3",
47 | "sass": "^1.61.0",
48 | "sass-loader": "^13.2.2",
49 | "terser": "^5.18.1",
50 | "typescript": "^4.9.3",
51 | "unplugin-auto-import": "^0.16.6",
52 | "vite": "^6.0.3",
53 | "vue-tsc": "^1.0.11"
54 | },
55 | "dependencies": {
56 | "vue": "^3.5.13"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Inputs/components/CMYKInputs.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
20 |
26 |
27 |
28 |
64 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 向 color-gradient-picker-vue3 提交 Bug
2 | description: 创建一个 Issue 描述你遇到的问题。
3 | title: '[Bug 上报] 请在此填写标题'
4 | labels: ['🐞bug: need confirm']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | 在向我们提交 Bug 报告前,请优先使用以下方式尝试解决问题:
10 | - 在文档 [docs](https://qiu-jun.github.io/color-gradient-picker-vue3/index.html) 确认使用方法是否正确
11 | - 尝试在 [Issue](https://github.com/Qiu-Jun/color-gradient-picker-vue3/issues) 列表中搜索相同问题
12 |
13 | - type: input
14 | id: version
15 | attributes:
16 | label: color-gradient-picker-vue3 版本号
17 | description: 你正在使用的组件库版本号
18 | placeholder: 例如:0.1.1
19 | validations:
20 | required: true
21 |
22 | - type: dropdown
23 | id: platform
24 | attributes:
25 | label: 浏览器
26 | multiple: true
27 | description: 选择对应的平台
28 | options:
29 | - chrome
30 | - 火狐
31 | - Edge
32 | - 其他
33 | validations:
34 | required: true
35 |
36 | - type: input
37 | id: reproduce
38 | attributes:
39 | label: 复现Demo地址
40 | description: |
41 | 我们需要你提供一个最小重现demo,以便于我们帮你排查问题。
42 | validations:
43 | required: true
44 |
45 | - type: textarea
46 | id: reproduce-steps
47 | attributes:
48 | label: 重现步骤
49 | description: |
50 | 请提供一个最简洁清晰的重现步骤,方便我们快速重现问题。
51 | validations:
52 | required: true
53 |
54 | - type: textarea
55 | id: expected
56 | attributes:
57 | label: 期望的结果是什么?
58 | validations:
59 | required: true
60 |
61 | - type: textarea
62 | id: actually-happening
63 | attributes:
64 | label: 实际的结果是什么?
65 | validations:
66 | required: true
67 |
68 | - type: textarea
69 | id: extra
70 | attributes:
71 | label: 其他补充信息
72 | description: |
73 | 根据你的分析,出现这个问题的原因可能在哪里,或者你认为可能产生关联的信息:比如 Vue 版本、vite 版本、Node 版本、采用哪种自动引入方案等,或者进行了哪些配置,使用了哪些插件等信息。
74 |
--------------------------------------------------------------------------------
/lib/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Descripttion:
3 | * @version:
4 | * @Author: June
5 | * @Date: 2023-06-27 13:16:45
6 | * @LastEditors: June
7 | * @LastEditTime: 2024-12-22 00:02:02
8 | */
9 | import { InputType, Modes } from '@/enums'
10 |
11 | export interface ColorPickerProps {
12 | width: number
13 | height?: number
14 | gradientColorsIdx?: number // 当前的渐变点下标
15 | degrees?: number
16 | degreesStr?: string
17 | gradientColor?: string
18 | value?: string
19 | hideGradient?: boolean
20 | showAdvancedSliders?: boolean
21 | hideInputs?: boolean
22 | hideOpacity?: boolean
23 | hc?: IColorValue
24 | isGradient?: boolean
25 | inputType?: InputType
26 | onChange?: any
27 | mode?: IMode
28 | gradientColors?: GradientProps[] // 渐变点颜色
29 | presetColors?: string[]
30 | hidePresets?: boolean
31 | }
32 |
33 | export type GradientProps = {
34 | value: string
35 | index?: number
36 | left?: number
37 | }
38 |
39 | export type IMode = Modes.solid | Modes.gradient
40 |
41 | /**
42 | * 颜色值对象
43 | */
44 | export interface IColorValue {
45 | /** 红色分量 (0-255) */
46 | r: number
47 | /** 绿色分量 (0-255) */
48 | g: number
49 | /** 蓝色分量 (0-255) */
50 | b: number
51 | /** 透明度 (0-1) */
52 | a: number
53 | /** 色相 (0-360) */
54 | h: number
55 | /** 饱和度 (0-100) */
56 | s: number
57 | /** 明度 (0-100) */
58 | v: number
59 | }
60 |
61 | /**
62 | * 颜色对象接口
63 | */
64 | export interface IColor {
65 | /** 颜色模式 */
66 | mode?: IMode
67 | /** 颜色值 */
68 | color?: string
69 | /** 角度 */
70 | angle?: number
71 | /** 度数 */
72 | degrees?: number
73 | /** 渐变颜色数组 */
74 | colors?: { color: string; offset: number }[]
75 | /** 渐变类型 */
76 | gradientType?: string
77 | /** 渐变颜色点 */
78 | gradientColors?: { color: string; left?: number }[]
79 | [key: string]: any
80 | }
81 |
82 | export interface IProvide extends ColorPickerProps {
83 | value: string
84 | width: number
85 | height: number
86 | hc: any
87 | }
88 | export type IColorPicker = any
89 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Opacity.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
27 |
28 |
29 |
79 |
--------------------------------------------------------------------------------
/lib/.eslintrc-auto-import.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "Component": true,
4 | "ComponentPublicInstance": true,
5 | "ComputedRef": true,
6 | "EffectScope": true,
7 | "InjectionKey": true,
8 | "PropType": true,
9 | "Ref": true,
10 | "VNode": true,
11 | "computed": true,
12 | "createApp": true,
13 | "customRef": true,
14 | "defineAsyncComponent": true,
15 | "defineComponent": true,
16 | "effectScope": true,
17 | "getCurrentInstance": true,
18 | "getCurrentScope": true,
19 | "h": true,
20 | "inject": true,
21 | "isProxy": true,
22 | "isReactive": true,
23 | "isReadonly": true,
24 | "isRef": true,
25 | "markRaw": true,
26 | "nextTick": true,
27 | "onActivated": true,
28 | "onBeforeMount": true,
29 | "onBeforeUnmount": true,
30 | "onBeforeUpdate": true,
31 | "onDeactivated": true,
32 | "onErrorCaptured": true,
33 | "onMounted": true,
34 | "onRenderTracked": true,
35 | "onRenderTriggered": true,
36 | "onScopeDispose": true,
37 | "onServerPrefetch": true,
38 | "onUnmounted": true,
39 | "onUpdated": true,
40 | "provide": true,
41 | "reactive": true,
42 | "readonly": true,
43 | "ref": true,
44 | "resolveComponent": true,
45 | "shallowReactive": true,
46 | "shallowReadonly": true,
47 | "shallowRef": true,
48 | "toRaw": true,
49 | "toRef": true,
50 | "toRefs": true,
51 | "toValue": true,
52 | "triggerRef": true,
53 | "unref": true,
54 | "useAttrs": true,
55 | "useCssModule": true,
56 | "useCssVars": true,
57 | "useSlots": true,
58 | "watch": true,
59 | "watchEffect": true,
60 | "watchPostEffect": true,
61 | "watchSyncEffect": true,
62 | "ExtractDefaultPropTypes": true,
63 | "ExtractPropTypes": true,
64 | "ExtractPublicPropTypes": true,
65 | "WritableComputedRef": true,
66 | "cloneDeep": true,
67 | "debounce": true,
68 | "onBeforeRouteLeave": true,
69 | "onBeforeRouteUpdate": true,
70 | "throttle": true,
71 | "useLink": true,
72 | "useRoute": true,
73 | "useRouter": true,
74 | "DirectiveBinding": true,
75 | "MaybeRef": true,
76 | "MaybeRefOrGetter": true,
77 | "onWatcherCleanup": true,
78 | "useId": true,
79 | "useModel": true,
80 | "useTemplateRef": true
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/GradientBar.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
29 |
30 |
31 |
86 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/AdvancedControls/components/AdvLightnessBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
Lightness
9 |
15 |
16 |
17 |
18 |
92 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Hue.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
29 |
30 |
31 |
95 |
--------------------------------------------------------------------------------
/lib/src/styles/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'iconfont'; /* Project id 4773720 */
3 | src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAWsAAsAAAAACsQAAAVdAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDVgqIEIcCATYCJAMcCxAABCAFhGcHWRthCciOkcvLsvLHyrH6edy095NgCaJt55kYq4jTiQdo50ig1A/SiYY7D3eOTM0oZ8rOHYJsuObYCxojcBjZFF241uJLlzikRiji0e3/j3uVs/wHcD3LdJt72adobFIwVFxjivcqUOY+wDMMVDfvAwgAKVM5rNYYx4ByPeA6oDdCMIxAXYrWC7cNpLoeMUNEpCqxdN8M6A4igN2w2wDwWv7z6B+ykxQAIwoITjJpWueE8/E+3/hwPsED/AXpECC4PgZABAAHkHpkRnSat8+Hl9LUXE6O/Q1hHAA5KQzx7hsfGDc/Wf6NL5Eg3qclDpKInDJsBMDgRIgz++ehIpBcjoRGrdg1FMQ0gIF4XxrAQXwgDYhA3EwDBHiynAYQ+MangKuKk2ysBJAEUA2gBxByszFiiCjNxMoig0GjCL5TShApJKlLVihkMp1+ETM5tGL59s1bkyzOoJ4J7ky2RpLdlvC6XccMk627YtEH0cdznIzVvShgjoYIzzxGBEsP4rfGdGahLeNHZQGU+XFJJhCN8o8f9wg4rRY+Gg3EYv6eGYlQ3s4fi1UJx3fze04ETu3z7z1pnbfrwQhu+0M3hZHT4xdJ/p2kXNgOQKIyf4ZSBM4hEYXs1whFBUIA8sVl++QL7UQixYKSEA5HkhdFtm1ZoZ+vCYT4oJ0UwskR5O+xhFeu2mY4vGrg9cHgdCHsPz/uLPdMMyghyRL2N7QG8+KTWYWz/nOBM/xpi7CZ3xLY5t+6SC3MmCjV/DKhYJCL+JMsqkDqCZNqoSkNjVwvkPf0kKZptSsWtuYc3bai3nT6tLnr2ZQLg/qmPCthnOVNpu/JD9Z/zzQ1vVilDrWgTo9dUn70qLqxUXX0GNBhOnjKXCUlrjLPnxEnm+8q8ZT92Vt2UWHbjLym3NymvBlvE4c4FaeJ/rZZ0TraGjZbwnuj7cOWvTCm0mA0NqSO3Z86xjiaOby8tbBhocm04E3TwoX1bxAn45TLLVhoanjKWDdiZB1DSN1IRoD1IyK9b6zOXX0jN62BB6/27//dqQ8TNlj3Az2JXjcxu/OHOYpBCjpmn/PMnJPZ29z7PJezEhWSFyUfo7dSZ2dKtn7JjT6Gwm7JeMm1mf1r0z5rc1cpCHflUHUS9b3h+9xb/ZIfkXJNy4yTTEX3kPPV3dPrcti8lnk6pezbwXmSHSpm7mFUK1S1C0+tXdIg1J5EtaP6ud5MnjmvLq+293RKlaJKjreDov4oo+yW9pbsj+eeZ7XFsuYdO2tSx26dN4TVsj35H8MZRsnWsak12k3NWlgKACBxMJ7BcYDE8biONwAAvFm4rZB+DB5Xb3wl1uEsSCRiN46+5uvB2Ycz+s1Wlf4mpT76I5/daxmX+xIHM8j5X1MFOPwvtWKGEJVwAwCSPNY/5pNTwv72RzEAZDgA8D12a324PaVOLIf/jVJXA0aiD+CkBmEIghFEKNkgJlUGpFHqDqbodMGhLAOAYZ4BQNR2AEbpBODUrmEIQgxEkjwBMbX/gGRJiuNRhiVH1cvaONZJ2xfQHkdnhytScfmiU1h3d5vNOxSxsF6fB0N0VnpmN2ViO1jvJnbwzXVnc5yLdnk72+l69WJsW1sn3eXtbGEdXHozx3UVZ2S4xt4k3dHZDjW8WDYclhPNbgGah0OnDq4UnWv/vilYbt3a2HiFGIvVy8c8WpyWJV2mAGOSd4h6Hkm1z1xu2TiKutBcWnGndrT69aystjDeidY1vlYLlgMnXfMa0S7FMgjNJZWn71/Z/iDPAEAKnlOSMIQjAomQGEmQFIjNwseKvQOnx9Ymblt0sDav2DlpYzkWz1ZzS9iNo9nmZgEAAAAA')
4 | format('woff2'),
5 | url('iconfont.woff?t=1739176396764') format('woff'),
6 | url('iconfont.ttf?t=1739176396764') format('truetype');
7 | }
8 |
9 | .iconfont {
10 | font-family: 'iconfont' !important;
11 | font-size: 16px;
12 | font-style: normal;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-osx-font-smoothing: grayscale;
15 | }
16 |
17 | .cpg-xise:before {
18 | content: '\e798';
19 | }
20 |
21 | .cpg-radial:before {
22 | content: '\e61b';
23 | }
24 |
25 | .cpg-linear:before {
26 | content: '\e61c';
27 | }
28 |
29 | .cpg-delete:before {
30 | content: '\e655';
31 | }
32 |
33 | .cpg-deg:before {
34 | content: '\e621';
35 | }
36 |
37 | .cpg-exchage:before {
38 | content: '\eb73';
39 | }
40 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/OperationGradient.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
28 |
29 |
34 |
35 |
°
36 |
37 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
98 |
--------------------------------------------------------------------------------
/lib/tests/enums.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import {
3 | InputType,
4 | GradientType,
5 | Modes,
6 | EventType,
7 | DEFAULT_VALUES,
8 | } from '@/enums'
9 |
10 | describe('枚举定义', () => {
11 | describe('InputType', () => {
12 | it('应该包含正确的输入类型', () => {
13 | expect(InputType.hsl).toBe('HSL')
14 | expect(InputType.rgb).toBe('RGB')
15 | expect(InputType.hsv).toBe('HSV')
16 | expect(InputType.cmyk).toBe('CMYK')
17 | })
18 |
19 | it('应该包含所有预期的输入类型', () => {
20 | const expectedTypes = ['HSL', 'RGB', 'HSV', 'CMYK']
21 | const actualTypes = Object.values(InputType)
22 | expect(actualTypes).toEqual(expectedTypes)
23 | })
24 | })
25 |
26 | describe('GradientType', () => {
27 | it('应该包含正确的渐变类型', () => {
28 | expect(GradientType.linear).toBe('linear')
29 | expect(GradientType.radial).toBe('radial')
30 | })
31 |
32 | it('应该包含所有预期的渐变类型', () => {
33 | const expectedTypes = ['linear', 'radial']
34 | const actualTypes = Object.values(GradientType)
35 | expect(actualTypes).toEqual(expectedTypes)
36 | })
37 | })
38 |
39 | describe('Modes', () => {
40 | it('应该包含正确的模式', () => {
41 | expect(Modes.solid).toBe('solid')
42 | expect(Modes.gradient).toBe('gradient')
43 | })
44 |
45 | it('应该包含所有预期的模式', () => {
46 | const expectedModes = ['solid', 'gradient']
47 | const actualModes = Object.values(Modes)
48 | expect(actualModes).toEqual(expectedModes)
49 | })
50 | })
51 |
52 | describe('EventType', () => {
53 | it('应该包含正确的事件类型', () => {
54 | expect(EventType.change).toBe('change')
55 | expect(EventType.update).toBe('update:value')
56 | })
57 |
58 | it('应该包含所有预期的事件类型', () => {
59 | const expectedEvents = ['change', 'update:value']
60 | const actualEvents = Object.values(EventType)
61 | expect(actualEvents).toEqual(expectedEvents)
62 | })
63 | })
64 |
65 | describe('DEFAULT_VALUES', () => {
66 | it('应该包含正确的默认值', () => {
67 | expect(DEFAULT_VALUES.DEFAULT_COLOR).toBe('rgba(175, 51, 242, 1)')
68 | expect(DEFAULT_VALUES.DEFAULT_WIDTH).toBe(300)
69 | expect(DEFAULT_VALUES.DEFAULT_DEGREES).toBe(90)
70 | expect(DEFAULT_VALUES.MAX_PRESET_COLORS).toBe(18)
71 | expect(DEFAULT_VALUES.MIN_GRADIENT_POINTS).toBe(2)
72 | })
73 |
74 | it('应该包含所有预期的默认值', () => {
75 | const expectedKeys = [
76 | 'DEFAULT_COLOR',
77 | 'DEFAULT_WIDTH',
78 | 'DEFAULT_DEGREES',
79 | 'MAX_PRESET_COLORS',
80 | 'MIN_GRADIENT_POINTS',
81 | ]
82 | const actualKeys = Object.keys(DEFAULT_VALUES)
83 | expect(actualKeys).toEqual(expectedKeys)
84 | })
85 |
86 | it('应该具有正确的类型', () => {
87 | expect(typeof DEFAULT_VALUES.DEFAULT_COLOR).toBe('string')
88 | expect(typeof DEFAULT_VALUES.DEFAULT_WIDTH).toBe('number')
89 | expect(typeof DEFAULT_VALUES.DEFAULT_DEGREES).toBe('number')
90 | expect(typeof DEFAULT_VALUES.MAX_PRESET_COLORS).toBe('number')
91 | expect(typeof DEFAULT_VALUES.MIN_GRADIENT_POINTS).toBe('number')
92 | })
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/lib/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: 格式化工具函数
4 | * @Date: 2024-11-30 21:03:17
5 | * @LastEditTime: 2024-12-07 11:10:04
6 | * @LastEditors: June
7 | */
8 | import { gradientParser } from './gradientParser'
9 | import { config } from '@/constants'
10 | import type { GradientProps } from '@/interfaces'
11 |
12 | const { defaultColor, defaultGradient } = config
13 |
14 | /**
15 | * 将颜色值转换为小写
16 | * @param color 渐变颜色对象
17 | * @returns 小写的颜色值
18 | */
19 | export const low = (color: GradientProps): string => {
20 | return color.value?.toLowerCase() || ''
21 | }
22 |
23 | /**
24 | * 将颜色值转换为大写
25 | * @param color 渐变颜色对象
26 | * @returns 大写的颜色值
27 | */
28 | export const high = (color: GradientProps): string => {
29 | return color.value?.toUpperCase() || ''
30 | }
31 |
32 | /**
33 | * 从颜色值中提取颜色数组
34 | * @param value 颜色值字符串
35 | * @returns 颜色数组
36 | */
37 | export const getColors = (value: string): GradientProps[] => {
38 | if (!value || typeof value !== 'string') {
39 | console.warn('getColors: invalid value provided')
40 | return [{ value: defaultColor }]
41 | }
42 |
43 | const isGradient = value.includes('gradient')
44 |
45 | if (isGradient) {
46 | const isConic = value.includes('conic')
47 | const safeValue = !isConic ? value : defaultGradient
48 |
49 | if (isConic) {
50 | console.warn("Sorry we can't handle conic gradients yet")
51 | }
52 |
53 | try {
54 | const obj = gradientParser(safeValue)
55 | return obj?.colorStops || [{ value: defaultColor }]
56 | } catch (error) {
57 | console.error('Error parsing gradient:', error)
58 | return [{ value: defaultColor }]
59 | }
60 | } else {
61 | const safeValue = value || defaultColor
62 | return [{ value: safeValue }]
63 | }
64 | }
65 |
66 | /**
67 | * 格式化输入值,确保在指定范围内
68 | * @param value 输入值
69 | * @param min 最小值
70 | * @param max 最大值
71 | * @returns 格式化后的值
72 | */
73 | export const formatInputValues = (
74 | value: number,
75 | min: number,
76 | max: number,
77 | ): number => {
78 | if (typeof value !== 'number' || isNaN(value)) {
79 | return min
80 | }
81 |
82 | if (value < min) return min
83 | if (value > max) return max
84 |
85 | return value
86 | }
87 |
88 | /**
89 | * 四舍五入数值
90 | * @param val 数值
91 | * @returns 四舍五入后的数值
92 | */
93 | export const round = (val: number): number => {
94 | if (typeof val !== 'number' || isNaN(val)) {
95 | return 0
96 | }
97 | return Math.round(val)
98 | }
99 |
100 | /**
101 | * 限制数值在指定范围内
102 | * @param value 数值
103 | * @param min 最小值
104 | * @param max 最大值
105 | * @returns 限制后的数值
106 | */
107 | export const clamp = (value: number, min: number, max: number): number => {
108 | return Math.min(Math.max(value, min), max)
109 | }
110 |
111 | /**
112 | * 将百分比值转换为0-1范围
113 | * @param percent 百分比值 (0-100)
114 | * @returns 0-1范围的值
115 | */
116 | export const percentToDecimal = (percent: number): number => {
117 | return clamp(percent, 0, 100) / 100
118 | }
119 |
120 | /**
121 | * 将0-1范围的值转换为百分比
122 | * @param decimal 0-1范围的值
123 | * @returns 百分比值 (0-100)
124 | */
125 | export const decimalToPercent = (decimal: number): number => {
126 | return round(clamp(decimal, 0, 1) * 100)
127 | }
128 |
--------------------------------------------------------------------------------
/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "color-gradient-picker-vue3",
3 | "version": "3.1.3",
4 | "type": "module",
5 | "author": "June",
6 | "description": "A modern color and gradient picker component for Vue 3 with TypeScript support",
7 | "keywords": [
8 | "vue",
9 | "vue.js",
10 | "vue3",
11 | "color-picker",
12 | "gradient-picker",
13 | "color",
14 | "gradient",
15 | "typescript",
16 | "component",
17 | "ui"
18 | ],
19 | "tags": [
20 | "vue",
21 | "vue.js",
22 | "vue3",
23 | "color-picker",
24 | "gradient"
25 | ],
26 | "files": [
27 | "dist",
28 | "index.d.ts",
29 | "types"
30 | ],
31 | "main": "./dist/color-gradient-picker-vue3.umd.js",
32 | "module": "./dist/color-gradient-picker-vue3.es.js",
33 | "types": "./index.d.ts",
34 | "exports": {
35 | ".": {
36 | "types": "./index.d.ts",
37 | "require": "./dist/color-gradient-picker-vue3.umd.js",
38 | "import": "./dist/color-gradient-picker-vue3.es.js"
39 | },
40 | "./dist/style.css": "./dist/color-gradient-picker-vue3.css"
41 | },
42 | "scripts": {
43 | "dev": "vite",
44 | "build": "vue-tsc && vite build",
45 | "preview": "vite preview",
46 | "eslint": "eslint --ext .ts,.tsx,.jsx,.js,.vue --ignore-path .gitignore --fix src",
47 | "prettier": "prettier --write .",
48 | "type-check": "vue-tsc --noEmit",
49 | "lint": "eslint --ext .ts,.tsx,.jsx,.js,.vue src",
50 | "lint:fix": "eslint --ext .ts,.tsx,.jsx,.js,.vue src --fix",
51 | "format": "prettier --write \"*.{ts,tsx,js,jsx,vue,scss,css}\"",
52 | "clean": "rimraf dist",
53 | "prebuild": "npm run clean",
54 | "test": "vitest",
55 | "test:ui": "vitest --ui",
56 | "test:run": "vitest run",
57 | "test:coverage": "vitest run --coverage",
58 | "test:watch": "vitest --watch"
59 | },
60 | "config": {
61 | "commitizen": {
62 | "path": "node_modules/cz-conventional-changelog"
63 | }
64 | },
65 | "license": "MIT",
66 | "homepage": "https://github.com/Qiu-Jun/color-gradient-picker-vue3.git#readme",
67 | "repository": {
68 | "type": "git",
69 | "url": "https://github.com/Qiu-Jun/color-gradient-picker-vue3.git.git"
70 | },
71 | "bugs": {
72 | "url": "https://github.com/Qiu-Jun/color-gradient-picker-vue3.git/issues"
73 | },
74 | "engines": {
75 | "node": ">=16.0.0",
76 | "npm": ">=8.0.0"
77 | },
78 | "peerDependencies": {
79 | "vue": "^3.0.0"
80 | },
81 | "devDependencies": {
82 | "@types/node": "^20.0.0",
83 | "@vitejs/plugin-vue": "^5.0.0",
84 | "@vitejs/plugin-vue-jsx": "^3.0.0",
85 | "@vitest/coverage-v8": "^1.0.0",
86 | "@vitest/ui": "^1.0.0",
87 | "@vue/test-utils": "^2.4.0",
88 | "@vue/tsconfig": "^0.5.0",
89 | "autoprefixer": "^10.4.0",
90 | "eslint": "^8.0.0",
91 | "jsdom": "^23.0.0",
92 | "prettier": "^3.0.0",
93 | "rimraf": "^5.0.0",
94 | "rollup-plugin-visualizer": "^5.9.2",
95 | "sass": "^1.69.0",
96 | "terser": "^5.24.0",
97 | "typescript": "^5.0.0",
98 | "unocss": "^0.58.0",
99 | "unplugin-auto-import": "^0.17.0",
100 | "vite": "^5.0.0",
101 | "vitest": "^1.0.0"
102 | },
103 | "dependencies": {
104 | "html2canvas": "^1.4.1",
105 | "lodash-es": "^4.17.21",
106 | "tinycolor2": "^1.6.0",
107 | "uuid": "^9.0.1"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/lib/tests/README.md:
--------------------------------------------------------------------------------
1 | # 测试文档
2 |
3 | ## 概述
4 |
5 | 本项目使用 Vitest 作为测试框架,提供了完整的单元测试覆盖。
6 |
7 | ## 测试结构
8 |
9 | ```
10 | tests/
11 | ├── setup.ts # 测试设置文件
12 | ├── index.test.ts # 基础测试
13 | ├── enums.test.ts # 枚举测试
14 | ├── interfaces.test.ts # 接口类型测试
15 | ├── components/
16 | │ └── ColorPicker.test.ts # 组件配置测试
17 | └── utils/
18 | ├── color.test.ts # 颜色工具函数测试
19 | ├── format.test.ts # 格式化工具函数测试
20 | └── utils.test.ts # 通用工具函数测试
21 | ```
22 |
23 | ## 运行测试
24 |
25 | ### 开发模式
26 |
27 | ```bash
28 | npm run test
29 | ```
30 |
31 | ### 运行所有测试
32 |
33 | ```bash
34 | npm run test:run
35 | ```
36 |
37 | ### 监听模式
38 |
39 | ```bash
40 | npm run test:watch
41 | ```
42 |
43 | ### 生成覆盖率报告
44 |
45 | ```bash
46 | npm run test:coverage
47 | ```
48 |
49 | ### UI 模式
50 |
51 | ```bash
52 | npm run test:ui
53 | ```
54 |
55 | ## 测试覆盖率
56 |
57 | 当前测试覆盖率:
58 |
59 | - **语句覆盖率**: 16.09%
60 | - **分支覆盖率**: 70.46%
61 | - **函数覆盖率**: 47.27%
62 | - **行覆盖率**: 16.09%
63 |
64 | ### 详细覆盖率
65 |
66 | | 文件 | 语句覆盖率 | 分支覆盖率 | 函数覆盖率 | 行覆盖率 |
67 | | --------------------- | ---------- | ---------- | ---------- | -------- |
68 | | `src/enums/index.ts` | 100% | 100% | 100% | 100% |
69 | | `src/utils/color.ts` | 100% | 95.83% | 100% | 100% |
70 | | `src/utils/format.ts` | 97.63% | 90.9% | 100% | 97.63% |
71 | | `src/utils/utils.ts` | 94.56% | 80% | 100% | 94.56% |
72 |
73 | ## 测试内容
74 |
75 | ### 1. 枚举测试 (`enums.test.ts`)
76 |
77 | - 测试所有枚举值的正确性
78 | - 验证枚举类型的完整性
79 | - 检查默认值配置
80 |
81 | ### 2. 接口测试 (`interfaces.test.ts`)
82 |
83 | - 验证 TypeScript 接口定义
84 | - 测试类型约束
85 | - 检查可选属性
86 |
87 | ### 3. 颜色工具函数测试 (`color.test.ts`)
88 |
89 | - `createGradientStr`: 渐变字符串生成
90 | - `isValidColor`: 颜色值验证
91 | - `formatColor`: 颜色格式化
92 | - `getColorContrast`: 对比度计算
93 |
94 | ### 4. 格式化工具函数测试 (`format.test.ts`)
95 |
96 | - `low/high`: 字符串大小写转换
97 | - `getColors`: 颜色数组提取
98 | - `formatInputValues`: 输入值格式化
99 | - `round/clamp`: 数值处理
100 | - `percentToDecimal/decimalToPercent`: 百分比转换
101 |
102 | ### 5. 通用工具函数测试 (`utils.test.ts`)
103 |
104 | - `safeBounds`: 边界计算
105 | - `getHandleValue`: 句柄值计算
106 | - `computeSquareXY`: 坐标计算
107 | - `computePickerPosition`: 选择器位置
108 | - `isUpperCase`: 大写检测
109 | - `objectToString`: 对象转字符串
110 | - `getColorObj`: 颜色对象处理
111 | - `getIsGradient`: 渐变检测
112 | - `getDetails`: 渐变详情解析
113 |
114 | ### 6. 组件配置测试 (`ColorPicker.test.ts`)
115 |
116 | - 默认值验证
117 | - 属性类型检查
118 | - 事件处理
119 | - v-model 支持
120 |
121 | ## 测试最佳实践
122 |
123 | ### 1. 测试命名
124 |
125 | - 使用描述性的测试名称
126 | - 遵循 "应该..." 的命名模式
127 | - 清晰表达测试意图
128 |
129 | ### 2. 测试结构
130 |
131 | - 使用 `describe` 分组相关测试
132 | - 使用 `beforeEach` 设置测试环境
133 | - 保持测试的独立性
134 |
135 | ### 3. 断言
136 |
137 | - 使用具体的断言方法
138 | - 避免过于复杂的断言
139 | - 提供清晰的错误信息
140 |
141 | ### 4. Mock 使用
142 |
143 | - 合理使用 Mock 避免外部依赖
144 | - 保持 Mock 的简单性
145 | - 确保 Mock 行为的一致性
146 |
147 | ## 持续集成
148 |
149 | 测试已集成到 CI/CD 流程中:
150 |
151 | 1. **代码提交**: 自动运行测试
152 | 2. **覆盖率检查**: 确保覆盖率不低于阈值
153 | 3. **测试报告**: 生成详细的测试报告
154 |
155 | ## 故障排除
156 |
157 | ### 常见问题
158 |
159 | 1. **模块找不到**: 检查路径别名配置
160 | 2. **Mock 不工作**: 确保 Mock 在正确的位置
161 | 3. **测试超时**: 检查异步操作的处理
162 |
163 | ### 调试技巧
164 |
165 | 1. 使用 `console.log` 调试测试
166 | 2. 使用 `--reporter=verbose` 获取详细输出
167 | 3. 使用 `--ui` 模式进行交互式调试
168 |
169 | ## 贡献指南
170 |
171 | 添加新测试时请遵循:
172 |
173 | 1. 为新功能编写测试
174 | 2. 确保测试覆盖率
175 | 3. 遵循现有的测试模式
176 | 4. 更新测试文档
177 |
--------------------------------------------------------------------------------
/lib/vite.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: Vite构建配置文件
4 | * @Date: 2023-04-11 11:17:35
5 | * @LastEditors: June
6 | * @LastEditTime: 2024-12-22 12:46:23
7 | */
8 | import type { ConfigEnv, UserConfigExport } from 'vite'
9 | import vueJsx from '@vitejs/plugin-vue-jsx'
10 | import vue from '@vitejs/plugin-vue'
11 | import * as path from 'path'
12 | import AutoImport from 'unplugin-auto-import/vite'
13 | import UnoCSS from 'unocss/vite'
14 | import autoprefixer from 'autoprefixer'
15 | import { visualizer } from 'rollup-plugin-visualizer'
16 |
17 | export default ({ command }: ConfigEnv): UserConfigExport => {
18 | const isDev = command === 'serve'
19 |
20 | return {
21 | define: {
22 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true',
23 | __VUE_OPTIONS_API__: 'true',
24 | __VUE_PROD_DEVTOOLS__: 'false',
25 | },
26 | plugins: [
27 | vue({
28 | script: {
29 | defineModel: true,
30 | propsDestructure: true,
31 | },
32 | }),
33 | vueJsx(),
34 | UnoCSS(),
35 | AutoImport({
36 | imports: [
37 | 'vue',
38 | 'vue-router',
39 | {
40 | 'lodash-es': ['cloneDeep', 'debounce', 'throttle'],
41 | },
42 | ],
43 | dts: true,
44 | eslintrc: {
45 | enabled: true,
46 | },
47 | }),
48 | visualizer({
49 | open: false,
50 | gzipSize: true,
51 | brotliSize: true,
52 | }),
53 | ],
54 | css: {
55 | postcss: {
56 | plugins: [autoprefixer],
57 | },
58 | preprocessorOptions: {
59 | scss: {
60 | // 移除variables.scss的引用,因为文件不存在
61 | // additionalData: `@import "@/styles/variables.scss";`,
62 | },
63 | },
64 | },
65 | resolve: {
66 | alias: {
67 | '@': path.resolve(__dirname, 'src'),
68 | },
69 | extensions: [
70 | '.ts',
71 | '.tsx',
72 | '.js',
73 | '.mjs',
74 | '.vue',
75 | '.json',
76 | '.less',
77 | '.css',
78 | '.scss',
79 | ],
80 | },
81 | server: {
82 | port: 3000,
83 | host: true,
84 | open: true,
85 | },
86 | build: {
87 | target: 'es2015',
88 | outDir: 'dist',
89 | minify: 'terser',
90 | terserOptions: {
91 | compress: {
92 | // 生产环境时移除console和debugger
93 | drop_console: !isDev,
94 | drop_debugger: !isDev,
95 | pure_funcs: isDev ? [] : ['console.log', 'console.info'],
96 | },
97 | mangle: {
98 | safari10: true,
99 | },
100 | },
101 | lib: {
102 | entry: path.resolve(__dirname, './index.ts'),
103 | name: 'color-gradient-picker-vue3',
104 | fileName: (format) => `color-gradient-picker-vue3.${format}.js`,
105 | formats: ['es', 'umd'],
106 | },
107 | rollupOptions: {
108 | external: ['vue', 'tinycolor2', 'lodash-es'],
109 | output: {
110 | globals: {
111 | vue: 'Vue',
112 | tinycolor2: 'tinycolor',
113 | 'lodash-es': '_',
114 | },
115 | exports: 'named',
116 | assetFileNames: (assetInfo) => {
117 | if (assetInfo.name === 'style.css') {
118 | return 'color-gradient-picker-vue3.css'
119 | }
120 | return assetInfo.name || 'asset'
121 | },
122 | },
123 | },
124 | sourcemap: isDev,
125 | emptyOutDir: true,
126 | },
127 | optimizeDeps: {
128 | include: ['vue', 'tinycolor2', 'lodash-es'],
129 | },
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/lib/src/utils/color.ts:
--------------------------------------------------------------------------------
1 | import { GradientProps, ColorPickerProps } from '@/interfaces'
2 | import { GradientType } from '@/enums'
3 |
4 | /**
5 | * 生成渐变色字符串
6 | * @param newColors 渐变颜色数组
7 | * @param gradientType 渐变类型
8 | * @param colorState 颜色状态对象
9 | * @returns 渐变色字符串
10 | */
11 | export function createGradientStr(
12 | newColors: GradientProps[],
13 | gradientType: GradientType,
14 | colorState: ColorPickerProps,
15 | ): string {
16 | if (!newColors || newColors.length === 0) {
17 | console.warn('createGradientStr: newColors is empty')
18 | return ''
19 | }
20 |
21 | // 按位置排序
22 | const sorted = newColors
23 | .filter((color) => color.left !== undefined)
24 | .sort((a: GradientProps, b: GradientProps) => (a.left || 0) - (b.left || 0))
25 |
26 | if (sorted.length === 0) {
27 | console.warn('createGradientStr: no valid colors with left position')
28 | return ''
29 | }
30 |
31 | // 构建颜色字符串
32 | const colorString = sorted.map((color) => {
33 | const left = Math.max(0, Math.min(100, color.left || 0))
34 | return `${color.value} ${left}%`
35 | })
36 |
37 | // 构建渐变字符串
38 | const degreesStr = colorState.degreesStr || `${colorState.degrees || 90}deg`
39 | const newGrade = `${gradientType}-gradient(${degreesStr}, ${colorString.join(
40 | ', ',
41 | )})`
42 |
43 | return newGrade
44 | }
45 |
46 | /**
47 | * 验证颜色值是否有效
48 | * @param color 颜色值
49 | * @returns 是否有效
50 | */
51 | export function isValidColor(color: string): boolean {
52 | if (!color || typeof color !== 'string') return false
53 |
54 | const trimmedColor = color.trim()
55 |
56 | // 检查是否为渐变字符串
57 | if (trimmedColor.includes('gradient')) {
58 | // 简化的渐变验证:只要包含有效的渐变类型和至少两个颜色就认为是有效的
59 | const gradientTypes = [
60 | 'linear-gradient',
61 | 'radial-gradient',
62 | 'conic-gradient',
63 | ]
64 | const hasValidType = gradientTypes.some((type) =>
65 | trimmedColor.includes(type),
66 | )
67 | if (!hasValidType) return false
68 |
69 | // 检查基本结构:gradient-type(...)
70 | // 使用更宽松的正则表达式,允许嵌套括号
71 | const gradientRegex =
72 | /^(linear-gradient|radial-gradient|conic-gradient)\s*\(.*\)$/i
73 | if (!gradientRegex.test(trimmedColor)) return false
74 |
75 | // 简单验证:至少包含两个颜色值
76 | const colorMatches = trimmedColor.match(
77 | /(#([0-9A-F]{3}){1,2}|rgb\(|rgba\(|hsl\(|hsla\()/gi,
78 | )
79 | return !!(colorMatches && colorMatches.length >= 2)
80 | }
81 |
82 | // 检查是否为有效的CSS颜色值
83 | const colorRegex =
84 | /^(#([0-9A-F]{3}){1,2}|rgb\(\s*(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})\s*\)|rgba\(\s*(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})\s*,\s*(?:0|1|0\.\d+)\s*\)|hsl\(\s*(?:[0-2]?[0-9]?[0-9]|3[0-5][0-9]|360)\s*,\s*(?:100|[1-9]?\d)%\s*,\s*(?:100|[1-9]?\d)%\s*\)|hsla\(\s*(?:[0-2]?[0-9]?[0-9]|3[0-5][0-9]|360)\s*,\s*(?:100|[1-9]?\d)%\s*,\s*(?:100|[1-9]?\d)%\s*,\s*(?:0|1|0\.\d+)\s*\))$/i
85 | return colorRegex.test(trimmedColor)
86 | }
87 |
88 | /**
89 | * 格式化颜色值为小写
90 | * @param color 颜色值
91 | * @returns 格式化后的颜色值
92 | */
93 | export function formatColor(color: string): string {
94 | if (!color) return ''
95 | return color.toLowerCase().trim()
96 | }
97 |
98 | /**
99 | * 获取颜色的对比度
100 | * @param color 颜色值
101 | * @returns 对比度值 (0-1)
102 | */
103 | export function getColorContrast(color: string): number {
104 | // 简单的对比度计算,实际项目中可能需要更复杂的算法
105 | if (!color) return 0
106 |
107 | // 移除透明度,只考虑RGB分量
108 | const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
109 | if (!rgbMatch) return 0
110 |
111 | const [, r, g, b] = rgbMatch
112 | const brightness =
113 | (parseInt(r) * 299 + parseInt(g) * 587 + parseInt(b) * 114) / 1000
114 | return brightness / 255
115 | }
116 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 贡献指南
2 |
3 | ### 介绍
4 |
5 | 感谢你使用 color-gradient-picker-vue3。
6 |
7 | 以下是关于向 color-gradient-picker-vue3 提交反馈或代码的指南。在向 color-gradient-picker-vue3 提交 issue 或者 PR 之前,请先花几分钟时间阅读以下内容。
8 |
9 | ### Issue 规范
10 |
11 | - 遇到问题时,请先确认这个问题是否已经在 issue 中有记录或者已被修复。
12 | - 提 issue 时,请用简短的语言描述遇到的问题,并添加出现问题时的环境和复现步骤。
13 | - 如果在提问前,无法确认怎样的一个问题是好的、更容易被回复的问题,可以读一读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md)。
14 |
15 | ## 参与开发
16 |
17 | - 请先 [fork](https://help.github.com/cn/github/getting-started-with-github/fork-a-repo) 一份组件库源码到自己的 github。
18 | - 使用 `git clone` 将自己 github 上 fork 得到的源码同步到到你的本地
19 | - 请确保基于 `master` 分支进行开发,我们只接受此分支上的代码贡献。
20 |
21 | ### 代码规范
22 |
23 | 在编写代码时,请注意:
24 |
25 | - 确保代码可以通过仓库的 `ESLint` 校验。
26 | - 确保代码格式是规范的,使用 `prettier` 进行代码格式化。
27 |
28 | ## Commit
29 |
30 | 开发之后,在 commit 代码时,commit message 请遵循以下格式:
31 |
32 | > 如不按照此以下格式,`git commit` 可能无法正常工作。
33 |
34 | ```
35 | (Commit 修改项):
36 | // 或者
37 | :
38 | ```
39 |
40 | 例如:
41 |
42 | ```shell script
43 | npm run commit
44 | # 或者
45 | yarn commit
46 |
47 | # 或者
48 | pnpm commit
49 |
50 | ## 执行命令后根据修改类型选择
51 | ```
52 |
53 | ### commit 类型
54 |
55 | ```json
56 | [
57 | {
58 | "type": "feature",
59 | "section": "feature: 增加新功能",
60 | "hidden": false
61 | },
62 | {
63 | "type": "bug",
64 | "section": "🐛 Bug Fixes | Bug 修复",
65 | "hidden": false
66 | },
67 | {
68 | "type": "fix",
69 | "section": "fix: 修复bug",
70 | "hidden": true
71 | },
72 | {
73 | "type": "docs",
74 | "section": "docs: 文档变",
75 | "hidden": false
76 | },
77 | {
78 | "type": "style",
79 | "section": "style: 代码格式(不影响代码运行的变动)",
80 | "hidden": true
81 | },
82 | {
83 | "type": "refactor",
84 | "section": "refactor: 重构(既不是增加feature,也不是修复bug)",
85 | "hidden": true
86 | },
87 | {
88 | "type": "perf",
89 | "section": "perf: 性能优化",
90 | "hidden": true
91 | },
92 | {
93 | "type": "test",
94 | "section": "test: 增加测试",
95 | "hidden": true
96 | },
97 | {
98 | "type": "revert",
99 | "section": "revert: 回退",
100 | "hidden": true
101 | },
102 | {
103 | "type": "build",
104 | "section": "build: 打包",
105 | "hidden": true
106 | },
107 | {
108 | "type": "chore",
109 | "section": "chore: 构建过程或辅助工具的变动(更改配置文件)",
110 | "hidden": true
111 | }
112 | ]
113 | ```
114 |
115 | ### commit 描述
116 |
117 | commit 描述说明了我们本次提交的具体描述,具体内容视情况而定,无固定规范。
118 |
119 | ## 提交 Pull Request
120 |
121 | ### 参考指南
122 |
123 | 如果你是第一次在 GitHub 上提 Pull Request ,可以阅读下面这两篇文章来学习:
124 |
125 | - [第一次参与开源](https://github.com/firstcontributions/first-contributions/blob/main/translations/README.zh-cn.md)
126 | - [如何优雅地在 GitHub 上贡献代码](https://segmentfault.com/a/1190000000736629)
127 |
128 | ### Pull Request 规范
129 |
130 | 在提交 Pull Request 时,请注意:
131 |
132 | - 保持你的 PR 足够小,一个 PR 只解决单个问题或添加单个功能。
133 | - 当新增组件或者修改原有组件时,记得增加或者修改对应的文档、Usage 和暗黑模式样式。
134 | - 在 PR 中请添加合适的描述,并关联相关的 Issue。
135 |
136 | ### Pull Request 流程
137 |
138 | 1. fork 主仓库,如果已经 fork 过,请同步主仓库的最新代码。
139 | 2. 基于 fork 后仓库的 master 分支新建一个分支,比如 `feature/add_button`。
140 | 3. 在新分支上进行开发,开发完成后,提 Pull Request 到主仓库的 main 分支。
141 | 4. Pull Request 会在 Review 通过后被合并到主仓库。
142 | 5. 等待组件库发布新版本。
143 |
144 | ### Pull Request 标题格式
145 |
146 | Pull Request 的标题应该遵循以下格式:
147 |
148 | ```bash
149 | type:commit message
150 | ```
151 |
152 | 示例:
153 |
154 | - docs: fix typo in quickstart
155 | - build: optimize build speed
156 | - fix: incorrect style
157 | - feat: add color prop
158 |
159 | 可选的类型:
160 |
161 | - fix
162 | - feat
163 | - docs
164 | - perf
165 | - test
166 | - types
167 | - style
168 | - build
169 | - chore
170 | - release
171 | - refactor
172 | - breaking change
173 | - revert
174 |
175 | ### 同步最新代码
176 |
177 | 提 Pull Request 前,请依照下面的流程同步主仓库的最新代码:
178 |
179 | ```bash
180 | # 添加主仓库到 remote
181 | git remote add upstream https://github.com/Qiu-Jun/color-gradient-picker-vue3.git
182 |
183 | # 拉取主仓库最新代码
184 | git fetch upstream
185 |
186 | # 切换至 master 分支
187 | git checkout master
188 |
189 | # 合并主仓库代码
190 | git merge upstream/master
191 | ```
192 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/PickerArea.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
35 |
138 |
--------------------------------------------------------------------------------
/lib/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | export {}
7 | declare global {
8 | const EffectScope: typeof import('vue')['EffectScope']
9 | const cloneDeep: typeof import('lodash-es')['cloneDeep']
10 | const computed: typeof import('vue')['computed']
11 | const createApp: typeof import('vue')['createApp']
12 | const customRef: typeof import('vue')['customRef']
13 | const debounce: typeof import('lodash-es')['debounce']
14 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
15 | const defineComponent: typeof import('vue')['defineComponent']
16 | const effectScope: typeof import('vue')['effectScope']
17 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
18 | const getCurrentScope: typeof import('vue')['getCurrentScope']
19 | const h: typeof import('vue')['h']
20 | const inject: typeof import('vue')['inject']
21 | const isProxy: typeof import('vue')['isProxy']
22 | const isReactive: typeof import('vue')['isReactive']
23 | const isReadonly: typeof import('vue')['isReadonly']
24 | const isRef: typeof import('vue')['isRef']
25 | const markRaw: typeof import('vue')['markRaw']
26 | const nextTick: typeof import('vue')['nextTick']
27 | const onActivated: typeof import('vue')['onActivated']
28 | const onBeforeMount: typeof import('vue')['onBeforeMount']
29 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
30 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
31 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
32 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
33 | const onDeactivated: typeof import('vue')['onDeactivated']
34 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
35 | const onMounted: typeof import('vue')['onMounted']
36 | const onRenderTracked: typeof import('vue')['onRenderTracked']
37 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
38 | const onScopeDispose: typeof import('vue')['onScopeDispose']
39 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
40 | const onUnmounted: typeof import('vue')['onUnmounted']
41 | const onUpdated: typeof import('vue')['onUpdated']
42 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
43 | const provide: typeof import('vue')['provide']
44 | const reactive: typeof import('vue')['reactive']
45 | const readonly: typeof import('vue')['readonly']
46 | const ref: typeof import('vue')['ref']
47 | const resolveComponent: typeof import('vue')['resolveComponent']
48 | const shallowReactive: typeof import('vue')['shallowReactive']
49 | const shallowReadonly: typeof import('vue')['shallowReadonly']
50 | const shallowRef: typeof import('vue')['shallowRef']
51 | const throttle: typeof import('lodash-es')['throttle']
52 | const toRaw: typeof import('vue')['toRaw']
53 | const toRef: typeof import('vue')['toRef']
54 | const toRefs: typeof import('vue')['toRefs']
55 | const toValue: typeof import('vue')['toValue']
56 | const triggerRef: typeof import('vue')['triggerRef']
57 | const unref: typeof import('vue')['unref']
58 | const useAttrs: typeof import('vue')['useAttrs']
59 | const useCssModule: typeof import('vue')['useCssModule']
60 | const useCssVars: typeof import('vue')['useCssVars']
61 | const useId: typeof import('vue')['useId']
62 | const useLink: typeof import('vue-router')['useLink']
63 | const useModel: typeof import('vue')['useModel']
64 | const useRoute: typeof import('vue-router')['useRoute']
65 | const useRouter: typeof import('vue-router')['useRouter']
66 | const useSlots: typeof import('vue')['useSlots']
67 | const useTemplateRef: typeof import('vue')['useTemplateRef']
68 | const watch: typeof import('vue')['watch']
69 | const watchEffect: typeof import('vue')['watchEffect']
70 | const watchPostEffect: typeof import('vue')['watchPostEffect']
71 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
72 | }
73 | // for type re-export
74 | declare global {
75 | // @ts-ignore
76 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
77 | import('vue')
78 | }
79 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: June
3 | * @Description: 颜色选择器组件类型声明文件
4 | * @Date: 2023-04-11 12:35:04
5 | * @LastEditors: June
6 | * @LastEditTime: 2024-12-10 13:30:30
7 | */
8 |
9 | declare module 'color-gradient-picker-vue3' {
10 | import { DefineComponent } from 'vue'
11 |
12 | // 主组件类型
13 | export const ColorPicker: DefineComponent<
14 | {
15 | value?: string
16 | width?: number
17 | hideInputs?: boolean
18 | hideOpacity?: boolean
19 | hideGradient?: boolean
20 | presetColors?: string[]
21 | hidePresets?: boolean
22 | showAdvancedSliders?: boolean
23 | inputType?: 'HSL' | 'RGB' | 'HSV' | 'CMYK'
24 | },
25 | {},
26 | any
27 | >
28 |
29 | // 类型定义
30 | export interface ColorPickerProps {
31 | width: number
32 | height?: number
33 | gradientColorsIdx?: number
34 | degrees?: number
35 | degreesStr?: string
36 | gradientColor?: string
37 | value?: string
38 | hideGradient?: boolean
39 | showAdvancedSliders?: boolean
40 | hideInputs?: boolean
41 | hideOpacity?: boolean
42 | hc?: IColorValue
43 | isGradient?: boolean
44 | inputType?: InputType
45 | onChange?: (color: IColor) => void
46 | mode?: IMode
47 | gradientColors?: GradientProps[]
48 | presetColors?: string[]
49 | hidePresets?: boolean
50 | }
51 |
52 | export interface GradientProps {
53 | value: string
54 | index?: number
55 | left?: number
56 | }
57 |
58 | export interface IColorValue {
59 | r: number
60 | g: number
61 | b: number
62 | a: number
63 | h: number
64 | s: number
65 | v: number
66 | }
67 |
68 | export interface IColor {
69 | mode?: IMode
70 | color?: string
71 | angle?: number
72 | degrees?: number
73 | colors?: { color: string; offset: number }[]
74 | gradientType?: string
75 | gradientColors?: { color: string; left: number }[]
76 | [key: string]: any
77 | }
78 |
79 | export interface IProvide extends ColorPickerProps {
80 | value: string
81 | width: number
82 | height: number
83 | hc: IColorValue
84 | }
85 |
86 | export interface IColorPicker {
87 | setValue: (color?: string) => void
88 | setMode: (mode: IMode) => void
89 | updateSelectColor: (value: string) => void
90 | handleGradient: (newColor: string, left?: number) => void
91 | changeColor: (newColor: string) => void
92 | setHcH: (h: number) => void
93 | setInputType: (type: InputType) => void
94 | setLinear: () => void
95 | setRadial: () => void
96 | setDegrees: (val: number) => void
97 | setSelectColorIdx: (idx: number) => void
98 | addPoint: (left: number) => void
99 | deletePoint: (index?: number) => void
100 | }
101 |
102 | export type IMode = 'solid' | 'gradient'
103 |
104 | // 枚举
105 | export enum InputType {
106 | hsl = 'HSL',
107 | rgb = 'RGB',
108 | hsv = 'HSV',
109 | cmyk = 'CMYK',
110 | }
111 |
112 | export enum GradientType {
113 | linear = 'linear',
114 | radial = 'radial',
115 | }
116 |
117 | export enum Modes {
118 | solid = 'solid',
119 | gradient = 'gradient',
120 | }
121 |
122 | export enum EventType {
123 | change = 'change',
124 | update = 'update:value',
125 | }
126 |
127 | export const DEFAULT_VALUES: {
128 | DEFAULT_COLOR: string
129 | DEFAULT_WIDTH: number
130 | DEFAULT_DEGREES: number
131 | MAX_PRESET_COLORS: number
132 | MIN_GRADIENT_POINTS: number
133 | }
134 |
135 | // 工具函数
136 | export function createGradientStr(
137 | newColors: GradientProps[],
138 | gradientType: GradientType,
139 | colorState: ColorPickerProps,
140 | ): string
141 |
142 | export function isValidColor(color: string): boolean
143 | export function formatColor(color: string): string
144 | export function getColorContrast(color: string): number
145 | export function getColors(value: string): GradientProps[]
146 | export function formatInputValues(
147 | value: number,
148 | min: number,
149 | max: number,
150 | ): number
151 | export function round(val: number): number
152 | export function clamp(value: number, min: number, max: number): number
153 | export function percentToDecimal(percent: number): number
154 | export function decimalToPercent(decimal: number): number
155 | }
156 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/components/Operation.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
17 | Solid
18 |
19 |
25 | Gradient
26 |
27 |
28 |
29 |
30 |
85 |
86 |
87 |
88 |
145 |
--------------------------------------------------------------------------------
/lib/tests/components/ColorPicker.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, beforeEach, vi } from 'vitest'
2 | import { InputType, Modes, DEFAULT_VALUES } from '@/enums'
3 |
4 | // Mock dependencies
5 | vi.mock('@/utils/utils', () => ({
6 | getDetails: vi.fn(() => ({ degreeStr: '90deg' })),
7 | getIsGradient: vi.fn(() => false),
8 | }))
9 |
10 | vi.mock('@/utils/format', () => ({
11 | getColors: vi.fn(() => [{ value: '#ff0000' }]),
12 | formatInputValues: vi.fn((val) => val),
13 | low: vi.fn((color) => color.value?.toLowerCase()),
14 | high: vi.fn((color) => color.value?.toUpperCase()),
15 | }))
16 |
17 | vi.mock('@/utils/color', () => ({
18 | createGradientStr: vi.fn(() => 'linear-gradient(90deg, #ff0000, #00ff00)'),
19 | isValidColor: vi.fn(() => true),
20 | }))
21 |
22 | vi.mock('@/constants', () => ({
23 | presetColors: ['#ff0000', '#00ff00', '#0000ff'],
24 | }))
25 |
26 | describe('ColorPicker组件配置', () => {
27 | beforeEach(() => {
28 | vi.clearAllMocks()
29 | })
30 |
31 | it('应该使用正确的默认值', () => {
32 | expect(DEFAULT_VALUES.DEFAULT_COLOR).toBe('rgba(175, 51, 242, 1)')
33 | expect(DEFAULT_VALUES.DEFAULT_WIDTH).toBe(300)
34 | expect(DEFAULT_VALUES.DEFAULT_DEGREES).toBe(90)
35 | expect(DEFAULT_VALUES.MAX_PRESET_COLORS).toBe(18)
36 | expect(DEFAULT_VALUES.MIN_GRADIENT_POINTS).toBe(2)
37 | })
38 |
39 | it('应该支持所有输入类型', () => {
40 | expect(InputType.hsl).toBe('HSL')
41 | expect(InputType.rgb).toBe('RGB')
42 | expect(InputType.hsv).toBe('HSV')
43 | expect(InputType.cmyk).toBe('CMYK')
44 | })
45 |
46 | it('应该支持所有模式', () => {
47 | expect(Modes.solid).toBe('solid')
48 | expect(Modes.gradient).toBe('gradient')
49 | })
50 |
51 | it('应该验证颜色值', () => {
52 | // 测试颜色验证逻辑
53 | const validColors = ['#ff0000', 'rgb(255, 0, 0)', 'rgba(255, 0, 0, 1)']
54 | validColors.forEach((color) => {
55 | expect(typeof color).toBe('string')
56 | expect(color.length).toBeGreaterThan(0)
57 | })
58 | })
59 |
60 | it('应该格式化颜色值', () => {
61 | // 测试颜色格式化逻辑
62 | const color = '#ff0000'
63 | expect(color.toLowerCase()).toBe('#ff0000')
64 | expect(color.toUpperCase()).toBe('#FF0000')
65 | })
66 |
67 | it('应该处理渐变值', () => {
68 | // 测试渐变检测逻辑
69 | const gradientValue = 'linear-gradient(90deg, #ff0000, #00ff00)'
70 | expect(gradientValue.includes('gradient')).toBe(true)
71 | expect(gradientValue.includes('linear')).toBe(true)
72 | })
73 |
74 | it('应该创建渐变字符串', () => {
75 | // 测试渐变字符串创建逻辑
76 | const gradientType = 'linear'
77 | const degrees = '90deg'
78 | const colors = ['#ff0000', '#00ff00']
79 | const expected = `linear-gradient(90deg, #ff0000, #00ff00)`
80 | expect(expected).toContain(gradientType)
81 | expect(expected).toContain(degrees)
82 | })
83 |
84 | it('应该获取渐变详情', () => {
85 | // 测试渐变详情解析逻辑
86 | const gradientValue = 'linear-gradient(90deg, #ff0000, #00ff00)'
87 | const parts = gradientValue.split('(')
88 | expect(parts[0]).toBe('linear-gradient')
89 | expect(parts[1]).toContain('90deg')
90 | })
91 |
92 | it('应该支持预设颜色', () => {
93 | // 测试预设颜色配置
94 | const presetColors = ['#ff0000', '#00ff00', '#0000ff']
95 | expect(presetColors).toContain('#ff0000')
96 | expect(presetColors).toContain('#00ff00')
97 | expect(presetColors).toContain('#0000ff')
98 | })
99 |
100 | it('应该限制预设颜色数量', () => {
101 | const manyColors = Array.from(
102 | { length: 25 },
103 | (_, i) => `#${i.toString(16).padStart(6, '0')}`,
104 | )
105 | expect(manyColors.length).toBe(25)
106 | // 组件内部应该限制为18个
107 | const limitedColors = manyColors.slice(0, DEFAULT_VALUES.MAX_PRESET_COLORS)
108 | expect(limitedColors.length).toBe(18)
109 | })
110 |
111 | it('应该处理组件属性', () => {
112 | const props = {
113 | value: '#ff0000',
114 | width: 300,
115 | hideInputs: false,
116 | hideOpacity: false,
117 | hideGradient: false,
118 | showAdvancedSliders: false,
119 | inputType: InputType.rgb,
120 | presetColors: ['#ff0000', '#00ff00'],
121 | hidePresets: false,
122 | }
123 |
124 | expect(props.value).toBe('#ff0000')
125 | expect(props.width).toBe(300)
126 | expect(props.hideInputs).toBe(false)
127 | expect(props.inputType).toBe(InputType.rgb)
128 | })
129 |
130 | it('应该处理事件', () => {
131 | const events = {
132 | 'update:value': (value: string) => value,
133 | change: (color: any) => color,
134 | }
135 |
136 | expect(typeof events['update:value']).toBe('function')
137 | expect(typeof events.change).toBe('function')
138 | })
139 |
140 | it('应该处理v-model', () => {
141 | const modelValue = '#ff0000'
142 | const updateValue = (value: string) => value
143 |
144 | expect(modelValue).toBe('#ff0000')
145 | expect(typeof updateValue).toBe('function')
146 | })
147 | })
148 |
--------------------------------------------------------------------------------
/lib/tests/interfaces.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import type {
3 | ColorPickerProps,
4 | GradientProps,
5 | IColor,
6 | IColorValue,
7 | IMode,
8 | IProvide,
9 | } from '@/interfaces'
10 | import { InputType, Modes } from '@/enums'
11 |
12 | describe('接口类型定义', () => {
13 | describe('ColorPickerProps', () => {
14 | it('应该具有正确的属性类型', () => {
15 | const props: ColorPickerProps = {
16 | width: 300,
17 | height: 300,
18 | gradientColorsIdx: 0,
19 | degrees: 90,
20 | degreesStr: '90deg',
21 | gradientColor: 'linear-gradient(90deg, #ff0000, #00ff00)',
22 | value: '#ff0000',
23 | hideGradient: false,
24 | showAdvancedSliders: false,
25 | hideInputs: false,
26 | hideOpacity: false,
27 | hc: {
28 | r: 255,
29 | g: 0,
30 | b: 0,
31 | a: 1,
32 | h: 0,
33 | s: 100,
34 | v: 100,
35 | },
36 | isGradient: false,
37 | inputType: InputType.rgb,
38 | onChange: () => {},
39 | mode: Modes.solid,
40 | gradientColors: [],
41 | presetColors: ['#ff0000', '#00ff00'],
42 | hidePresets: false,
43 | }
44 |
45 | expect(props.width).toBe(300)
46 | expect(props.value).toBe('#ff0000')
47 | expect(props.hideGradient).toBe(false)
48 | expect(props.inputType).toBe(InputType.rgb)
49 | expect(props.mode).toBe(Modes.solid)
50 | })
51 | })
52 |
53 | describe('GradientProps', () => {
54 | it('应该具有正确的属性类型', () => {
55 | const gradient: GradientProps = {
56 | value: '#ff0000',
57 | index: 0,
58 | left: 50,
59 | }
60 |
61 | expect(gradient.value).toBe('#ff0000')
62 | expect(gradient.index).toBe(0)
63 | expect(gradient.left).toBe(50)
64 | })
65 |
66 | it('应该支持可选属性', () => {
67 | const gradient: GradientProps = {
68 | value: '#ff0000',
69 | }
70 |
71 | expect(gradient.value).toBe('#ff0000')
72 | expect(gradient.index).toBeUndefined()
73 | expect(gradient.left).toBeUndefined()
74 | })
75 | })
76 |
77 | describe('IColorValue', () => {
78 | it('应该具有正确的颜色值属性', () => {
79 | const colorValue: IColorValue = {
80 | r: 255,
81 | g: 0,
82 | b: 0,
83 | a: 1,
84 | h: 0,
85 | s: 100,
86 | v: 100,
87 | }
88 |
89 | expect(colorValue.r).toBe(255)
90 | expect(colorValue.g).toBe(0)
91 | expect(colorValue.b).toBe(0)
92 | expect(colorValue.a).toBe(1)
93 | expect(colorValue.h).toBe(0)
94 | expect(colorValue.s).toBe(100)
95 | expect(colorValue.v).toBe(100)
96 | })
97 | })
98 |
99 | describe('IColor', () => {
100 | it('应该具有正确的颜色对象属性', () => {
101 | const color: IColor = {
102 | mode: Modes.solid,
103 | color: '#ff0000',
104 | angle: 90,
105 | degrees: 90,
106 | colors: [
107 | { color: '#ff0000', offset: 0 },
108 | { color: '#00ff00', offset: 100 },
109 | ],
110 | gradientType: 'linear',
111 | gradientColors: [
112 | { color: '#ff0000', left: 0 },
113 | { color: '#00ff00', left: 100 },
114 | ],
115 | }
116 |
117 | expect(color.mode).toBe(Modes.solid)
118 | expect(color.color).toBe('#ff0000')
119 | expect(color.angle).toBe(90)
120 | expect(color.degrees).toBe(90)
121 | expect(color.colors).toHaveLength(2)
122 | expect(color.gradientType).toBe('linear')
123 | expect(color.gradientColors).toHaveLength(2)
124 | })
125 |
126 | it('应该支持可选属性', () => {
127 | const color: IColor = {
128 | color: '#ff0000',
129 | }
130 |
131 | expect(color.color).toBe('#ff0000')
132 | expect(color.mode).toBeUndefined()
133 | expect(color.angle).toBeUndefined()
134 | })
135 | })
136 |
137 | describe('IProvide', () => {
138 | it('应该继承ColorPickerProps并添加必需属性', () => {
139 | const provide: IProvide = {
140 | width: 300,
141 | height: 300,
142 | value: '#ff0000',
143 | hc: {
144 | r: 255,
145 | g: 0,
146 | b: 0,
147 | a: 1,
148 | h: 0,
149 | s: 100,
150 | v: 100,
151 | },
152 | }
153 |
154 | expect(provide.width).toBe(300)
155 | expect(provide.height).toBe(300)
156 | expect(provide.value).toBe('#ff0000')
157 | expect(provide.hc.r).toBe(255)
158 | })
159 | })
160 |
161 | describe('IMode', () => {
162 | it('应该是Modes的联合类型', () => {
163 | const solidMode: IMode = Modes.solid
164 | const gradientMode: IMode = Modes.gradient
165 |
166 | expect(solidMode).toBe('solid')
167 | expect(gradientMode).toBe('gradient')
168 | })
169 | })
170 | })
171 |
--------------------------------------------------------------------------------
/lib/README.md:
--------------------------------------------------------------------------------
1 | # Color Gradient Picker Vue3
2 |
3 | 一个现代化的 Vue 3 颜色和渐变选择器组件,支持 TypeScript。
4 |
5 | ## 安装
6 |
7 | ```bash
8 | npm install color-gradient-picker-vue3
9 | # 或
10 | yarn add color-gradient-picker-vue3
11 | # 或
12 | pnpm add color-gradient-picker-vue3
13 | ```
14 |
15 | ## 基本使用
16 |
17 | ```vue
18 |
19 |
20 |
25 |
26 |
27 |
28 |
39 | ```
40 |
41 | ## API
42 |
43 | ### Props
44 |
45 | | 属性 | 类型 | 默认值 | 说明 |
46 | | ------------------- | --------- | ----------------------- | -------------------- |
47 | | value | string | 'rgba(175, 51, 242, 1)' | 当前颜色值 |
48 | | width | number | 320 | 组件宽度(最小 320) |
49 | | hideInputs | boolean | false | 是否隐藏输入框 |
50 | | hideOpacity | boolean | false | 是否隐藏透明度控制 |
51 | | hideGradient | boolean | false | 是否隐藏渐变功能 |
52 | | presetColors | string[] | 预设颜色数组 | 预设颜色 |
53 | | hidePresets | boolean | false | 是否隐藏预设颜色 |
54 | | showAdvancedSliders | boolean | false | 是否显示高级滑块控制 |
55 | | inputType | InputType | 'RGB' | 输入框类型 |
56 |
57 | ### Events
58 |
59 | | 事件名 | 参数 | 说明 |
60 | | ------------ | ------ | ---------- |
61 | | update:value | string | 颜色值更新 |
62 | | change | IColor | 颜色变化 |
63 |
64 | ### 类型定义
65 |
66 | ```typescript
67 | interface IColor {
68 | mode?: 'solid' | 'gradient'
69 | color?: string
70 | angle?: number
71 | degrees?: number
72 | colors?: { color: string; offset: number }[]
73 | gradientType?: string
74 | gradientColors?: { color: string; left: number }[]
75 | }
76 |
77 | interface ColorPickerProps {
78 | width: number
79 | height?: number
80 | gradientColorsIdx?: number
81 | degrees?: number
82 | degreesStr?: string
83 | gradientColor?: string
84 | value?: string
85 | hideGradient?: boolean
86 | showAdvancedSliders?: boolean
87 | hideInputs?: boolean
88 | hideOpacity?: boolean
89 | hc?: IColorValue
90 | isGradient?: boolean
91 | inputType?: InputType
92 | onChange?: (color: IColor) => void
93 | mode?: IMode
94 | gradientColors?: GradientProps[]
95 | presetColors?: string[]
96 | hidePresets?: boolean
97 | }
98 | ```
99 |
100 | ## 高级用法
101 |
102 | ### 渐变模式
103 |
104 | ```vue
105 |
106 |
112 |
113 |
114 |
124 | ```
125 |
126 | ### 自定义预设颜色
127 |
128 | ```vue
129 |
130 |
135 |
136 |
137 |
151 | ```
152 |
153 | ### 隐藏特定功能
154 |
155 | ```vue
156 |
157 |
164 |
165 | ```
166 |
167 | ## 工具函数
168 |
169 | 组件还提供了一些有用的工具函数:
170 |
171 | ```typescript
172 | import {
173 | createGradientStr,
174 | isValidColor,
175 | formatColor,
176 | getColorContrast,
177 | getColors,
178 | formatInputValues,
179 | round,
180 | clamp,
181 | percentToDecimal,
182 | decimalToPercent,
183 | } from 'color-gradient-picker-vue3'
184 |
185 | // 验证颜色值
186 | const isValid = isValidColor('#ff0000') // true
187 |
188 | // 格式化颜色值
189 | const formatted = formatColor('RGB(255, 0, 0)') // 'rgb(255, 0, 0)'
190 |
191 | // 获取颜色对比度
192 | const contrast = getColorContrast('#ffffff') // 1.0
193 |
194 | // 限制数值范围
195 | const clamped = clamp(150, 0, 100) // 100
196 | ```
197 |
198 | ## 开发
199 |
200 | ```bash
201 | # 安装依赖
202 | npm install
203 |
204 | # 开发模式
205 | npm run dev
206 |
207 | # 构建
208 | npm run build
209 |
210 | # 类型检查
211 | npm run type-check
212 |
213 | # 代码格式化
214 | npm run format
215 |
216 | # 代码检查
217 | npm run lint
218 | ```
219 |
220 | ## 许可证
221 |
222 | MIT License
223 |
224 | ## 贡献
225 |
226 | 欢迎提交 Issue 和 Pull Request!
227 |
--------------------------------------------------------------------------------
/lib/tests/utils/format.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, beforeEach, vi } from 'vitest'
2 | import {
3 | low,
4 | high,
5 | getColors,
6 | formatInputValues,
7 | round,
8 | clamp,
9 | percentToDecimal,
10 | decimalToPercent,
11 | } from '@/utils/format'
12 | import type { GradientProps } from '@/interfaces'
13 |
14 | // Mock gradientParser
15 | vi.mock('@/utils/gradientParser', () => ({
16 | gradientParser: vi.fn((value) => {
17 | if (value.includes('linear-gradient')) {
18 | return {
19 | colorStops: [
20 | { value: '#ff0000', left: 0 },
21 | { value: '#00ff00', left: 100 },
22 | ],
23 | }
24 | }
25 | if (value.includes('conic-gradient')) {
26 | return { colorStops: [] }
27 | }
28 | // 对于defaultGradient,返回空数组,这样会使用defaultColor
29 | return { colorStops: [] }
30 | }),
31 | }))
32 |
33 | // Mock config
34 | vi.mock('@/constants', () => ({
35 | config: {
36 | defaultColor: 'rgba(175, 51, 242, 1)',
37 | defaultGradient: 'linear-gradient(90deg, #ff0000 0%, #00ff00 100%)',
38 | },
39 | }))
40 |
41 | describe('格式化工具函数', () => {
42 | beforeEach(() => {
43 | vi.clearAllMocks()
44 | })
45 |
46 | describe('low', () => {
47 | it('应该将颜色值转换为小写', () => {
48 | const color: GradientProps = { value: '#FF0000' }
49 | expect(low(color)).toBe('#ff0000')
50 | })
51 |
52 | it('应该处理空值', () => {
53 | const color: GradientProps = { value: '' }
54 | expect(low(color)).toBe('')
55 | })
56 |
57 | it('应该处理undefined值', () => {
58 | const color: GradientProps = { value: undefined as any }
59 | expect(low(color)).toBe('')
60 | })
61 | })
62 |
63 | describe('high', () => {
64 | it('应该将颜色值转换为大写', () => {
65 | const color: GradientProps = { value: '#ff0000' }
66 | expect(high(color)).toBe('#FF0000')
67 | })
68 |
69 | it('应该处理空值', () => {
70 | const color: GradientProps = { value: '' }
71 | expect(high(color)).toBe('')
72 | })
73 |
74 | it('应该处理undefined值', () => {
75 | const color: GradientProps = { value: undefined as any }
76 | expect(high(color)).toBe('')
77 | })
78 | })
79 |
80 | describe('getColors', () => {
81 | it('应该从纯色值中提取颜色', () => {
82 | const result = getColors('#ff0000')
83 | expect(result).toEqual([{ value: '#ff0000' }])
84 | })
85 |
86 | it('应该从渐变值中提取颜色', () => {
87 | const result = getColors(
88 | 'linear-gradient(90deg, #ff0000 0%, #00ff00 100%)',
89 | )
90 | expect(result).toEqual([
91 | { value: '#ff0000', left: 0 },
92 | { value: '#00ff00', left: 100 },
93 | ])
94 | })
95 |
96 | it('应该处理无效的输入值', () => {
97 | const result = getColors('')
98 | expect(result).toEqual([{ value: 'rgba(175, 51, 242, 1)' }])
99 | })
100 |
101 | it('应该处理非字符串输入', () => {
102 | const result = getColors(null as any)
103 | expect(result).toEqual([{ value: 'rgba(175, 51, 242, 1)' }])
104 | })
105 |
106 | it('应该处理conic渐变', () => {
107 | const result = getColors('conic-gradient(red, blue)')
108 | // conic渐变会被转换为defaultGradient,然后解析为线性渐变
109 | expect(result).toEqual([
110 | { value: '#ff0000', left: 0 },
111 | { value: '#00ff00', left: 100 },
112 | ])
113 | })
114 | })
115 |
116 | describe('formatInputValues', () => {
117 | it('应该限制值在指定范围内', () => {
118 | expect(formatInputValues(50, 0, 100)).toBe(50)
119 | expect(formatInputValues(-10, 0, 100)).toBe(0)
120 | expect(formatInputValues(150, 0, 100)).toBe(100)
121 | })
122 |
123 | it('应该处理NaN值', () => {
124 | expect(formatInputValues(NaN, 0, 100)).toBe(0)
125 | })
126 |
127 | it('应该处理非数字值', () => {
128 | expect(formatInputValues('invalid' as any, 0, 100)).toBe(0)
129 | })
130 | })
131 |
132 | describe('round', () => {
133 | it('应该四舍五入数值', () => {
134 | expect(round(3.4)).toBe(3)
135 | expect(round(3.5)).toBe(4)
136 | expect(round(3.6)).toBe(4)
137 | })
138 |
139 | it('应该处理NaN值', () => {
140 | expect(round(NaN)).toBe(0)
141 | })
142 |
143 | it('应该处理非数字值', () => {
144 | expect(round('invalid' as any)).toBe(0)
145 | })
146 | })
147 |
148 | describe('clamp', () => {
149 | it('应该限制值在指定范围内', () => {
150 | expect(clamp(50, 0, 100)).toBe(50)
151 | expect(clamp(-10, 0, 100)).toBe(0)
152 | expect(clamp(150, 0, 100)).toBe(100)
153 | })
154 |
155 | it('应该处理边界值', () => {
156 | expect(clamp(0, 0, 100)).toBe(0)
157 | expect(clamp(100, 0, 100)).toBe(100)
158 | })
159 | })
160 |
161 | describe('percentToDecimal', () => {
162 | it('应该将百分比转换为小数', () => {
163 | expect(percentToDecimal(0)).toBe(0)
164 | expect(percentToDecimal(50)).toBe(0.5)
165 | expect(percentToDecimal(100)).toBe(1)
166 | })
167 |
168 | it('应该限制值在0-100范围内', () => {
169 | expect(percentToDecimal(-10)).toBe(0)
170 | expect(percentToDecimal(150)).toBe(1)
171 | })
172 | })
173 |
174 | describe('decimalToPercent', () => {
175 | it('应该将小数转换为百分比', () => {
176 | expect(decimalToPercent(0)).toBe(0)
177 | expect(decimalToPercent(0.5)).toBe(50)
178 | expect(decimalToPercent(1)).toBe(100)
179 | })
180 |
181 | it('应该限制值在0-1范围内', () => {
182 | expect(decimalToPercent(-0.1)).toBe(0)
183 | expect(decimalToPercent(1.5)).toBe(100)
184 | })
185 |
186 | it('应该四舍五入结果', () => {
187 | expect(decimalToPercent(0.333)).toBe(33)
188 | expect(decimalToPercent(0.666)).toBe(67)
189 | })
190 | })
191 | })
192 |
--------------------------------------------------------------------------------
/lib/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import { formatInputValues } from './format'
2 | import { config } from '@/constants'
3 | import type { GradientProps } from '@/interfaces'
4 |
5 | const { barSize, crossSize } = config
6 |
7 | export const safeBounds = (e: any) => {
8 | const client = e.target.parentNode.getBoundingClientRect()
9 | const className = e.target.className
10 | const adjuster = className === 'c-resize ps-rl' ? 15 : 0
11 | return {
12 | offsetLeft: client?.x + adjuster,
13 | offsetTop: client?.y,
14 | clientWidth: client?.width,
15 | clientHeight: client?.height,
16 | }
17 | }
18 |
19 | export function getHandleValue(e: any) {
20 | const { offsetLeft, clientWidth } = safeBounds(e)
21 | const pos = e.clientX - offsetLeft - barSize / 2
22 | const adjuster = clientWidth - 18
23 | const bounded = formatInputValues(pos, 0, adjuster)
24 | return Math.round(bounded / (adjuster / 100))
25 | }
26 |
27 | export function computeSquareXY(
28 | s: number,
29 | v: number,
30 | squareWidth: number,
31 | squareHeight: number,
32 | ) {
33 | const x = s * squareWidth - crossSize / 2
34 | const y = ((100 - v) / 100) * squareHeight - crossSize / 2
35 | return [x, y]
36 | }
37 |
38 | const getClientXY = (e: any) => {
39 | if (e.clientX) {
40 | return { clientX: e.clientX, clientY: e.clientY }
41 | } else {
42 | const touch = e.touches[0] || {}
43 | return { clientX: touch.clientX, clientY: touch.clientY }
44 | }
45 | }
46 |
47 | export function computePickerPosition(e: any) {
48 | const { offsetLeft, offsetTop, clientWidth, clientHeight } = safeBounds(e)
49 | const { clientX, clientY } = getClientXY(e)
50 |
51 | const getX = () => {
52 | const xPos = clientX - offsetLeft - crossSize / 2
53 | return formatInputValues(xPos, -9, clientWidth - 10)
54 | }
55 | const getY = () => {
56 | const yPos = clientY - offsetTop - crossSize / 2
57 | return formatInputValues(yPos, -9, clientHeight - 10)
58 | }
59 |
60 | return [getX(), getY()]
61 | }
62 |
63 | // export const getGradientType = (value: string) => {
64 | // return value?.split('(')[0]
65 | // }
66 |
67 | export const isUpperCase = (str: string) => {
68 | if (!str || typeof str !== 'string') return false
69 | // 检查字符串中是否包含大写字母
70 | return /[A-Z]/.test(str)
71 | }
72 |
73 | // export const compareGradients = (g1: string, g2: string) => {
74 | // const ng1 = g1?.toLowerCase()?.replaceAll(' ', '')
75 | // const ng2 = g2?.toLowerCase()?.replaceAll(' ', '')
76 | // if (ng1 === ng2) {
77 | // return true
78 | // } else {
79 | // return false
80 | // }
81 | // }
82 |
83 | const convertShortHandDeg = (dir: any) => {
84 | if (dir === 'to top') {
85 | return 0
86 | } else if (dir === 'to bottom') {
87 | return 180
88 | } else if (dir === 'to left') {
89 | return 270
90 | } else if (dir === 'to right') {
91 | return 90
92 | } else if (dir === 'to top right') {
93 | return 45
94 | } else if (dir === 'to bottom right') {
95 | return 135
96 | } else if (dir === 'to bottom left') {
97 | return 225
98 | } else if (dir === 'to top left') {
99 | return 315
100 | } else {
101 | const safeDir = dir || 0
102 | const parsed = parseInt(safeDir)
103 | return isNaN(parsed) ? 0 : parsed
104 | }
105 | }
106 |
107 | export const objectToString = (value: any) => {
108 | if (typeof value === 'string') {
109 | return value
110 | } else {
111 | if (value?.type?.includes('gradient')) {
112 | const sorted = value?.colorStops?.sort(
113 | (a: any, b: any) => a?.left - b?.left,
114 | )
115 | const string = sorted
116 | ?.map((c: any) => `${c?.value} ${c?.left}%`)
117 | ?.join(', ')
118 | const type = value?.type
119 | const degs = convertShortHandDeg(value?.orientation?.value)
120 | const gradientStr = type === 'linear-gradient' ? `${degs}deg` : 'circle'
121 | return `${type}(${gradientStr}, ${string})`
122 | } else {
123 | const color = value?.colorStops[0]?.value || 'rgba(175, 51, 242, 1)'
124 | return color
125 | }
126 | }
127 | }
128 |
129 | export const getColorObj = (colors: GradientProps[]) => {
130 | if (!colors || colors.length === 0) {
131 | return {
132 | currentColor: config?.defaultGradient,
133 | selectedColor: 0,
134 | currentLeft: 0,
135 | }
136 | }
137 |
138 | const idxCols = colors.map((c: GradientProps, i: number) => ({
139 | ...c,
140 | index: i,
141 | }))
142 |
143 | const upperObj = idxCols.find((c: GradientProps) => isUpperCase(c.value))
144 | const ccObj = upperObj || idxCols[0]
145 |
146 | return {
147 | currentColor: ccObj?.value || config?.defaultGradient,
148 | selectedColor: ccObj?.index || 0,
149 | currentLeft: ccObj?.left || 0,
150 | }
151 | }
152 |
153 | const getDegrees = (value: string) => {
154 | if (!value || typeof value !== 'string') return 0
155 | const s1 = value.split(',')[0]
156 | const s2 = s1?.split('(')[1]?.replace('deg', '')
157 | return convertShortHandDeg(s2)
158 | }
159 |
160 | export const getIsGradient = (value: string) => {
161 | if (!value || typeof value !== 'string') return false
162 | return value.includes('gradient')
163 | }
164 |
165 | export const getDetails = (value: string) => {
166 | if (!value || typeof value !== 'string') {
167 | return {
168 | degrees: 0,
169 | degreeStr: '0deg',
170 | gradientType: '',
171 | }
172 | }
173 |
174 | const gradientType = value.split('(')[0]
175 | const degrees = getDegrees(value)
176 | const degreeStr =
177 | gradientType === 'linear-gradient' ? `${degrees}deg` : 'circle'
178 |
179 | return {
180 | degrees,
181 | degreeStr,
182 | gradientType,
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/lib/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import url(./iconfont.css);
2 |
3 | .cpg-box {
4 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.25);
5 | min-width: 320px !important;
6 | @apply box-border rounded-8px bg-#fff relative p-8px z-100 text-#323136;
7 |
8 | .cpg-picker-wrap {
9 | @apply rounded-6px overflow-hidden;
10 | .cpg-picker-area {
11 | @apply rounded-6px overflow-hidden;
12 | }
13 | }
14 |
15 | .cpg-gradient-controls-wrapper {
16 | @apply bg-#e9e9f5 rounded-4px box-border p-4px text-12px mt-12px;
17 | }
18 |
19 | .cpg-gradientBar-warp {
20 | @apply h-14px mt-14px relative;
21 | .cpg-gradientBar {
22 | @apply h-full rounded-10px;
23 | }
24 | }
25 |
26 | .cpg-hue-wrap {
27 | @apply h-14px mt-14px relative;
28 | .cpg-hue-colors {
29 | @apply w-full relative rounded-14px vertical-top;
30 | }
31 | }
32 |
33 | .cpg-inputs-wrap {
34 | @apply mt-14px box-border select-none flex justify-between items-center overflow-hidden;
35 | .cpg-inputItem-wrap {
36 | @apply text-center text-12px;
37 | .cpg-input {
38 | @apply border-1px border-solid border-#bebebe rounded-6px box-border text-#000 h-32px font-400 outline-none p-2px w-full text-center;
39 | }
40 | .cpg-input-label {
41 | @apply text-#565656 font-700;
42 | }
43 | }
44 | }
45 |
46 | .cpg-opacity-wrap {
47 | @apply w-full h-14px mt-14px relative box-border;
48 | .cpg-opacity-bar {
49 | background: linear-gradient(
50 | 45deg,
51 | rgba(0, 0, 0, 0.18) 25%,
52 | transparent 0,
53 | transparent 75%,
54 | rgba(0, 0, 0, 0.18) 0,
55 | rgba(0, 0, 0, 0.18) 0
56 | ),
57 | linear-gradient(
58 | 45deg,
59 | rgba(0, 0, 0, 0.18) 25%,
60 | transparent 0,
61 | transparent 75%,
62 | rgba(0, 0, 0, 0.18) 0,
63 | rgba(0, 0, 0, 0.18) 0
64 | ),
65 | #fff;
66 | background-clip: border-box, border-box;
67 | background-origin: padding-box, padding-box;
68 | background-position: 0 0, 7px 7px;
69 | background-repeat: repeat, repeat;
70 | background-size: 14px 14px, 14px 14px;
71 | transition: none;
72 | box-shadow: none;
73 | text-shadow: none;
74 | transform: scaleX(1) scaleY(1) scaleZ(1);
75 | transform-origin: 0 0 0;
76 | @apply w-full h-14px rounded-10px;
77 | }
78 | .cpg-opacity-color {
79 | @apply box-border absolute left-0 top-0 w-full h-14px rounded-10px overflow-hidden;
80 | }
81 | }
82 |
83 | .cpg-controls-wrapper {
84 | @apply flex justify-between items-center mt-4px;
85 | .cpg-controls-item {
86 | box-shadow: 1px 1px 3px transparent;
87 | @apply f-center text-12px text-#565656 select-none box-border bg-#e9e9f5 rounded-4px p-4px;
88 | .cpg-controls-item-btn {
89 | transition: all 0.16s ease;
90 | @apply relative h-24px f-center px-8px rounded-4px;
91 | .cpg-controls-inputType {
92 | box-shadow: 1px 1px 14px 1px rgba(0, 0, 0, 0.25);
93 | transition: opacity 120ms linear, visibility linear, z-index linear;
94 | @apply box-border absolute -right-2px top-34px p-5px rounded-6px bg-#e9e9f5 z-1000 opacity-100 visible;
95 | .cpg-control-inputType-item {
96 | transition: all 0.16s ease;
97 | @apply f-center text-12px font-700 leading-1 h-24px px-8px rounded-4px;
98 | }
99 | .cpg-control-inputType-item-active {
100 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
101 | @apply bg-#fff text-#568cf5;
102 | }
103 | }
104 | .cpg-controls-hideInputType {
105 | transition: opacity 150ms linear 50ms, visibility 100ms linear 150ms,
106 | z-index 100ms linear 150ms;
107 | @apply visible-hidden opacity-0 -z-100;
108 | }
109 | }
110 | }
111 | .cpg-control-active {
112 | @apply bg-#fff text-#568cf5;
113 | }
114 | }
115 |
116 | .cpg-gradient-controls-wrapper {
117 | @apply w-full flex justify-between items-center h-28px;
118 | .cpg-deg-input {
119 | @apply bg-transparent border-none text-#323136 text-12px font-500 h-24px outline-none w-28px;
120 | }
121 | .cpg-gradient-btn {
122 | @apply box-border rounded-4px f-center px-8px py-4px cursor-pointer;
123 | }
124 | .cpg-gradient-btn-active {
125 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
126 | transition: all 0.16s ease;
127 | @apply bg-#fff text-#568cf5;
128 | }
129 | }
130 |
131 | .cpg-preview-wrap {
132 | @apply mt-14px flex w-full justify-between items-center select-none;
133 | .cpg-preview-color {
134 | @apply w-50px h-50px rounded-6px shrink-0 box-border;
135 | }
136 | .cpg-preview-presetColor {
137 | @apply h-50px flex flex-wrap flex-1 justify-start ml-12px;
138 | .cpg-preview-presetItem {
139 | @apply w-[10.2%] h-24px line-block rounded-4px ml-2px mb-2px box-border cursor-pointer;
140 | }
141 | }
142 | }
143 |
144 | .cpg-advance-wrap {
145 | transition: 120ms linear;
146 | @apply w-full h-80px mt-12px select-none;
147 | .cpg-advance-item {
148 | @apply w-full mt-8px box-border relative;
149 | .cpg-advance-text {
150 | text-shadow: rgba(0, 0, 0, 0.6) 1px 1px 1px;
151 | @apply absolute left-50% top-50% -translate-y-50% -translate-x-50% z-1 text-center text-#fff text-12px;
152 | }
153 | .cpg-advance-canvas {
154 | @apply relative rounded-14px;
155 | }
156 | }
157 | }
158 |
159 | // pointer
160 | .cpg-pointer {
161 | transition: all 10ms linear;
162 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
163 | @apply w-18px h-18px absolute left-0 -top-2px box-border border-2px border-solid border-#fff z-10 rounded-50%;
164 | }
165 | .cpg-pointer-centerPoint {
166 | &::after {
167 | content: '';
168 | @apply inline-block absolute left-50% top-50% -translate-x-50% -translate-y-50% w-5px h-5px rounded-50% bg-#fff;
169 | }
170 | }
171 |
172 | .cpg-cursor-pointer {
173 | cursor: pointer;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/lib/tests/utils/color.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import {
3 | createGradientStr,
4 | isValidColor,
5 | formatColor,
6 | getColorContrast,
7 | } from '../../src/utils/color'
8 | import { GradientType } from '../../src/enums'
9 | import type { GradientProps, ColorPickerProps } from '../../src/interfaces'
10 |
11 | describe('Color Utils', () => {
12 | describe('createGradientStr', () => {
13 | it('should create linear gradient string', () => {
14 | const colors: GradientProps[] = [
15 | { value: '#ff0000', left: 0 },
16 | { value: '#00ff00', left: 100 },
17 | ]
18 | const colorState: ColorPickerProps = {
19 | degrees: 90,
20 | degreesStr: '90deg',
21 | width: 300,
22 | }
23 |
24 | const result = createGradientStr(colors, GradientType.linear, colorState)
25 | expect(result).toBe('linear-gradient(90deg, #ff0000 0%, #00ff00 100%)')
26 | })
27 |
28 | it('should handle empty colors array', () => {
29 | const colors: GradientProps[] = []
30 | const colorState: ColorPickerProps = { degrees: 90, width: 300 }
31 |
32 | const result = createGradientStr(colors, GradientType.linear, colorState)
33 | expect(result).toBe('')
34 | })
35 |
36 | it('should filter and sort colors by left position', () => {
37 | const colors: GradientProps[] = [
38 | { value: '#00ff00', left: 100 },
39 | { value: '#ff0000', left: 0 },
40 | { value: '#0000ff', left: 50 },
41 | ]
42 | const colorState: ColorPickerProps = { degrees: 90, width: 300 }
43 |
44 | const result = createGradientStr(colors, GradientType.linear, colorState)
45 | expect(result).toBe(
46 | 'linear-gradient(90deg, #ff0000 0%, #0000ff 50%, #00ff00 100%)',
47 | )
48 | })
49 | })
50 |
51 | describe('isValidColor', () => {
52 | describe('Single Colors', () => {
53 | it('should validate hex colors', () => {
54 | expect(isValidColor('#ff0000')).toBe(true)
55 | expect(isValidColor('#f00')).toBe(true)
56 | expect(isValidColor('#gggggg')).toBe(false)
57 | })
58 |
59 | it('should validate rgb colors', () => {
60 | expect(isValidColor('rgb(255, 0, 0)')).toBe(true)
61 | expect(isValidColor('rgb(0, 255, 0)')).toBe(true)
62 | expect(isValidColor('rgb(300, 0, 0)')).toBe(false)
63 | })
64 |
65 | it('should validate rgba colors', () => {
66 | expect(isValidColor('rgba(255, 0, 0, 0.5)')).toBe(true)
67 | expect(isValidColor('rgba(0, 255, 0, 1)')).toBe(true)
68 | expect(isValidColor('rgba(255, 0, 0, 1.5)')).toBe(false)
69 | })
70 |
71 | it('should validate hsl colors', () => {
72 | expect(isValidColor('hsl(0, 100%, 50%)')).toBe(true)
73 | expect(isValidColor('hsl(120, 50%, 75%)')).toBe(true)
74 | expect(isValidColor('hsl(400, 100%, 50%)')).toBe(false)
75 | })
76 |
77 | it('should validate hsla colors', () => {
78 | expect(isValidColor('hsla(0, 100%, 50%, 0.5)')).toBe(true)
79 | expect(isValidColor('hsla(120, 50%, 75%, 1)')).toBe(true)
80 | expect(isValidColor('hsla(0, 100%, 50%, 1.5)')).toBe(false)
81 | })
82 |
83 | it('should reject invalid colors', () => {
84 | expect(isValidColor('invalid-color')).toBe(false)
85 | expect(isValidColor('')).toBe(false)
86 | expect(isValidColor(null as any)).toBe(false)
87 | expect(isValidColor(undefined as any)).toBe(false)
88 | })
89 | })
90 |
91 | describe('Gradient Colors', () => {
92 | it('should validate linear gradients', () => {
93 | expect(
94 | isValidColor(
95 | 'linear-gradient(90deg, rgb(245, 66, 245) 0%, rgb(0, 0, 255) 100%)',
96 | ),
97 | ).toBe(true)
98 | expect(
99 | isValidColor('linear-gradient(to right, #ff0000 0%, #00ff00 100%)'),
100 | ).toBe(true)
101 | expect(
102 | isValidColor(
103 | 'linear-gradient(45deg, rgba(255, 0, 0, 0.5) 0%, rgba(0, 255, 0, 0.8) 100%)',
104 | ),
105 | ).toBe(true)
106 | })
107 |
108 | it('should validate radial gradients', () => {
109 | expect(
110 | isValidColor(
111 | 'radial-gradient(circle, #ff0000 0%, #00ff00 50%, #0000ff 100%)',
112 | ),
113 | ).toBe(true)
114 | expect(
115 | isValidColor(
116 | 'radial-gradient(ellipse, rgb(255, 0, 0) 0%, rgb(0, 255, 0) 100%)',
117 | ),
118 | ).toBe(true)
119 | })
120 |
121 | it('should validate conic gradients', () => {
122 | expect(
123 | isValidColor('conic-gradient(from 0deg, #ff0000, #00ff00, #0000ff)'),
124 | ).toBe(true)
125 | expect(
126 | isValidColor(
127 | 'conic-gradient(from 45deg, rgb(255, 0, 0) 0deg, rgb(0, 255, 0) 180deg)',
128 | ),
129 | ).toBe(true)
130 | })
131 |
132 | it('should reject invalid gradients', () => {
133 | expect(
134 | isValidColor(
135 | 'invalid-gradient(90deg, rgb(245, 66, 245) 0%, rgb(0, 0, 255) 100%)',
136 | ),
137 | ).toBe(false)
138 | expect(
139 | isValidColor(
140 | 'linear-gradient(90deg, invalid-color 0%, rgb(0, 0, 255) 100%)',
141 | ),
142 | ).toBe(false)
143 | expect(
144 | isValidColor('linear-gradient(90deg, rgb(245, 66, 245) 0%)'),
145 | ).toBe(false) // 只有一个颜色停止点
146 | expect(isValidColor('linear-gradient(90deg)')).toBe(false) // 没有颜色停止点
147 | })
148 |
149 | it('should handle edge cases', () => {
150 | expect(
151 | isValidColor(
152 | 'linear-gradient(90deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)',
153 | ),
154 | ).toBe(true)
155 | expect(isValidColor('linear-gradient(90deg, #ff0000, #00ff00)')).toBe(
156 | true,
157 | ) // 没有位置
158 | expect(
159 | isValidColor(
160 | 'linear-gradient(90deg, #ff0000 0%, #00ff00 50%, #0000ff 100%, #ffff00 150%)',
161 | ),
162 | ).toBe(true) // 超过100%的位置
163 | })
164 | })
165 | })
166 |
167 | describe('formatColor', () => {
168 | it('should format color to lowercase', () => {
169 | expect(formatColor('#FF0000')).toBe('#ff0000')
170 | expect(formatColor('RGB(255, 0, 0)')).toBe('rgb(255, 0, 0)')
171 | expect(formatColor(' #00FF00 ')).toBe('#00ff00')
172 | })
173 |
174 | it('should handle empty input', () => {
175 | expect(formatColor('')).toBe('')
176 | expect(formatColor(null as any)).toBe('')
177 | expect(formatColor(undefined as any)).toBe('')
178 | })
179 | })
180 |
181 | describe('getColorContrast', () => {
182 | it('should calculate contrast for rgb colors', () => {
183 | expect(getColorContrast('rgb(255, 255, 255)')).toBeCloseTo(1, 2) // 白色
184 | expect(getColorContrast('rgb(0, 0, 0)')).toBeCloseTo(0, 2) // 黑色
185 | expect(getColorContrast('rgb(128, 128, 128)')).toBeCloseTo(0.5, 1) // 灰色
186 | })
187 |
188 | it('should handle rgba colors', () => {
189 | expect(getColorContrast('rgba(255, 255, 255, 0.5)')).toBeCloseTo(1, 2)
190 | expect(getColorContrast('rgba(0, 0, 0, 0.8)')).toBeCloseTo(0, 2)
191 | })
192 |
193 | it('should return default for invalid colors', () => {
194 | expect(getColorContrast('invalid-color')).toBe(0)
195 | expect(getColorContrast('')).toBe(0)
196 | expect(getColorContrast(null as any)).toBe(0)
197 | })
198 | })
199 | })
200 |
--------------------------------------------------------------------------------
/lib/tests/utils/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, beforeEach, vi } from 'vitest'
2 | import {
3 | safeBounds,
4 | getHandleValue,
5 | computeSquareXY,
6 | computePickerPosition,
7 | isUpperCase,
8 | objectToString,
9 | getColorObj,
10 | getIsGradient,
11 | getDetails,
12 | } from '@/utils/utils'
13 | import type { GradientProps } from '@/interfaces'
14 |
15 | // Mock config
16 | vi.mock('@/constants', () => ({
17 | config: {
18 | barSize: 20,
19 | crossSize: 18,
20 | defaultGradient: 'linear-gradient(90deg, #ff0000, #00ff00)',
21 | },
22 | }))
23 |
24 | describe('工具函数', () => {
25 | beforeEach(() => {
26 | vi.clearAllMocks()
27 | })
28 |
29 | describe('safeBounds', () => {
30 | it('应该返回正确的边界信息', () => {
31 | const mockEvent = {
32 | target: {
33 | parentNode: {
34 | getBoundingClientRect: () => ({
35 | x: 100,
36 | y: 200,
37 | width: 300,
38 | height: 400,
39 | }),
40 | },
41 | className: 'test-class',
42 | },
43 | }
44 |
45 | const result = safeBounds(mockEvent)
46 | expect(result.offsetLeft).toBe(100)
47 | expect(result.offsetTop).toBe(200)
48 | expect(result.clientWidth).toBe(300)
49 | expect(result.clientHeight).toBe(400)
50 | })
51 |
52 | it('应该处理c-resize ps-rl类名的调整', () => {
53 | const mockEvent = {
54 | target: {
55 | parentNode: {
56 | getBoundingClientRect: () => ({
57 | x: 100,
58 | y: 200,
59 | width: 300,
60 | height: 400,
61 | }),
62 | },
63 | className: 'c-resize ps-rl',
64 | },
65 | }
66 |
67 | const result = safeBounds(mockEvent)
68 | expect(result.offsetLeft).toBe(115) // 100 + 15
69 | })
70 | })
71 |
72 | describe('getHandleValue', () => {
73 | it('应该计算正确的句柄值', () => {
74 | const mockEvent = {
75 | clientX: 150,
76 | target: {
77 | parentNode: {
78 | getBoundingClientRect: () => ({
79 | x: 100,
80 | width: 300,
81 | }),
82 | },
83 | },
84 | }
85 |
86 | const result = getHandleValue(mockEvent)
87 | expect(typeof result).toBe('number')
88 | expect(result).toBeGreaterThanOrEqual(0)
89 | expect(result).toBeLessThanOrEqual(100)
90 | })
91 | })
92 |
93 | describe('computeSquareXY', () => {
94 | it('应该计算正确的正方形坐标', () => {
95 | const result = computeSquareXY(0.5, 0.5, 200, 200)
96 | expect(result).toHaveLength(2)
97 | expect(typeof result[0]).toBe('number')
98 | expect(typeof result[1]).toBe('number')
99 | })
100 |
101 | it('应该处理边界值', () => {
102 | const result1 = computeSquareXY(0, 0, 200, 200)
103 | const result2 = computeSquareXY(1, 1, 200, 200)
104 |
105 | expect(result1[0]).toBeLessThan(result2[0])
106 | expect(result1[1]).toBeGreaterThan(result2[1])
107 | })
108 | })
109 |
110 | describe('computePickerPosition', () => {
111 | it('应该计算正确的选择器位置', () => {
112 | const mockEvent = {
113 | clientX: 150,
114 | clientY: 250,
115 | target: {
116 | parentNode: {
117 | getBoundingClientRect: () => ({
118 | x: 100,
119 | y: 200,
120 | width: 300,
121 | height: 400,
122 | }),
123 | },
124 | },
125 | }
126 |
127 | const result = computePickerPosition(mockEvent)
128 | expect(result).toHaveLength(2)
129 | expect(typeof result[0]).toBe('number')
130 | expect(typeof result[1]).toBe('number')
131 | })
132 |
133 | it('应该处理触摸事件', () => {
134 | const mockTouchEvent = {
135 | touches: [{ clientX: 150, clientY: 250 }],
136 | target: {
137 | parentNode: {
138 | getBoundingClientRect: () => ({
139 | x: 100,
140 | y: 200,
141 | width: 300,
142 | height: 400,
143 | }),
144 | },
145 | },
146 | }
147 |
148 | const result = computePickerPosition(mockTouchEvent)
149 | expect(result).toHaveLength(2)
150 | })
151 | })
152 |
153 | describe('isUpperCase', () => {
154 | it('应该检测大写字符串', () => {
155 | expect(isUpperCase('ABC')).toBe(true)
156 | expect(isUpperCase('abc')).toBe(false)
157 | expect(isUpperCase('Abc')).toBe(true)
158 | expect(isUpperCase('')).toBe(false)
159 | })
160 |
161 | it('应该处理空值', () => {
162 | expect(isUpperCase(null as any)).toBe(false)
163 | expect(isUpperCase(undefined as any)).toBe(false)
164 | })
165 | })
166 |
167 | describe('objectToString', () => {
168 | it('应该处理字符串输入', () => {
169 | expect(objectToString('#ff0000')).toBe('#ff0000')
170 | expect(objectToString('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)')
171 | })
172 |
173 | it('应该处理渐变对象', () => {
174 | const gradientObj = {
175 | type: 'linear-gradient',
176 | orientation: { value: '90deg' },
177 | colorStops: [
178 | { value: '#ff0000', left: 0 },
179 | { value: '#00ff00', left: 100 },
180 | ],
181 | }
182 |
183 | const result = objectToString(gradientObj)
184 | expect(result).toContain('linear-gradient')
185 | expect(result).toContain('#ff0000')
186 | expect(result).toContain('#00ff00')
187 | })
188 |
189 | it('应该处理简写方向', () => {
190 | const gradientObj = {
191 | type: 'linear-gradient',
192 | orientation: { value: 'to top' },
193 | colorStops: [
194 | { value: '#ff0000', left: 0 },
195 | { value: '#00ff00', left: 100 },
196 | ],
197 | }
198 |
199 | const result = objectToString(gradientObj)
200 | expect(result).toContain('0deg')
201 | })
202 |
203 | it('应该处理径向渐变', () => {
204 | const gradientObj = {
205 | type: 'radial-gradient',
206 | orientation: { value: 'circle' },
207 | colorStops: [
208 | { value: '#ff0000', left: 0 },
209 | { value: '#00ff00', left: 100 },
210 | ],
211 | }
212 |
213 | const result = objectToString(gradientObj)
214 | expect(result).toContain('radial-gradient')
215 | expect(result).toContain('circle')
216 | })
217 | })
218 |
219 | describe('getColorObj', () => {
220 | it('应该返回正确的颜色对象', () => {
221 | const colors: GradientProps[] = [
222 | { value: '#ff0000', left: 0 },
223 | { value: '#00ff00', left: 50 },
224 | { value: '#0000ff', left: 100 },
225 | ]
226 |
227 | const result = getColorObj(colors)
228 | expect(result.currentColor).toBe('#ff0000')
229 | expect(result.selectedColor).toBe(0)
230 | expect(result.currentLeft).toBe(0)
231 | })
232 |
233 | it('应该优先选择大写颜色', () => {
234 | const colors: GradientProps[] = [
235 | { value: '#ff0000', left: 0 },
236 | { value: '#00FF00', left: 50 }, // 大写
237 | { value: '#0000ff', left: 100 },
238 | ]
239 |
240 | const result = getColorObj(colors)
241 | expect(result.currentColor).toBe('#00FF00')
242 | expect(result.selectedColor).toBe(1)
243 | expect(result.currentLeft).toBe(50)
244 | })
245 |
246 | it('应该处理空数组', () => {
247 | const result = getColorObj([])
248 | expect(result.currentColor).toBe(
249 | 'linear-gradient(90deg, #ff0000, #00ff00)',
250 | )
251 | expect(result.selectedColor).toBe(0)
252 | expect(result.currentLeft).toBe(0)
253 | })
254 | })
255 |
256 | describe('getIsGradient', () => {
257 | it('应该检测渐变字符串', () => {
258 | expect(getIsGradient('linear-gradient(90deg, #ff0000, #00ff00)')).toBe(
259 | true,
260 | )
261 | expect(getIsGradient('radial-gradient(circle, #ff0000, #00ff00)')).toBe(
262 | true,
263 | )
264 | expect(getIsGradient('#ff0000')).toBe(false)
265 | expect(getIsGradient('rgb(255, 0, 0)')).toBe(false)
266 | })
267 |
268 | it('应该处理空值', () => {
269 | expect(getIsGradient('')).toBe(false)
270 | expect(getIsGradient(null as any)).toBe(false)
271 | expect(getIsGradient(undefined as any)).toBe(false)
272 | })
273 | })
274 |
275 | describe('getDetails', () => {
276 | it('应该解析线性渐变', () => {
277 | const result = getDetails('linear-gradient(90deg, #ff0000, #00ff00)')
278 | expect(result.degrees).toBe(90)
279 | expect(result.degreeStr).toBe('90deg')
280 | expect(result.gradientType).toBe('linear-gradient')
281 | })
282 |
283 | it('应该解析径向渐变', () => {
284 | const result = getDetails('radial-gradient(circle, #ff0000, #00ff00)')
285 | expect(result.degrees).toBe(0)
286 | expect(result.degreeStr).toBe('circle')
287 | expect(result.gradientType).toBe('radial-gradient')
288 | })
289 |
290 | it('应该处理简写方向', () => {
291 | const result = getDetails('linear-gradient(to top, #ff0000, #00ff00)')
292 | expect(result.degrees).toBe(0)
293 | expect(result.degreeStr).toBe('0deg')
294 | })
295 |
296 | it('应该处理无效输入', () => {
297 | const result = getDetails('')
298 | expect(result.degrees).toBe(0)
299 | expect(result.degreeStr).toBe('0deg')
300 | expect(result.gradientType).toBe('')
301 | })
302 | })
303 | })
304 |
--------------------------------------------------------------------------------
/lib/src/utils/gradientParser.ts:
--------------------------------------------------------------------------------
1 | import { high, low } from './format'
2 | import { isUpperCase } from './utils'
3 | import tinycolor from 'tinycolor2'
4 |
5 | export const gradientParser = (input = '') => {
6 | const tokens = {
7 | linearGradient: /^(-(webkit|o|ms|moz)-)?(linear-gradient)/i,
8 | repeatingLinearGradient:
9 | /^(-(webkit|o|ms|moz)-)?(repeating-linear-gradient)/i,
10 | radialGradient: /^(-(webkit|o|ms|moz)-)?(radial-gradient)/i,
11 | repeatingRadialGradient:
12 | /^(-(webkit|o|ms|moz)-)?(repeating-radial-gradient)/i,
13 | sideOrCorner:
14 | /^to (left (top|bottom)|right (top|bottom)|top (left|right)|bottom (left|right)|left|right|top|bottom)/i,
15 | extentKeywords:
16 | /^(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)/,
17 | positionKeywords: /^(left|center|right|top|bottom)/i,
18 | pixelValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))px/,
19 | percentageValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))%/,
20 | emValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))em/,
21 | angleValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))deg/,
22 | startCall: /^\(/,
23 | endCall: /^\)/,
24 | comma: /^,/,
25 | hexColor: /^#([0-9a-fA-F]+)/,
26 | literalColor: /^([a-zA-Z]+)/,
27 | rgbColor: /^rgb/i,
28 | spacedRgbColor: /^(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})\s+\/\s+([0-1](\.\d+)?)/,
29 | rgbaColor: /^rgba/i,
30 | hslColor: /^hsl/i,
31 | hsvColor: /^hsv/i,
32 | number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/,
33 | }
34 |
35 | function error(msg: any) {
36 | const err = new Error(input + ': ' + msg)
37 | // err.source = input
38 | throw err
39 | }
40 |
41 | function consume(size: any) {
42 | input = input.substr(size)
43 | }
44 |
45 | function scan(regexp: any) {
46 | const blankCaptures = /^[\n\r\t\s]+/.exec(input)
47 | if (blankCaptures) {
48 | consume(blankCaptures[0].length)
49 | }
50 |
51 | const captures = regexp.exec(input)
52 | if (captures) {
53 | consume(captures[0].length)
54 | }
55 |
56 | return captures
57 | }
58 |
59 | function matchListing(matcher: any) {
60 | let captures = matcher() as unknown
61 | const result: any[] = []
62 |
63 | if (captures) {
64 | result.push(captures)
65 | while (scan(tokens.comma)) {
66 | captures = matcher()
67 | if (captures) {
68 | result.push(captures)
69 | } else {
70 | error('One extra comma')
71 | }
72 | }
73 | }
74 |
75 | return result
76 | }
77 |
78 | function match(type: string, pattern: any, captureIndex: number) {
79 | const captures = scan(pattern)
80 | if (captures) {
81 | return {
82 | type: type,
83 | value: captures[captureIndex],
84 | }
85 | }
86 | }
87 |
88 | function matchHexColor() {
89 | const hexObj = match('hex', tokens.hexColor, 1)
90 | if (hexObj?.value) {
91 | const { r, g, b, a } = tinycolor(hexObj?.value).toRgb()
92 | return {
93 | value: `rgba(${r}, ${g}, ${b}, ${a})`,
94 | }
95 | }
96 | }
97 |
98 | const checkCaps = (val: any) => {
99 | const capIt = isUpperCase(val?.[0])
100 | return {
101 | value: `${capIt ? 'RGBA' : 'rgba'}(${matchListing(matchNumber)})`,
102 | }
103 | }
104 |
105 | function matchCall(pattern: any, callback: any) {
106 | const captures = scan(pattern)
107 |
108 | if (captures) {
109 | if (!scan(tokens.startCall)) {
110 | error('Missing (')
111 | }
112 |
113 | const result = callback(captures)
114 |
115 | if (!scan(tokens.endCall)) {
116 | error('Missing )')
117 | }
118 |
119 | return result
120 | }
121 | }
122 |
123 | function matchHSLColor() {
124 | return matchCall(tokens.hslColor, convertHsl)
125 | }
126 |
127 | function matchRGBAColor() {
128 | return matchCall(tokens.rgbaColor, checkCaps)
129 | }
130 |
131 | function matchRGBColor() {
132 | return matchCall(tokens.rgbColor, convertRgb)
133 | }
134 |
135 | function matchLiteralColor() {
136 | const litObj = match('literal', tokens.literalColor, 0)
137 | if (litObj?.value) {
138 | const { r, g, b, a } = tinycolor(litObj?.value).toRgb()
139 | return {
140 | value: `rgba(${r}, ${g}, ${b}, ${a})`,
141 | }
142 | }
143 | }
144 |
145 | function matchHSVColor() {
146 | return matchCall(tokens.hsvColor, convertHsv)
147 | }
148 |
149 | function matchColor() {
150 | return (
151 | matchHexColor() ||
152 | matchHSLColor() ||
153 | matchRGBAColor() ||
154 | matchRGBColor() ||
155 | matchLiteralColor() ||
156 | matchHSVColor()
157 | )
158 | }
159 |
160 | function matchColorStop() {
161 | const color = matchColor()
162 |
163 | if (!color) {
164 | error('Expected color definition')
165 | }
166 |
167 | color.left = ~~matchDistance()?.value
168 | return color
169 | }
170 |
171 | function matchGradient(
172 | gradientType: any,
173 | pattern: any,
174 | orientationMatcher: any,
175 | ) {
176 | return matchCall(pattern, function () {
177 | const orientation = orientationMatcher()
178 | if (orientation) {
179 | if (!scan(tokens.comma)) {
180 | error('Missing comma before color stops')
181 | }
182 | }
183 |
184 | return {
185 | type: gradientType,
186 | orientation: orientation,
187 | colorStops: matchListing(matchColorStop),
188 | }
189 | })
190 | }
191 |
192 | function matchLinearOrientation() {
193 | return matchSideOrCorner() || matchAngle()
194 | }
195 |
196 | function matchDefinition() {
197 | return (
198 | matchGradient(
199 | 'linear-gradient',
200 | tokens.linearGradient,
201 | matchLinearOrientation,
202 | ) ||
203 | matchGradient(
204 | 'repeating-linear-gradient',
205 | tokens.repeatingLinearGradient,
206 | matchLinearOrientation,
207 | ) ||
208 | matchGradient(
209 | 'radial-gradient',
210 | tokens.radialGradient,
211 | matchListRadialOrientations,
212 | ) ||
213 | matchGradient(
214 | 'repeating-radial-gradient',
215 | tokens.repeatingRadialGradient,
216 | matchListRadialOrientations,
217 | )
218 | )
219 | }
220 |
221 | function matchListDefinitions() {
222 | return matchListing(matchDefinition)
223 | }
224 |
225 | function getAST() {
226 | const ast = matchListDefinitions()
227 | if (input.length > 0) {
228 | error('Invalid input not EOF')
229 | }
230 |
231 | const ast0 = ast[0]
232 | const checkSelected = ast0?.colorStops?.filter((c: any) =>
233 | isUpperCase(c.value),
234 | ).length
235 |
236 | const getGradientObj = () => {
237 | if (checkSelected > 0) {
238 | return ast0
239 | } else {
240 | const val = (c: any, i: number) => (i === 0 ? high(c) : low(c))
241 | return {
242 | ...ast0,
243 | colorStops: ast0.colorStops.map((c: any, i: number) => ({
244 | ...c,
245 | value: val(c, i),
246 | })),
247 | }
248 | }
249 | }
250 |
251 | return getGradientObj()
252 | }
253 |
254 | function matchSideOrCorner() {
255 | return match('directional', tokens.sideOrCorner, 1)
256 | }
257 |
258 | function matchAngle() {
259 | return match('angular', tokens.angleValue, 1)
260 | }
261 |
262 | function matchListRadialOrientations() {
263 | let radialOrientations,
264 | radialOrientation = matchRadialOrientation(),
265 | lookaheadCache
266 |
267 | if (radialOrientation) {
268 | radialOrientations = []
269 | radialOrientations.push(radialOrientation)
270 |
271 | lookaheadCache = input
272 | if (scan(tokens.comma)) {
273 | radialOrientation = matchRadialOrientation()
274 | if (radialOrientation) {
275 | radialOrientations.push(radialOrientation)
276 | } else {
277 | input = lookaheadCache
278 | }
279 | }
280 | }
281 |
282 | return radialOrientations
283 | }
284 |
285 | function matchRadialOrientation() {
286 | let radialType = matchCircle() || matchEllipse()
287 |
288 | if (radialType) {
289 | // @ts-expect-error - need to circle back for these types
290 | radialType.at = matchAtPosition()
291 | } else {
292 | const extent = matchExtentKeyword()
293 | if (extent) {
294 | radialType = extent
295 | const positionAt = matchAtPosition()
296 | if (positionAt) {
297 | // @ts-expect-error - need to circle back for these types
298 | radialType.at = positionAt
299 | }
300 | } else {
301 | const defaultPosition = matchPositioning()
302 | if (defaultPosition) {
303 | radialType = {
304 | type: 'default-radial',
305 | // @ts-expect-error - need to circle back for these types
306 | at: defaultPosition,
307 | }
308 | }
309 | }
310 | }
311 |
312 | return radialType
313 | }
314 |
315 | function matchLength() {
316 | return match('px', tokens.pixelValue, 1) || match('em', tokens.emValue, 1)
317 | }
318 |
319 | function matchCircle() {
320 | const circle = match('shape', /^(circle)/i, 0)
321 |
322 | if (circle) {
323 | // @ts-expect-error - need to circle back for these types
324 | circle.style = matchLength() || matchExtentKeyword()
325 | }
326 |
327 | return circle
328 | }
329 |
330 | function matchEllipse() {
331 | const ellipse = match('shape', /^(ellipse)/i, 0)
332 |
333 | if (ellipse) {
334 | // @ts-expect-error - need to circle back for these types
335 | ellipse.style = matchDistance() || matchExtentKeyword()
336 | }
337 |
338 | return ellipse
339 | }
340 |
341 | function matchExtentKeyword() {
342 | return match('extent-keyword', tokens.extentKeywords, 1)
343 | }
344 |
345 | function matchAtPosition() {
346 | if (match('position', /^at/, 0)) {
347 | const positioning = matchPositioning()
348 |
349 | if (!positioning) {
350 | error('Missing positioning value')
351 | }
352 |
353 | return positioning
354 | }
355 | }
356 |
357 | function matchPositioning() {
358 | const location = matchCoordinates()
359 |
360 | if (location.x || location.y) {
361 | return {
362 | type: 'position',
363 | value: location,
364 | }
365 | }
366 | }
367 |
368 | function matchCoordinates() {
369 | return {
370 | x: matchDistance(),
371 | y: matchDistance(),
372 | }
373 | }
374 |
375 | function matchNumber() {
376 | return scan(tokens.number)[1]
377 | }
378 |
379 | const convertHsl = (val: any) => {
380 | const capIt = isUpperCase(val?.[0])
381 | const hsl = matchListing(matchNumber)
382 | const { r, g, b, a } = tinycolor({
383 | h: hsl[0],
384 | s: hsl[1],
385 | l: hsl[2],
386 | a: hsl[3] || 1,
387 | }).toRgb()
388 | return {
389 | value: `${capIt ? 'RGBA' : 'rgba'}(${r}, ${g}, ${b}, ${a})`,
390 | }
391 | }
392 |
393 | const convertHsv = (val: any) => {
394 | const capIt = isUpperCase(val?.[0])
395 | const hsv = matchListing(matchNumber)
396 | const { r, g, b, a } = tinycolor({
397 | h: hsv[0],
398 | s: hsv[1],
399 | v: hsv[2],
400 | a: hsv[3] || 1,
401 | }).toRgb()
402 | return {
403 | value: `${capIt ? 'RGBA' : 'rgba'}(${r}, ${g}, ${b}, ${a})`,
404 | }
405 | }
406 |
407 | const convertRgb = (val: any) => {
408 | const capIt = isUpperCase(val?.[0])
409 | const captures = scan(tokens.spacedRgbColor)
410 | const [, r, g, b, a = 1] = captures || [null, ...matchListing(matchNumber)]
411 | return {
412 | value: `${capIt ? 'RGBA' : 'rgba'}(${r}, ${g}, ${b}, ${a})`,
413 | }
414 | }
415 |
416 | function matchDistance() {
417 | return (
418 | match('%', tokens.percentageValue, 1) ||
419 | matchPositionKeyword() ||
420 | matchLength()
421 | )
422 | }
423 |
424 | function matchPositionKeyword() {
425 | return match('position-keyword', tokens.positionKeywords, 1)
426 | }
427 |
428 | return getAST()
429 | }
430 |
--------------------------------------------------------------------------------
/lib/src/components/ColorPicker/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
448 |
--------------------------------------------------------------------------------