├── .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 | 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 | 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 | 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 | 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 | [![npm](https://badgen.net/npm/v/color-gradient-picker-vue3)](https://www.npmjs.com/package/color-gradient-picker-vue3) 15 | [![downloads](https://badgen.net/npm/dt/color-gradient-picker-vue3)](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 | 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 | 8 | 9 | 35 | -------------------------------------------------------------------------------- /lib/src/components/ColorPicker/components/Inputs/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 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 | 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 | 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 | 14 | 15 | 57 | -------------------------------------------------------------------------------- /lib/src/components/ColorPicker/components/Inputs/components/HSLInputs.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 50 | -------------------------------------------------------------------------------- /lib/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 36 | 37 | 57 | -------------------------------------------------------------------------------- /lib/src/components/ColorPicker/components/Preview.vue: -------------------------------------------------------------------------------- 1 | 8 | 34 | 35 | 60 | -------------------------------------------------------------------------------- /lib/src/components/ColorPicker/components/Inputs/components/HSVInputs.vue: -------------------------------------------------------------------------------- 1 | 8 | 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 | 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 | 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 | 30 | 31 | 86 | -------------------------------------------------------------------------------- /lib/src/components/ColorPicker/components/AdvancedControls/components/AdvLightnessBar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 92 | -------------------------------------------------------------------------------- /lib/src/components/ColorPicker/components/Hue.vue: -------------------------------------------------------------------------------- 1 | 8 | 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 | 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 | 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 | 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 | 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 | 113 | 114 | 124 | ``` 125 | 126 | ### 自定义预设颜色 127 | 128 | ```vue 129 | 136 | 137 | 151 | ``` 152 | 153 | ### 隐藏特定功能 154 | 155 | ```vue 156 | 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 | 38 | 39 | 448 | --------------------------------------------------------------------------------