diff --git a/.ua/backlog.json b/.ua/backlog.json index 71670a7..f11dfed 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1460,6 +1460,14 @@ "acceptance": "演示页头部提供暗色主题开关(写 html[data-theme],localStorage 记忆,初值跟随系统);--page-* 变量响应 data-theme,媒体查询只在未显式指定时生效;npm run check 全绿", "notes": "自补:演示页完善", "status": "done" + }, + { + "id": "g138", + "title": "日期选择支持区间模式", + "motivation": "筛选条件几乎都要「起止日期」,datePicker 只有单日:调用方只能并排放两个选择器,用户看不出选了什么区间,也拿不到排序保证", + "acceptance": "datePicker 新增 range/end/onRangeChange:首点定起点不收弹层、次点定终点并排序,区间内部有底色;文档 props/steps 与演示同步;npm run check 全绿", + "notes": "自补:既有组件可用性细节", + "status": "open" } ] } diff --git a/.ua/index.json b/.ua/index.json index d2cf815..5543757 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T07:28:06.168Z", + "generatedAt": "2026-10-26T07:31:10.742Z", "repoRoot": ".", "packages": [ { @@ -2609,12 +2609,29 @@ { "n": "value", "t": "Signal", - "d": "选中日期(YYYY-MM-DD)信号;不传则内部维护" + "d": "选中日期(YYYY-MM-DD)信号;区间模式下是起始日,不传则内部维护" + }, + { + "n": "range", + "t": "boolean", + "df": "false", + "d": "区间模式:首次点击定起点(弹层不收起),再次点击定终点;点早了自动交换先后" + }, + { + "n": "end", + "t": "Signal", + "d": "区间模式的结束日信号;不传则内部维护" + }, + { + "n": "onRangeChange", + "t": "(start: string, end: string) => void", + "k": 1, + "d": "区间两端都选定后的回调,参数已按先后排序(单日模式不触发)" }, { "n": "placeholder", "t": "string", - "d": "未选时占位文案;缺省「请选择日期」" + "d": "未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」)" }, { "n": "min", @@ -2643,7 +2660,9 @@ "‹ › 翻月,底部「今天」一键跳今天并提交;今天用描边标记、选中用实心底色", "键盘:←→ 逐日、↑↓ 逐周、PageUp/PageDown 逐月、Enter 提交,跨月会自动翻页", "min / max 为闭区间,越界日期置灰不可点也不可键盘选中", - "value 传信号即受控;Esc 收起(与弹窗/抽屉共用逐层关闭栈)" + "value 传信号即受控;Esc 收起(与弹窗/抽屉共用逐层关闭栈)", + "选日期区间传 range: true:第一次点击只定起点(触发器显示「起 ~ …」),第二次点击补终点并收起,两端之间用低比重底色连成一条", + "区间回调走 onRangeChange(start, end),参数已排序——用户先点晚的日期也不会出现 start > end;只需检视当前值就同时读 value 与 end 两个信号" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 0ea08ae..1a55e66 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -230,6 +230,8 @@ const tagPick = signal(['ts']); // 多选下拉选中值(外部受 const permPick = signal(['read']); // 多选组选中值(外部受控演示) const colorVal = signal('var(--ua-color-accent)'); // 颜色选择值(外部受控演示) const dateVal = signal('2026-10-26'); // 日期选择值(外部受控演示) +const rangeFrom = signal(''); // 区间起点 +const rangeTo = signal(''); // 区间终点 const acValue = signal(''); // 自动补全输入值(外部受控演示) const treeChecked = signal(['core']); // 多选树勾选集合(外部受控演示) const treePick = signal('core'); // 树形选中节点 key @@ -982,7 +984,9 @@ const content = h( { class: 'row' }, datePicker({ value: dateVal, onChange: (d) => toast.success(`已选 ${d}`) }), datePicker({ placeholder: '只能选 10 月下旬', min: '2026-10-20', max: '2026-10-31' }), + datePicker({ range: true, value: rangeFrom, end: rangeTo, onRangeChange: (a, b) => toast.success(`区间 ${a} ~ ${b}`) }), h('span', { class: 'out' }, () => (dateVal() ? `当前日期:${dateVal()}` : '未选择')), + h('span', { class: 'out' }, () => (rangeFrom() ? `区间:${rangeFrom()} ~ ${rangeTo() || '待定'}` : '区间未选择')), ), ), section( diff --git a/llms-full.txt b/llms-full.txt index ee07b97..ab9a7bc 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T07:28:06.168Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T07:31:10.742Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -1386,8 +1386,11 @@ autoComplete({ value: acValue, placeholder: '输入组件名', options: [{ value 参数: -- value:Signal —— 选中日期(YYYY-MM-DD)信号;不传则内部维护 -- placeholder:string —— 未选时占位文案;缺省「请选择日期」 +- value:Signal —— 选中日期(YYYY-MM-DD)信号;区间模式下是起始日,不传则内部维护 +- range(默认 false):boolean —— 区间模式:首次点击定起点(弹层不收起),再次点击定终点;点早了自动交换先后 +- end:Signal —— 区间模式的结束日信号;不传则内部维护 +- onRangeChange(回调):(start: string, end: string) => void —— 区间两端都选定后的回调,参数已按先后排序(单日模式不触发) +- placeholder:string —— 未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」) - min:string —— 最早可选日期(含,YYYY-MM-DD);更早的日置灰不可点 - max:string —— 最晚可选日期(含,YYYY-MM-DD);更晚的日置灰不可点 - disabled:boolean | Signal —— 禁用触发器 @@ -1406,6 +1409,8 @@ datePicker({ value: dateVal, min: '2026-10-20', max: '2026-10-31', onChange: (d) 3. 键盘:←→ 逐日、↑↓ 逐周、PageUp/PageDown 逐月、Enter 提交,跨月会自动翻页 4. min / max 为闭区间,越界日期置灰不可点也不可键盘选中 5. value 传信号即受控;Esc 收起(与弹窗/抽屉共用逐层关闭栈) +6. 选日期区间传 range: true:第一次点击只定起点(触发器显示「起 ~ …」),第二次点击补终点并收起,两端之间用低比重底色连成一条 +7. 区间回调走 onRangeChange(start, end),参数已排序——用户先点晚的日期也不会出现 start > end;只需检视当前值就同时读 value 与 end 两个信号 现场演示:apps/docs/demo.html#sec-datepicker diff --git a/packages/skin/ui/src/datePicker.ts b/packages/skin/ui/src/datePicker.ts index d319bd3..b7c6390 100644 --- a/packages/skin/ui/src/datePicker.ts +++ b/packages/skin/ui/src/datePicker.ts @@ -8,7 +8,8 @@ import { fmt, pad, parse, shiftDay, today } from './dateKit.js'; /*** 日期选择:单月网格 + 上下月翻页 + 今天快捷键,value 信号双向绑定,方向键/PageUp/PageDown 可导航 ***/ export function datePicker(props: DatePickerProps): HTMLElement { // —— 状态集中声明 —— - const value: Signal = props.value ?? signal(''); // 已选日期 + const value: Signal = props.value ?? signal(''); // 已选日期(区间模式下是起点) + const end: Signal = props.end ?? signal(''); // 区间终点,非区间模式恒为空 const open = signal(false); // 弹层开合 const place = signal<'top' | 'bottom'>('bottom'); // 实际方位 const initial = parse(value()) ?? today(); // 初始视图:跟随已选,否则今天 @@ -41,10 +42,25 @@ export function datePicker(props: DatePickerProps): HTMLElement { view([d.getFullYear(), d.getMonth()]); } - /*** 提交:写值 + 回调 + 收起 + 焦点归还触发器 ***/ + /*** 提交:单日模式写值即收起;区间模式首点定起点并留在弹层,次点补终点后收起 ***/ function commit(date: string): void { - value(date); - props.onChange?.(date); + if (!props.range) { + value(date); + props.onChange?.(date); + open(false); + trigger.focus(); + return; + } + if (!value() || end()) { // 未选起点或上一段区间已完整:从这个日期重开一段 + value(date); + end(''); + render(); // 清掉上一段的区间底色 + return; + } + const [start, stop] = date < value() ? [date, value()] : [value(), date]; // 点早了就自动交换 + value(start); + end(stop); + props.onRangeChange?.(start, stop); open(false); trigger.focus(); } @@ -62,14 +78,16 @@ export function datePicker(props: DatePickerProps): HTMLElement { for (let d = 1; d <= total; d++) { const date = fmt(y, m, d); const dead = outOfRange(date); - const cls = `ua-datepicker-day${date === value() ? ' is-selected' : ''}${date === fmt(...today()) ? ' is-today' : ''}${dead ? ' is-disabled' : ''}`; + const edge = date === value() || Boolean(props.range && end() && date === end()); // 区间两端 + const between = Boolean(props.range && value() && end() && date > value() && date < end()); // 区间内部 + const cls = `ua-datepicker-day${edge ? ' is-selected' : ''}${between ? ' is-in-range' : ''}${date === fmt(...today()) ? ' is-today' : ''}${dead ? ' is-disabled' : ''}`; cells.push( h('button', { type: 'button', class: cls, role: 'gridcell', tabindex: date === focus() ? '0' : '-1', // 漫游 tabindex:只有焦点日可 Tab 进入 - 'aria-selected': String(date === value()), + 'aria-selected': String(edge), disabled: dead, 'aria-label': date, onClick: () => commit(date), @@ -84,6 +102,12 @@ export function datePicker(props: DatePickerProps): HTMLElement { }); } + /*** 触发器文案:单日回显日期;区间回显「起 ~ 止」,只定了起点时后面跟省略号 ***/ + const labelText = (): string => { + if (!props.range) return value() || props.placeholder || '请选择日期'; + if (!value()) return props.placeholder || '请选择日期范围'; + return end() ? `${value()} ~ ${end()}` : `${value()} ~ …`; + }; const trigger = h( 'button', { @@ -101,7 +125,7 @@ export function datePicker(props: DatePickerProps): HTMLElement { open(!open()); }, }, - h('span', { class: () => `ua-datepicker-value${value() ? '' : ' is-empty'}` }, () => value() || props.placeholder || '请选择日期'), + h('span', { class: () => `ua-datepicker-value${value() ? '' : ' is-empty'}` }, () => labelText()), h('span', { class: 'ua-datepicker-caret', 'aria-hidden': 'true' }, '▤'), ); const root = h('div', { class: () => `ua-datepicker${open() ? ' ua-open' : ''}` }, trigger, pop) as HTMLElement; diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 97d233c..514a7b9 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -594,8 +594,11 @@ export interface AutoCompleteProps { /*** 日期选择入参 ***/ export interface DatePickerProps { - value?: Signal; // 选中日期(YYYY-MM-DD);不传则内部维护 - placeholder?: string; // 未选时占位文案;缺省「请选择日期」 + value?: Signal; // 选中日期(YYYY-MM-DD);区间模式下是起始日 + range?: boolean; // 区间模式:首次点击定起点、再次点击定终点(颠倒自动交换);缺省 false + end?: Signal; // 区间模式的结束日;不传则内部维护 + onRangeChange?: (start: string, end: string) => void; // 区间两端都选定后的回调(已按先后排序) + placeholder?: string; // 未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」) min?: string; // 最早可选日期(含);早于此的日置灰 max?: string; // 最晚可选日期(含);晚于此的日置灰 disabled?: boolean | Signal; // 禁用触发器 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 8e29ff7..f3bb3bb 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -2480,12 +2480,29 @@ { "n": "value", "t": "Signal", - "d": "选中日期(YYYY-MM-DD)信号;不传则内部维护" + "d": "选中日期(YYYY-MM-DD)信号;区间模式下是起始日,不传则内部维护" + }, + { + "n": "range", + "t": "boolean", + "df": "false", + "d": "区间模式:首次点击定起点(弹层不收起),再次点击定终点;点早了自动交换先后" + }, + { + "n": "end", + "t": "Signal", + "d": "区间模式的结束日信号;不传则内部维护" + }, + { + "n": "onRangeChange", + "t": "(start: string, end: string) => void", + "k": 1, + "d": "区间两端都选定后的回调,参数已按先后排序(单日模式不触发)" }, { "n": "placeholder", "t": "string", - "d": "未选时占位文案;缺省「请选择日期」" + "d": "未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」)" }, { "n": "min", @@ -2514,7 +2531,9 @@ "‹ › 翻月,底部「今天」一键跳今天并提交;今天用描边标记、选中用实心底色", "键盘:←→ 逐日、↑↓ 逐周、PageUp/PageDown 逐月、Enter 提交,跨月会自动翻页", "min / max 为闭区间,越界日期置灰不可点也不可键盘选中", - "value 传信号即受控;Esc 收起(与弹窗/抽屉共用逐层关闭栈)" + "value 传信号即受控;Esc 收起(与弹窗/抽屉共用逐层关闭栈)", + "选日期区间传 range: true:第一次点击只定起点(触发器显示「起 ~ …」),第二次点击补终点并收起,两端之间用低比重底色连成一条", + "区间回调走 onRangeChange(start, end),参数已排序——用户先点晚的日期也不会出现 start > end;只需检视当前值就同时读 value 与 end 两个信号" ] }, { diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index fed7d20..c908d0a 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -4360,3 +4360,12 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } transition: none; } } + +/* —— 日期区间:两端仍走 is-selected,中间用低比重底色连成一条 —— */ +.ua-datepicker-day.is-in-range { + background: color-mix(in srgb, var(--ua-color-accent) 12%, transparent); + border-radius: 0; +} +.ua-datepicker-day.is-in-range:hover { + background: color-mix(in srgb, var(--ua-color-accent) 20%, transparent); +}