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
+31 -7
View File
@@ -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<string> = props.value ?? signal(''); // 已选日期
const value: Signal<string> = props.value ?? signal(''); // 已选日期(区间模式下是起点)
const end: Signal<string> = 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;
+5 -2
View File
@@ -594,8 +594,11 @@ export interface AutoCompleteProps {
/*** 日期选择入参 ***/
export interface DatePickerProps {
value?: Signal<string>; // 选中日期(YYYY-MM-DD);不传则内部维护
placeholder?: string; // 未选时占位文案;缺省「请选择日期」
value?: Signal<string>; // 选中日期(YYYY-MM-DD);区间模式下是起始日
range?: boolean; // 区间模式:首次点击定起点、再次点击定终点(颠倒自动交换);缺省 false
end?: Signal<string>; // 区间模式的结束日;不传则内部维护
onRangeChange?: (start: string, end: string) => void; // 区间两端都选定后的回调(已按先后排序)
placeholder?: string; // 未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」)
min?: string; // 最早可选日期(含);早于此的日置灰
max?: string; // 最晚可选日期(含);晚于此的日置灰
disabled?: boolean | Signal<boolean>; // 禁用触发器
+22 -3
View File
@@ -2480,12 +2480,29 @@
{
"n": "value",
"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",
"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 两个信号"
]
},
{
+9
View File
@@ -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);
}