diff --git a/CCUI-OPTIMIZATION-RISK-REPORT.md b/CCUI-OPTIMIZATION-RISK-REPORT.md new file mode 100644 index 00000000..36c4e15b --- /dev/null +++ b/CCUI-OPTIMIZATION-RISK-REPORT.md @@ -0,0 +1,391 @@ +# ccui 组件待人工介入清单(58 条) + +> 经多代理重确认(每条 2 个对立视角 + 分歧裁决):原 131 条待审项中,0 误报;73 条已确认为安全自动修并已提交(commit 759b980);下列为**真正需要人工决策**的项——涉及对外行为/API 变更、快照/测试契约、设计取舍或较大重构。13 条标注「未复核」是裁决代理偶发失败、保守列入。 + + +## 响应式(行为/重构/契约) — 8 条 + + +### avatar +- **[high]** img/name/error/nobody 元素在 setup 期一次性构建,prop 变更后不重渲 + `avatar/src/avatar.tsx` @ 54-124, 138-145 + 方向:Move all four JSX const blocks AND the hasImgElement/hasNameElement helper bodies inside the returned render function so createVNode re-runs per render. Keep ns, styleNs, the backgroundNs computed, showErrorAvatar, and the watch outside. Concretely: `return () + +### config-provider +- **[high]** provide 注入的是 computed 的快照 ctx.value,子树永久丢失响应性 + `config-provider/src/config-provider.tsx` @ 76 + 方向:Provide the computed ref and unwrap it inside useConfig so all existing consumers keep reading plain properties. (1) Change line 76 to `provide(CONFIG_INJECT_KEY, ctx)`. (2) Type the key as `InjectionKey>` (or cast) and rewrite useCo + +### tabs +- **[medium]** 外部修改 modelValue 不会同步 active(缺 watch) + `tabs/src/tabs.tsx` @ 16 + 方向:需在 import 中加入 watch(当前 line 2 只导入了 defineComponent, provide, reactive),再在 setup 内 state 声明后添加同步逻辑: + +import { defineComponent, provide, reactive, watch } from 'vue' + +watch(() => props.modelValue, (v) => { if (v !== state.active) state.active = v }) + +加 v !== sta + +### tree +- **[medium]** defaultExpandAll 只在 setup 同步执行一次,异步/后到的 data 不生效 + `tree/src/tree.tsx` @ 72 + 方向:The bug is real but requires changes in BOTH files because the proposed watch alone won't propagate. Option A (recommended, minimal-risk): keep computing initialExpandedAll reactively AND make useControllableSet react to defaultValue changes when uncontrolled. + +### descriptions +- **[medium]** computed 内调用 slot 渲染函数并缓存 VNode,存在 VNode 复用风险 + `descriptions/src/descriptions.tsx` @ 36-37 + 方向:把 slot 的 VNode 调用从 computed 内推迟到 render 内(thunk 形态),slot 路径不缓存 VNode 实例。最小修改如下: + +1) ResolvedItem 的 label/content 改为可承载 thunk: + label: VNode | string | (() => VNode) + content: VNode | string | number | (() => VNode) + +2) resolveFromSlots 第36-37行存 thunk 而非立刻调 + +### textarea +- **[medium]** defaultValue 会吞掉“受控且初值为空字符串”的合法场景 + `textarea/src/textarea.tsx` @ 53 + 方向:The proposed fix `props.defaultValue ?? props.modelValue` is a no-op: in that branch props.modelValue is already '' (its default), so it equals the current `props.defaultValue ?? ''`. A correct fix requires making "provided" detectable: in textarea-types.ts ch + +### masonry +- **[medium]** ⚠️未复核 用数组下标 ci/ii 作 key,列数变化时 DOM 复用错位 + `masonry/src/masonry.tsx` @ 109,112 + 方向:用全局序号 gi 做稳定身份,优先 VNode 自带 key。修改 columns 计算携带 gi,并在渲染用 key={vnode.key ?? gi}。 + +1) 把 columns 计算改为携带原始全局序号(合并 sequential/默认两分支,因二者逻辑当前完全相同): + +const columns = computed(() => { + const items = flatChildren(slots.default?.() ?? []) + const cols: { vnode: VNode, gi + +### affix +- **[low]** watch target 仅监听 prop 引用变化,函数型 target 返回值改变时不会重新绑定 + `affix/src/affix.tsx` @ 153-160 + 方向:优先采用文档化方案:明确"函数型 target 仅在挂载时解析一次,引用不变则不重绑"。若要代码修复,不应放进每帧 scroll 都会跑的 update()(会引入每次滚动调用 resolveTarget + DOM 查询的开销与潜在反复 add/removeEventListener),而应仅在 onMounted 的 requestAnimationFrame 回调里或 resize/ResizeObserver 等低频时机做一次比对:`const next = resolveTarget(props.targ + +## 逻辑(设计取舍/时序) — 8 条 + + +### date-picker +- **[high]** showTime 下点击 clear 未重置 pendingValue / pendingDirty,面板残留旧高亮且 ok 仍可点 + `date-picker/src/date-picker.tsx` @ 401-405 + 方向:The proposed fix is directionally right but unsafe as written: it calls initialPendingTime(), which reads selectedDayjs.value (a computed off props.modelValue). emitChange(null) only emits update:modelValue and does NOT synchronously update selectedDayjs, so i + +### tag +- **[high]** variant 的 filled/solid/outlined 修饰类无任何样式,filled 与 solid 渲染完全一致 + `tag/src/tag.scss` @ 119 + 方向:The defect is real, but a blanket `&--variant-solid` background rule is not minimal/safe: solid's promise is per-preset-color saturation, and the preset colors are generated in the `@each` loop (lines 119-126) as `background: var(--ccui-#{$name}-1)` plus statu + +### message +- **[medium]** enforceMaxCount 用 arr.shift() 直接移除最旧消息,跳过 leave 过渡动画 + `message/src/use-message.ts` @ 80 + 方向:The bug is real but the suggested "call the oldest item's close" fix is NOT directly applicable: MessageItem (message-item.tsx) keeps `visible` and `close` internal to setup and never `expose`s them, and the holder renders children via h(MessageItem,...) with + +### modal +- **[medium]** Escape 关闭对所有打开的 modal 全局生效,栈式多层 modal 会被同时关闭 + `modal/src/modal.tsx` @ 80-84 + 方向:引入模块级栈并把 push/pop 绑定到 open/close 生命周期(不能只在 keydown 内处理): +1) 文件顶部加 `const modalEscStack: number[] = []`。 +2) onKeydown 改为仅当本实例位于栈顶才响应,并阻止冒泡: +``` +const onKeydown = (e: KeyboardEvent) => { + if (e.key !== 'Escape' || !props.closeOnEsc || !isOpen.value) return + if + +### table-column +- **[medium]** 全局自增 columnSeq 决定列序,动态重挂载的列会跳到末尾 + `table-column/src/table-column.tsx` @ 7, 19 + 方向:Do not apply the accuser's "minimal" variant as-is: locating by a stable key within the original `columns` order is invalid here, because in template-collection mode there is no `columns` array to index into — columns exist only via register(). The correct fix + +### input-number +- **[medium]** handleInput 在每次击键时即 clamp 到 min/max 并按 precision 取整,妨碍正常输入 + `input-number/src/input-number.tsx` @ 111 + 方向:Two-part change. (1) In handleInput, parse the raw value without clamp/precision so typing isn't disrupted, e.g. replace lines 111-112 with: `const raw = Number.parseFloat(value); if (!Number.isNaN(raw)) updateValue(raw, false)` — leaving clamp+precision to ha + +### space-compact +- **[medium]** size 生成的修饰类无任何样式承接,且未透传到子项,是死输出 + `space-compact/src/space-compact.tsx` @ 16 + 方向:Do NOT delete line 16 — that breaks existing assertions in test/space-compact.test.ts:24-26 requiring ccui-space-compact--large / --small. Take option (a): add the missing scss rules so the modifier is consumed. In space-compact.scss, inside the root block, ad + +### time-picker +- **[low]** 「此刻」按钮不遵守 step 与 disabled 约束 + `time-picker/src/time-picker.tsx` @ 196-203 (clickNow) + 方向:Align `now` to an existing, non-disabled cell using the same column value lists that render the panel, instead of naive step rounding (columns floor to multiples of step starting at 0, and disabled cells must be skipped). Add a helper and use it in clickNow: + + + +## 可访问性 — 25 条 + + +### color-picker +- **[high]** 清除按钮是嵌套在 )} diff --git a/packages/ccui/ui/modal/src/modal.scss b/packages/ccui/ui/modal/src/modal.scss index 5066678d..b4310dd9 100644 --- a/packages/ccui/ui/modal/src/modal.scss +++ b/packages/ccui/ui/modal/src/modal.scss @@ -61,7 +61,7 @@ &:hover { color: $ccui-color-text; - background-color: rgba(0, 0, 0, 0.06); + background-color: $ccui-color-fill-secondary; } } @@ -108,38 +108,30 @@ border-color: $ccui-color-primary; } - &--cancel { - // default - } - &--primary { - color: #ffffff; + color: $ccui-color-text-light-solid; background-color: $ccui-color-primary; border-color: $ccui-color-primary; &:hover { background-color: $ccui-color-primary-hover; border-color: $ccui-color-primary-hover; - color: #ffffff; + color: $ccui-color-text-light-solid; } } &--danger { - color: #ffffff; + color: $ccui-color-text-light-solid; background-color: $ccui-color-error; border-color: $ccui-color-error; &:hover { background-color: $ccui-color-error-hover; border-color: $ccui-color-error-hover; - color: #ffffff; + color: $ccui-color-text-light-solid; } } - &--default { - // default - } - &.is-loading { cursor: not-allowed; opacity: 0.65; @@ -202,7 +194,7 @@ width: 22px; height: 22px; border-radius: 50%; - color: #ffffff; + color: $ccui-color-text-light-solid; font-size: 14px; font-weight: bold; line-height: 1; diff --git a/packages/ccui/ui/notification/src/notification-item.tsx b/packages/ccui/ui/notification/src/notification-item.tsx index 326b1f91..454b42ff 100644 --- a/packages/ccui/ui/notification/src/notification-item.tsx +++ b/packages/ccui/ui/notification/src/notification-item.tsx @@ -67,6 +67,7 @@ export default defineComponent({ style={props.styles?.root} role={props.role} aria-live={props.role === 'alert' ? 'assertive' : 'polite'} + aria-atomic={true} onMouseenter={onMouseenter} onMouseleave={onMouseleave} > diff --git a/packages/ccui/ui/notification/src/notification.scss b/packages/ccui/ui/notification/src/notification.scss index b96eba99..4b89364c 100644 --- a/packages/ccui/ui/notification/src/notification.scss +++ b/packages/ccui/ui/notification/src/notification.scss @@ -82,10 +82,8 @@ padding: 16px 24px; background-color: $ccui-color-bg-elevated; border-radius: $ccui-border-radius; - box-shadow: - 0 6px 16px 0 rgba(0, 0, 0, 0.08), - 0 3px 6px -4px rgba(0, 0, 0, 0.12), - 0 9px 28px 8px rgba(0, 0, 0, 0.05); + // 弹层阴影统一走 token,与 dropdown/select/menu 等浮层族对齐,深色模式下自动加深 + box-shadow: $ccui-box-shadow-secondary; } &__icon { diff --git a/packages/ccui/ui/notification/src/notification.ts b/packages/ccui/ui/notification/src/notification.ts index 34be62cd..4dcf794f 100644 --- a/packages/ccui/ui/notification/src/notification.ts +++ b/packages/ccui/ui/notification/src/notification.ts @@ -8,6 +8,7 @@ import type { NotificationType, } from './notification-types' import { createApp, h, reactive } from 'vue' +import { useNamespace } from '../../shared/hooks/use-namespace' import NotificationItem from './notification-item' import './notification.scss' @@ -19,6 +20,7 @@ interface NotificationInstance { const PLACEMENTS: NotificationPlacement[] = ['top', 'topRight', 'topLeft', 'bottom', 'bottomRight', 'bottomLeft'] let counter = 0 +const ns = useNamespace('notification') const containers = new Map() const apps = new Map>() const lists = reactive>({ @@ -50,8 +52,8 @@ function normalizeDuration(input: number | undefined, defaultSeconds: number): n function ensureContainer(placement: NotificationPlacement) { if (containers.has(placement)) return const el = document.createElement('div') - el.className = `ccui-notification ccui-notification--${placement}` - if (globalConfig.stack) el.classList.add('ccui-notification--stack') + el.className = `${ns.b()} ${ns.m(placement)}` + if (globalConfig.stack) el.classList.add(ns.m('stack')) if (placement === 'top' || placement.startsWith('top')) { if (globalConfig.top !== undefined) { el.style.top = typeof globalConfig.top === 'number' ? `${globalConfig.top}px` : globalConfig.top @@ -106,7 +108,11 @@ function enforceMaxCount(placement: NotificationPlacement) { const cap = globalConfig.maxCount ?? Infinity if (!isFinite(cap) || cap <= 0) return const arr = lists[placement] - while (arr.length > cap) arr.shift() + // 被挤掉的通知也要触发 onClose,避免用户注册的回调被静默吞掉 + while (arr.length > cap) { + const removed = arr.shift() + removed?.options.onClose?.() + } } function open(options: NotificationOptions): NotificationHandle { @@ -138,8 +144,8 @@ const notification = { config(cfg: NotificationGlobalConfig) { Object.assign(globalConfig, cfg) containers.forEach((el) => { - if (globalConfig.stack) el.classList.add('ccui-notification--stack') - else el.classList.remove('ccui-notification--stack') + if (globalConfig.stack) el.classList.add(ns.m('stack')) + else el.classList.remove(ns.m('stack')) }) }, destroy(): void { diff --git a/packages/ccui/ui/notification/src/use-notification.ts b/packages/ccui/ui/notification/src/use-notification.ts index aef53e8a..36f9ec8f 100644 --- a/packages/ccui/ui/notification/src/use-notification.ts +++ b/packages/ccui/ui/notification/src/use-notification.ts @@ -7,6 +7,7 @@ import type { NotificationType, } from './notification-types' import { defineComponent, h, reactive, Teleport } from 'vue' +import { useNamespace } from '../../shared/hooks/use-namespace' import NotificationItem from './notification-item' import './notification.scss' @@ -83,7 +84,11 @@ export function useNotification(): UseNotificationReturn { const cap = config.maxCount ?? Infinity if (!isFinite(cap) || cap <= 0) return const arr = lists[placement] - while (arr.length > cap) arr.shift() + // 被挤掉的通知也要触发 onClose,避免用户注册的回调被静默吞掉 + while (arr.length > cap) { + const removed = arr.shift() + removed?.options.onClose?.() + } } function open(options: NotificationOptions): NotificationHandle { @@ -123,6 +128,7 @@ export function useNotification(): UseNotificationReturn { const holder: Component = defineComponent({ name: 'CNotificationHolder', setup() { + const ns = useNamespace('notification') const onDestroy = (placement: NotificationPlacement, id: string) => { const arr = lists[placement] const idx = arr.findIndex((it) => it.id === id) @@ -143,7 +149,7 @@ export function useNotification(): UseNotificationReturn { 'div', { key: p, - class: ['ccui-notification', `ccui-notification--${p}`, config.stack && 'ccui-notification--stack'], + class: [ns.b(), ns.m(p), config.stack && ns.m('stack')], style: containerStyle, }, lists[p].map((it) => diff --git a/packages/ccui/ui/pagination/src/pagination.tsx b/packages/ccui/ui/pagination/src/pagination.tsx index b1260b4a..0d3f4a70 100644 --- a/packages/ccui/ui/pagination/src/pagination.tsx +++ b/packages/ccui/ui/pagination/src/pagination.tsx @@ -153,8 +153,18 @@ export default defineComponent({
  • goTo(page)} + onKeydown={(e: KeyboardEvent) => { + // 键盘可达:Enter / Space 触发与点击相同的跳页逻辑 + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + goTo(page) + } + }} > {page}
  • @@ -216,7 +226,19 @@ export default defineComponent({ {renderTotal()}
  • { + // 键盘可达:禁用态忽略,Enter / Space 触发与点击相同的上一页逻辑 + if (prevDisabled) { + return + } + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handlePrev() + } + }} aria-disabled={prevDisabled} aria-label={locale.value.prevPage || '上一页'} > @@ -246,7 +268,19 @@ export default defineComponent({
  • { + // 键盘可达:禁用态忽略,Enter / Space 触发与点击相同的下一页逻辑 + if (nextDisabled) { + return + } + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleNext() + } + }} aria-disabled={nextDisabled} aria-label={locale.value.nextPage || '下一页'} > diff --git a/packages/ccui/ui/popconfirm/src/popconfirm-types.ts b/packages/ccui/ui/popconfirm/src/popconfirm-types.ts index 581376e2..a239d286 100644 --- a/packages/ccui/ui/popconfirm/src/popconfirm-types.ts +++ b/packages/ccui/ui/popconfirm/src/popconfirm-types.ts @@ -40,7 +40,7 @@ export const popconfirmProps = { }, iconColor: { type: String, - default: '#faad14', + default: '', }, hideIcon: { type: Boolean, diff --git a/packages/ccui/ui/popconfirm/src/popconfirm.scss b/packages/ccui/ui/popconfirm/src/popconfirm.scss index cbe95cd4..67149ad4 100644 --- a/packages/ccui/ui/popconfirm/src/popconfirm.scss +++ b/packages/ccui/ui/popconfirm/src/popconfirm.scss @@ -27,7 +27,7 @@ height: 16px; font-size: 12px; font-weight: bold; - color: #ffffff; + color: $ccui-color-white; background: $ccui-color-warning; border-radius: 50%; line-height: 1; @@ -77,36 +77,30 @@ border-color: $ccui-color-primary; } - &--cancel { - // default style - } + // --cancel / --default 继承基础按钮样式 &--primary { - color: #ffffff; + color: $ccui-color-white; background-color: $ccui-color-primary; border-color: $ccui-color-primary; &:hover { - color: #ffffff; + color: $ccui-color-white; background-color: $ccui-color-primary-hover; border-color: $ccui-color-primary-hover; } } &--danger { - color: #ffffff; + color: $ccui-color-white; background-color: $ccui-color-error; border-color: $ccui-color-error; &:hover { - color: #ffffff; + color: $ccui-color-white; background-color: $ccui-color-error-hover; border-color: $ccui-color-error-hover; } } - - &--default { - // default - } } } diff --git a/packages/ccui/ui/popconfirm/src/popconfirm.tsx b/packages/ccui/ui/popconfirm/src/popconfirm.tsx index f4948342..bb686f7b 100644 --- a/packages/ccui/ui/popconfirm/src/popconfirm.tsx +++ b/packages/ccui/ui/popconfirm/src/popconfirm.tsx @@ -32,11 +32,13 @@ export default defineComponent({ }) const close = () => { - if (!isControlled.value) { - innerVisible.value = false + if (isControlled.value) { + // 受控:直接通知父,由父改 visible 收口关闭,不再调用 popover.hide() 触发二次 emit + emit('update:visible', false) + } else { + // 非受控:仅调 popover.hide(),由其 emit 经 onUpdate:visible 同步 innerVisible 并向上转发一次 + popoverRef.value?.hide?.() } - emit('update:visible', false) - popoverRef.value?.hide?.() } const onConfirm = (e: MouseEvent) => { @@ -70,7 +72,7 @@ export default defineComponent({
    {!props.hideIcon && ( - + {props.icon ? : !} )} diff --git a/packages/ccui/ui/popover/src/popover.scss b/packages/ccui/ui/popover/src/popover.scss index f1839be9..839a6218 100644 --- a/packages/ccui/ui/popover/src/popover.scss +++ b/packages/ccui/ui/popover/src/popover.scss @@ -24,14 +24,14 @@ background: $ccui-feedback-overlay-bg; color: $ccui-light-text; border: 1px solid $ccui-feedback-overlay-bg; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + box-shadow: $ccui-box-shadow-secondary; } &--light { background: $ccui-base-bg; color: $ccui-aide-text-stress; border: 1px solid $ccui-color-border; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + box-shadow: $ccui-box-shadow-secondary; } } diff --git a/packages/ccui/ui/popover/src/popover.tsx b/packages/ccui/ui/popover/src/popover.tsx index f4325da1..f064409d 100644 --- a/packages/ccui/ui/popover/src/popover.tsx +++ b/packages/ccui/ui/popover/src/popover.tsx @@ -271,7 +271,37 @@ export default defineComponent({ let cleanup: (() => void) | undefined + // 虚拟触发:把触发事件绑定到外部 virtualRef 元素上 + // boundVirtualEl 记录实际绑定的元素,确保 virtualRef 变化时能从正确的节点上解绑 + let boundVirtualEl: HTMLElement | undefined + const bindVirtualTriggerEvents = () => { + if (!props.virtualTriggering || !props.virtualRef) return + const el = props.virtualRef + el.addEventListener('mouseenter', handleMouseEnter) + el.addEventListener('mouseleave', handleMouseLeave) + el.addEventListener('click', handleClick) + el.addEventListener('focus', handleFocus) + el.addEventListener('blur', handleBlur) + el.addEventListener('keydown', handleKeydown) + el.addEventListener('contextmenu', handleContextMenu) + boundVirtualEl = el + } + const unbindVirtualTriggerEvents = () => { + const el = boundVirtualEl + if (!el) return + el.removeEventListener('mouseenter', handleMouseEnter) + el.removeEventListener('mouseleave', handleMouseLeave) + el.removeEventListener('click', handleClick) + el.removeEventListener('focus', handleFocus) + el.removeEventListener('blur', handleBlur) + el.removeEventListener('keydown', handleKeydown) + el.removeEventListener('contextmenu', handleContextMenu) + boundVirtualEl = undefined + } + onMounted(() => { + // 虚拟触发模式下,把触发事件绑定到外部元素 + bindVirtualTriggerEvents() // 初始化时如果 visible 为 true,确保显示并监听文档级事件 if (props.visible) { void nextTick(() => { @@ -284,6 +314,14 @@ export default defineComponent({ }) } }) + // virtualRef / virtualTriggering 变化时,先从旧元素解绑再绑定到新元素 + watch( + () => [props.virtualTriggering, props.virtualRef] as const, + () => { + unbindVirtualTriggerEvents() + bindVirtualTriggerEvents() + }, + ) onUnmounted(() => { clearTimers() cleanup?.() @@ -291,16 +329,7 @@ export default defineComponent({ window.removeEventListener('keydown', onDocumentKeydown, true) // 清理虚拟触发元素的事件监听 - if (props.virtualTriggering && props.virtualRef) { - const virtualEl = props.virtualRef - virtualEl.removeEventListener('mouseenter', handleMouseEnter) - virtualEl.removeEventListener('mouseleave', handleMouseLeave) - virtualEl.removeEventListener('click', handleClick) - virtualEl.removeEventListener('focus', handleFocus) - virtualEl.removeEventListener('blur', handleBlur) - virtualEl.removeEventListener('keydown', handleKeydown) - virtualEl.removeEventListener('contextmenu', handleContextMenu) - } + unbindVirtualTriggerEvents() }) watch(externalOpen, (newVal) => { if (isControlled.value) { @@ -389,9 +418,8 @@ export default defineComponent({ style={{ ...floatingStyles.value, ...inlineColorStyle.value, - zIndex: 2000, pointerEvents: props.enterable ? 'auto' : 'none', - width: props.width || undefined, + width: props.width ? (typeof props.width === 'number' ? `${props.width}px` : props.width) : undefined, }} onMouseenter={handlePopperMouseEnter} onMouseleave={handlePopperMouseLeave} diff --git a/packages/ccui/ui/progress/src/progress-types.ts b/packages/ccui/ui/progress/src/progress-types.ts index 9745025e..46a648cd 100644 --- a/packages/ccui/ui/progress/src/progress-types.ts +++ b/packages/ccui/ui/progress/src/progress-types.ts @@ -38,7 +38,7 @@ export const progressProps = { default: 120, }, size: { - type: [String, Number, Array] as PropType, + type: String as PropType, default: 'default', }, format: { diff --git a/packages/ccui/ui/progress/src/progress.scss b/packages/ccui/ui/progress/src/progress.scss index c84b44d2..42bf0c6a 100644 --- a/packages/ccui/ui/progress/src/progress.scss +++ b/packages/ccui/ui/progress/src/progress.scss @@ -25,7 +25,7 @@ width: 100%; overflow: hidden; vertical-align: middle; - background-color: rgba(0, 0, 0, 0.06); + background-color: $ccui-color-fill-secondary; border-radius: 100px; } diff --git a/packages/ccui/ui/progress/src/progress.tsx b/packages/ccui/ui/progress/src/progress.tsx index a74cbaf5..1ec1c61b 100644 --- a/packages/ccui/ui/progress/src/progress.tsx +++ b/packages/ccui/ui/progress/src/progress.tsx @@ -121,7 +121,7 @@ export default defineComponent({ cy="50" r={radius} fill="none" - stroke={props.trailColor || 'rgba(0, 0, 0, 0.06)'} + stroke={props.trailColor || 'var(--ccui-color-fill-secondary, rgba(0, 0, 0, 0.06))'} stroke-width={strokeWidth} stroke-dasharray={isDashboard ? `${adjustedDashArray} ${circumference}` : `${circumference}`} stroke-linecap="round" diff --git a/packages/ccui/ui/radio/src/radio-group.tsx b/packages/ccui/ui/radio/src/radio-group.tsx index e956aafe..9f616149 100644 --- a/packages/ccui/ui/radio/src/radio-group.tsx +++ b/packages/ccui/ui/radio/src/radio-group.tsx @@ -23,13 +23,8 @@ export default defineComponent({ emitChangeValue, }) - const directionType = { - row: 'is-row', - column: 'is-column', - } - const radioGroupClass = computed(() => { - return `${ns.b()} ${directionType[props.direction]}` + return `${ns.b()} ${ns.is(props.direction)}` }) return () => { diff --git a/packages/ccui/ui/range-picker/src/range-picker-types.ts b/packages/ccui/ui/range-picker/src/range-picker-types.ts index eaa9541e..6b643fd4 100644 --- a/packages/ccui/ui/range-picker/src/range-picker-types.ts +++ b/packages/ccui/ui/range-picker/src/range-picker-types.ts @@ -156,11 +156,6 @@ export const rangePickerProps = { type: [Boolean, Object] as PropType, default: false, }, - // 是否在 footer 显示「此刻」按钮。仅 showTime 启用时显示。 - showNow: { - type: Boolean, - default: true, - }, /** * 录入组件统一 variant 形态。 */ diff --git a/packages/ccui/ui/range-picker/src/range-picker.tsx b/packages/ccui/ui/range-picker/src/range-picker.tsx index b693a450..482a7398 100644 --- a/packages/ccui/ui/range-picker/src/range-picker.tsx +++ b/packages/ccui/ui/range-picker/src/range-picker.tsx @@ -286,6 +286,9 @@ export default defineComponent({ if (!selectedStart.value && !selectedEnd.value) return pendingStart.value = null pendingEnd.value = null + // 复位选择阶段与 hover,保证清空后从 start 端重新选起 + phase.value = 'start' + hoverDate.value = null emitRange(null, null) } @@ -448,7 +451,13 @@ export default defineComponent({ function renderGrid(cells: ReturnType) { return ( -
    +
    { + // 指针移出整个网格时清除 hover 预览,避免残留高亮 + hoverDate.value = null + }} + > {cells.map((cell) => { // 显示禁用以当前 phase 为准(用户先填 start 再填 end,对应该侧的限制) const disabled = isDisabledFor(cell.date, phase.value) @@ -545,7 +554,8 @@ export default defineComponent({ } function renderFooter() { - const okDisabled = !pendingStart.value || !pendingEnd.value + const okDisabled = + (!pendingStart.value && !allowEmptyTuple.value[0]) || (!pendingEnd.value && !allowEmptyTuple.value[1]) return (