iter(g138): 日期选择支持区间模式

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