@veltra/desktop 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/collapse/collapse-item.js +3 -3
- package/dist/components/collapse/collapse-item.js.map +1 -1
- package/dist/components/collapse/collapse.js +7 -5
- package/dist/components/collapse/collapse.js.map +1 -1
- package/dist/components/date-panel/use-date-select.js +10 -9
- package/dist/components/date-panel/use-date-select.js.map +1 -1
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/number-input/number-input.js +22 -8
- package/dist/components/number-input/number-input.js.map +1 -1
- package/dist/components/palette/color-transform.js +36 -15
- package/dist/components/palette/color-transform.js.map +1 -1
- package/dist/components/palette/palette.js +3 -1
- package/dist/components/palette/palette.js.map +1 -1
- package/dist/components/rich-text-editor/toolbar.js +8 -4
- package/dist/components/rich-text-editor/toolbar.js.map +1 -1
- package/dist/node_modules/.bun/@codemirror_autocomplete@6.20.3/node_modules/@codemirror/autocomplete/dist/index.js +20 -14
- package/dist/node_modules/.bun/@codemirror_autocomplete@6.20.3/node_modules/@codemirror/autocomplete/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@codemirror_lang-html@6.4.11/node_modules/@codemirror/lang-html/dist/index.js +4 -2
- package/dist/node_modules/.bun/@codemirror_lang-html@6.4.11/node_modules/@codemirror/lang-html/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@codemirror_language@6.12.4/node_modules/@codemirror/language/dist/index.js +26 -21
- package/dist/node_modules/.bun/@codemirror_language@6.12.4/node_modules/@codemirror/language/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@codemirror_search@6.7.1/node_modules/@codemirror/search/dist/index.js +22 -18
- package/dist/node_modules/.bun/@codemirror_search@6.7.1/node_modules/@codemirror/search/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@codemirror_state@6.7.1/node_modules/@codemirror/state/dist/index.js +7 -6
- package/dist/node_modules/.bun/@codemirror_state@6.7.1/node_modules/@codemirror/state/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@codemirror_view@6.43.8/node_modules/@codemirror/view/dist/index.js +142 -117
- package/dist/node_modules/.bun/@codemirror_view@6.43.8/node_modules/@codemirror/view/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@lezer_common@1.5.2/node_modules/@lezer/common/dist/index.js +16 -11
- package/dist/node_modules/.bun/@lezer_common@1.5.2/node_modules/@lezer/common/dist/index.js.map +1 -1
- package/dist/node_modules/.bun/@lezer_lr@1.4.10/node_modules/@lezer/lr/dist/index.js +25 -15
- package/dist/node_modules/.bun/@lezer_lr@1.4.10/node_modules/@lezer/lr/dist/index.js.map +1 -1
- package/dist/types/collapse.d.ts +1 -1
- package/package.json +15 -6
- package/src/components/collapse/__test__/collapse.test.ts +30 -0
- package/src/components/collapse/collapse-item.vue +7 -7
- package/src/components/number-input/__test__/number-input.test.ts +38 -0
- package/src/components/number-input/number-input.vue +28 -14
- package/src/components/palette/__test__/color-transform.test.ts +35 -0
- package/src/components/palette/__test__/palette.test.ts +102 -0
- package/src/components/palette/color-transform.ts +35 -11
- package/src/components/palette/palette.vue +5 -1
- package/src/types/collapse.ts +1 -1
package/dist/types/collapse.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/desktop",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
|
+
"homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/cabinet-fe/ultra-ui/issues"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/cabinet-fe/ultra-ui",
|
|
11
|
+
"directory": "packages/desktop"
|
|
12
|
+
},
|
|
4
13
|
"files": [
|
|
5
14
|
"dist",
|
|
6
15
|
"src"
|
|
@@ -71,12 +80,12 @@
|
|
|
71
80
|
"peerDependencies": {
|
|
72
81
|
"@cat-kit/core": ">=1.1.8",
|
|
73
82
|
"@cat-kit/fe": ">=1.1.8",
|
|
74
|
-
"@veltra/compositions": "^1.6.
|
|
75
|
-
"@veltra/directives": "^1.6.
|
|
83
|
+
"@veltra/compositions": "^1.6.3",
|
|
84
|
+
"@veltra/directives": "^1.6.3",
|
|
76
85
|
"@veltra/icons": "^1.5.0",
|
|
77
|
-
"@veltra/sheet-core": "^2.3.
|
|
78
|
-
"@veltra/styles": "^1.6.
|
|
79
|
-
"@veltra/utils": "^1.6.
|
|
86
|
+
"@veltra/sheet-core": "^2.3.1",
|
|
87
|
+
"@veltra/styles": "^1.6.3",
|
|
88
|
+
"@veltra/utils": "^1.6.3",
|
|
80
89
|
"vue": ">=3.5.41"
|
|
81
90
|
},
|
|
82
91
|
"peerDependenciesMeta": {
|
|
@@ -223,11 +223,41 @@ describe('CollapseItem standalone', () => {
|
|
|
223
223
|
expect(host.querySelector('.u-collapse__item')).toBeTruthy()
|
|
224
224
|
expect(host.querySelector('.u-collapse__header')).toBeTruthy()
|
|
225
225
|
expect(host.querySelector('.u-collapse__title')?.textContent).toBe('Standalone')
|
|
226
|
+
expect(host.querySelector('.u-collapse__icon')).toBeTruthy()
|
|
226
227
|
} finally {
|
|
227
228
|
unmount()
|
|
228
229
|
}
|
|
229
230
|
})
|
|
230
231
|
|
|
232
|
+
it('keeps the expand icon when #header slot is provided', async () => {
|
|
233
|
+
const host = document.createElement('div')
|
|
234
|
+
document.body.appendChild(host)
|
|
235
|
+
|
|
236
|
+
const app = createApp({
|
|
237
|
+
render() {
|
|
238
|
+
return h(
|
|
239
|
+
UCollapseItem,
|
|
240
|
+
{ title: 'Fallback' },
|
|
241
|
+
{ header: () => 'Custom header', default: () => 'Body' }
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
app.mount(host)
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await nextTick()
|
|
249
|
+
expect(host.querySelector('.u-collapse__title')?.textContent).toBe('Custom header')
|
|
250
|
+
expect(host.querySelector('.u-collapse__icon')).toBeTruthy()
|
|
251
|
+
|
|
252
|
+
;(host.querySelector('.u-collapse__header') as HTMLElement).click()
|
|
253
|
+
await nextTick()
|
|
254
|
+
expect(host.querySelector('.u-collapse__header.is-active')).toBeTruthy()
|
|
255
|
+
} finally {
|
|
256
|
+
app.unmount()
|
|
257
|
+
host.remove()
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
231
261
|
it('defaults to collapsed when no v-model is provided', async () => {
|
|
232
262
|
const { host, model, unmount } = mountStandaloneItem({ title: 'Standalone' })
|
|
233
263
|
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
@keydown.enter.prevent="handleClick"
|
|
11
11
|
@keydown.space.prevent="handleClick"
|
|
12
12
|
>
|
|
13
|
-
<
|
|
14
|
-
<
|
|
13
|
+
<span :class="cls.e('title')">
|
|
14
|
+
<slot name="header" :is-active="isActive">{{ title }}</slot>
|
|
15
|
+
</span>
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</slot>
|
|
17
|
+
<span :class="cls.e('icon')">
|
|
18
|
+
<UIcon><component :is="iconComponent" /></UIcon>
|
|
19
|
+
</span>
|
|
20
20
|
</div>
|
|
21
21
|
<div ref="wrapperEl" :class="cls.e('content-wrapper')" :aria-hidden="!isActive">
|
|
22
22
|
<div v-if="contentMounted" :class="cls.e('content')">
|
|
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<CollapseItemProps>(), { modelValue: false
|
|
|
42
42
|
const emit = defineEmits<CollapseItemEmits>()
|
|
43
43
|
|
|
44
44
|
defineSlots<{
|
|
45
|
-
/**
|
|
45
|
+
/** 自定义标题区,作用域参数 `isActive` 指示展开状态;缺省渲染 title。展开图标始终由组件渲染,活动态旋转 180° */
|
|
46
46
|
header?: (scope: { isActive: boolean }) => any
|
|
47
47
|
/** 折叠面板内容 */
|
|
48
48
|
default?: () => any
|
|
@@ -57,4 +57,42 @@ describe('NumberInput', () => {
|
|
|
57
57
|
unmount()
|
|
58
58
|
}
|
|
59
59
|
})
|
|
60
|
+
|
|
61
|
+
it('updates the input value immediately without animation when step is 1', async () => {
|
|
62
|
+
const { host, unmount } = mountNumberInput({ modelValue: 1, step: 1 })
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const input = host.querySelector('input')!
|
|
66
|
+
const [upIcon, downIcon] = Array.from(
|
|
67
|
+
host.querySelectorAll('.u-number-input__step .u-icon')
|
|
68
|
+
) as HTMLElement[]
|
|
69
|
+
|
|
70
|
+
upIcon!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
71
|
+
await nextTick()
|
|
72
|
+
expect(input.value).toBe('2')
|
|
73
|
+
|
|
74
|
+
downIcon!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
75
|
+
await nextTick()
|
|
76
|
+
expect(input.value).toBe('1')
|
|
77
|
+
} finally {
|
|
78
|
+
unmount()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('keeps the rolling animation when step is greater than 1', () => {
|
|
83
|
+
const { host, unmount } = mountNumberInput({ modelValue: 0, step: 5 })
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const input = host.querySelector('input')!
|
|
87
|
+
const [upIcon] = Array.from(
|
|
88
|
+
host.querySelectorAll('.u-number-input__step .u-icon')
|
|
89
|
+
) as HTMLElement[]
|
|
90
|
+
|
|
91
|
+
upIcon!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
92
|
+
// 动画通过 requestAnimationFrame 逐帧更新, 同步阶段不应直接跳到目标值
|
|
93
|
+
expect(input.value).toBe('0')
|
|
94
|
+
} finally {
|
|
95
|
+
unmount()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
60
98
|
})
|
|
@@ -301,6 +301,28 @@ const tween = new Tween(
|
|
|
301
301
|
}
|
|
302
302
|
)
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* 步进后更新输入框的展示值
|
|
306
|
+
* 步长为 1 时直接展示目标值, 不播放数字滚动动画
|
|
307
|
+
* @param rawFrom 步进前的原始值(存在倍数时为除以倍数后的值)
|
|
308
|
+
* @param rawTarget 步进后的原始值(存在倍数时为除以倍数后的值)
|
|
309
|
+
*/
|
|
310
|
+
function updateStepDisplay(rawFrom: number, rawTarget: number): void {
|
|
311
|
+
tween.state.n = rawFrom
|
|
312
|
+
|
|
313
|
+
if (stepVal.value === 1) {
|
|
314
|
+
tween.state.n = rawTarget
|
|
315
|
+
const _rawInput = inputDom.value
|
|
316
|
+
if (!_rawInput) return
|
|
317
|
+
const { multiple } = props
|
|
318
|
+
_rawInput.value = getDisplayed(multiple ? $n.mul(rawTarget, multiple) : rawTarget)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// tween 动画在原始值上进行
|
|
323
|
+
tween.to({ n: rawTarget })
|
|
324
|
+
}
|
|
325
|
+
|
|
304
326
|
/** 增 */
|
|
305
327
|
function increase(): void {
|
|
306
328
|
if (disabled.value) return
|
|
@@ -313,17 +335,13 @@ function increase(): void {
|
|
|
313
335
|
// 在原始值基础上加步长
|
|
314
336
|
const newRawVal = $n.plus(rawVal, stepVal.value)
|
|
315
337
|
// 乘以倍数得到新的实际值
|
|
316
|
-
const
|
|
317
|
-
const target = getValidValue(newVal)
|
|
338
|
+
const target = getValidValue($n.mul(newRawVal, multiple))
|
|
318
339
|
model.value = target
|
|
319
|
-
|
|
320
|
-
tween.state.n = $n.div(target, multiple)
|
|
321
|
-
tween.to({ n: $n.div(target, multiple) })
|
|
340
|
+
updateStepDisplay($n.div(val, multiple), $n.div(target, multiple))
|
|
322
341
|
} else {
|
|
323
|
-
tween.state.n = val
|
|
324
342
|
const target = getValidValue($n.plus(val, stepVal.value))
|
|
325
343
|
model.value = target
|
|
326
|
-
|
|
344
|
+
updateStepDisplay(val, target)
|
|
327
345
|
}
|
|
328
346
|
}
|
|
329
347
|
|
|
@@ -339,17 +357,13 @@ function decrease(): void {
|
|
|
339
357
|
// 在原始值基础上减步长
|
|
340
358
|
const newRawVal = $n.minus(rawVal, stepVal.value)
|
|
341
359
|
// 乘以倍数得到新的实际值
|
|
342
|
-
const
|
|
343
|
-
const target = getValidValue(newVal)
|
|
360
|
+
const target = getValidValue($n.mul(newRawVal, multiple))
|
|
344
361
|
model.value = target
|
|
345
|
-
|
|
346
|
-
tween.state.n = $n.div(target, multiple)
|
|
347
|
-
tween.to({ n: $n.div(target, multiple) })
|
|
362
|
+
updateStepDisplay($n.div(val, multiple), $n.div(target, multiple))
|
|
348
363
|
} else {
|
|
349
|
-
tween.state.n = val
|
|
350
364
|
const target = getValidValue($n.minus(val, stepVal.value))
|
|
351
365
|
model.value = target
|
|
352
|
-
|
|
366
|
+
updateStepDisplay(val, target)
|
|
353
367
|
}
|
|
354
368
|
}
|
|
355
369
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { HEX2RGBA } from '../color-transform'
|
|
4
|
+
|
|
5
|
+
describe('HEX2RGBA', () => {
|
|
6
|
+
it('解析 6 位 HEX(# 可省略、大小写不敏感)', () => {
|
|
7
|
+
expect(HEX2RGBA('#FF0000')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 1 })
|
|
8
|
+
expect(HEX2RGBA('ff0000')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 1 })
|
|
9
|
+
expect(HEX2RGBA(' #00ff00 ')).toEqual({ RGB: { r: 0, g: 255, b: 0 }, alpha: 1 })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('解析 3/4 位缩写 HEX', () => {
|
|
13
|
+
expect(HEX2RGBA('#f00')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 1 })
|
|
14
|
+
expect(HEX2RGBA('#f008')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 0x88 / 255 })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('解析 8 位 HEX(RRGGBBAA)', () => {
|
|
18
|
+
expect(HEX2RGBA('#FF000080')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 0x80 / 255 })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('解析 rgb() / rgba() 函数表示', () => {
|
|
22
|
+
expect(HEX2RGBA('rgb(255, 0, 0)')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 1 })
|
|
23
|
+
expect(HEX2RGBA('rgba(255,0,0,0.5)')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 0.5 })
|
|
24
|
+
expect(HEX2RGBA('rgba(255, 0, 0, 50%)')).toEqual({ RGB: { r: 255, g: 0, b: 0 }, alpha: 0.5 })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('非法输入返回 null', () => {
|
|
28
|
+
expect(HEX2RGBA('')).toBeNull()
|
|
29
|
+
expect(HEX2RGBA('#FF')).toBeNull()
|
|
30
|
+
expect(HEX2RGBA('#GGGGGG')).toBeNull()
|
|
31
|
+
expect(HEX2RGBA('red')).toBeNull()
|
|
32
|
+
expect(HEX2RGBA('rgb(256, 0, 0)')).toBeNull()
|
|
33
|
+
expect(HEX2RGBA('rgba(0, 0, 0, 2)')).toBeNull()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import UPalette from '../palette.vue'
|
|
5
|
+
|
|
6
|
+
function mountPalette(initialColor?: string) {
|
|
7
|
+
const host = document.createElement('div')
|
|
8
|
+
document.body.appendChild(host)
|
|
9
|
+
|
|
10
|
+
const model = ref(initialColor)
|
|
11
|
+
const app = createApp({
|
|
12
|
+
render() {
|
|
13
|
+
return h(UPalette, {
|
|
14
|
+
modelValue: model.value,
|
|
15
|
+
'onUpdate:modelValue': (value: string | undefined) => {
|
|
16
|
+
model.value = value
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
app.mount(host)
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
host,
|
|
26
|
+
model,
|
|
27
|
+
setModel(value: string) {
|
|
28
|
+
model.value = value
|
|
29
|
+
},
|
|
30
|
+
async unmount() {
|
|
31
|
+
// 等待 usePop 打开面板时的异步定位完成,避免卸载后回调访问已销毁元素
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
33
|
+
app.unmount()
|
|
34
|
+
await nextTick()
|
|
35
|
+
host.remove()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function openPanel(host: HTMLElement) {
|
|
41
|
+
host.querySelector('.u-palette')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
42
|
+
await nextTick()
|
|
43
|
+
await nextTick()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('UPalette', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
// happy-dom 中 offsetWidth/offsetHeight 恒为 0,这里模拟 200x200 的画布
|
|
49
|
+
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(200)
|
|
50
|
+
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(200)
|
|
51
|
+
|
|
52
|
+
// usePop 的浮层容器是模块级单例,上一个用例清空 body 后需补回 teleport 目标
|
|
53
|
+
if (!document.getElementById('pop-container')) {
|
|
54
|
+
const popContainer = document.createElement('div')
|
|
55
|
+
popContainer.id = 'pop-container'
|
|
56
|
+
document.body.appendChild(popContainer)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.restoreAllMocks()
|
|
62
|
+
document.body.innerHTML = ''
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('SV 面板 thumb 跟随初始绑定的颜色', async () => {
|
|
66
|
+
const { host, unmount } = mountPalette('#FF0000')
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await openPanel(host)
|
|
70
|
+
|
|
71
|
+
const thumb = document.body.querySelector<HTMLElement>('.u-palette__sv-thumb')
|
|
72
|
+
expect(thumb).not.toBeNull()
|
|
73
|
+
// #FF0000 → hsv(0, 1, 1),thumb 应在右上角: translate(200px, 0px)
|
|
74
|
+
expect(thumb!.style.transform).toBe('translate(200px, 0px)')
|
|
75
|
+
|
|
76
|
+
const hueThumb = document.body.querySelector<HTMLElement>('.u-palette__hue-thumb')
|
|
77
|
+
expect(hueThumb!.style.transform).toBe('translateX(0px)')
|
|
78
|
+
} finally {
|
|
79
|
+
unmount()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('面板关闭时外部 modelValue 变化,打开后 thumb 跟随', async () => {
|
|
84
|
+
const { host, setModel, unmount } = mountPalette('')
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
setModel('#0000FF')
|
|
88
|
+
await nextTick()
|
|
89
|
+
|
|
90
|
+
await openPanel(host)
|
|
91
|
+
|
|
92
|
+
const thumb = document.body.querySelector<HTMLElement>('.u-palette__sv-thumb')
|
|
93
|
+
// #0000FF → hsv(240, 1, 1),thumb 应在右上角: translate(200px, 0px)
|
|
94
|
+
expect(thumb!.style.transform).toBe('translate(200px, 0px)')
|
|
95
|
+
|
|
96
|
+
const hueThumb = document.body.querySelector<HTMLElement>('.u-palette__hue-thumb')
|
|
97
|
+
expect(hueThumb!.style.transform).toBe(`translateX(${(200 * 240) / 360}px)`)
|
|
98
|
+
} finally {
|
|
99
|
+
unmount()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -103,18 +103,35 @@ export function RGB2HSV(RGB: PaletteRGB): PaletteHSV {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
107
|
-
* @
|
|
108
|
-
*
|
|
106
|
+
* 将颜色字符串转换为 RGBA 颜色
|
|
107
|
+
* @description
|
|
108
|
+
* 支持 `#rgb`、`#rgba`、`#rrggbb`、`#rrggbbaa`(`#` 可省略)以及 `rgb()` / `rgba()`
|
|
109
|
+
* 函数表示;无法解析时返回 `null`,调用方应视为未绑定颜色而不是沿用错误的解析结果。
|
|
110
|
+
* @param color 颜色字符串
|
|
111
|
+
* @returns RGBA 颜色,解析失败返回 null
|
|
109
112
|
*/
|
|
110
|
-
export function HEX2RGBA(
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
+
export function HEX2RGBA(color: string): { RGB: PaletteRGB; alpha: number } | null {
|
|
114
|
+
const input = color.trim()
|
|
115
|
+
|
|
116
|
+
// rgb() / rgba() 函数表示
|
|
117
|
+
const fnMatch = input.match(
|
|
118
|
+
/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?%?)\s*)?\)$/i
|
|
119
|
+
)
|
|
120
|
+
if (fnMatch) {
|
|
121
|
+
const [, r, g, b, a] = fnMatch
|
|
122
|
+
const rgb = { r: +r!, g: +g!, b: +b! }
|
|
123
|
+
if (rgb.r > 255 || rgb.g > 255 || rgb.b > 255) return null
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
let alpha = 1
|
|
126
|
+
if (a !== undefined) {
|
|
127
|
+
alpha = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a)
|
|
128
|
+
if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) return null
|
|
129
|
+
}
|
|
130
|
+
return { RGB: rgb, alpha }
|
|
116
131
|
}
|
|
117
132
|
|
|
133
|
+
let hex = input.startsWith('#') ? input.slice(1) : input
|
|
134
|
+
|
|
118
135
|
if (hex.length === 3 || hex.length === 4) {
|
|
119
136
|
hex = hex
|
|
120
137
|
.split('')
|
|
@@ -122,13 +139,20 @@ export function HEX2RGBA(hex: string): { RGB: PaletteRGB; alpha: number } {
|
|
|
122
139
|
.join('')
|
|
123
140
|
}
|
|
124
141
|
|
|
142
|
+
if (hex.length !== 6 && hex.length !== 8) return null
|
|
143
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return null
|
|
144
|
+
|
|
145
|
+
let alpha = 1
|
|
125
146
|
const alphaStr = hex.slice(6)
|
|
126
147
|
if (alphaStr) {
|
|
127
148
|
alpha = parseInt(alphaStr, 16) / 255
|
|
128
149
|
}
|
|
129
|
-
|
|
130
|
-
rgb
|
|
131
|
-
|
|
150
|
+
|
|
151
|
+
const rgb = {
|
|
152
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
153
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
154
|
+
b: parseInt(hex.slice(4, 6), 16)
|
|
155
|
+
}
|
|
132
156
|
|
|
133
157
|
return { RGB: rgb, alpha }
|
|
134
158
|
}
|
|
@@ -94,7 +94,11 @@ watch(
|
|
|
94
94
|
if (!color) return
|
|
95
95
|
if (isUserActive()) return
|
|
96
96
|
|
|
97
|
-
const
|
|
97
|
+
const parsed = HEX2RGBA(color)
|
|
98
|
+
// 无法解析的颜色(如非法字符)视为未绑定,跳过同步,避免把 NaN 写回 modelValue
|
|
99
|
+
if (!parsed) return
|
|
100
|
+
|
|
101
|
+
const { RGB, alpha } = parsed
|
|
98
102
|
const hsv = RGB2HSV(RGB)
|
|
99
103
|
rest.updateHue(hsv.h)
|
|
100
104
|
rest.updateSV({ s: hsv.s, v: hsv.v })
|