@veltra/desktop 1.6.0 → 1.6.2

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.
@@ -213,6 +213,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
213
213
  _rawInput.value = getDisplayed(model.value);
214
214
  }
215
215
  });
216
+ /**
217
+ * 步进后更新输入框的展示值
218
+ * 步长为 1 时直接展示目标值, 不播放数字滚动动画
219
+ * @param rawFrom 步进前的原始值(存在倍数时为除以倍数后的值)
220
+ * @param rawTarget 步进后的原始值(存在倍数时为除以倍数后的值)
221
+ */
222
+ function updateStepDisplay(rawFrom, rawTarget) {
223
+ tween.state.n = rawFrom;
224
+ if (stepVal.value === 1) {
225
+ tween.state.n = rawTarget;
226
+ const _rawInput = inputDom.value;
227
+ if (!_rawInput) return;
228
+ const { multiple } = props;
229
+ _rawInput.value = getDisplayed(multiple ? $n.mul(rawTarget, multiple) : rawTarget);
230
+ return;
231
+ }
232
+ tween.to({ n: rawTarget });
233
+ }
216
234
  /** 增 */
217
235
  function increase() {
218
236
  if (disabled.value) return;
@@ -223,13 +241,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
223
241
  const newRawVal = $n.plus(rawVal, stepVal.value);
224
242
  const target = getValidValue($n.mul(newRawVal, multiple));
225
243
  model.value = target;
226
- tween.state.n = $n.div(target, multiple);
227
- tween.to({ n: $n.div(target, multiple) });
244
+ updateStepDisplay($n.div(val, multiple), $n.div(target, multiple));
228
245
  } else {
229
- tween.state.n = val;
230
246
  const target = getValidValue($n.plus(val, stepVal.value));
231
247
  model.value = target;
232
- tween.to({ n: target });
248
+ updateStepDisplay(val, target);
233
249
  }
234
250
  }
235
251
  /** 减 */
@@ -242,13 +258,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
242
258
  const newRawVal = $n.minus(rawVal, stepVal.value);
243
259
  const target = getValidValue($n.mul(newRawVal, multiple));
244
260
  model.value = target;
245
- tween.state.n = $n.div(target, multiple);
246
- tween.to({ n: $n.div(target, multiple) });
261
+ updateStepDisplay($n.div(val, multiple), $n.div(target, multiple));
247
262
  } else {
248
- tween.state.n = val;
249
263
  const target = getValidValue($n.minus(val, stepVal.value));
250
264
  model.value = target;
251
- tween.to({ n: target });
265
+ updateStepDisplay(val, target);
252
266
  }
253
267
  }
254
268
  function handleKeydown(e) {
@@ -1 +1 @@
1
- {"version":3,"file":"number-input.js","names":[],"sources":["../../../src/components/number-input/number-input.vue"],"sourcesContent":["<template>\n <u-input\n v-if=\"!readonly\"\n ref=\"inputRef\"\n :class=\"className\"\n :model-value=\"displayed\"\n v-bind=\"inputProps\"\n @update:model-value=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @keydown.stop=\"handleKeydown\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n :size=\"size\"\n :readonly=\"readonly\"\n :disabled=\"disabled\"\n :clearable=\"false\"\n >\n <template #suffix v-if=\"hasSuffix\">\n <span :class=\"cls.e('suffix')\">\n <u-icon\n v-if=\"props.clearable\"\n :class=\"[cls.e('clear'), bem.is('hidden', !showClear)]\"\n :title=\"showClear ? '清除' : undefined\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <span v-if=\"hasCustomSuffix\" :class=\"cls.e('suffix-content')\">\n {{ suffix }}\n <slot name=\"suffix\" />\n </span>\n </span>\n\n <div v-if=\"showStep\" :class=\"cls.e('step')\">\n <u-icon\n @click=\"increase\"\n v-ripple=\"!disabled && increasable\"\n :class=\"bem.is('disabled', disabled || !increasable)\"\n >\n <ArrowUp />\n </u-icon>\n <u-icon\n @click=\"decrease\"\n v-ripple=\"!disabled && reducible\"\n :class=\"bem.is('disabled', disabled || !reducible)\"\n >\n <ArrowDown />\n </u-icon>\n </div>\n </template>\n\n <template #prefix v-if=\"slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n </u-input>\n\n <template v-else>\n {{ generateDisplayed || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { $n, n, o, isUndef } from '@cat-kit/core'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { vRipple } from '@veltra/directives'\nimport { ArrowDown, ArrowUp, Close } from '@veltra/icons/normal'\nimport { bem, FORM_EMPTY_CONTENT, Tween } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, shallowRef, useAttrs, watch } from 'vue'\n\nimport type { NumberInputEmits, NumberInputProps, InputExposed } from '../../types'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\n\ndefineOptions({ name: 'UNumberInput', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<NumberInputProps>(), {\n placeholder: '请输入',\n clearable: true,\n disabled: undefined,\n readonly: undefined\n})\nconst emit = defineEmits<NumberInputEmits>()\n\nconst slots = defineSlots<{ prefix?: () => any; suffix?: () => any }>()\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n size: 'default',\n disabled: false,\n readonly: false\n})\n\nconst attrs = useAttrs()\n\nconst inputProps = computed(() => ({\n ...attrs,\n ...o(props).pick(['disabled', 'placeholder', 'size', 'prefix'])\n}))\n\nconst inputRef = shallowRef<InputExposed>()\n\nconst inputDom = computed(() => inputRef.value?.el)\n\nconst cls = bem('number-input')\n\nconst className = computed(() => {\n return [cls.b, cls.m(size.value)]\n})\n\n/** 实际值 */\nconst model = defineModel<NumberInputProps['modelValue']>()\n\n// 展示值\nconst displayed = shallowRef('')\n\nconst focused = shallowRef(false)\nconst hovered = shallowRef(false)\n\nconst generateDisplayed = computed(() => {\n if (!displayed.value) return ''\n\n return `${props.prefix ?? ''}${displayed.value}${props.suffix ?? ''}`\n})\n\nconst showStep = computed(() => props.step !== undefined && props.step !== false)\n\nconst hasCustomSuffix = computed(() => !!props.suffix || !!slots.suffix)\n\nconst hasSuffix = computed(() => props.clearable || hasCustomSuffix.value || showStep.value)\n\nconst showClear = computed(() => {\n return props.clearable && !disabled.value && displayed.value !== '' && hovered.value\n})\n\n/** 步长 */\nconst stepVal = computed<number>(() => {\n const { step } = props\n if (step === undefined) return 1\n return typeof step === 'boolean' ? 1 : step\n})\n\n/** 是否可增 */\nconst increasable = computed(() => {\n const { max, multiple } = props\n if (isUndef(max) || isUndef(model.value)) return true\n // 如果存在倍数,先将 model.value 除以倍数得到原始值再比较\n const rawValue = multiple ? $n.div(model.value, multiple) : model.value\n return rawValue < max\n})\n\n/** 是否可减 */\nconst reducible = computed(() => {\n const { min, multiple } = props\n if (isUndef(min) || isUndef(model.value)) return true\n // 如果存在倍数,先将 model.value 除以倍数得到原始值再比较\n const rawValue = multiple ? $n.div(model.value, multiple) : model.value\n return rawValue > min\n})\n\n// 通过值和步长值计算默认的最大精度\nconst defaultMaxPrecision = computed(() => {\n const { multiple } = props\n // 如果存在倍数,基于原始值计算精度\n const rawValue =\n multiple && model.value !== undefined ? $n.div(model.value, multiple) : model.value\n return Math.max(\n String(rawValue).split('.')[1]?.length ?? 0,\n String(stepVal.value).split('.')[1]?.length ?? 0\n )\n})\n\n/**\n * 获取展示值\n * @param num 实际值\n */\nfunction getDisplayed(num?: number): string {\n if (!num && num !== 0) return ''\n\n const {\n currency,\n precision,\n minPrecision,\n // 如果没有指定最大精度那么设置默认为值和步长值中的较大值\n maxPrecision = defaultMaxPrecision.value,\n multiple\n } = props\n\n // 如果存在倍数,先将实际值除以倍数得到原始值\n const displayValue = multiple ? $n.div(num, multiple) : num\n\n return currency\n ? n(displayValue).currency('CNY', { precision, minPrecision, maxPrecision })\n : n(displayValue).fixed(precision ?? { minPrecision, maxPrecision })\n}\n\nwatch(\n [model, focused, () => props.currency],\n ([modelValue, inputFocused]) => {\n if (inputFocused) return\n displayed.value = getDisplayed(modelValue)\n },\n { immediate: true }\n)\n\n/**\n * 解析展示值\n * @param str 展示值\n */\nfunction parseDisplayed(str: string): number | undefined {\n if (!str) return undefined\n\n // 将货币格式去掉再转化为数字\n const number = +str.replace(/,/g, '')\n const { multiple } = props\n\n // 如果解析失败,使用当前值作为回退\n // 如果存在倍数,需要先将 model.value 除以倍数得到原始值\n let result: number | undefined\n if (isNaN(number)) {\n if (model.value === undefined) return undefined\n result = multiple ? $n.div(model.value, multiple) : model.value\n } else {\n result = number\n }\n\n if (result === undefined) return undefined\n\n const { precision, maxPrecision, minPrecision } = props\n\n const fixedResult = +n(result).fixed(precision ?? { minPrecision, maxPrecision })\n\n // 如果存在倍数,将原始值乘以倍数返回实际值\n return multiple ? $n.mul(fixedResult, multiple) : fixedResult\n}\n\n/**\n * 获取有效值\n * @param val 值\n */\nfunction getValidValue<T extends undefined | number>(val: T): T {\n if (val === undefined) return val\n const { min, max, multiple } = props\n\n // 如果存在倍数,先将值除以倍数得到原始值进行验证,验证后再乘以倍数\n if (multiple) {\n const rawVal = $n.div(val, multiple)\n let validRawVal = rawVal\n if (min !== undefined && rawVal < min) validRawVal = min\n if (max !== undefined && rawVal > max) validRawVal = max\n return $n.mul(validRawVal, multiple) as T\n }\n\n if (min !== undefined && val < min) return min as T\n if (max !== undefined && val > max) return max as T\n return val\n}\n\nfunction handleUpdateModelValue(input: string): void {\n const newVal = parseDisplayed(input)\n model.value = getValidValue(newVal)\n displayed.value = input\n}\n\nfunction handleClear(): void {\n if (!showClear.value) return\n model.value = undefined\n displayed.value = ''\n emit('clear')\n}\n\n/**\n * 处理输入值变化的函数, 该函数仅在输入框失去焦点时触发,\n * 主要用于在输入非数字的情况下的修正处理\n * @param input 输入的值\n */\nfunction handleChange() {\n emit('change', model.value)\n}\n\nconst tween = new Tween(\n { n: model.value ?? 0 },\n {\n onUpdate(state) {\n const _rawInput = inputDom.value\n if (!_rawInput) return\n const { multiple } = props\n // 如果存在倍数,tween.state.n 存储的是原始值,需要乘以倍数后传给 getDisplayed\n const actualValue = multiple ? $n.mul(state.n, multiple) : state.n\n _rawInput.value = getDisplayed(actualValue)\n },\n // 动画进行的过程值有可能被改变, 因此在onComplete中确保还原的是原本的值\n onComplete() {\n const _rawInput = inputDom.value\n if (!_rawInput) return\n _rawInput.value = getDisplayed(model.value)\n }\n }\n)\n\n/** 增 */\nfunction increase(): void {\n if (disabled.value) return\n const { multiple } = props\n const val = model.value ?? 0\n\n if (multiple) {\n // 如果存在倍数,先将实际值除以倍数得到原始值\n const rawVal = $n.div(val, multiple)\n // 在原始值基础上加步长\n const newRawVal = $n.plus(rawVal, stepVal.value)\n // 乘以倍数得到新的实际值\n const newVal = $n.mul(newRawVal, multiple)\n const target = getValidValue(newVal)\n model.value = target\n // tween 动画在原始值上进行\n tween.state.n = $n.div(target, multiple)\n tween.to({ n: $n.div(target, multiple) })\n } else {\n tween.state.n = val\n const target = getValidValue($n.plus(val, stepVal.value))\n model.value = target\n tween.to({ n: target })\n }\n}\n\n/** 减 */\nfunction decrease(): void {\n if (disabled.value) return\n const { multiple } = props\n const val = model.value ?? 0\n\n if (multiple) {\n // 如果存在倍数,先将实际值除以倍数得到原始值\n const rawVal = $n.div(val, multiple)\n // 在原始值基础上减步长\n const newRawVal = $n.minus(rawVal, stepVal.value)\n // 乘以倍数得到新的实际值\n const newVal = $n.mul(newRawVal, multiple)\n const target = getValidValue(newVal)\n model.value = target\n // tween 动画在原始值上进行\n tween.state.n = $n.div(target, multiple)\n tween.to({ n: $n.div(target, multiple) })\n } else {\n tween.state.n = val\n const target = getValidValue($n.minus(val, stepVal.value))\n model.value = target\n tween.to({ n: target })\n }\n}\n\nfunction handleKeydown(e: KeyboardEvent): void {\n if (!props.step) return\n if (e.key === 'ArrowUp') {\n e.preventDefault()\n return increase()\n }\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n return decrease()\n }\n}\n\nfunction handleFocus(): void {\n focused.value = true\n\n // 如果初始值和当前设置的精度不匹配则进行精度修正\n if (model.value === undefined) return\n const { precision, maxPrecision, minPrecision, multiple } = props\n\n if (multiple) {\n // 如果存在倍数,先除以倍数得到原始值进行精度修正,再乘以倍数\n const rawVal = $n.div(model.value, multiple)\n const fixedRawVal = +n(rawVal).fixed(precision ?? { maxPrecision, minPrecision })\n model.value = $n.mul(fixedRawVal, multiple)\n } else {\n model.value = +n(model.value).fixed(precision ?? { maxPrecision, minPrecision })\n }\n}\n\nfunction handleBlur(): void {\n focused.value = false\n model.value = props.modelValue\n}\n\nfunction handleMouseEnter(): void {\n hovered.value = true\n}\n\nfunction handleMouseLeave(): void {\n hovered.value = false\n}\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"number-input.js","names":[],"sources":["../../../src/components/number-input/number-input.vue"],"sourcesContent":["<template>\n <u-input\n v-if=\"!readonly\"\n ref=\"inputRef\"\n :class=\"className\"\n :model-value=\"displayed\"\n v-bind=\"inputProps\"\n @update:model-value=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @keydown.stop=\"handleKeydown\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n :size=\"size\"\n :readonly=\"readonly\"\n :disabled=\"disabled\"\n :clearable=\"false\"\n >\n <template #suffix v-if=\"hasSuffix\">\n <span :class=\"cls.e('suffix')\">\n <u-icon\n v-if=\"props.clearable\"\n :class=\"[cls.e('clear'), bem.is('hidden', !showClear)]\"\n :title=\"showClear ? '清除' : undefined\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <span v-if=\"hasCustomSuffix\" :class=\"cls.e('suffix-content')\">\n {{ suffix }}\n <slot name=\"suffix\" />\n </span>\n </span>\n\n <div v-if=\"showStep\" :class=\"cls.e('step')\">\n <u-icon\n @click=\"increase\"\n v-ripple=\"!disabled && increasable\"\n :class=\"bem.is('disabled', disabled || !increasable)\"\n >\n <ArrowUp />\n </u-icon>\n <u-icon\n @click=\"decrease\"\n v-ripple=\"!disabled && reducible\"\n :class=\"bem.is('disabled', disabled || !reducible)\"\n >\n <ArrowDown />\n </u-icon>\n </div>\n </template>\n\n <template #prefix v-if=\"slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n </u-input>\n\n <template v-else>\n {{ generateDisplayed || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { $n, n, o, isUndef } from '@cat-kit/core'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { vRipple } from '@veltra/directives'\nimport { ArrowDown, ArrowUp, Close } from '@veltra/icons/normal'\nimport { bem, FORM_EMPTY_CONTENT, Tween } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, shallowRef, useAttrs, watch } from 'vue'\n\nimport type { NumberInputEmits, NumberInputProps, InputExposed } from '../../types'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\n\ndefineOptions({ name: 'UNumberInput', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<NumberInputProps>(), {\n placeholder: '请输入',\n clearable: true,\n disabled: undefined,\n readonly: undefined\n})\nconst emit = defineEmits<NumberInputEmits>()\n\nconst slots = defineSlots<{ prefix?: () => any; suffix?: () => any }>()\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {\n size: 'default',\n disabled: false,\n readonly: false\n})\n\nconst attrs = useAttrs()\n\nconst inputProps = computed(() => ({\n ...attrs,\n ...o(props).pick(['disabled', 'placeholder', 'size', 'prefix'])\n}))\n\nconst inputRef = shallowRef<InputExposed>()\n\nconst inputDom = computed(() => inputRef.value?.el)\n\nconst cls = bem('number-input')\n\nconst className = computed(() => {\n return [cls.b, cls.m(size.value)]\n})\n\n/** 实际值 */\nconst model = defineModel<NumberInputProps['modelValue']>()\n\n// 展示值\nconst displayed = shallowRef('')\n\nconst focused = shallowRef(false)\nconst hovered = shallowRef(false)\n\nconst generateDisplayed = computed(() => {\n if (!displayed.value) return ''\n\n return `${props.prefix ?? ''}${displayed.value}${props.suffix ?? ''}`\n})\n\nconst showStep = computed(() => props.step !== undefined && props.step !== false)\n\nconst hasCustomSuffix = computed(() => !!props.suffix || !!slots.suffix)\n\nconst hasSuffix = computed(() => props.clearable || hasCustomSuffix.value || showStep.value)\n\nconst showClear = computed(() => {\n return props.clearable && !disabled.value && displayed.value !== '' && hovered.value\n})\n\n/** 步长 */\nconst stepVal = computed<number>(() => {\n const { step } = props\n if (step === undefined) return 1\n return typeof step === 'boolean' ? 1 : step\n})\n\n/** 是否可增 */\nconst increasable = computed(() => {\n const { max, multiple } = props\n if (isUndef(max) || isUndef(model.value)) return true\n // 如果存在倍数,先将 model.value 除以倍数得到原始值再比较\n const rawValue = multiple ? $n.div(model.value, multiple) : model.value\n return rawValue < max\n})\n\n/** 是否可减 */\nconst reducible = computed(() => {\n const { min, multiple } = props\n if (isUndef(min) || isUndef(model.value)) return true\n // 如果存在倍数,先将 model.value 除以倍数得到原始值再比较\n const rawValue = multiple ? $n.div(model.value, multiple) : model.value\n return rawValue > min\n})\n\n// 通过值和步长值计算默认的最大精度\nconst defaultMaxPrecision = computed(() => {\n const { multiple } = props\n // 如果存在倍数,基于原始值计算精度\n const rawValue =\n multiple && model.value !== undefined ? $n.div(model.value, multiple) : model.value\n return Math.max(\n String(rawValue).split('.')[1]?.length ?? 0,\n String(stepVal.value).split('.')[1]?.length ?? 0\n )\n})\n\n/**\n * 获取展示值\n * @param num 实际值\n */\nfunction getDisplayed(num?: number): string {\n if (!num && num !== 0) return ''\n\n const {\n currency,\n precision,\n minPrecision,\n // 如果没有指定最大精度那么设置默认为值和步长值中的较大值\n maxPrecision = defaultMaxPrecision.value,\n multiple\n } = props\n\n // 如果存在倍数,先将实际值除以倍数得到原始值\n const displayValue = multiple ? $n.div(num, multiple) : num\n\n return currency\n ? n(displayValue).currency('CNY', { precision, minPrecision, maxPrecision })\n : n(displayValue).fixed(precision ?? { minPrecision, maxPrecision })\n}\n\nwatch(\n [model, focused, () => props.currency],\n ([modelValue, inputFocused]) => {\n if (inputFocused) return\n displayed.value = getDisplayed(modelValue)\n },\n { immediate: true }\n)\n\n/**\n * 解析展示值\n * @param str 展示值\n */\nfunction parseDisplayed(str: string): number | undefined {\n if (!str) return undefined\n\n // 将货币格式去掉再转化为数字\n const number = +str.replace(/,/g, '')\n const { multiple } = props\n\n // 如果解析失败,使用当前值作为回退\n // 如果存在倍数,需要先将 model.value 除以倍数得到原始值\n let result: number | undefined\n if (isNaN(number)) {\n if (model.value === undefined) return undefined\n result = multiple ? $n.div(model.value, multiple) : model.value\n } else {\n result = number\n }\n\n if (result === undefined) return undefined\n\n const { precision, maxPrecision, minPrecision } = props\n\n const fixedResult = +n(result).fixed(precision ?? { minPrecision, maxPrecision })\n\n // 如果存在倍数,将原始值乘以倍数返回实际值\n return multiple ? $n.mul(fixedResult, multiple) : fixedResult\n}\n\n/**\n * 获取有效值\n * @param val 值\n */\nfunction getValidValue<T extends undefined | number>(val: T): T {\n if (val === undefined) return val\n const { min, max, multiple } = props\n\n // 如果存在倍数,先将值除以倍数得到原始值进行验证,验证后再乘以倍数\n if (multiple) {\n const rawVal = $n.div(val, multiple)\n let validRawVal = rawVal\n if (min !== undefined && rawVal < min) validRawVal = min\n if (max !== undefined && rawVal > max) validRawVal = max\n return $n.mul(validRawVal, multiple) as T\n }\n\n if (min !== undefined && val < min) return min as T\n if (max !== undefined && val > max) return max as T\n return val\n}\n\nfunction handleUpdateModelValue(input: string): void {\n const newVal = parseDisplayed(input)\n model.value = getValidValue(newVal)\n displayed.value = input\n}\n\nfunction handleClear(): void {\n if (!showClear.value) return\n model.value = undefined\n displayed.value = ''\n emit('clear')\n}\n\n/**\n * 处理输入值变化的函数, 该函数仅在输入框失去焦点时触发,\n * 主要用于在输入非数字的情况下的修正处理\n * @param input 输入的值\n */\nfunction handleChange() {\n emit('change', model.value)\n}\n\nconst tween = new Tween(\n { n: model.value ?? 0 },\n {\n onUpdate(state) {\n const _rawInput = inputDom.value\n if (!_rawInput) return\n const { multiple } = props\n // 如果存在倍数,tween.state.n 存储的是原始值,需要乘以倍数后传给 getDisplayed\n const actualValue = multiple ? $n.mul(state.n, multiple) : state.n\n _rawInput.value = getDisplayed(actualValue)\n },\n // 动画进行的过程值有可能被改变, 因此在onComplete中确保还原的是原本的值\n onComplete() {\n const _rawInput = inputDom.value\n if (!_rawInput) return\n _rawInput.value = getDisplayed(model.value)\n }\n }\n)\n\n/**\n * 步进后更新输入框的展示值\n * 步长为 1 时直接展示目标值, 不播放数字滚动动画\n * @param rawFrom 步进前的原始值(存在倍数时为除以倍数后的值)\n * @param rawTarget 步进后的原始值(存在倍数时为除以倍数后的值)\n */\nfunction updateStepDisplay(rawFrom: number, rawTarget: number): void {\n tween.state.n = rawFrom\n\n if (stepVal.value === 1) {\n tween.state.n = rawTarget\n const _rawInput = inputDom.value\n if (!_rawInput) return\n const { multiple } = props\n _rawInput.value = getDisplayed(multiple ? $n.mul(rawTarget, multiple) : rawTarget)\n return\n }\n\n // tween 动画在原始值上进行\n tween.to({ n: rawTarget })\n}\n\n/** 增 */\nfunction increase(): void {\n if (disabled.value) return\n const { multiple } = props\n const val = model.value ?? 0\n\n if (multiple) {\n // 如果存在倍数,先将实际值除以倍数得到原始值\n const rawVal = $n.div(val, multiple)\n // 在原始值基础上加步长\n const newRawVal = $n.plus(rawVal, stepVal.value)\n // 乘以倍数得到新的实际值\n const target = getValidValue($n.mul(newRawVal, multiple))\n model.value = target\n updateStepDisplay($n.div(val, multiple), $n.div(target, multiple))\n } else {\n const target = getValidValue($n.plus(val, stepVal.value))\n model.value = target\n updateStepDisplay(val, target)\n }\n}\n\n/** 减 */\nfunction decrease(): void {\n if (disabled.value) return\n const { multiple } = props\n const val = model.value ?? 0\n\n if (multiple) {\n // 如果存在倍数,先将实际值除以倍数得到原始值\n const rawVal = $n.div(val, multiple)\n // 在原始值基础上减步长\n const newRawVal = $n.minus(rawVal, stepVal.value)\n // 乘以倍数得到新的实际值\n const target = getValidValue($n.mul(newRawVal, multiple))\n model.value = target\n updateStepDisplay($n.div(val, multiple), $n.div(target, multiple))\n } else {\n const target = getValidValue($n.minus(val, stepVal.value))\n model.value = target\n updateStepDisplay(val, target)\n }\n}\n\nfunction handleKeydown(e: KeyboardEvent): void {\n if (!props.step) return\n if (e.key === 'ArrowUp') {\n e.preventDefault()\n return increase()\n }\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n return decrease()\n }\n}\n\nfunction handleFocus(): void {\n focused.value = true\n\n // 如果初始值和当前设置的精度不匹配则进行精度修正\n if (model.value === undefined) return\n const { precision, maxPrecision, minPrecision, multiple } = props\n\n if (multiple) {\n // 如果存在倍数,先除以倍数得到原始值进行精度修正,再乘以倍数\n const rawVal = $n.div(model.value, multiple)\n const fixedRawVal = +n(rawVal).fixed(precision ?? { maxPrecision, minPrecision })\n model.value = $n.mul(fixedRawVal, multiple)\n } else {\n model.value = +n(model.value).fixed(precision ?? { maxPrecision, minPrecision })\n }\n}\n\nfunction handleBlur(): void {\n focused.value = false\n model.value = props.modelValue\n}\n\nfunction handleMouseEnter(): void {\n hovered.value = true\n}\n\nfunction handleMouseLeave(): void {\n hovered.value = false\n}\n</script>\n"],"mappings":"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"}
@@ -100,26 +100,47 @@ function RGB2HSV(RGB) {
100
100
  };
101
101
  }
102
102
  /**
103
- * 将 HEX 颜色转换为 RGB 颜色
104
- * @param hex HEX 颜色
105
- * @returns RGB 颜色
103
+ * 将颜色字符串转换为 RGBA 颜色
104
+ * @description
105
+ * 支持 `#rgb`、`#rgba`、`#rrggbb`、`#rrggbbaa`(`#` 可省略)以及 `rgb()` / `rgba()`
106
+ * 函数表示;无法解析时返回 `null`,调用方应视为未绑定颜色而不是沿用错误的解析结果。
107
+ * @param color 颜色字符串
108
+ * @returns RGBA 颜色,解析失败返回 null
106
109
  */
107
- function HEX2RGBA(hex) {
108
- const rgb = {
109
- r: 0,
110
- g: 0,
111
- b: 0
112
- };
113
- let alpha = 1;
114
- if (hex.startsWith("#")) hex = hex.slice(1);
110
+ function HEX2RGBA(color) {
111
+ const input = color.trim();
112
+ const fnMatch = input.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?%?)\s*)?\)$/i);
113
+ if (fnMatch) {
114
+ const [, r, g, b, a] = fnMatch;
115
+ const rgb = {
116
+ r: +r,
117
+ g: +g,
118
+ b: +b
119
+ };
120
+ if (rgb.r > 255 || rgb.g > 255 || rgb.b > 255) return null;
121
+ let alpha = 1;
122
+ if (a !== void 0) {
123
+ alpha = a.endsWith("%") ? parseFloat(a) / 100 : parseFloat(a);
124
+ if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) return null;
125
+ }
126
+ return {
127
+ RGB: rgb,
128
+ alpha
129
+ };
130
+ }
131
+ let hex = input.startsWith("#") ? input.slice(1) : input;
115
132
  if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((char) => char + char).join("");
133
+ if (hex.length !== 6 && hex.length !== 8) return null;
134
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
135
+ let alpha = 1;
116
136
  const alphaStr = hex.slice(6);
117
137
  if (alphaStr) alpha = parseInt(alphaStr, 16) / 255;
118
- rgb.r = parseInt(hex.slice(0, 2), 16);
119
- rgb.g = parseInt(hex.slice(2, 4), 16);
120
- rgb.b = parseInt(hex.slice(4, 6), 16);
121
138
  return {
122
- RGB: rgb,
139
+ RGB: {
140
+ r: parseInt(hex.slice(0, 2), 16),
141
+ g: parseInt(hex.slice(2, 4), 16),
142
+ b: parseInt(hex.slice(4, 6), 16)
143
+ },
123
144
  alpha
124
145
  };
125
146
  }
@@ -1 +1 @@
1
- {"version":3,"file":"color-transform.js","names":[],"sources":["../../../src/components/palette/color-transform.ts"],"sourcesContent":["import type { PaletteRGB, PaletteHSV } from '../../types'\n\n/**\n * 将色相值转换为 RGB 颜色\n * @param h 色相值\n * @returns RGB 颜色\n */\nexport function HUE2RGB(h: number): PaletteRGB {\n // 将色相值映射到0-5的范围\n const segment = h / 60\n const x = 1 - Math.abs((segment % 2) - 1) // 计算中间值\n let r = 0,\n g = 0,\n b = 0\n // 根据色相所在的区间计算RGB值\n if (segment >= 0 && segment < 1) {\n ;[r, g, b] = [1, x, 0]\n } else if (segment < 2) {\n ;[r, g, b] = [x, 1, 0]\n } else if (segment < 3) {\n ;[r, g, b] = [0, 1, x]\n } else if (segment < 4) {\n ;[r, g, b] = [0, x, 1]\n } else if (segment < 5) {\n ;[r, g, b] = [x, 0, 1]\n } else {\n ;[r, g, b] = [1, 0, x]\n }\n // 将RGB值从0-1范围映射到0-255范围\n return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }\n}\n\n/**\n * 将色调、饱和度和亮度转换为 RGB 颜色\n * @param hsv 色调、饱和度和亮度\n */\nexport function HSV2RGB(hsv: PaletteHSV): PaletteRGB {\n const { h, s, v } = hsv\n\n // 使用色相区间映射获取基础RGB值\n const { r, g, b } = HUE2RGB(h)\n\n // 应用饱和度和亮度\n const saturationFactor = 1 - s\n const valueFactor = v\n\n return {\n r: Math.round((r + (255 - r) * saturationFactor) * valueFactor),\n g: Math.round((g + (255 - g) * saturationFactor) * valueFactor),\n b: Math.round((b + (255 - b) * saturationFactor) * valueFactor)\n }\n}\n\n/**\n * 将 RGB 颜色转换为 HEX 颜色\n * @param RGB RGB 颜色\n * @param alpha 透明度\n * @returns HEX 颜色\n */\nexport function RGB2HEX(RGB: PaletteRGB, alpha = 1): string {\n let hexStr = ['r', 'g', 'b']\n .map((key) => RGB[key].toString(16).padStart(2, '0'))\n .join('')\n .toUpperCase()\n\n if (alpha < 1) {\n hexStr += Math.round(alpha * 255)\n .toString(16)\n .padStart(2, '0')\n .toUpperCase()\n }\n\n return hexStr\n}\n\n/**\n * 将 RGB 颜色转换为 HSV 颜色\n * @param RGB RGB 颜色\n * @returns HSV 颜色\n */\nexport function RGB2HSV(RGB: PaletteRGB): PaletteHSV {\n // 归一化到0-1范围\n const red = RGB.r / 255\n const green = RGB.g / 255\n const blue = RGB.b / 255\n const max = Math.max(red, green, blue)\n const min = Math.min(red, green, blue)\n const delta = max - min\n let h = 0\n if (delta !== 0) {\n if (max === red) {\n h = ((green - blue) / delta) % 6\n } else if (max === green) {\n h = (blue - red) / delta + 2\n } else {\n h = (red - green) / delta + 4\n }\n h = (h * 60 + 360) % 360 // 确保正值\n }\n const s = max === 0 ? 0 : delta / max\n const v = max\n return { h, s, v }\n}\n\n/**\n * 将 HEX 颜色转换为 RGB 颜色\n * @param hex HEX 颜色\n * @returns RGB 颜色\n */\nexport function HEX2RGBA(hex: string): { RGB: PaletteRGB; alpha: number } {\n const rgb = { r: 0, g: 0, b: 0 }\n let alpha = 1\n\n if (hex.startsWith('#')) {\n hex = hex.slice(1)\n }\n\n if (hex.length === 3 || hex.length === 4) {\n hex = hex\n .split('')\n .map((char) => char + char)\n .join('')\n }\n\n const alphaStr = hex.slice(6)\n if (alphaStr) {\n alpha = parseInt(alphaStr, 16) / 255\n }\n rgb.r = parseInt(hex.slice(0, 2), 16)\n rgb.g = parseInt(hex.slice(2, 4), 16)\n rgb.b = parseInt(hex.slice(4, 6), 16)\n\n return { RGB: rgb, alpha }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"color-transform.js","names":[],"sources":["../../../src/components/palette/color-transform.ts"],"sourcesContent":["import type { PaletteRGB, PaletteHSV } from '../../types'\n\n/**\n * 将色相值转换为 RGB 颜色\n * @param h 色相值\n * @returns RGB 颜色\n */\nexport function HUE2RGB(h: number): PaletteRGB {\n // 将色相值映射到0-5的范围\n const segment = h / 60\n const x = 1 - Math.abs((segment % 2) - 1) // 计算中间值\n let r = 0,\n g = 0,\n b = 0\n // 根据色相所在的区间计算RGB值\n if (segment >= 0 && segment < 1) {\n ;[r, g, b] = [1, x, 0]\n } else if (segment < 2) {\n ;[r, g, b] = [x, 1, 0]\n } else if (segment < 3) {\n ;[r, g, b] = [0, 1, x]\n } else if (segment < 4) {\n ;[r, g, b] = [0, x, 1]\n } else if (segment < 5) {\n ;[r, g, b] = [x, 0, 1]\n } else {\n ;[r, g, b] = [1, 0, x]\n }\n // 将RGB值从0-1范围映射到0-255范围\n return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }\n}\n\n/**\n * 将色调、饱和度和亮度转换为 RGB 颜色\n * @param hsv 色调、饱和度和亮度\n */\nexport function HSV2RGB(hsv: PaletteHSV): PaletteRGB {\n const { h, s, v } = hsv\n\n // 使用色相区间映射获取基础RGB值\n const { r, g, b } = HUE2RGB(h)\n\n // 应用饱和度和亮度\n const saturationFactor = 1 - s\n const valueFactor = v\n\n return {\n r: Math.round((r + (255 - r) * saturationFactor) * valueFactor),\n g: Math.round((g + (255 - g) * saturationFactor) * valueFactor),\n b: Math.round((b + (255 - b) * saturationFactor) * valueFactor)\n }\n}\n\n/**\n * 将 RGB 颜色转换为 HEX 颜色\n * @param RGB RGB 颜色\n * @param alpha 透明度\n * @returns HEX 颜色\n */\nexport function RGB2HEX(RGB: PaletteRGB, alpha = 1): string {\n let hexStr = ['r', 'g', 'b']\n .map((key) => RGB[key].toString(16).padStart(2, '0'))\n .join('')\n .toUpperCase()\n\n if (alpha < 1) {\n hexStr += Math.round(alpha * 255)\n .toString(16)\n .padStart(2, '0')\n .toUpperCase()\n }\n\n return hexStr\n}\n\n/**\n * 将 RGB 颜色转换为 HSV 颜色\n * @param RGB RGB 颜色\n * @returns HSV 颜色\n */\nexport function RGB2HSV(RGB: PaletteRGB): PaletteHSV {\n // 归一化到0-1范围\n const red = RGB.r / 255\n const green = RGB.g / 255\n const blue = RGB.b / 255\n const max = Math.max(red, green, blue)\n const min = Math.min(red, green, blue)\n const delta = max - min\n let h = 0\n if (delta !== 0) {\n if (max === red) {\n h = ((green - blue) / delta) % 6\n } else if (max === green) {\n h = (blue - red) / delta + 2\n } else {\n h = (red - green) / delta + 4\n }\n h = (h * 60 + 360) % 360 // 确保正值\n }\n const s = max === 0 ? 0 : delta / max\n const v = max\n return { h, s, v }\n}\n\n/**\n * 将颜色字符串转换为 RGBA 颜色\n * @description\n * 支持 `#rgb`、`#rgba`、`#rrggbb`、`#rrggbbaa`(`#` 可省略)以及 `rgb()` / `rgba()`\n * 函数表示;无法解析时返回 `null`,调用方应视为未绑定颜色而不是沿用错误的解析结果。\n * @param color 颜色字符串\n * @returns RGBA 颜色,解析失败返回 null\n */\nexport function HEX2RGBA(color: string): { RGB: PaletteRGB; alpha: number } | null {\n const input = color.trim()\n\n // rgb() / rgba() 函数表示\n const fnMatch = input.match(\n /^rgba?\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})(?:\\s*,\\s*(\\d+(?:\\.\\d+)?%?)\\s*)?\\)$/i\n )\n if (fnMatch) {\n const [, r, g, b, a] = fnMatch\n const rgb = { r: +r!, g: +g!, b: +b! }\n if (rgb.r > 255 || rgb.g > 255 || rgb.b > 255) return null\n\n let alpha = 1\n if (a !== undefined) {\n alpha = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a)\n if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) return null\n }\n return { RGB: rgb, alpha }\n }\n\n let hex = input.startsWith('#') ? input.slice(1) : input\n\n if (hex.length === 3 || hex.length === 4) {\n hex = hex\n .split('')\n .map((char) => char + char)\n .join('')\n }\n\n if (hex.length !== 6 && hex.length !== 8) return null\n if (!/^[0-9a-fA-F]+$/.test(hex)) return null\n\n let alpha = 1\n const alphaStr = hex.slice(6)\n if (alphaStr) {\n alpha = parseInt(alphaStr, 16) / 255\n }\n\n const rgb = {\n r: parseInt(hex.slice(0, 2), 16),\n g: parseInt(hex.slice(2, 4), 16),\n b: parseInt(hex.slice(4, 6), 16)\n }\n\n return { RGB: rgb, alpha }\n}\n"],"mappings":"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"}
@@ -72,7 +72,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
72
72
  watch(color, (color) => {
73
73
  if (!color) return;
74
74
  if (isUserActive()) return;
75
- const { RGB, alpha } = HEX2RGBA(color);
75
+ const parsed = HEX2RGBA(color);
76
+ if (!parsed) return;
77
+ const { RGB, alpha } = parsed;
76
78
  const hsv = RGB2HSV(RGB);
77
79
  rest.updateHue(hsv.h);
78
80
  rest.updateSV({
@@ -1 +1 @@
1
- {"version":3,"file":"palette.js","names":[],"sources":["../../../src/components/palette/palette.vue"],"sourcesContent":["<template>\n <u-tip\n trigger=\"click\"\n :class=\"cls.e('panel')\"\n v-model:visible=\"visible\"\n :disabled=\"disabled || readonly\"\n >\n <span :class=\"className\" :style=\"{ backgroundColor: color }\"> </span>\n\n <template #content>\n <!-- 饱和度和亮度 -->\n <PaletteSV ref=\"palette-sv\" />\n\n <!-- 色相 -->\n <PaletteHue ref=\"palette-hue\" />\n\n <!-- 透明度 -->\n <PaletteAlpha ref=\"palette-alpha\" />\n\n <!-- 颜色切换 -->\n <PaletteColorSwitch v-model:color=\"color\" @clear=\"handleClear\" />\n </template>\n </u-tip>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useFormFallbackProps, useUserAction } from '@veltra/compositions'\nimport { bem } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, provide, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { PaletteProps } from '../../types'\nimport { UTip } from '../tip'\nimport { HSV2RGB, RGB2HEX, HEX2RGBA, RGB2HSV } from './color-transform'\nimport { PaletteDIKey } from './di'\nimport PaletteAlpha from './palette-alpha.vue'\nimport PaletteColorSwitch from './palette-color-switch.vue'\nimport PaletteHue from './palette-hue.vue'\nimport PaletteSV from './palette-sv.vue'\nimport { useHSV } from './use-hsv'\n\ndefineOptions({ name: 'UPalette' })\n\nconst props = withDefaults(defineProps<PaletteProps>(), {\n disabled: undefined,\n readonly: undefined\n})\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst cls = bem('palette')\n\nconst className = computed(() => {\n return [cls.b, cls.m(size.value), bem.is('no-color', !color.value)]\n})\n\nconst { HSV, alpha, ...rest } = useHSV()\n\nconst color = defineModel<string>()\nconst RGB = computed(() => HSV2RGB(HSV))\nconst { userAction, isUserActive } = useUserAction()\n\nconst visible = shallowRef(false)\n\n/** 清除时跳过一次内部状态到 color 的同步,防止 updateAlpha 把已清空的 color 覆盖回来 */\nlet skipColorUpdate = false\n\nconst handleClear = userAction(() => {\n if (alpha.value !== 1) {\n skipColorUpdate = true\n }\n color.value = ''\n rest.updateAlpha(1)\n visible.value = false\n})\n\nwatch([alpha, RGB], ([alpha, RGB]) => {\n if (skipColorUpdate) {\n skipColorUpdate = false\n return\n }\n color.value = `#${RGB2HEX(RGB, alpha)}`\n})\n\nconst paletteSVRef = useTemplateRef('palette-sv')\nconst paletteHueRef = useTemplateRef('palette-hue')\nconst paletteAlphaRef = useTemplateRef('palette-alpha')\n\nwatch(\n color,\n (color) => {\n if (!color) return\n if (isUserActive()) return\n\n const { RGB, alpha } = HEX2RGBA(color)\n const hsv = RGB2HSV(RGB)\n rest.updateHue(hsv.h)\n rest.updateSV({ s: hsv.s, v: hsv.v })\n rest.updateAlpha(alpha)\n\n paletteSVRef.value?.init()\n paletteHueRef.value?.init()\n paletteAlphaRef.value?.init()\n },\n { immediate: true }\n)\n\nprovide(PaletteDIKey, { cls, HSV, RGB, alpha, userAction, isUserActive, ...rest })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CA,MAAM,QAAQ;EAKd,MAAM,EAAE,cAAc,kBAAkB;EAExC,MAAM,EAAE,MAAM,UAAU,aAAa,qBAAqB,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC;EAElF,MAAM,MAAM,IAAI,SAAS;EAEzB,MAAM,YAAY,eAAe;GAC/B,OAAO;IAAC,IAAI;IAAG,IAAI,EAAE,KAAK,KAAK;IAAG,IAAI,GAAG,YAAY,CAAC,MAAM,KAAK;GAAC;EACpE,CAAC;EAED,MAAM,EAAE,KAAK,OAAO,GAAG,SAAS,OAAO;EAEvC,MAAM,QAAQ,SAAmB,SAAA,YAAC;EAClC,MAAM,MAAM,eAAe,QAAQ,GAAG,CAAC;EACvC,MAAM,EAAE,YAAY,iBAAiB,cAAc;EAEnD,MAAM,UAAU,WAAW,KAAK;;EAGhC,IAAI,kBAAkB;EAEtB,MAAM,cAAc,iBAAiB;GACnC,IAAI,MAAM,UAAU,GAClB,kBAAkB;GAEpB,MAAM,QAAQ;GACd,KAAK,YAAY,CAAC;GAClB,QAAQ,QAAQ;EAClB,CAAC;EAED,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS;GACpC,IAAI,iBAAiB;IACnB,kBAAkB;IAClB;GACF;GACA,MAAM,QAAQ,IAAI,QAAQ,KAAK,KAAK;EACtC,CAAC;EAED,MAAM,eAAe,eAAe,YAAY;EAChD,MAAM,gBAAgB,eAAe,aAAa;EAClD,MAAM,kBAAkB,eAAe,eAAe;EAEtD,MACE,QACC,UAAU;GACT,IAAI,CAAC,OAAO;GACZ,IAAI,aAAa,GAAG;GAEpB,MAAM,EAAE,KAAK,UAAU,SAAS,KAAK;GACrC,MAAM,MAAM,QAAQ,GAAG;GACvB,KAAK,UAAU,IAAI,CAAC;GACpB,KAAK,SAAS;IAAE,GAAG,IAAI;IAAG,GAAG,IAAI;GAAE,CAAC;GACpC,KAAK,YAAY,KAAK;GAEtB,aAAa,OAAO,KAAK;GACzB,cAAc,OAAO,KAAK;GAC1B,gBAAgB,OAAO,KAAK;EAC9B,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,QAAQ,cAAc;GAAE;GAAK;GAAK;GAAK;GAAO;GAAY;GAAc,GAAG;EAAK,CAAC;;GA5G/E,OAAA,UAAA,GAAA,YAqBQ,MAAA,WAAA,GAAA;IApBN,SAAQ;IACP,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;IACL,SAAS,QAAA;IAAA,oBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,QAAO,QAAA;IACvB,UAAU,MAAA,QAAA,KAAY,MAAA,QAAA;;IAIZ,SAAO,cACD;KAAf,mBAAA,UAAA;KACA,YAA8B,aAAA,EAAnB,KAAI,aAAY,GAAA,MAAA,GAAA;KAE3B,mBAAA,MAAA;KACA,YAAgC,aAAA,EAApB,KAAI,cAAa,GAAA,MAAA,GAAA;KAE7B,mBAAA,OAAA;KACA,YAAoC,aAAA,EAAtB,KAAI,gBAAe,GAAA,MAAA,GAAA;KAEjC,mBAAA,QAAA;KACA,YAAiE,aAAA;MAArC,OAAO,MAAA;MAAA,kBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAK,QAAA;MAAG,SAAO,MAAA,WAAA;;;IAbiB,SAAA,cAAA,CAArE,mBAAqE,QAAA;KAA9D,OAAK,eAAE,UAAA,KAAS;KAAG,OAAK,eAAA,EAAA,iBAAqB,MAAA,MAAK,CAAA"}
1
+ {"version":3,"file":"palette.js","names":[],"sources":["../../../src/components/palette/palette.vue"],"sourcesContent":["<template>\n <u-tip\n trigger=\"click\"\n :class=\"cls.e('panel')\"\n v-model:visible=\"visible\"\n :disabled=\"disabled || readonly\"\n >\n <span :class=\"className\" :style=\"{ backgroundColor: color }\"> </span>\n\n <template #content>\n <!-- 饱和度和亮度 -->\n <PaletteSV ref=\"palette-sv\" />\n\n <!-- 色相 -->\n <PaletteHue ref=\"palette-hue\" />\n\n <!-- 透明度 -->\n <PaletteAlpha ref=\"palette-alpha\" />\n\n <!-- 颜色切换 -->\n <PaletteColorSwitch v-model:color=\"color\" @clear=\"handleClear\" />\n </template>\n </u-tip>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useFormFallbackProps, useUserAction } from '@veltra/compositions'\nimport { bem } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, provide, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { PaletteProps } from '../../types'\nimport { UTip } from '../tip'\nimport { HSV2RGB, RGB2HEX, HEX2RGBA, RGB2HSV } from './color-transform'\nimport { PaletteDIKey } from './di'\nimport PaletteAlpha from './palette-alpha.vue'\nimport PaletteColorSwitch from './palette-color-switch.vue'\nimport PaletteHue from './palette-hue.vue'\nimport PaletteSV from './palette-sv.vue'\nimport { useHSV } from './use-hsv'\n\ndefineOptions({ name: 'UPalette' })\n\nconst props = withDefaults(defineProps<PaletteProps>(), {\n disabled: undefined,\n readonly: undefined\n})\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst cls = bem('palette')\n\nconst className = computed(() => {\n return [cls.b, cls.m(size.value), bem.is('no-color', !color.value)]\n})\n\nconst { HSV, alpha, ...rest } = useHSV()\n\nconst color = defineModel<string>()\nconst RGB = computed(() => HSV2RGB(HSV))\nconst { userAction, isUserActive } = useUserAction()\n\nconst visible = shallowRef(false)\n\n/** 清除时跳过一次内部状态到 color 的同步,防止 updateAlpha 把已清空的 color 覆盖回来 */\nlet skipColorUpdate = false\n\nconst handleClear = userAction(() => {\n if (alpha.value !== 1) {\n skipColorUpdate = true\n }\n color.value = ''\n rest.updateAlpha(1)\n visible.value = false\n})\n\nwatch([alpha, RGB], ([alpha, RGB]) => {\n if (skipColorUpdate) {\n skipColorUpdate = false\n return\n }\n color.value = `#${RGB2HEX(RGB, alpha)}`\n})\n\nconst paletteSVRef = useTemplateRef('palette-sv')\nconst paletteHueRef = useTemplateRef('palette-hue')\nconst paletteAlphaRef = useTemplateRef('palette-alpha')\n\nwatch(\n color,\n (color) => {\n if (!color) return\n if (isUserActive()) return\n\n const parsed = HEX2RGBA(color)\n // 无法解析的颜色(如非法字符)视为未绑定,跳过同步,避免把 NaN 写回 modelValue\n if (!parsed) return\n\n const { RGB, alpha } = parsed\n const hsv = RGB2HSV(RGB)\n rest.updateHue(hsv.h)\n rest.updateSV({ s: hsv.s, v: hsv.v })\n rest.updateAlpha(alpha)\n\n paletteSVRef.value?.init()\n paletteHueRef.value?.init()\n paletteAlphaRef.value?.init()\n },\n { immediate: true }\n)\n\nprovide(PaletteDIKey, { cls, HSV, RGB, alpha, userAction, isUserActive, ...rest })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CA,MAAM,QAAQ;EAKd,MAAM,EAAE,cAAc,kBAAkB;EAExC,MAAM,EAAE,MAAM,UAAU,aAAa,qBAAqB,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC;EAElF,MAAM,MAAM,IAAI,SAAS;EAEzB,MAAM,YAAY,eAAe;GAC/B,OAAO;IAAC,IAAI;IAAG,IAAI,EAAE,KAAK,KAAK;IAAG,IAAI,GAAG,YAAY,CAAC,MAAM,KAAK;GAAC;EACpE,CAAC;EAED,MAAM,EAAE,KAAK,OAAO,GAAG,SAAS,OAAO;EAEvC,MAAM,QAAQ,SAAmB,SAAA,YAAC;EAClC,MAAM,MAAM,eAAe,QAAQ,GAAG,CAAC;EACvC,MAAM,EAAE,YAAY,iBAAiB,cAAc;EAEnD,MAAM,UAAU,WAAW,KAAK;;EAGhC,IAAI,kBAAkB;EAEtB,MAAM,cAAc,iBAAiB;GACnC,IAAI,MAAM,UAAU,GAClB,kBAAkB;GAEpB,MAAM,QAAQ;GACd,KAAK,YAAY,CAAC;GAClB,QAAQ,QAAQ;EAClB,CAAC;EAED,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,SAAS;GACpC,IAAI,iBAAiB;IACnB,kBAAkB;IAClB;GACF;GACA,MAAM,QAAQ,IAAI,QAAQ,KAAK,KAAK;EACtC,CAAC;EAED,MAAM,eAAe,eAAe,YAAY;EAChD,MAAM,gBAAgB,eAAe,aAAa;EAClD,MAAM,kBAAkB,eAAe,eAAe;EAEtD,MACE,QACC,UAAU;GACT,IAAI,CAAC,OAAO;GACZ,IAAI,aAAa,GAAG;GAEpB,MAAM,SAAS,SAAS,KAAK;GAE7B,IAAI,CAAC,QAAQ;GAEb,MAAM,EAAE,KAAK,UAAU;GACvB,MAAM,MAAM,QAAQ,GAAG;GACvB,KAAK,UAAU,IAAI,CAAC;GACpB,KAAK,SAAS;IAAE,GAAG,IAAI;IAAG,GAAG,IAAI;GAAE,CAAC;GACpC,KAAK,YAAY,KAAK;GAEtB,aAAa,OAAO,KAAK;GACzB,cAAc,OAAO,KAAK;GAC1B,gBAAgB,OAAO,KAAK;EAC9B,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,QAAQ,cAAc;GAAE;GAAK;GAAK;GAAK;GAAO;GAAY;GAAc,GAAG;EAAK,CAAC;;GAhH/E,OAAA,UAAA,GAAA,YAqBQ,MAAA,WAAA,GAAA;IApBN,SAAQ;IACP,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;IACL,SAAS,QAAA;IAAA,oBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,QAAO,QAAA;IACvB,UAAU,MAAA,QAAA,KAAY,MAAA,QAAA;;IAIZ,SAAO,cACD;KAAf,mBAAA,UAAA;KACA,YAA8B,aAAA,EAAnB,KAAI,aAAY,GAAA,MAAA,GAAA;KAE3B,mBAAA,MAAA;KACA,YAAgC,aAAA,EAApB,KAAI,cAAa,GAAA,MAAA,GAAA;KAE7B,mBAAA,OAAA;KACA,YAAoC,aAAA,EAAtB,KAAI,gBAAe,GAAA,MAAA,GAAA;KAEjC,mBAAA,QAAA;KACA,YAAiE,aAAA;MAArC,OAAO,MAAA;MAAA,kBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAK,QAAA;MAAG,SAAO,MAAA,WAAA;;;IAbiB,SAAA,cAAA,CAArE,mBAAqE,QAAA;KAA9D,OAAK,eAAE,UAAA,KAAS;KAAG,OAAK,eAAA,EAAA,iBAAqB,MAAA,MAAK,CAAA"}
@@ -5,13 +5,16 @@
5
5
  * - `explicitWidth` 列:保持配置/拖拽锁定的宽度
6
6
  * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分
7
7
  * - 其余未显式设宽的列:均分容器剩余宽度
8
+ *
9
+ * 分配结果写入列节点自身的 `allocatedWidth`,不回写列配置的 `data.width`,
10
+ * 避免污染用户数据后在列森林重建时被误判为显式宽度。
8
11
  */
9
12
  function allocateLeafColumnWidths(leafColumns, containerWidth) {
10
13
  if (!containerWidth || !leafColumns.length) return;
11
14
  const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed);
12
15
  const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed);
13
16
  fixedFlexColumns.forEach((column) => {
14
- column.data.width = void 0;
17
+ column.allocatedWidth = void 0;
15
18
  });
16
19
  const explicitWidthTotal = leafColumns.filter((column) => column.explicitWidth).reduce((acc, cur) => acc + (cur.width ?? cur.minWidth), 0);
17
20
  const growBaseTotal = growColumns.reduce((acc, cur) => acc + cur.minWidth, 0);
@@ -20,10 +23,10 @@ function allocateLeafColumnWidths(leafColumns, containerWidth) {
20
23
  if (growColumns.length > 0 && totalBase < containerWidth) {
21
24
  const allocatedWidth = (containerWidth - totalBase) / growColumns.length;
22
25
  growColumns.forEach((column) => {
23
- column.width = column.minWidth + allocatedWidth;
26
+ column.allocatedWidth = column.minWidth + allocatedWidth;
24
27
  });
25
28
  } else growColumns.forEach((column) => {
26
- column.data.width = void 0;
29
+ column.allocatedWidth = void 0;
27
30
  });
28
31
  }
29
32
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"allocate-column-widths.js","names":[],"sources":["../../../src/components/table/allocate-column-widths.ts"],"sourcesContent":["import type { ColumnNode } from './node/col'\n\n/**\n * 按容器宽度分配叶子列宽。\n *\n * - `explicitWidth` 列:保持配置/拖拽锁定的宽度\n * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分\n * - 其余未显式设宽的列:均分容器剩余宽度\n */\nexport function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWidth: number): void {\n if (!containerWidth || !leafColumns.length) return\n\n const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed)\n // fixed 且未显式设宽:清掉可能被旧算法撑大的残留 width,回落到 minWidth\n const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed)\n\n fixedFlexColumns.forEach((column) => {\n column.data.width = undefined\n })\n\n const explicitWidthTotal = leafColumns\n .filter((column) => column.explicitWidth)\n .reduce((acc, cur) => acc + (cur.width ?? cur.minWidth!), 0)\n const growBaseTotal = growColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const fixedFlexBaseTotal = fixedFlexColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const totalBase = explicitWidthTotal + growBaseTotal + fixedFlexBaseTotal\n\n if (growColumns.length > 0 && totalBase < containerWidth) {\n const allocatedWidth = (containerWidth - totalBase) / growColumns.length\n growColumns.forEach((column) => {\n column.width = column.minWidth! + allocatedWidth\n })\n } else {\n growColumns.forEach((column) => {\n column.data.width = undefined\n })\n }\n}\n"],"mappings":";;;;;;;;AASA,SAAgB,yBAAyB,aAA2B,gBAA8B;CAChG,IAAI,CAAC,kBAAkB,CAAC,YAAY,QAAQ;CAE5C,MAAM,cAAc,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,CAAC,OAAO,KAAK;CAEzF,MAAM,mBAAmB,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,OAAO,KAAK;CAE7F,iBAAiB,SAAS,WAAW;EACnC,OAAO,KAAK,QAAQ,KAAA;CACtB,CAAC;CAED,MAAM,qBAAqB,YACxB,QAAQ,WAAW,OAAO,aAAa,CAAC,CACxC,QAAQ,KAAK,QAAQ,OAAO,IAAI,SAAS,IAAI,WAAY,CAAC;CAC7D,MAAM,gBAAgB,YAAY,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CAC7E,MAAM,qBAAqB,iBAAiB,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CACvF,MAAM,YAAY,qBAAqB,gBAAgB;CAEvD,IAAI,YAAY,SAAS,KAAK,YAAY,gBAAgB;EACxD,MAAM,kBAAkB,iBAAiB,aAAa,YAAY;EAClE,YAAY,SAAS,WAAW;GAC9B,OAAO,QAAQ,OAAO,WAAY;EACpC,CAAC;CACH,OACE,YAAY,SAAS,WAAW;EAC9B,OAAO,KAAK,QAAQ,KAAA;CACtB,CAAC;AAEL"}
1
+ {"version":3,"file":"allocate-column-widths.js","names":[],"sources":["../../../src/components/table/allocate-column-widths.ts"],"sourcesContent":["import type { ColumnNode } from './node/col'\n\n/**\n * 按容器宽度分配叶子列宽。\n *\n * - `explicitWidth` 列:保持配置/拖拽锁定的宽度\n * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分\n * - 其余未显式设宽的列:均分容器剩余宽度\n *\n * 分配结果写入列节点自身的 `allocatedWidth`,不回写列配置的 `data.width`,\n * 避免污染用户数据后在列森林重建时被误判为显式宽度。\n */\nexport function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWidth: number): void {\n if (!containerWidth || !leafColumns.length) return\n\n const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed)\n // fixed 且未显式设宽:清掉之前的分配结果,回落到 minWidth\n const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed)\n\n fixedFlexColumns.forEach((column) => {\n column.allocatedWidth = undefined\n })\n\n const explicitWidthTotal = leafColumns\n .filter((column) => column.explicitWidth)\n .reduce((acc, cur) => acc + (cur.width ?? cur.minWidth!), 0)\n const growBaseTotal = growColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const fixedFlexBaseTotal = fixedFlexColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const totalBase = explicitWidthTotal + growBaseTotal + fixedFlexBaseTotal\n\n if (growColumns.length > 0 && totalBase < containerWidth) {\n const allocatedWidth = (containerWidth - totalBase) / growColumns.length\n growColumns.forEach((column) => {\n column.allocatedWidth = column.minWidth! + allocatedWidth\n })\n } else {\n growColumns.forEach((column) => {\n column.allocatedWidth = undefined\n })\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,yBAAyB,aAA2B,gBAA8B;CAChG,IAAI,CAAC,kBAAkB,CAAC,YAAY,QAAQ;CAE5C,MAAM,cAAc,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,CAAC,OAAO,KAAK;CAEzF,MAAM,mBAAmB,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,OAAO,KAAK;CAE7F,iBAAiB,SAAS,WAAW;EACnC,OAAO,iBAAiB,KAAA;CAC1B,CAAC;CAED,MAAM,qBAAqB,YACxB,QAAQ,WAAW,OAAO,aAAa,CAAC,CACxC,QAAQ,KAAK,QAAQ,OAAO,IAAI,SAAS,IAAI,WAAY,CAAC;CAC7D,MAAM,gBAAgB,YAAY,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CAC7E,MAAM,qBAAqB,iBAAiB,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CACvF,MAAM,YAAY,qBAAqB,gBAAgB;CAEvD,IAAI,YAAY,SAAS,KAAK,YAAY,gBAAgB;EACxD,MAAM,kBAAkB,iBAAiB,aAAa,YAAY;EAClE,YAAY,SAAS,WAAW;GAC9B,OAAO,iBAAiB,OAAO,WAAY;EAC7C,CAAC;CACH,OACE,YAAY,SAAS,WAAW;EAC9B,OAAO,iBAAiB,KAAA;CAC1B,CAAC;AAEL"}
@@ -37,13 +37,26 @@ var ColumnNode = class extends TreeNode {
37
37
  }
38
38
  /** 宽度 */
39
39
  get width() {
40
- const { width } = this.data;
40
+ const width = this.data.width ?? this.derived.allocatedWidth;
41
41
  if (!width || this.minWidth && width && width < this.minWidth) return this.minWidth;
42
42
  return width;
43
43
  }
44
44
  set width(val) {
45
45
  if (this.minWidth && val && val < this.minWidth) val = this.minWidth;
46
46
  this.data.width = val;
47
+ this.derived.allocatedWidth = void 0;
48
+ }
49
+ /**
50
+ * 列宽分配算法写入的宽度。
51
+ * 与用户列配置 `data.width` 隔离:分配结果不回写列配置对象,
52
+ * 避免列森林重建(重挂载 / v-if / columns 引用变化)时把上一次的
53
+ * 分配残留误判为显式 width,导致列宽被永久锁定、容器变窄后出现滚动条。
54
+ */
55
+ get allocatedWidth() {
56
+ return this.derived.allocatedWidth;
57
+ }
58
+ set allocatedWidth(val) {
59
+ this.derived.allocatedWidth = val;
47
60
  }
48
61
  /** 最小宽度 */
49
62
  get minWidth() {
@@ -80,6 +93,8 @@ var ColumnNode = class extends TreeNode {
80
93
  */
81
94
  explicitWidth;
82
95
  style = reactive({});
96
+ /** 派生状态(响应式),与用户列配置 data 隔离 */
97
+ derived = shallowReactive({ allocatedWidth: void 0 });
83
98
  constructor(val, index, depth, parent) {
84
99
  super(isReactive(val) ? val : shallowReactive(val), index, depth, parent);
85
100
  this.explicitWidth = val.width !== void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"col.js","names":[],"sources":["../../../../src/components/table/node/col.ts"],"sourcesContent":["import { TreeNode } from '@cat-kit/core'\nimport { isReactive, reactive, shallowReactive } from 'vue'\n\nimport type { TableColumn, TableColumnAlign } from '../../../types'\n\nexport class ColumnNode extends TreeNode<TableColumn, ColumnNode> {\n declare children?: ColumnNode[] | undefined\n declare parent?: ColumnNode\n /** 叶子节点数量 */\n leafs?: number\n\n nextNode: ColumnNode | null = null\n\n /** 列key */\n get key(): string {\n return this.data.key\n }\n set key(val) {\n this.data.key = val\n }\n /** 列名 */\n get name(): string {\n return this.data.name\n }\n set name(val) {\n this.data.name = val\n }\n\n /**\n * 列对齐方式\n * @default 'left'\n */\n get align(): TableColumnAlign {\n return this.data.align ?? 'left'\n }\n set align(val) {\n this.data.align = val\n }\n\n get headerAlign(): TableColumnAlign {\n return this.data.headerAlign ?? this.align\n }\n set headerAlign(val) {\n this.data.headerAlign = val\n }\n\n /** 宽度 */\n get width(): number | undefined {\n const { width } = this.data\n if (!width || (this.minWidth && width && width < this.minWidth)) {\n return this.minWidth\n }\n return width\n }\n set width(val) {\n if (this.minWidth && val && val < this.minWidth) {\n val = this.minWidth\n }\n this.data.width = val\n }\n /** 最小宽度 */\n get minWidth(): number | undefined {\n const minWidth = this.data.minWidth\n if (!minWidth && this.isLeaf) {\n return 100\n }\n return minWidth\n }\n set minWidth(val) {\n this.data.minWidth = val\n }\n\n /** 列固定方向 */\n get fixed(): 'left' | 'right' | undefined {\n if (this.depth > 0) return\n return this.data.fixed\n }\n set fixed(val) {\n this.data.fixed = val\n }\n\n /** 是否是左侧的最后一个固定列 */\n get isLastFixed(): boolean {\n return this.data.isLastFixed ?? false\n }\n\n /** 是否是右侧的第一个固定列 */\n get isFirstFixed(): boolean {\n return this.data.isFirstFixed ?? false\n }\n\n /** 是否是可调整宽度的列 */\n get resizable(): boolean {\n return this.data.resizable ?? true\n }\n\n /**\n * 是否在列配置上显式指定了 width,或经表头拖拽锁定。\n * 此类列不参与剩余宽度的均分。\n */\n explicitWidth: boolean\n\n style: Record<string, number> = reactive({})\n\n constructor(val: TableColumn, index: number, depth: number, parent?: ColumnNode) {\n super(isReactive(val) ? val : shallowReactive(val), index, depth, parent)\n this.explicitWidth = val.width !== undefined\n }\n}\n"],"mappings":";;;AAKA,IAAa,aAAb,cAAgC,SAAkC;;CAIhE;CAEA,WAA8B;;CAG9B,IAAI,MAAc;EAChB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,IAAI,KAAK;EACX,KAAK,KAAK,MAAM;CAClB;;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,KAAK,KAAK;EACZ,KAAK,KAAK,OAAO;CACnB;;;;;CAMA,IAAI,QAA0B;EAC5B,OAAO,KAAK,KAAK,SAAS;CAC5B;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;CAEA,IAAI,cAAgC;EAClC,OAAO,KAAK,KAAK,eAAe,KAAK;CACvC;CACA,IAAI,YAAY,KAAK;EACnB,KAAK,KAAK,cAAc;CAC1B;;CAGA,IAAI,QAA4B;EAC9B,MAAM,EAAE,UAAU,KAAK;EACvB,IAAI,CAAC,SAAU,KAAK,YAAY,SAAS,QAAQ,KAAK,UACpD,OAAO,KAAK;EAEd,OAAO;CACT;CACA,IAAI,MAAM,KAAK;EACb,IAAI,KAAK,YAAY,OAAO,MAAM,KAAK,UACrC,MAAM,KAAK;EAEb,KAAK,KAAK,QAAQ;CACpB;;CAEA,IAAI,WAA+B;EACjC,MAAM,WAAW,KAAK,KAAK;EAC3B,IAAI,CAAC,YAAY,KAAK,QACpB,OAAO;EAET,OAAO;CACT;CACA,IAAI,SAAS,KAAK;EAChB,KAAK,KAAK,WAAW;CACvB;;CAGA,IAAI,QAAsC;EACxC,IAAI,KAAK,QAAQ,GAAG;EACpB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,KAAK,eAAe;CAClC;;CAGA,IAAI,eAAwB;EAC1B,OAAO,KAAK,KAAK,gBAAgB;CACnC;;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,KAAK,aAAa;CAChC;;;;;CAMA;CAEA,QAAgC,SAAS,CAAC,CAAC;CAE3C,YAAY,KAAkB,OAAe,OAAe,QAAqB;EAC/E,MAAM,WAAW,GAAG,IAAI,MAAM,gBAAgB,GAAG,GAAG,OAAO,OAAO,MAAM;EACxE,KAAK,gBAAgB,IAAI,UAAU,KAAA;CACrC;AACF"}
1
+ {"version":3,"file":"col.js","names":[],"sources":["../../../../src/components/table/node/col.ts"],"sourcesContent":["import { TreeNode } from '@cat-kit/core'\nimport { isReactive, reactive, shallowReactive } from 'vue'\n\nimport type { TableColumn, TableColumnAlign } from '../../../types'\n\nexport class ColumnNode extends TreeNode<TableColumn, ColumnNode> {\n declare children?: ColumnNode[] | undefined\n declare parent?: ColumnNode\n /** 叶子节点数量 */\n leafs?: number\n\n nextNode: ColumnNode | null = null\n\n /** 列key */\n get key(): string {\n return this.data.key\n }\n set key(val) {\n this.data.key = val\n }\n /** 列名 */\n get name(): string {\n return this.data.name\n }\n set name(val) {\n this.data.name = val\n }\n\n /**\n * 列对齐方式\n * @default 'left'\n */\n get align(): TableColumnAlign {\n return this.data.align ?? 'left'\n }\n set align(val) {\n this.data.align = val\n }\n\n get headerAlign(): TableColumnAlign {\n return this.data.headerAlign ?? this.align\n }\n set headerAlign(val) {\n this.data.headerAlign = val\n }\n\n /** 宽度 */\n get width(): number | undefined {\n const width = this.data.width ?? this.derived.allocatedWidth\n if (!width || (this.minWidth && width && width < this.minWidth)) {\n return this.minWidth\n }\n return width\n }\n set width(val) {\n if (this.minWidth && val && val < this.minWidth) {\n val = this.minWidth\n }\n this.data.width = val\n // 显式宽度(配置/拖拽)优先于分配宽度,旧的分配结果直接作废\n this.derived.allocatedWidth = undefined\n }\n\n /**\n * 列宽分配算法写入的宽度。\n * 与用户列配置 `data.width` 隔离:分配结果不回写列配置对象,\n * 避免列森林重建(重挂载 / v-if / columns 引用变化)时把上一次的\n * 分配残留误判为显式 width,导致列宽被永久锁定、容器变窄后出现滚动条。\n */\n get allocatedWidth(): number | undefined {\n return this.derived.allocatedWidth\n }\n set allocatedWidth(val: number | undefined) {\n this.derived.allocatedWidth = val\n }\n /** 最小宽度 */\n get minWidth(): number | undefined {\n const minWidth = this.data.minWidth\n if (!minWidth && this.isLeaf) {\n return 100\n }\n return minWidth\n }\n set minWidth(val) {\n this.data.minWidth = val\n }\n\n /** 列固定方向 */\n get fixed(): 'left' | 'right' | undefined {\n if (this.depth > 0) return\n return this.data.fixed\n }\n set fixed(val) {\n this.data.fixed = val\n }\n\n /** 是否是左侧的最后一个固定列 */\n get isLastFixed(): boolean {\n return this.data.isLastFixed ?? false\n }\n\n /** 是否是右侧的第一个固定列 */\n get isFirstFixed(): boolean {\n return this.data.isFirstFixed ?? false\n }\n\n /** 是否是可调整宽度的列 */\n get resizable(): boolean {\n return this.data.resizable ?? true\n }\n\n /**\n * 是否在列配置上显式指定了 width,或经表头拖拽锁定。\n * 此类列不参与剩余宽度的均分。\n */\n explicitWidth: boolean\n\n style: Record<string, number> = reactive({})\n\n /** 派生状态(响应式),与用户列配置 data 隔离 */\n private readonly derived = shallowReactive<{ allocatedWidth?: number }>({\n allocatedWidth: undefined\n })\n\n constructor(val: TableColumn, index: number, depth: number, parent?: ColumnNode) {\n super(isReactive(val) ? val : shallowReactive(val), index, depth, parent)\n this.explicitWidth = val.width !== undefined\n }\n}\n"],"mappings":";;;AAKA,IAAa,aAAb,cAAgC,SAAkC;;CAIhE;CAEA,WAA8B;;CAG9B,IAAI,MAAc;EAChB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,IAAI,KAAK;EACX,KAAK,KAAK,MAAM;CAClB;;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,KAAK,KAAK;EACZ,KAAK,KAAK,OAAO;CACnB;;;;;CAMA,IAAI,QAA0B;EAC5B,OAAO,KAAK,KAAK,SAAS;CAC5B;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;CAEA,IAAI,cAAgC;EAClC,OAAO,KAAK,KAAK,eAAe,KAAK;CACvC;CACA,IAAI,YAAY,KAAK;EACnB,KAAK,KAAK,cAAc;CAC1B;;CAGA,IAAI,QAA4B;EAC9B,MAAM,QAAQ,KAAK,KAAK,SAAS,KAAK,QAAQ;EAC9C,IAAI,CAAC,SAAU,KAAK,YAAY,SAAS,QAAQ,KAAK,UACpD,OAAO,KAAK;EAEd,OAAO;CACT;CACA,IAAI,MAAM,KAAK;EACb,IAAI,KAAK,YAAY,OAAO,MAAM,KAAK,UACrC,MAAM,KAAK;EAEb,KAAK,KAAK,QAAQ;EAElB,KAAK,QAAQ,iBAAiB,KAAA;CAChC;;;;;;;CAQA,IAAI,iBAAqC;EACvC,OAAO,KAAK,QAAQ;CACtB;CACA,IAAI,eAAe,KAAyB;EAC1C,KAAK,QAAQ,iBAAiB;CAChC;;CAEA,IAAI,WAA+B;EACjC,MAAM,WAAW,KAAK,KAAK;EAC3B,IAAI,CAAC,YAAY,KAAK,QACpB,OAAO;EAET,OAAO;CACT;CACA,IAAI,SAAS,KAAK;EAChB,KAAK,KAAK,WAAW;CACvB;;CAGA,IAAI,QAAsC;EACxC,IAAI,KAAK,QAAQ,GAAG;EACpB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,KAAK,eAAe;CAClC;;CAGA,IAAI,eAAwB;EAC1B,OAAO,KAAK,KAAK,gBAAgB;CACnC;;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,KAAK,aAAa;CAChC;;;;;CAMA;CAEA,QAAgC,SAAS,CAAC,CAAC;;CAG3C,UAA2B,gBAA6C,EACtE,gBAAgB,KAAA,EAClB,CAAC;CAED,YAAY,KAAkB,OAAe,OAAe,QAAqB;EAC/E,MAAM,WAAW,GAAG,IAAI,MAAM,gBAAgB,GAAG,GAAG,OAAO,OAAO,MAAM;EACxE,KAAK,gBAAgB,IAAI,UAAU,KAAA;CACrC;AACF"}
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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.0",
75
- "@veltra/directives": "^1.6.0",
83
+ "@veltra/compositions": "^1.6.2",
84
+ "@veltra/directives": "^1.6.2",
76
85
  "@veltra/icons": "^1.5.0",
77
- "@veltra/sheet-core": "^2.3.0",
78
- "@veltra/styles": "^1.6.0",
79
- "@veltra/utils": "^1.6.0",
86
+ "@veltra/sheet-core": "^2.3.1",
87
+ "@veltra/styles": "^1.6.2",
88
+ "@veltra/utils": "^1.6.2",
80
89
  "vue": ">=3.5.41"
81
90
  },
82
91
  "peerDependenciesMeta": {
@@ -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 newVal = $n.mul(newRawVal, multiple)
317
- const target = getValidValue(newVal)
338
+ const target = getValidValue($n.mul(newRawVal, multiple))
318
339
  model.value = target
319
- // tween 动画在原始值上进行
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
- tween.to({ n: target })
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 newVal = $n.mul(newRawVal, multiple)
343
- const target = getValidValue(newVal)
360
+ const target = getValidValue($n.mul(newRawVal, multiple))
344
361
  model.value = target
345
- // tween 动画在原始值上进行
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
- tween.to({ n: target })
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
- * 将 HEX 颜色转换为 RGB 颜色
107
- * @param hex HEX 颜色
108
- * @returns RGB 颜色
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(hex: string): { RGB: PaletteRGB; alpha: number } {
111
- const rgb = { r: 0, g: 0, b: 0 }
112
- let alpha = 1
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
- if (hex.startsWith('#')) {
115
- hex = hex.slice(1)
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
- rgb.r = parseInt(hex.slice(0, 2), 16)
130
- rgb.g = parseInt(hex.slice(2, 4), 16)
131
- rgb.b = parseInt(hex.slice(4, 6), 16)
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 { RGB, alpha } = HEX2RGBA(color)
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 })
@@ -13,14 +13,14 @@ describe('allocateLeafColumnWidths', () => {
13
13
  const name = createLeaf({ key: 'name', name: '姓名', width: 200 })
14
14
  const age = createLeaf({ key: 'age', name: '年龄', width: 200 })
15
15
  const action = createLeaf({ key: 'action', name: '操作', fixed: 'right' })
16
- // 模拟旧算法曾把操作列撑大
17
- action.data.width = 800
16
+ // 模拟上一次分配曾把操作列撑大
17
+ action.allocatedWidth = 800
18
18
 
19
19
  allocateLeafColumnWidths([name, age, action], 1000)
20
20
 
21
21
  expect(name.width).toBe(200)
22
22
  expect(age.width).toBe(200)
23
- expect(action.data.width).toBeUndefined()
23
+ expect(action.allocatedWidth).toBeUndefined()
24
24
  expect(action.width).toBe(100)
25
25
  })
26
26
 
@@ -45,7 +45,7 @@ describe('allocateLeafColumnWidths', () => {
45
45
  // totalBase = 100 + 100 + 100 = 300,剩余 700 只分给 age/score
46
46
  allocateLeafColumnWidths([name, age, score], 1000)
47
47
 
48
- expect(name.data.width).toBeUndefined()
48
+ expect(name.allocatedWidth).toBeUndefined()
49
49
  expect(name.width).toBe(100)
50
50
  expect(age.width).toBe(450)
51
51
  expect(score.width).toBe(450)
@@ -66,12 +66,53 @@ describe('allocateLeafColumnWidths', () => {
66
66
  it('无剩余空间时 grow 列回落到 minWidth', () => {
67
67
  const a = createLeaf({ key: 'a', name: 'A', width: 400 })
68
68
  const b = createLeaf({ key: 'b', name: 'B' })
69
- b.data.width = 300
69
+ b.allocatedWidth = 300
70
70
 
71
71
  allocateLeafColumnWidths([a, b], 450)
72
72
 
73
73
  expect(a.width).toBe(400)
74
- expect(b.data.width).toBeUndefined()
74
+ expect(b.allocatedWidth).toBeUndefined()
75
75
  expect(b.width).toBe(100)
76
76
  })
77
+
78
+ it('分配结果不写入列配置,森林重建后容器变窄仍会收缩', () => {
79
+ const config: TableColumn[] = [
80
+ { key: 'a', name: 'A' },
81
+ { key: 'b', name: 'B' }
82
+ ]
83
+
84
+ // 第一次挂载:宽容器
85
+ const first = config.map((c, i) => new ColumnNode(c, i, 0))
86
+ allocateLeafColumnWidths(first, 2000)
87
+ expect(first[0]!.width).toBe(1000)
88
+ expect(first[1]!.width).toBe(1000)
89
+
90
+ // 分配结果不得污染用户的列配置对象
91
+ expect(config[0]!.width).toBeUndefined()
92
+ expect(config[1]!.width).toBeUndefined()
93
+
94
+ // 模拟森林重建(路由返回 / v-if / columns 引用变化):
95
+ // 同一批列配置对象不应被误判为显式设宽
96
+ const rebuilt = config.map((c, i) => new ColumnNode(c, i, 0))
97
+ expect(rebuilt.every((node) => !node.explicitWidth)).toBe(true)
98
+
99
+ // 容器变窄后重新分配,列宽相应收缩
100
+ allocateLeafColumnWidths(rebuilt, 800)
101
+ expect(rebuilt[0]!.width).toBe(400)
102
+ expect(rebuilt[1]!.width).toBe(400)
103
+ })
104
+
105
+ it('显式宽度(配置/拖拽锁定)优先于分配宽度并使其作废', () => {
106
+ const col = createLeaf({ key: 'a', name: 'A' })
107
+ col.allocatedWidth = 500
108
+ expect(col.width).toBe(500)
109
+
110
+ // 模拟表头拖拽锁定
111
+ col.width = 300
112
+ col.explicitWidth = true
113
+
114
+ expect(col.data.width).toBe(300)
115
+ expect(col.allocatedWidth).toBeUndefined()
116
+ expect(col.width).toBe(300)
117
+ })
77
118
  })
@@ -6,16 +6,19 @@ import type { ColumnNode } from './node/col'
6
6
  * - `explicitWidth` 列:保持配置/拖拽锁定的宽度
7
7
  * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分
8
8
  * - 其余未显式设宽的列:均分容器剩余宽度
9
+ *
10
+ * 分配结果写入列节点自身的 `allocatedWidth`,不回写列配置的 `data.width`,
11
+ * 避免污染用户数据后在列森林重建时被误判为显式宽度。
9
12
  */
10
13
  export function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWidth: number): void {
11
14
  if (!containerWidth || !leafColumns.length) return
12
15
 
13
16
  const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed)
14
- // fixed 且未显式设宽:清掉可能被旧算法撑大的残留 width,回落到 minWidth
17
+ // fixed 且未显式设宽:清掉之前的分配结果,回落到 minWidth
15
18
  const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed)
16
19
 
17
20
  fixedFlexColumns.forEach((column) => {
18
- column.data.width = undefined
21
+ column.allocatedWidth = undefined
19
22
  })
20
23
 
21
24
  const explicitWidthTotal = leafColumns
@@ -28,11 +31,11 @@ export function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWid
28
31
  if (growColumns.length > 0 && totalBase < containerWidth) {
29
32
  const allocatedWidth = (containerWidth - totalBase) / growColumns.length
30
33
  growColumns.forEach((column) => {
31
- column.width = column.minWidth! + allocatedWidth
34
+ column.allocatedWidth = column.minWidth! + allocatedWidth
32
35
  })
33
36
  } else {
34
37
  growColumns.forEach((column) => {
35
- column.data.width = undefined
38
+ column.allocatedWidth = undefined
36
39
  })
37
40
  }
38
41
  }
@@ -46,7 +46,7 @@ export class ColumnNode extends TreeNode<TableColumn, ColumnNode> {
46
46
 
47
47
  /** 宽度 */
48
48
  get width(): number | undefined {
49
- const { width } = this.data
49
+ const width = this.data.width ?? this.derived.allocatedWidth
50
50
  if (!width || (this.minWidth && width && width < this.minWidth)) {
51
51
  return this.minWidth
52
52
  }
@@ -57,6 +57,21 @@ export class ColumnNode extends TreeNode<TableColumn, ColumnNode> {
57
57
  val = this.minWidth
58
58
  }
59
59
  this.data.width = val
60
+ // 显式宽度(配置/拖拽)优先于分配宽度,旧的分配结果直接作废
61
+ this.derived.allocatedWidth = undefined
62
+ }
63
+
64
+ /**
65
+ * 列宽分配算法写入的宽度。
66
+ * 与用户列配置 `data.width` 隔离:分配结果不回写列配置对象,
67
+ * 避免列森林重建(重挂载 / v-if / columns 引用变化)时把上一次的
68
+ * 分配残留误判为显式 width,导致列宽被永久锁定、容器变窄后出现滚动条。
69
+ */
70
+ get allocatedWidth(): number | undefined {
71
+ return this.derived.allocatedWidth
72
+ }
73
+ set allocatedWidth(val: number | undefined) {
74
+ this.derived.allocatedWidth = val
60
75
  }
61
76
  /** 最小宽度 */
62
77
  get minWidth(): number | undefined {
@@ -102,6 +117,11 @@ export class ColumnNode extends TreeNode<TableColumn, ColumnNode> {
102
117
 
103
118
  style: Record<string, number> = reactive({})
104
119
 
120
+ /** 派生状态(响应式),与用户列配置 data 隔离 */
121
+ private readonly derived = shallowReactive<{ allocatedWidth?: number }>({
122
+ allocatedWidth: undefined
123
+ })
124
+
105
125
  constructor(val: TableColumn, index: number, depth: number, parent?: ColumnNode) {
106
126
  super(isReactive(val) ? val : shallowReactive(val), index, depth, parent)
107
127
  this.explicitWidth = val.width !== undefined