vome-core 0.0.98 → 0.0.99

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.
Files changed (37) hide show
  1. package/dist/admin/config/plugin-dev.js +1 -1
  2. package/dist/admin/crud/components/vm-color-picker.vue +790 -62
  3. package/dist/admin/crud/config.js +1 -1
  4. package/dist/admin/crud/confirm.js +1 -1
  5. package/dist/admin/crud/dict.js +1 -1
  6. package/dist/admin/crud/index.js +1 -1
  7. package/dist/admin/crud/key.js +1 -1
  8. package/dist/admin/crud/mitt.js +1 -1
  9. package/dist/admin/crud/plugins.js +1 -1
  10. package/dist/admin/crud/span.js +1 -1
  11. package/dist/admin/crud/style.js +1 -1
  12. package/dist/admin/crud/validate.js +1 -1
  13. package/dist/admin/directives/perm.js +1 -1
  14. package/dist/admin/hooks/useUpload.js +1 -1
  15. package/dist/admin/lib/browser.js +1 -1
  16. package/dist/admin/lib/cn.js +1 -1
  17. package/dist/admin/lib/dialog-float.js +1 -1
  18. package/dist/admin/lib/export-excel.js +1 -1
  19. package/dist/admin/lib/import-excel.js +1 -1
  20. package/dist/admin/lib/json.js +1 -1
  21. package/dist/admin/lib/menu.js +1 -1
  22. package/dist/admin/lib/tree.js +1 -1
  23. package/dist/admin/lib/upload.js +1 -1
  24. package/dist/admin/lib/video-frame.js +1 -1
  25. package/dist/agent/index.js +0 -23
  26. package/dist/index.js +1 -1
  27. package/dist/server/index.js +1 -1
  28. package/dist/shared/excel.js +1 -1
  29. package/dist/shared/index.js +1 -1
  30. package/dist/shared/locale-json.js +1 -1
  31. package/dist/shared/tree.js +1 -1
  32. package/dist/src/agent/index.d.ts +0 -1
  33. package/dist/src/agent/protocol.d.ts +1 -6
  34. package/dist/src/agent/types.d.ts +0 -28
  35. package/package.json +1 -1
  36. package/typings/admin/base/auth.d.ts +17 -20
  37. package/dist/src/agent/guards.d.ts +0 -3
@@ -1,34 +1,137 @@
1
1
  <template>
2
- <div class="vm-color-picker" :class="{ 'is-disabled': disabled }">
3
- <label class="vm-color-picker__swatch" :title="disabled ? undefined : '选择颜色'">
2
+ <div
3
+ ref="rootRef"
4
+ class="vm-color-picker"
5
+ :class="{ 'is-disabled': disabled, 'is-sm': size === 'sm', 'is-open': open }"
6
+ >
7
+ <button
8
+ ref="triggerRef"
9
+ type="button"
10
+ class="vm-color-picker__swatch"
11
+ :disabled="disabled"
12
+ :title="disabled ? undefined : '选择颜色'"
13
+ @click="toggle"
14
+ >
4
15
  <span
5
16
  class="vm-color-picker__swatch-fill"
6
17
  :style="{ background: cssColor }"
7
18
  />
8
- <input
9
- type="color"
10
- class="vm-color-picker__native"
11
- :value="pickerValue"
12
- :disabled="disabled"
13
- @input="onPicker"
14
- />
15
- </label>
19
+ </button>
20
+
16
21
  <input
17
22
  type="text"
18
23
  class="vm-color-picker__input"
19
- :value="display"
20
- :placeholder="placeholder"
24
+ :value="inlineHex"
25
+ :placeholder="placeholderText"
21
26
  :disabled="disabled"
22
27
  spellcheck="false"
23
28
  autocomplete="off"
24
- @input="onText"
25
- @blur="onBlur"
29
+ @input="onInlineInput"
30
+ @focus="openPanel"
31
+ @blur="onInlineBlur"
32
+ @keydown.esc.stop="closePanel"
26
33
  />
34
+
35
+ <Teleport to="body">
36
+ <FocusScope
37
+ v-if="open"
38
+ as-child
39
+ loop
40
+ :trapped="true"
41
+ @unmount-auto-focus="onPanelUnmountAutoFocus"
42
+ >
43
+ <div
44
+ ref="panelRef"
45
+ class="vm-color-picker__panel vm-dialog-float"
46
+ :style="panelStyle"
47
+ @pointerdown.stop
48
+ @mousedown.stop
49
+ >
50
+ <div
51
+ ref="svRef"
52
+ class="vm-color-picker__sv"
53
+ :style="{ background: svBg }"
54
+ @pointerdown="onSvPointer"
55
+ >
56
+ <span
57
+ class="vm-color-picker__sv-cursor"
58
+ :style="svCursorStyle"
59
+ />
60
+ </div>
61
+
62
+ <div class="vm-color-picker__sliders">
63
+ <button
64
+ v-if="hasEyeDropper"
65
+ type="button"
66
+ class="vm-color-picker__eyedropper"
67
+ title="吸取颜色"
68
+ @click="pickEyeDropper"
69
+ >
70
+ <i class="ri-eyedropper-line" aria-hidden="true" />
71
+ </button>
72
+ <div class="vm-color-picker__slider-col">
73
+ <div
74
+ ref="hueRef"
75
+ class="vm-color-picker__hue"
76
+ @pointerdown="onHuePointer"
77
+ >
78
+ <span
79
+ class="vm-color-picker__slider-thumb"
80
+ :style="{ left: `${(hue / 360) * 100}%` }"
81
+ />
82
+ </div>
83
+ <div
84
+ ref="alphaRef"
85
+ class="vm-color-picker__alpha"
86
+ @pointerdown="onAlphaPointer"
87
+ >
88
+ <span
89
+ class="vm-color-picker__alpha-fill"
90
+ :style="{ background: alphaBg }"
91
+ />
92
+ <span
93
+ class="vm-color-picker__slider-thumb"
94
+ :style="{ left: `${alpha * 100}%` }"
95
+ />
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="vm-color-picker__footer">
101
+ <span class="vm-color-picker__format" title="颜色格式">
102
+ Hex
103
+ <i class="ri-arrow-down-s-line" aria-hidden="true" />
104
+ </span>
105
+ <input
106
+ type="text"
107
+ class="vm-color-picker__hex"
108
+ :value="panelHex"
109
+ spellcheck="false"
110
+ autocomplete="off"
111
+ @input="onPanelHexInput"
112
+ @blur="onPanelHexBlur"
113
+ @keydown.esc.stop="closePanel"
114
+ />
115
+ <input
116
+ type="text"
117
+ class="vm-color-picker__alpha-pct"
118
+ :value="alphaPctText"
119
+ spellcheck="false"
120
+ autocomplete="off"
121
+ @input="onAlphaPctInput"
122
+ @blur="onAlphaPctBlur"
123
+ @keydown.esc.stop="closePanel"
124
+ />
125
+ </div>
126
+ </div>
127
+ </FocusScope>
128
+ </Teleport>
27
129
  </div>
28
130
  </template>
29
131
 
30
132
  <script setup lang="ts">
31
- import { computed, ref, watch } from 'vue'
133
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
134
+ import { FocusScope } from 'reka-ui'
32
135
 
33
136
  defineOptions({ name: 'vm-color-picker' })
34
137
 
@@ -37,9 +140,11 @@ const props = withDefaults(
37
140
  modelValue?: string | null
38
141
  disabled?: boolean
39
142
  placeholder?: string
143
+ size?: 'default' | 'sm'
40
144
  }>(),
41
145
  {
42
146
  placeholder: '#000000',
147
+ size: 'default',
43
148
  },
44
149
  )
45
150
 
@@ -48,65 +153,489 @@ const emit = defineEmits<{
48
153
  change: [v: string]
49
154
  }>()
50
155
 
51
- const FALLBACK = '#000000'
156
+ type Rgba = { r: number; g: number; b: number; a: number }
157
+ type Hsv = { h: number; s: number; v: number }
52
158
 
53
- const display = ref(props.modelValue ?? '')
159
+ const rootRef = ref<HTMLElement | null>(null)
160
+ const triggerRef = ref<HTMLElement | null>(null)
161
+ const panelRef = ref<HTMLElement | null>(null)
162
+ const svRef = ref<HTMLElement | null>(null)
163
+ const hueRef = ref<HTMLElement | null>(null)
164
+ const alphaRef = ref<HTMLElement | null>(null)
54
165
 
55
- watch(
56
- () => props.modelValue,
57
- (v) => {
58
- display.value = v ?? ''
59
- },
60
- )
166
+ const open = ref(false)
167
+ const suppressOpen = ref(false)
168
+ const panelStyle = ref<Record<string, string>>({})
169
+
170
+ const hue = ref(0)
171
+ const sat = ref(0)
172
+ const val = ref(0)
173
+ const alpha = ref(1)
174
+
175
+ /** 行内 / 面板 Hex 编辑中的草稿(不含 #) */
176
+ const inlineHex = ref('')
177
+ const panelHex = ref('')
178
+ const alphaPctText = ref('100%')
179
+
180
+ const hasEyeDropper = ref(false)
61
181
 
62
- /** 解析为 #rrggbb;无效返回 null */
63
- function parseHex(raw: string): string | null {
182
+ const placeholderText = computed(() => {
183
+ const p = props.placeholder || '#000000'
184
+ return p.startsWith('#') ? p.slice(1) : p
185
+ })
186
+
187
+ function clamp(n: number, min: number, max: number) {
188
+ return Math.min(max, Math.max(min, n))
189
+ }
190
+
191
+ function toHex2(n: number) {
192
+ return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0')
193
+ }
194
+
195
+ function rgbToHex(r: number, g: number, b: number, a = 1): string {
196
+ const base = `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`
197
+ if (a >= 1 - 1e-6) return base
198
+ return `${base}${toHex2(a * 255)}`
199
+ }
200
+
201
+ function hsvToRgb(h: number, s: number, v: number): { r: number; g: number; b: number } {
202
+ const hh = ((h % 360) + 360) % 360
203
+ const c = v * s
204
+ const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
205
+ const m = v - c
206
+ let rp = 0
207
+ let gp = 0
208
+ let bp = 0
209
+ if (hh < 60) {
210
+ rp = c
211
+ gp = x
212
+ } else if (hh < 120) {
213
+ rp = x
214
+ gp = c
215
+ } else if (hh < 180) {
216
+ gp = c
217
+ bp = x
218
+ } else if (hh < 240) {
219
+ gp = x
220
+ bp = c
221
+ } else if (hh < 300) {
222
+ rp = x
223
+ bp = c
224
+ } else {
225
+ rp = c
226
+ bp = x
227
+ }
228
+ return {
229
+ r: (rp + m) * 255,
230
+ g: (gp + m) * 255,
231
+ b: (bp + m) * 255,
232
+ }
233
+ }
234
+
235
+ function rgbToHsv(r: number, g: number, b: number): Hsv {
236
+ const rr = r / 255
237
+ const gg = g / 255
238
+ const bb = b / 255
239
+ const max = Math.max(rr, gg, bb)
240
+ const min = Math.min(rr, gg, bb)
241
+ const d = max - min
242
+ let h = 0
243
+ if (d !== 0) {
244
+ if (max === rr) h = ((gg - bb) / d) % 6
245
+ else if (max === gg) h = (bb - rr) / d + 2
246
+ else h = (rr - gg) / d + 4
247
+ h *= 60
248
+ if (h < 0) h += 360
249
+ }
250
+ const s = max === 0 ? 0 : d / max
251
+ return { h, s, v: max }
252
+ }
253
+
254
+ /** 解析为 rgba;无效返回 null */
255
+ function parseColor(raw: string): Rgba | null {
64
256
  const s = raw.trim()
65
257
  if (!s) return null
66
- let h = s.startsWith('#') ? s.slice(1) : s
67
- if (/^[0-9a-fA-F]{3}$/.test(h)) {
68
- h = h
69
- .split('')
70
- .map((c) => c + c)
71
- .join('')
72
- }
73
- if (/^[0-9a-fA-F]{6}$/.test(h)) return `#${h.toLowerCase()}`
74
- if (/^[0-9a-fA-F]{8}$/.test(h)) return `#${h.slice(0, 6).toLowerCase()}`
258
+
259
+ if (s.startsWith('#')) {
260
+ let h = s.slice(1)
261
+ if (/^[0-9a-fA-F]{3}$/.test(h)) {
262
+ h = h
263
+ .split('')
264
+ .map((c) => c + c)
265
+ .join('')
266
+ }
267
+ if (/^[0-9a-fA-F]{6}$/.test(h)) {
268
+ return {
269
+ r: parseInt(h.slice(0, 2), 16),
270
+ g: parseInt(h.slice(2, 4), 16),
271
+ b: parseInt(h.slice(4, 6), 16),
272
+ a: 1,
273
+ }
274
+ }
275
+ if (/^[0-9a-fA-F]{8}$/.test(h)) {
276
+ return {
277
+ r: parseInt(h.slice(0, 2), 16),
278
+ g: parseInt(h.slice(2, 4), 16),
279
+ b: parseInt(h.slice(4, 6), 16),
280
+ a: parseInt(h.slice(6, 8), 16) / 255,
281
+ }
282
+ }
283
+ return null
284
+ }
285
+
286
+ const bare = s.startsWith('#') ? s.slice(1) : s
287
+ if (/^[0-9a-fA-F]{3}$/.test(bare) || /^[0-9a-fA-F]{6}$/.test(bare) || /^[0-9a-fA-F]{8}$/.test(bare)) {
288
+ return parseColor(`#${bare}`)
289
+ }
290
+
291
+ const m = s.match(
292
+ /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i,
293
+ )
294
+ if (m) {
295
+ return {
296
+ r: clamp(Number(m[1]), 0, 255),
297
+ g: clamp(Number(m[2]), 0, 255),
298
+ b: clamp(Number(m[3]), 0, 255),
299
+ a: m[4] != null ? clamp(Number(m[4]), 0, 1) : 1,
300
+ }
301
+ }
75
302
  return null
76
303
  }
77
304
 
78
- const normalized = computed(() => parseHex(display.value))
305
+ function currentRgb() {
306
+ return hsvToRgb(hue.value, sat.value, val.value)
307
+ }
308
+
309
+ function currentHex(): string {
310
+ const { r, g, b } = currentRgb()
311
+ return rgbToHex(r, g, b, alpha.value)
312
+ }
313
+
314
+ const cssColor = computed(() => {
315
+ const parsed = parseColor(props.modelValue ?? '')
316
+ if (!parsed && !(props.modelValue ?? '').trim()) return 'transparent'
317
+ if (!parsed) return 'transparent'
318
+ const { r, g, b, a } = parsed
319
+ return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`
320
+ })
321
+
322
+ const svBg = computed(() => {
323
+ const { r, g, b } = hsvToRgb(hue.value, 1, 1)
324
+ return `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)}))`
325
+ })
326
+
327
+ const alphaBg = computed(() => {
328
+ const { r, g, b } = currentRgb()
329
+ const c = `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`
330
+ return `linear-gradient(to right, transparent, ${c})`
331
+ })
332
+
333
+ const svCursorStyle = computed(() => ({
334
+ left: `${sat.value * 100}%`,
335
+ top: `${(1 - val.value) * 100}%`,
336
+ }))
337
+
338
+ function syncTextsFromState() {
339
+ const hex = currentHex()
340
+ const bare = hex.startsWith('#') ? hex.slice(1) : hex
341
+ // 行内只展示 RGB 六位;透明度在面板 % 里改
342
+ inlineHex.value = bare.slice(0, 6)
343
+ panelHex.value = bare.slice(0, 6)
344
+ alphaPctText.value = `${Math.round(alpha.value * 100)}%`
345
+ }
346
+
347
+ function applyRgba(c: Rgba, emitValue = true) {
348
+ const hsv = rgbToHsv(c.r, c.g, c.b)
349
+ hue.value = hsv.h
350
+ sat.value = hsv.s
351
+ val.value = hsv.v
352
+ alpha.value = clamp(c.a, 0, 1)
353
+ syncTextsFromState()
354
+ if (emitValue) commit(currentHex())
355
+ }
79
356
 
80
- const cssColor = computed(() => normalized.value || 'transparent')
357
+ function loadFromModel(raw: string | null | undefined) {
358
+ const s = raw ?? ''
359
+ if (!s.trim()) {
360
+ hue.value = 0
361
+ sat.value = 0
362
+ val.value = 0
363
+ alpha.value = 1
364
+ inlineHex.value = ''
365
+ panelHex.value = ''
366
+ alphaPctText.value = '100%'
367
+ return
368
+ }
369
+ const parsed = parseColor(s)
370
+ if (!parsed) {
371
+ inlineHex.value = s.startsWith('#') ? s.slice(1) : s
372
+ return
373
+ }
374
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
375
+ hue.value = hsv.h
376
+ sat.value = hsv.s
377
+ val.value = hsv.v
378
+ alpha.value = parsed.a
379
+ syncTextsFromState()
380
+ }
81
381
 
82
- const pickerValue = computed(() => normalized.value || FALLBACK)
382
+ watch(
383
+ () => props.modelValue,
384
+ (v) => {
385
+ if (open.value) {
386
+ // 面板打开时若外部写入且与当前一致则跳过抖动
387
+ const cur = currentHex()
388
+ const parsed = parseColor(v ?? '')
389
+ if (parsed && rgbToHex(parsed.r, parsed.g, parsed.b, parsed.a) === cur) return
390
+ }
391
+ loadFromModel(v)
392
+ },
393
+ { immediate: true },
394
+ )
83
395
 
84
396
  function commit(v: string) {
85
- display.value = v
86
397
  emit('update:modelValue', v)
87
398
  emit('change', v)
88
399
  }
89
400
 
90
- function onPicker(e: Event) {
91
- const v = (e.target as HTMLInputElement).value
92
- commit(v.toLowerCase())
401
+ function commitFromState() {
402
+ syncTextsFromState()
403
+ commit(currentHex())
93
404
  }
94
405
 
95
- function onText(e: Event) {
406
+ function onInlineInput(e: Event) {
96
407
  const v = (e.target as HTMLInputElement).value
97
- display.value = v
98
- emit('update:modelValue', v)
99
- emit('change', v)
408
+ inlineHex.value = v.replace(/^#/, '')
409
+ const parsed = parseColor(inlineHex.value)
410
+ if (parsed) {
411
+ parsed.a = alpha.value
412
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
413
+ hue.value = hsv.h
414
+ sat.value = hsv.s
415
+ val.value = hsv.v
416
+ panelHex.value = inlineHex.value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6)
417
+ commit(currentHex())
418
+ } else {
419
+ const raw = inlineHex.value.trim()
420
+ commit(raw ? (raw.startsWith('#') ? raw : `#${raw}`) : '')
421
+ }
100
422
  }
101
423
 
102
- function onBlur() {
103
- const n = parseHex(display.value)
104
- if (display.value.trim() === '') {
424
+ function onInlineBlur() {
425
+ const t = inlineHex.value.trim()
426
+ if (!t) {
105
427
  commit('')
428
+ syncTextsFromState()
106
429
  return
107
430
  }
108
- if (n && n !== display.value) commit(n)
431
+ const parsed = parseColor(t)
432
+ if (parsed) {
433
+ parsed.a = alpha.value
434
+ applyRgba(parsed, true)
435
+ } else {
436
+ loadFromModel(props.modelValue)
437
+ }
438
+ }
439
+
440
+ function onPanelHexInput(e: Event) {
441
+ const v = (e.target as HTMLInputElement).value.replace(/^#/, '')
442
+ panelHex.value = v
443
+ const parsed = parseColor(v)
444
+ if (parsed) {
445
+ parsed.a = alpha.value
446
+ const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b)
447
+ hue.value = hsv.h
448
+ sat.value = hsv.s
449
+ val.value = hsv.v
450
+ inlineHex.value = v.replace(/[^0-9a-fA-F]/g, '').slice(0, 6)
451
+ commit(currentHex())
452
+ }
453
+ }
454
+
455
+ function onPanelHexBlur() {
456
+ const parsed = parseColor(panelHex.value)
457
+ if (parsed) {
458
+ parsed.a = alpha.value
459
+ applyRgba(parsed, true)
460
+ } else {
461
+ syncTextsFromState()
462
+ }
463
+ }
464
+
465
+ function onAlphaPctInput(e: Event) {
466
+ alphaPctText.value = (e.target as HTMLInputElement).value
467
+ const n = Number(String(alphaPctText.value).replace(/%/g, '').trim())
468
+ if (!Number.isFinite(n)) return
469
+ alpha.value = clamp(n / 100, 0, 1)
470
+ commitFromState()
471
+ }
472
+
473
+ function onAlphaPctBlur() {
474
+ const n = Number(String(alphaPctText.value).replace(/%/g, '').trim())
475
+ if (Number.isFinite(n)) {
476
+ alpha.value = clamp(n / 100, 0, 1)
477
+ }
478
+ syncTextsFromState()
479
+ commit(currentHex())
480
+ }
481
+
482
+ function ratioFromEvent(el: HTMLElement, e: PointerEvent, axis: 'x' | 'y' | 'both') {
483
+ const r = el.getBoundingClientRect()
484
+ const x = clamp((e.clientX - r.left) / r.width, 0, 1)
485
+ const y = clamp((e.clientY - r.top) / r.height, 0, 1)
486
+ if (axis === 'x') return { x, y: 0 }
487
+ if (axis === 'y') return { x: 0, y }
488
+ return { x, y }
489
+ }
490
+
491
+ function bindDrag(
492
+ el: HTMLElement,
493
+ e: PointerEvent,
494
+ onMove: (ev: PointerEvent) => void,
495
+ ) {
496
+ e.preventDefault()
497
+ el.setPointerCapture(e.pointerId)
498
+ onMove(e)
499
+ const move = (ev: PointerEvent) => onMove(ev)
500
+ const up = (ev: PointerEvent) => {
501
+ el.releasePointerCapture(ev.pointerId)
502
+ el.removeEventListener('pointermove', move)
503
+ el.removeEventListener('pointerup', up)
504
+ el.removeEventListener('pointercancel', up)
505
+ }
506
+ el.addEventListener('pointermove', move)
507
+ el.addEventListener('pointerup', up)
508
+ el.addEventListener('pointercancel', up)
509
+ }
510
+
511
+ function onSvPointer(e: PointerEvent) {
512
+ const el = svRef.value
513
+ if (!el) return
514
+ bindDrag(el, e, (ev) => {
515
+ const { x, y } = ratioFromEvent(el, ev, 'both')
516
+ sat.value = x
517
+ val.value = 1 - y
518
+ commitFromState()
519
+ })
520
+ }
521
+
522
+ function onHuePointer(e: PointerEvent) {
523
+ const el = hueRef.value
524
+ if (!el) return
525
+ bindDrag(el, e, (ev) => {
526
+ const { x } = ratioFromEvent(el, ev, 'x')
527
+ hue.value = x * 360
528
+ commitFromState()
529
+ })
109
530
  }
531
+
532
+ function onAlphaPointer(e: PointerEvent) {
533
+ const el = alphaRef.value
534
+ if (!el) return
535
+ bindDrag(el, e, (ev) => {
536
+ const { x } = ratioFromEvent(el, ev, 'x')
537
+ alpha.value = x
538
+ commitFromState()
539
+ })
540
+ }
541
+
542
+ async function pickEyeDropper() {
543
+ const ED = (window as unknown as { EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> } }).EyeDropper
544
+ if (!ED) return
545
+ try {
546
+ const result = await new ED().open()
547
+ const parsed = parseColor(result.sRGBHex)
548
+ if (parsed) {
549
+ parsed.a = alpha.value
550
+ applyRgba(parsed, true)
551
+ }
552
+ } catch {
553
+ /* 用户取消 */
554
+ }
555
+ }
556
+
557
+ function syncPanelPos() {
558
+ const el = triggerRef.value
559
+ if (!el) return
560
+ const r = el.getBoundingClientRect()
561
+ const width = 240
562
+ let left = r.left
563
+ if (left + width > window.innerWidth - 8) {
564
+ left = Math.max(8, window.innerWidth - width - 8)
565
+ }
566
+ let top = r.bottom + 6
567
+ const estH = 280
568
+ if (top + estH > window.innerHeight - 8) {
569
+ top = Math.max(8, r.top - estH - 6)
570
+ }
571
+ panelStyle.value = {
572
+ position: 'fixed',
573
+ top: `${top}px`,
574
+ left: `${left}px`,
575
+ width: `${width}px`,
576
+ zIndex: '100',
577
+ }
578
+ }
579
+
580
+ async function openPanel() {
581
+ if (props.disabled || suppressOpen.value) return
582
+ loadFromModel(props.modelValue)
583
+ open.value = true
584
+ await nextTick()
585
+ syncPanelPos()
586
+ }
587
+
588
+ function onPanelUnmountAutoFocus(event: Event) {
589
+ event.preventDefault()
590
+ }
591
+
592
+ function closePanel() {
593
+ if (!open.value) return
594
+ suppressOpen.value = true
595
+ open.value = false
596
+ nextTick(() => {
597
+ requestAnimationFrame(() => {
598
+ suppressOpen.value = false
599
+ })
600
+ })
601
+ }
602
+
603
+ async function toggle() {
604
+ if (props.disabled) return
605
+ if (open.value) {
606
+ closePanel()
607
+ return
608
+ }
609
+ await openPanel()
610
+ }
611
+
612
+ function onDocPointer(e: Event) {
613
+ if (!open.value) return
614
+ const t = e.target as Node | null
615
+ if (!t) return
616
+ if (rootRef.value?.contains(t)) return
617
+ if (panelRef.value?.contains(t)) return
618
+ closePanel()
619
+ }
620
+
621
+ function onWinChange() {
622
+ if (open.value) syncPanelPos()
623
+ }
624
+
625
+ onMounted(() => {
626
+ hasEyeDropper.value =
627
+ typeof window !== 'undefined' &&
628
+ 'EyeDropper' in window
629
+ document.addEventListener('pointerdown', onDocPointer, true)
630
+ window.addEventListener('resize', onWinChange)
631
+ window.addEventListener('scroll', onWinChange, true)
632
+ })
633
+
634
+ onUnmounted(() => {
635
+ document.removeEventListener('pointerdown', onDocPointer, true)
636
+ window.removeEventListener('resize', onWinChange)
637
+ window.removeEventListener('scroll', onWinChange, true)
638
+ })
110
639
  </script>
111
640
 
112
641
  <style lang="scss" scoped>
@@ -128,19 +657,35 @@ function onBlur() {
128
657
  flex-shrink: 0;
129
658
  width: var(--vm-control-height);
130
659
  height: var(--vm-control-height);
660
+ padding: 0;
131
661
  border: 1px solid var(--border);
132
662
  border-radius: var(--vm-control-radius);
133
663
  background: var(--muted);
134
664
  cursor: pointer;
135
665
  overflow: hidden;
666
+
667
+ &:hover:not(:disabled),
668
+ .is-open & {
669
+ border-color: var(--brand, #4e5dff);
670
+ }
671
+
672
+ &:disabled {
673
+ cursor: not-allowed;
674
+ }
675
+ }
676
+
677
+ .vm-color-picker.is-sm .vm-color-picker__swatch {
678
+ width: 16px;
679
+ height: 16px;
680
+ border-radius: 3px;
136
681
  }
137
682
 
138
683
  .vm-color-picker__swatch-fill {
139
684
  display: block;
140
685
  width: 100%;
141
686
  height: 100%;
142
- /* 无效色时显示棋盘,便于识别透明/空 */
143
- background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
687
+ background-image:
688
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
144
689
  linear-gradient(-45deg, #ccc 25%, transparent 25%),
145
690
  linear-gradient(45deg, transparent 75%, #ccc 75%),
146
691
  linear-gradient(-45deg, transparent 75%, #ccc 75%);
@@ -152,17 +697,6 @@ function onBlur() {
152
697
  -4px 0;
153
698
  }
154
699
 
155
- .vm-color-picker__native {
156
- position: absolute;
157
- inset: 0;
158
- width: 100%;
159
- height: 100%;
160
- padding: 0;
161
- border: 0;
162
- opacity: 0;
163
- cursor: pointer;
164
- }
165
-
166
700
  .vm-color-picker__input {
167
701
  flex: 1;
168
702
  min-width: 0;
@@ -185,4 +719,198 @@ function onBlur() {
185
719
  color: var(--muted-foreground);
186
720
  }
187
721
  }
722
+
723
+ .vm-color-picker.is-sm .vm-color-picker__input {
724
+ height: 24px;
725
+ padding: 0 6px;
726
+ font-size: 12px;
727
+ border-radius: 4px;
728
+ }
729
+ </style>
730
+
731
+ <style lang="scss">
732
+ /* Teleport 到 body,不可 scoped */
733
+ .vm-color-picker__panel {
734
+ box-sizing: border-box;
735
+ padding: 12px;
736
+ border: 1px solid var(--border);
737
+ border-radius: 8px;
738
+ background: var(--popover, var(--background, #fff));
739
+ color: var(--foreground);
740
+ box-shadow:
741
+ 0 4px 6px -1px rgb(0 0 0 / 10%),
742
+ 0 10px 24px -4px rgb(0 0 0 / 12%);
743
+ user-select: none;
744
+ }
745
+
746
+ .vm-color-picker__sv {
747
+ position: relative;
748
+ width: 100%;
749
+ aspect-ratio: 1;
750
+ border-radius: 6px;
751
+ cursor: crosshair;
752
+ touch-action: none;
753
+ overflow: hidden;
754
+ }
755
+
756
+ .vm-color-picker__sv-cursor {
757
+ position: absolute;
758
+ width: 14px;
759
+ height: 14px;
760
+ margin: -7px 0 0 -7px;
761
+ border: 2px solid #fff;
762
+ border-radius: 50%;
763
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
764
+ pointer-events: none;
765
+ }
766
+
767
+ .vm-color-picker__sliders {
768
+ display: flex;
769
+ align-items: center;
770
+ gap: 10px;
771
+ margin-top: 12px;
772
+ }
773
+
774
+ .vm-color-picker__eyedropper {
775
+ flex-shrink: 0;
776
+ display: inline-flex;
777
+ align-items: center;
778
+ justify-content: center;
779
+ width: 28px;
780
+ height: 28px;
781
+ padding: 0;
782
+ border: 1px solid var(--border);
783
+ border-radius: 6px;
784
+ background: var(--muted);
785
+ color: var(--foreground);
786
+ font-size: 15px;
787
+ cursor: pointer;
788
+
789
+ &:hover {
790
+ border-color: var(--brand, #4e5dff);
791
+ color: var(--brand, #4e5dff);
792
+ }
793
+ }
794
+
795
+ .vm-color-picker__slider-col {
796
+ flex: 1;
797
+ min-width: 0;
798
+ display: flex;
799
+ flex-direction: column;
800
+ gap: 8px;
801
+ }
802
+
803
+ .vm-color-picker__hue,
804
+ .vm-color-picker__alpha {
805
+ position: relative;
806
+ height: 12px;
807
+ border-radius: 999px;
808
+ cursor: pointer;
809
+ touch-action: none;
810
+ }
811
+
812
+ .vm-color-picker__hue {
813
+ background: linear-gradient(
814
+ to right,
815
+ #f00 0%,
816
+ #ff0 17%,
817
+ #0f0 33%,
818
+ #0ff 50%,
819
+ #00f 67%,
820
+ #f0f 83%,
821
+ #f00 100%
822
+ );
823
+ }
824
+
825
+ .vm-color-picker__alpha {
826
+ background-image:
827
+ linear-gradient(45deg, #c8c8c8 25%, transparent 25%),
828
+ linear-gradient(-45deg, #c8c8c8 25%, transparent 25%),
829
+ linear-gradient(45deg, transparent 75%, #c8c8c8 75%),
830
+ linear-gradient(-45deg, transparent 75%, #c8c8c8 75%);
831
+ background-size: 8px 8px;
832
+ background-position:
833
+ 0 0,
834
+ 0 4px,
835
+ 4px -4px,
836
+ -4px 0;
837
+ background-color: #fff;
838
+ overflow: hidden;
839
+ }
840
+
841
+ .vm-color-picker__alpha-fill {
842
+ position: absolute;
843
+ inset: 0;
844
+ border-radius: inherit;
845
+ pointer-events: none;
846
+ }
847
+
848
+ .vm-color-picker__slider-thumb {
849
+ position: absolute;
850
+ top: 50%;
851
+ width: 14px;
852
+ height: 14px;
853
+ margin: -7px 0 0 -7px;
854
+ border: 2px solid #fff;
855
+ border-radius: 50%;
856
+ background: transparent;
857
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
858
+ pointer-events: none;
859
+ }
860
+
861
+ .vm-color-picker__footer {
862
+ display: flex;
863
+ align-items: center;
864
+ gap: 6px;
865
+ margin-top: 12px;
866
+ }
867
+
868
+ .vm-color-picker__format {
869
+ flex-shrink: 0;
870
+ display: inline-flex;
871
+ align-items: center;
872
+ gap: 2px;
873
+ height: 28px;
874
+ padding: 0 6px;
875
+ border: 1px solid var(--border);
876
+ border-radius: 6px;
877
+ background: var(--muted);
878
+ color: var(--muted-foreground, var(--foreground));
879
+ font-size: 12px;
880
+ line-height: 1;
881
+ cursor: default;
882
+
883
+ i {
884
+ font-size: 14px;
885
+ opacity: 0.7;
886
+ }
887
+ }
888
+
889
+ .vm-color-picker__hex,
890
+ .vm-color-picker__alpha-pct {
891
+ height: 28px;
892
+ padding: 0 8px;
893
+ border: 1px solid var(--border);
894
+ border-radius: 6px;
895
+ background: var(--muted);
896
+ color: var(--foreground);
897
+ font-size: 12px;
898
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
899
+ outline: none;
900
+
901
+ &:focus {
902
+ border-color: var(--brand, #4e5dff);
903
+ }
904
+ }
905
+
906
+ .vm-color-picker__hex {
907
+ flex: 1;
908
+ min-width: 0;
909
+ }
910
+
911
+ .vm-color-picker__alpha-pct {
912
+ width: 52px;
913
+ flex-shrink: 0;
914
+ text-align: center;
915
+ }
188
916
  </style>