iter(g115): 新增月历组件 calendar
This commit is contained in:
@@ -1276,6 +1276,14 @@
|
|||||||
"acceptance": "npm run changelog 重新生成(301 提交),changelog:check 与提交史一致;npm run check 全绿",
|
"acceptance": "npm run changelog 重新生成(301 提交),changelog:check 与提交史一致;npm run check 全绿",
|
||||||
"notes": "自补:发版前刷新步骤;本条为本次唤起收尾轮",
|
"notes": "自补:发版前刷新步骤;本条为本次唤起收尾轮",
|
||||||
"status": "done"
|
"status": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g115",
|
||||||
|
"title": "新增月历组件 calendar",
|
||||||
|
"motivation": "排期表、值班表、日程页需要「整月网格一直看得见」,datePicker 只能弹层选一天且收起来就没了;调用方要自己算月初留白、当月天数、今天与选中的标记,还要补键盘漫游,一写就是一百多行重复逻辑",
|
||||||
|
"acceptance": "新增 calendar(props)(< 200 行):value/month 信号、marker 每日标记(文案同时进 aria-label)、onSelect / onMonthChange;月初留白与天数计算抽到 dateKit.ts,datePicker 改为共用同一份(消除两处日期工具);组件 59→60,types/index/css/manifest/docs/README/AGENTS/ARCHITECTURE/demo 同步;npm run check 全绿",
|
||||||
|
"notes": "自补:组件补充 + 派生语义单一来源(日期工具下沉 dateKit)",
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T06:06:20.403Z",
|
"generatedAt": "2026-10-26T06:08:45.187Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -202,7 +202,8 @@
|
|||||||
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
|
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
|
||||||
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
|
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
|
||||||
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器",
|
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器",
|
||||||
"popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」"
|
"popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」",
|
||||||
|
"calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -2989,6 +2990,48 @@
|
|||||||
"点确定先收起弹层再触发 onConfirm,回调里可以放心同步改数据;取消、点外部、Esc 一律走 onCancel",
|
"点确定先收起弹层再触发 onConfirm,回调里可以放心同步改数据;取消、点外部、Esc 一律走 onCancel",
|
||||||
"展开时焦点自动落在「取消」上,误按回车不会执行危险操作;需要外部控制开合时传 open 信号"
|
"展开时焦点自动落在「取消」上,误按回车不会执行危险操作;需要外部控制开合时传 open 信号"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "calendar",
|
||||||
|
"title": "月历 Calendar",
|
||||||
|
"demo": "sec-calendar",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "value",
|
||||||
|
"t": "Signal<string>",
|
||||||
|
"d": "选中日期(YYYY-MM-DD)信号;不传则内部维护"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "month",
|
||||||
|
"t": "Signal<string>",
|
||||||
|
"d": "展示月份(YYYY-MM);不传则内部维护,初始跟随 value 或今天"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "marker",
|
||||||
|
"t": "(date: string) => string | null",
|
||||||
|
"k": 1,
|
||||||
|
"d": "每日标记:返回文案即渲染小徽标(如「3 项」),同时写进 aria-label;返回 null 不标记"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onSelect",
|
||||||
|
"t": "(date: string) => void",
|
||||||
|
"k": 1,
|
||||||
|
"d": "选中日期回调(点击或 Enter / 空格)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onMonthChange",
|
||||||
|
"t": "(month: string) => void",
|
||||||
|
"k": 1,
|
||||||
|
"d": "翻月回调,参数为 YYYY-MM(可在此拉取当月数据)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "const day = signal(''); calendar({ value: day, marker: (d) => (d === '2026-10-26' ? '2 项' : null), onSelect: (d) => toast.info(d) })",
|
||||||
|
"steps": [
|
||||||
|
"把 calendar 放在页面容器里即可,它自带标题行(月份 + 上下月箭头)、星期表头与 6 行网格",
|
||||||
|
"选中日期用 value 信号双向绑定;不传则组件内部维护,onSelect 里拿字符串自己处理",
|
||||||
|
"需要显示每天有几件事就传 marker:返回非空文案即渲染小徽标,并拼进 aria-label 让读屏也听得到",
|
||||||
|
"键盘:Tab 进入网格一次,←→ 逐日、↑↓ 逐周、Enter / 空格 选中;跨月会自动翻页并交接焦点"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
||||||
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
||||||
- `packages/skin/ui`(@ui-arch/ui):59 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
- `packages/skin/ui`(@ui-arch/ui):60 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
||||||
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
|
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
|
||||||
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
|
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
|
||||||
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ ui-arch-mvp/
|
|||||||
│ │ ├── table-core/ # 表格状态内核
|
│ │ ├── table-core/ # 表格状态内核
|
||||||
│ │ └── frame/ # 信号 + hyperscript 微框架
|
│ │ └── frame/ # 信号 + hyperscript 微框架
|
||||||
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
||||||
│ │ └── ui/ # 59 个组件 + ui.css
|
│ │ └── ui/ # 60 个组件 + ui.css
|
||||||
│ ├── design/ # 资产层(可发布)
|
│ ├── design/ # 资产层(可发布)
|
||||||
│ │ └── tokens/ # --ua-* 设计令牌
|
│ │ └── tokens/ # --ua-* 设计令牌
|
||||||
│ └── tools/ # 工程层(private,不发布)
|
│ └── tools/ # 工程层(private,不发布)
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全
|
|||||||
npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移)
|
npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件(共 59 个)
|
## 组件(共 60 个)
|
||||||
|
|
||||||
<!-- ua:components -->
|
<!-- ua:components -->
|
||||||
accordion · affix · alert · anchor · autoComplete · avatar · backTop · badge · breadcrumb · button · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · dataTable · datePicker · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · multiSelect · notification · pagination · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · upload · watermark
|
accordion · affix · alert · anchor · autoComplete · avatar · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · dataTable · datePicker · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · multiSelect · notification · pagination · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · upload · watermark
|
||||||
<!-- /ua:components -->
|
<!-- /ua:components -->
|
||||||
|
|
||||||
分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / popconfirm / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / anchor)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel)、其他(space / divider / splitter / backTop / affix / tour / watermark)。
|
分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / popconfirm / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / anchor)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel / calendar)、其他(space / divider / splitter / backTop / affix / tour / watermark)。
|
||||||
|
|
||||||
## 可访问性约定
|
## 可访问性约定
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · backTop · ba
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
|
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
|
||||||
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
|
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
|
||||||
| `packages/skin/ui` | @ui-arch/ui:59 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
| `packages/skin/ui` | @ui-arch/ui:60 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
||||||
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
|
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
|
||||||
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
|
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
|
||||||
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
|
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
badge,
|
badge,
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
button,
|
button,
|
||||||
|
calendar,
|
||||||
card,
|
card,
|
||||||
cascader,
|
cascader,
|
||||||
checkbox,
|
checkbox,
|
||||||
@@ -132,6 +133,15 @@ const users = [
|
|||||||
'Quinn', 'Rose', 'Sam', 'Tina', 'Uma', 'Victor', 'Wendy', 'Xavier',
|
'Quinn', 'Rose', 'Sam', 'Tina', 'Uma', 'Victor', 'Wendy', 'Xavier',
|
||||||
].map((name, i) => ({ id: i + 1, name, age: 22 + ((i * 7) % 20), city: cities[i % cities.length] }));
|
].map((name, i) => ({ id: i + 1, name, age: 22 + ((i * 7) % 20), city: cities[i % cities.length] }));
|
||||||
|
|
||||||
|
// —— 月历标记:以今天为基准前后几天,避免把日期写死 ——
|
||||||
|
const dayKey = (n: number): string => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + n);
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
const calendarMarks: Record<string, string> = { [dayKey(0)]: '2 项', [dayKey(2)]: '1 项', [dayKey(-3)]: '5 项' };
|
||||||
|
const calDay = signal(dayKey(0));
|
||||||
|
|
||||||
// —— 响应式状态演示 ——
|
// —— 响应式状态演示 ——
|
||||||
const count = signal(0);
|
const count = signal(0);
|
||||||
|
|
||||||
@@ -1218,6 +1228,17 @@ const content = h(
|
|||||||
}, button({ label: '删除记录', variant: 'danger' })),
|
}, button({ label: '删除记录', variant: 'danger' })),
|
||||||
popconfirm({ title: '发布到生产?', description: '发布后 1 分钟内全量生效。', confirmText: '发布', onConfirm: () => toast.success('已发布') }, button({ label: '发布', variant: 'ghost' })),
|
popconfirm({ title: '发布到生产?', description: '发布后 1 分钟内全量生效。', confirmText: '发布', onConfirm: () => toast.success('已发布') }, button({ label: '发布', variant: 'ghost' })),
|
||||||
),
|
),
|
||||||
|
section(
|
||||||
|
'sec-calendar',
|
||||||
|
'月历',
|
||||||
|
'calendar 把整月网格内嵌进页面(排期 / 值班 / 日程):每日可带标记文案,标记同时写进 aria-label 让读屏也听得到;←→ 逐日、↑↓ 逐周、Enter 选中,跨月自动翻页并交接焦点。',
|
||||||
|
calendar({
|
||||||
|
value: calDay,
|
||||||
|
marker: (date) => calendarMarks[date] ?? null,
|
||||||
|
onSelect: (date) => toast.info(`选中 ${date}`),
|
||||||
|
}),
|
||||||
|
h('span', { class: 'out' }, () => `当前选中:${calDay() || '无'}`),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 引导漫游实例:根节点需挂到 body(fixed 定位,不参与布局)
|
// 引导漫游实例:根节点需挂到 body(fixed 定位,不参与布局)
|
||||||
|
|||||||
+27
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T06:06:20.403Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T06:08:45.187Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -1629,6 +1629,31 @@ popconfirm({ title: '删除这条记录?', description: '删除后无法恢复
|
|||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-popconfirm
|
现场演示:apps/docs/demo.html#sec-popconfirm
|
||||||
|
|
||||||
|
### calendar — 月历 Calendar
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- value:Signal<string> —— 选中日期(YYYY-MM-DD)信号;不传则内部维护
|
||||||
|
- month:Signal<string> —— 展示月份(YYYY-MM);不传则内部维护,初始跟随 value 或今天
|
||||||
|
- marker(回调):(date: string) => string | null —— 每日标记:返回文案即渲染小徽标(如「3 项」),同时写进 aria-label;返回 null 不标记
|
||||||
|
- onSelect(回调):(date: string) => void —— 选中日期回调(点击或 Enter / 空格)
|
||||||
|
- onMonthChange(回调):(month: string) => void —— 翻月回调,参数为 YYYY-MM(可在此拉取当月数据)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
const day = signal(''); calendar({ value: day, marker: (d) => (d === '2026-10-26' ? '2 项' : null), onSelect: (d) => toast.info(d) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 把 calendar 放在页面容器里即可,它自带标题行(月份 + 上下月箭头)、星期表头与 6 行网格
|
||||||
|
2. 选中日期用 value 信号双向绑定;不传则组件内部维护,onSelect 里拿字符串自己处理
|
||||||
|
3. 需要显示每天有几件事就传 marker:返回非空文案即渲染小徽标,并拼进 aria-label 让读屏也听得到
|
||||||
|
4. 键盘:Tab 进入网格一次,←→ 逐日、↑↓ 逐周、Enter / 空格 选中;跨月会自动翻页并交接焦点
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-calendar
|
||||||
|
|
||||||
API 速览:
|
API 速览:
|
||||||
|
|
||||||
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
|
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
|
||||||
@@ -1692,6 +1717,7 @@ API 速览:
|
|||||||
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
|
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
|
||||||
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
|
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
|
||||||
- popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」
|
- popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」
|
||||||
|
- calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange
|
||||||
|
|
||||||
一行用法:
|
一行用法:
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
|
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
|
||||||
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
|
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
|
||||||
- popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」
|
- popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」
|
||||||
|
- calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange
|
||||||
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
||||||
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
||||||
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { effect, h, signal } from '@ui-arch/frame';
|
||||||
|
import type { CalendarProps } from './types.js';
|
||||||
|
import { fmt, monthGrid, pad, parse, shiftDay, today } from './dateKit.js';
|
||||||
|
|
||||||
|
/*** 月历:整月网格直接内嵌在页面里(排期表 / 值班表 / 日程选择),每日可带标记文案,方向键漫游 + Enter 选中 ***/
|
||||||
|
export function calendar(props: CalendarProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const value = props.value ?? signal(''); // 选中日期(YYYY-MM-DD)
|
||||||
|
const from = parse(value()) ?? today(); // 初始定位:跟随选中值,否则今天
|
||||||
|
const month = props.month ?? signal(`${from[0]}-${pad(from[1] + 1)}`); // 展示月份(YYYY-MM)
|
||||||
|
const focus = signal(value() || fmt(...today())); // 键盘漫游焦点日
|
||||||
|
const grid = h('div', { class: 'ua-calendar-grid', role: 'grid' });
|
||||||
|
const label = h('span', { class: 'ua-calendar-label' });
|
||||||
|
|
||||||
|
/*** 当前展示的年月:月份串非法时回落到初始定位 ***/
|
||||||
|
const view = (): [number, number] => {
|
||||||
|
const hit = /^(\d{4})-(\d{2})$/.exec(month());
|
||||||
|
return hit ? [Number(hit[1]), Number(hit[2]) - 1] : [from[0], from[1]];
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 翻月:只改展示,不动选中值;跨年自动进位 ***/
|
||||||
|
const turn = (delta: number): void => {
|
||||||
|
const [y, m] = view();
|
||||||
|
const d = new Date(y, m + delta, 1);
|
||||||
|
const next = `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
|
||||||
|
month(next);
|
||||||
|
props.onMonthChange?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 选择:写回信号 + 回调,焦点同步到该日 ***/
|
||||||
|
const pick = (date: string): void => {
|
||||||
|
value(date);
|
||||||
|
focus(date);
|
||||||
|
props.onSelect?.(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 重绘网格:月初留白、今天与选中分别打标,漫游 tabindex 保证 Tab 只进一次 ***/
|
||||||
|
const render = (): void => {
|
||||||
|
const [y, m] = view();
|
||||||
|
const { lead, total } = monthGrid(y, m);
|
||||||
|
const cells: HTMLElement[] = Array.from({ length: lead }, () => h('span', { class: 'ua-calendar-blank' }));
|
||||||
|
const nowKey = fmt(...today());
|
||||||
|
for (let d = 1; d <= total; d++) {
|
||||||
|
const date = fmt(y, m, d);
|
||||||
|
const mark = props.marker?.(date) ?? null;
|
||||||
|
const cls = `ua-calendar-day${date === value() ? ' is-selected' : ''}${date === nowKey ? ' is-today' : ''}`;
|
||||||
|
cells.push(h('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: cls,
|
||||||
|
role: 'gridcell',
|
||||||
|
'aria-selected': String(date === value()),
|
||||||
|
tabindex: date === focus() ? '0' : '-1',
|
||||||
|
'aria-label': mark ? `${date},${mark}` : date, // 标记文案同时进无障碍名,读屏也能听到「几项」
|
||||||
|
onClick: () => pick(date),
|
||||||
|
}, h('span', { class: 'ua-calendar-num' }, String(d)), mark ? h('span', { class: 'ua-calendar-mark' }, mark) : null));
|
||||||
|
}
|
||||||
|
grid.replaceChildren(...cells);
|
||||||
|
label.textContent = `${y} 年 ${m + 1} 月`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// —— 键盘:←→ 逐日、↑↓ 逐周、Enter / 空格 选中;跨月自动翻页后把焦点交给新网格 ——
|
||||||
|
grid.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
const step: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
||||||
|
if (!step[e.key]) {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return; // 卫语句:只接管选中键
|
||||||
|
e.preventDefault();
|
||||||
|
pick(focus());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
const next = shiftDay(focus(), step[e.key]);
|
||||||
|
const p = parse(next) ?? today();
|
||||||
|
focus(next);
|
||||||
|
const [y, m] = view();
|
||||||
|
if (p[0] !== y || p[1] !== m) month(`${p[0]}-${pad(p[1] + 1)}`); // 跨月:先翻页,重绘交给 effect
|
||||||
|
requestAnimationFrame(() => (grid.querySelector('.ua-calendar-day[tabindex="0"]') as HTMLElement | null)?.focus());
|
||||||
|
});
|
||||||
|
|
||||||
|
const head = h(
|
||||||
|
'div',
|
||||||
|
{ class: 'ua-calendar-head' },
|
||||||
|
h('button', { type: 'button', class: 'ua-calendar-nav', 'aria-label': '上一月', onClick: () => turn(-1) }, '‹'),
|
||||||
|
label,
|
||||||
|
h('button', { type: 'button', class: 'ua-calendar-nav', 'aria-label': '下一月', onClick: () => turn(1) }, '›'),
|
||||||
|
);
|
||||||
|
const week = h('div', { class: 'ua-calendar-week' }, ...['日', '一', '二', '三', '四', '五', '六'].map((w) => h('span', {}, w)));
|
||||||
|
effect(render); // month / value / focus 任一变化即重绘
|
||||||
|
return h('div', { class: 'ua-calendar' }, head, week, grid);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*** 日期工具:datePicker 与 calendar 共用一套格式化与网格语义,避免两处各写一遍 ***/
|
||||||
|
export const pad = (n: number): string => String(n).padStart(2, '0'); // 两位补零
|
||||||
|
|
||||||
|
/*** 拼成 YYYY-MM-DD ***/
|
||||||
|
export const fmt = (y: number, m: number, d: number): string => `${y}-${pad(m + 1)}-${pad(d)}`;
|
||||||
|
|
||||||
|
/*** 解析 YYYY-MM-DD:非法串返回 null,调用方自行回落 ***/
|
||||||
|
export const parse = (s: string): [number, number, number] | null => {
|
||||||
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
||||||
|
return m ? [Number(m[1]), Number(m[2]) - 1, Number(m[3])] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 今天 ***/
|
||||||
|
export const today = (): [number, number, number] => {
|
||||||
|
const d = new Date();
|
||||||
|
return [d.getFullYear(), d.getMonth(), d.getDate()];
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 按天偏移:跨月 / 跨年自动进位,非法输入回落到今天 ***/
|
||||||
|
export const shiftDay = (key: string, delta: number): string => {
|
||||||
|
const p = parse(key) ?? today();
|
||||||
|
const d = new Date(p[0], p[1], p[2] + delta);
|
||||||
|
return fmt(d.getFullYear(), d.getMonth(), d.getDate());
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 单月网格元数据:月初留白格数与当月天数 ***/
|
||||||
|
export const monthGrid = (y: number, m: number): { lead: number; total: number } => ({
|
||||||
|
lead: new Date(y, m, 1).getDay(), // 1 号是周几 → 前面留几个空位
|
||||||
|
total: new Date(y, m + 1, 0).getDate(), // 下月第 0 天即本月最后一天
|
||||||
|
});
|
||||||
@@ -3,22 +3,7 @@ import type { Signal } from '@ui-arch/frame';
|
|||||||
import { bindOutside, pushOverlay } from './overlay.js';
|
import { bindOutside, pushOverlay } from './overlay.js';
|
||||||
import { flip } from './position.js';
|
import { flip } from './position.js';
|
||||||
import type { DatePickerProps } from './types.js';
|
import type { DatePickerProps } from './types.js';
|
||||||
|
import { fmt, pad, parse, shiftDay, today } from './dateKit.js';
|
||||||
const pad = (n: number): string => String(n).padStart(2, '0'); // 两位补零
|
|
||||||
const fmt = (y: number, m: number, d: number): string => `${y}-${pad(m + 1)}-${pad(d)}`; // 拼成 YYYY-MM-DD
|
|
||||||
const parse = (s: string): [number, number, number] | null => {
|
|
||||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
||||||
return m ? [Number(m[1]), Number(m[2]) - 1, Number(m[3])] : null; // 非法串返回 null,调用方回落到今天
|
|
||||||
};
|
|
||||||
const today = (): [number, number, number] => {
|
|
||||||
const d = new Date();
|
|
||||||
return [d.getFullYear(), d.getMonth(), d.getDate()];
|
|
||||||
};
|
|
||||||
const shiftDay = (key: string, delta: number): string => {
|
|
||||||
const p = parse(key) ?? today();
|
|
||||||
const d = new Date(p[0], p[1], p[2] + delta);
|
|
||||||
return fmt(d.getFullYear(), d.getMonth(), d.getDate());
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** 日期选择:单月网格 + 上下月翻页 + 今天快捷键,value 信号双向绑定,方向键/PageUp/PageDown 可导航 ***/
|
/*** 日期选择:单月网格 + 上下月翻页 + 今天快捷键,value 信号双向绑定,方向键/PageUp/PageDown 可导航 ***/
|
||||||
export function datePicker(props: DatePickerProps): HTMLElement {
|
export function datePicker(props: DatePickerProps): HTMLElement {
|
||||||
|
|||||||
@@ -59,3 +59,4 @@ export { watermark } from './watermark.js';
|
|||||||
export { affix } from './affix.js';
|
export { affix } from './affix.js';
|
||||||
export { anchor } from './anchor.js';
|
export { anchor } from './anchor.js';
|
||||||
export { popconfirm } from './popconfirm.js';
|
export { popconfirm } from './popconfirm.js';
|
||||||
|
export { calendar } from './calendar.js';
|
||||||
|
|||||||
@@ -725,4 +725,13 @@ export interface PopconfirmProps {
|
|||||||
open?: Signal<boolean>; // 受控开合信号;不传则内部维护
|
open?: Signal<boolean>; // 受控开合信号;不传则内部维护
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 月历入参 ***/
|
||||||
|
export interface CalendarProps {
|
||||||
|
value?: Signal<string>; // 选中日期(YYYY-MM-DD)信号;不传则内部维护
|
||||||
|
month?: Signal<string>; // 展示月份(YYYY-MM);不传则内部维护,初始跟随 value 或今天
|
||||||
|
marker?: (date: string) => string | null; // 每日标记:返回文案即渲染小徽标(如「3 项」),返回 null 不标记
|
||||||
|
onSelect?: (date: string) => void; // 选中日期回调(点击或 Enter / 空格)
|
||||||
|
onMonthChange?: (month: string) => void; // 翻月回调,参数为 YYYY-MM
|
||||||
|
}
|
||||||
|
|
||||||
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
||||||
|
|||||||
@@ -73,7 +73,8 @@
|
|||||||
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
|
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
|
||||||
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
|
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
|
||||||
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器",
|
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器",
|
||||||
"popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」"
|
"popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」",
|
||||||
|
"calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -2860,6 +2861,48 @@
|
|||||||
"点确定先收起弹层再触发 onConfirm,回调里可以放心同步改数据;取消、点外部、Esc 一律走 onCancel",
|
"点确定先收起弹层再触发 onConfirm,回调里可以放心同步改数据;取消、点外部、Esc 一律走 onCancel",
|
||||||
"展开时焦点自动落在「取消」上,误按回车不会执行危险操作;需要外部控制开合时传 open 信号"
|
"展开时焦点自动落在「取消」上,误按回车不会执行危险操作;需要外部控制开合时传 open 信号"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "calendar",
|
||||||
|
"title": "月历 Calendar",
|
||||||
|
"demo": "sec-calendar",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "value",
|
||||||
|
"t": "Signal<string>",
|
||||||
|
"d": "选中日期(YYYY-MM-DD)信号;不传则内部维护"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "month",
|
||||||
|
"t": "Signal<string>",
|
||||||
|
"d": "展示月份(YYYY-MM);不传则内部维护,初始跟随 value 或今天"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "marker",
|
||||||
|
"t": "(date: string) => string | null",
|
||||||
|
"k": 1,
|
||||||
|
"d": "每日标记:返回文案即渲染小徽标(如「3 项」),同时写进 aria-label;返回 null 不标记"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onSelect",
|
||||||
|
"t": "(date: string) => void",
|
||||||
|
"k": 1,
|
||||||
|
"d": "选中日期回调(点击或 Enter / 空格)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onMonthChange",
|
||||||
|
"t": "(month: string) => void",
|
||||||
|
"k": 1,
|
||||||
|
"d": "翻月回调,参数为 YYYY-MM(可在此拉取当月数据)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "const day = signal(''); calendar({ value: day, marker: (d) => (d === '2026-10-26' ? '2 项' : null), onSelect: (d) => toast.info(d) })",
|
||||||
|
"steps": [
|
||||||
|
"把 calendar 放在页面容器里即可,它自带标题行(月份 + 上下月箭头)、星期表头与 6 行网格",
|
||||||
|
"选中日期用 value 信号双向绑定;不传则组件内部维护,onSelect 里拿字符串自己处理",
|
||||||
|
"需要显示每天有几件事就传 marker:返回非空文案即渲染小徽标,并拼进 aria-label 让读屏也听得到",
|
||||||
|
"键盘:Tab 进入网格一次,←→ 逐日、↑↓ 逐周、Enter / 空格 选中;跨月会自动翻页并交接焦点"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3826,3 +3826,99 @@ button.ua-step-dot:hover {
|
|||||||
outline: 2px solid var(--ua-color-accent);
|
outline: 2px solid var(--ua-color-accent);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* —— 月历 —— */
|
||||||
|
.ua-calendar {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 252px;
|
||||||
|
padding: var(--ua-space-3);
|
||||||
|
border: 1px solid var(--ua-color-border);
|
||||||
|
border-radius: var(--ua-radius-md);
|
||||||
|
background: var(--ua-color-bg);
|
||||||
|
}
|
||||||
|
.ua-calendar-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--ua-space-2);
|
||||||
|
}
|
||||||
|
.ua-calendar-label {
|
||||||
|
color: var(--ua-color-text);
|
||||||
|
font-size: var(--ua-font-size-md);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.ua-calendar-nav {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--ua-radius-sm);
|
||||||
|
background: none;
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, background-color 0.15s;
|
||||||
|
}
|
||||||
|
.ua-calendar-nav:hover {
|
||||||
|
color: var(--ua-color-accent);
|
||||||
|
background: color-mix(in srgb, var(--ua-color-accent) 8%, transparent);
|
||||||
|
}
|
||||||
|
.ua-calendar-nav:focus-visible {
|
||||||
|
outline: 2px solid var(--ua-color-accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.ua-calendar-week,
|
||||||
|
.ua-calendar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.ua-calendar-week {
|
||||||
|
margin-bottom: var(--ua-space-1);
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
font-size: var(--ua-font-size-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.ua-calendar-day {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--ua-radius-sm);
|
||||||
|
background: none;
|
||||||
|
color: var(--ua-color-text);
|
||||||
|
font-size: var(--ua-font-size-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s, color 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.ua-calendar-day:hover {
|
||||||
|
background: color-mix(in srgb, var(--ua-color-accent) 8%, transparent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.ua-calendar-day.is-today {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--ua-color-border); /* 今天只用细内框,不与选中态抢视觉 */
|
||||||
|
}
|
||||||
|
.ua-calendar-day.is-selected {
|
||||||
|
background: color-mix(in srgb, var(--ua-color-accent) 10%, transparent);
|
||||||
|
color: var(--ua-color-accent);
|
||||||
|
font-weight: 500;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
.ua-calendar-day:focus-visible {
|
||||||
|
outline: 2px solid var(--ua-color-accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.ua-calendar-mark {
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ua-calendar-day {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.ua-calendar-day:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user