From 17da5618f5d686a1d8f94561f95de33fdc9d777b Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 19:57:20 +0800 Subject: [PATCH] =?UTF-8?q?iter(g216):=20=E6=97=B6=E9=97=B4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E6=94=AF=E6=8C=81=E6=B8=85=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/demo/main.ts | 2 +- llms-full.txt | 4 +++- packages/skin/ui/src/timePicker.ts | 27 ++++++++++++++++++++++++++- packages/skin/ui/src/types.ts | 1 + packages/skin/ui/ua.manifest.json | 8 +++++++- packages/skin/ui/ui.css | 29 +++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 9e7b40e..faa4744 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -1066,7 +1066,7 @@ const content = h( h( 'div', { class: 'row' }, - timePicker({ value: atTime, minuteStep: 15, onChange: (t) => toast.success(`定在 ${t}`) }), + timePicker({ value: atTime, minuteStep: 15, clearable: true, onChange: (t) => toast.success(t ? `定在 ${t}` : '已清空时间') }), timePicker({ placeholder: '禁用态', disabled: true }), h('span', { class: 'out' }, () => `当前时间:${atTime() || '未选'}`), ), diff --git a/llms-full.txt b/llms-full.txt index 6a7a401..91dbae4 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T11:54:31.013Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T11:56:46.981Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -1994,6 +1994,7 @@ const cmdk = commandPalette({ open: signal(false), items: [{ id: 'save', label: - value:Signal —— 选中时间信号(HH:mm);不传则内部维护 - minuteStep:number —— 分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45) +- clearable:boolean —— 显示清空按钮 ×(仅在有值时出现);缺省 false,键盘 Delete / Backspace 等价 - placeholder:string —— 未选时占位文案;缺省「请选择时间」 - disabled:boolean | Signal —— 禁用触发器 - onChange:(time: string) => void —— 写回回调:选时即触发一次(分默认 00),选完分再触发一次 @@ -2011,6 +2012,7 @@ const at = signal('09:30'); timePicker({ value: at, minuteStep: 15, onChange: (t 3. 先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器 4. 列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起 5. 需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档 +6. 需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串 现场演示:apps/docs/demo.html#sec-timepicker diff --git a/packages/skin/ui/src/timePicker.ts b/packages/skin/ui/src/timePicker.ts index fd622f5..098f4ce 100644 --- a/packages/skin/ui/src/timePicker.ts +++ b/packages/skin/ui/src/timePicker.ts @@ -28,6 +28,14 @@ export function timePicker(props: TimePickerProps): HTMLElement { props.onChange?.(next); }; + /*** 清空:回到「未选」态,收起弹层并写回空串(与 select / multiSelect 的清空语义一致) ***/ + const clear = () => { + if (!value()) return; // 卫语句:本来就没选,不重复派发回调 + value(''); + props.onChange?.(''); + open(false); + }; + /*** 列内聚焦导航:↑↓ 环形移动、Home/End 直达首末(列内按钮顺序即时间顺序) ***/ const navigate = (col: HTMLElement, cells: HTMLButtonElement[]) => { col.addEventListener('keydown', (e: KeyboardEvent) => { @@ -82,13 +90,23 @@ export function timePicker(props: TimePickerProps): HTMLElement { h('span', { class: 'ua-timepicker-arrow', 'aria-hidden': 'true' }, '▾'), ); + // —— 清空按钮:clearable 且有值时出现,压在触发器右缘(与箭头错开)—— + const clearable = props.clearable ?? false; // 缺省不带清空:没选的场景多,按钮会白占位置 + const clearBtn = h('button', { + type: 'button', + class: 'ua-timepicker-clear', + 'aria-label': '清空时间', + tabindex: '-1', // 不进 Tab 序:清空是鼠标 / 触屏便利,键盘用 Delete / Backspace + onClick: () => clear(), + }, '×') as HTMLElement; + const pop = h( 'div', { class: () => `ua-timepicker-pop${open() ? ' ua-open' : ''}${place() === 'top' ? ' is-flip' : ''}`, role: 'dialog', 'aria-label': '选择时间' }, hourCol.col, minuteCol.col, ); - const root = h('div', { class: () => `ua-timepicker${open() ? ' is-open' : ''}` }, trigger, pop); + const root = h('div', { class: () => `ua-timepicker${open() ? ' is-open' : ''}${props.clearable && value() ? ' has-clear' : ''}` }, trigger, clearable ? clearBtn : null, pop); /*** 把激活格滚到列中部:列是独立滚动容器,直接算 scrollTop 不惊动页面 ***/ const reveal = (col: HTMLElement, cells: HTMLButtonElement[]) => { @@ -98,6 +116,13 @@ export function timePicker(props: TimePickerProps): HTMLElement { col.scrollTop = Math.max(0, target); }; + effect(() => { clearBtn.hidden = !clearable || !value(); }); // 无值或未开启时藏起来(hidden 一并移出点击区) + root.addEventListener('keydown', (e: KeyboardEvent) => { + if (open() || !clearable) return; // 卫语句:展开时按键归列内导航,未开启清空则不接管 + if (e.key !== 'Backspace' && e.key !== 'Delete') return; // 卫语句:只接管这两个键 + e.preventDefault(); + clear(); // 键盘等价:不必伸手去点 × + }); bindOutside(root, open, () => open(false)); // 点外部收起:仅展开期间挂监听 effect(() => { if (open()) { diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index a59cccd..51e0871 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -670,6 +670,7 @@ export interface TimePickerProps { value?: Signal; // 选中时间(HH:mm),未传则组件内部持有 minuteStep?: number; // 分钟步长;缺省 5,超过 30 按 30 处理 placeholder?: string; // 未选时占位文案;缺省「请选择时间」 + clearable?: boolean; // 显示清空按钮 ×(有值时才出现);缺省 false,键盘 Delete / Backspace 等价 disabled?: boolean | Signal; // 禁用触发器 onChange?: (time: string) => void; // 写回回调(选时也会触发,分默认落到首个步长值) } diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 0346280..3a3e02e 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -3669,6 +3669,11 @@ "t": "number", "d": "分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45)" }, + { + "n": "clearable", + "t": "boolean", + "d": "显示清空按钮 ×(仅在有值时出现);缺省 false,键盘 Delete / Backspace 等价" + }, { "n": "placeholder", "t": "string", @@ -3691,7 +3696,8 @@ "点触发器展开时 / 分两列,当前值所在格高亮并自动滚到列中部,不必手动找", "先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器", "列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起", - "需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档" + "需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档", + "需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串" ] } ] diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 51f916e..d41a590 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -5144,3 +5144,32 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } .ua-multiselect-pop.ua-open.is-flip { animation-name: ua-pop-rise; /* 翻到上方:入场方向随位置镜像 */ } + +/* ---- 时间选择:清空按钮(clearable 且有值时才出现) ---- */ +.ua-timepicker-clear { + position: absolute; + top: 50%; + right: var(--ua-space-6); /* 让到箭头左侧,不与 ▾ 重叠 */ + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + border: none; + border-radius: 50%; + background: none; + color: var(--ua-color-text-muted); + font-size: var(--ua-font-size-md); + line-height: var(--ua-leading-none); /* 行高压到 1:× 在 16px 圆里垂直居中 */ + cursor: pointer; + transform: translateY(-50%); + transition: color var(--ua-duration-fast) var(--ua-ease), background-color var(--ua-duration-fast) var(--ua-ease); +} +.ua-timepicker-clear:hover { + background: var(--ua-color-surface-2); /* 悬停给一层淡底:小 × 只变颜色太难点中 */ + color: var(--ua-color-text); +} +.ua-timepicker.has-clear .ua-timepicker-trigger { + padding-right: var(--ua-space-6); /* 给 × 让出位置:文案不会被压在按钮下面 */ +}