diff --git a/.ua/index.json b/.ua/index.json index 226d29c..4f928da 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T14:58:51.630Z", + "generatedAt": "2026-10-26T15:00:29.937Z", "repoRoot": ".", "packages": [ { @@ -225,7 +225,8 @@ "diffView(props):文本差异对比,行级 LCS 对齐 + 增删着色 + 双行号列,顶部带 +N / −N 统计", "codeBlock(props):代码块,等宽多行 + 行号列 + 标题行 + 一键复制原文,只展示不高亮", "signaturePad(props):手写签名板,指针绘制 + DPR 位图,返回 handle(el / clear / toDataURL / isEmpty)", - "loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁" + "loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁", + "relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值" ], "keyTypes": [ "ButtonProps", @@ -4388,6 +4389,46 @@ "遮罩文案写明在等什么(「正在刷新订单」),读屏同一句也会播报;只要转圈就省略 text", "内容可能为空时给 minHeight,否则高度塌成 0 遮罩无处可盖" ] + }, + { + "id": "relativeTime", + "title": "相对时间 RelativeTime", + "demo": "sec-reltime", + "props": [ + { + "n": "value", + "t": "string | number | Date", + "d": "目标时间:ISO 串 / 时间戳(10 位按秒、13 位按毫秒)/ Date" + }, + { + "n": "refresh", + "t": "number", + "d": "自动刷新间隔(ms);缺省 30000,传 0 不刷新" + }, + { + "n": "threshold", + "t": "number", + "d": "超过该秒数改用绝对日期;缺省 7 天" + }, + { + "n": "fallback", + "t": "string", + "d": "时间非法时的占位文案;缺省「—」" + }, + { + "n": "ariaLabel", + "t": "string", + "d": "可访问名(如「下单时间」),让读屏知道这个时间是什么" + } + ], + "example": "relativeTime({ value: row.createdAt, ariaLabel: '下单时间' })", + "steps": [ + "把后端给的时间原样交进来:ISO 串、10 位秒级戳、13 位毫秒戳、Date 都认,不用自己 new Date", + "默认每 30 秒刷新一次文案(「3 分钟前」会在不刷新页面的情况下变「4 分钟前」);列表里条数极多时可把 refresh 调大或传 0", + "超过 7 天自动改显示绝对日期(YYYY-MM-DD HH:mm),想更早切换就调小 threshold", + "精确时间放在 title:鼠标悬停可看,不必为「想知道具体哪天」再点开详情", + "时间非法(空串 / 格式错)不猜也不抛,显示 fallback(缺省「—」),列表不会因一条脏数据空白" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index f6f78f7..e92750c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ - `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核 - `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架 -- `packages/skin/ui`(@ui-arch/ui):82 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):83 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 - `packages/design/tokens`(@ui-arch/tokens):设计令牌 - `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private) - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 98ffd4c..b072c4b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 82 个组件 + ui.css +│ │ └── ui/ # 83 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index 96cc54d..8297b59 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全 npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移) ``` -## 组件(共 82 个) +## 组件(共 83 个) -accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · codeBlock · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · diffView · divider · drawer · empty · floatButton · form · grid · highlight · image · inputNumber · inputTag · jsonViewer · kbd · layout · link · list · loading · menu · modal · mentions · multiSelect · notification · pagination · pinInput · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · signaturePad · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark +accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · codeBlock · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · diffView · divider · drawer · empty · floatButton · form · grid · highlight · image · inputNumber · inputTag · jsonViewer · kbd · layout · link · list · loading · menu · modal · mentions · multiSelect · notification · pagination · pinInput · popover · popconfirm · progress · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark -分组:表单(button / copyButton / signaturePad / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / loading / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / jsonViewer / diffView / codeBlock / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / watermark)。 +分组:表单(button / copyButton / signaturePad / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / loading / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / jsonViewer / diffView / codeBlock / relativeTime / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / watermark)。 ## 可访问性约定 @@ -34,7 +34,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup |---|---| | `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) | | `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) | -| `packages/skin/ui` | @ui-arch/ui:82 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:83 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | | `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) | | `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server | | `apps/demo` | 交互演示(esbuild 打包成单文件) | diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 48dc9f9..a48475f 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -69,6 +69,7 @@ import { popover, progress, radioGroup, + relativeTime, rate, segmented, select, @@ -1813,6 +1814,18 @@ const content = h( ); })(), ), + section( + 'sec-reltime', + '相对时间', + 'relativeTime:把时间戳渲染成「3 分钟前」并按间隔自动刷新,超阈值改显示绝对日期;精确时间在 title 里,悬停即见。', + h('div', { class: 'row' }, + h('span', null, '刚刚:'), relativeTime({ value: Date.now() - 20 * 1000 }), + h('span', null, '半小时前:'), relativeTime({ value: Date.now() - 30 * 60 * 1000 }), + h('span', null, '三天前:'), relativeTime({ value: Date.now() - 3 * 86400 * 1000 }), + h('span', null, '两个月前:'), relativeTime({ value: Date.now() - 60 * 86400 * 1000 }), + h('span', null, '脏数据:'), relativeTime({ value: 'not-a-date' }), + ), + ), ); // 悬浮按钮:fixed 定位,挂 body 不参与页面布局 diff --git a/llms-full.txt b/llms-full.txt index 5b91107..9f15ee7 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T14:58:51.630Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T15:00:29.937Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -2341,6 +2341,32 @@ loading({ active: busy, text: '正在刷新订单' }, dataTable({ ... })) 现场演示:apps/docs/demo.html#sec-loading +### relativeTime — 相对时间 RelativeTime + +参数: + +- value:string | number | Date —— 目标时间:ISO 串 / 时间戳(10 位按秒、13 位按毫秒)/ Date +- refresh:number —— 自动刷新间隔(ms);缺省 30000,传 0 不刷新 +- threshold:number —— 超过该秒数改用绝对日期;缺省 7 天 +- fallback:string —— 时间非法时的占位文案;缺省「—」 +- ariaLabel:string —— 可访问名(如「下单时间」),让读屏知道这个时间是什么 + +示例: + +``` +relativeTime({ value: row.createdAt, ariaLabel: '下单时间' }) +``` + +手把手: + +1. 把后端给的时间原样交进来:ISO 串、10 位秒级戳、13 位毫秒戳、Date 都认,不用自己 new Date +2. 默认每 30 秒刷新一次文案(「3 分钟前」会在不刷新页面的情况下变「4 分钟前」);列表里条数极多时可把 refresh 调大或传 0 +3. 超过 7 天自动改显示绝对日期(YYYY-MM-DD HH:mm),想更早切换就调小 threshold +4. 精确时间放在 title:鼠标悬停可看,不必为「想知道具体哪天」再点开详情 +5. 时间非法(空串 / 格式错)不猜也不抛,显示 fallback(缺省「—」),列表不会因一条脏数据空白 + +现场演示:apps/docs/demo.html#sec-reltime + API 速览: - button(props):按钮,variant 三档视觉强度,disabled / loading 支持布尔或信号动态同步(loading 转圈并禁点),onClick 点击回调 @@ -2427,6 +2453,7 @@ API 速览: - codeBlock(props):代码块,等宽多行 + 行号列 + 标题行 + 一键复制原文,只展示不高亮 - signaturePad(props):手写签名板,指针绘制 + DPR 位图,返回 handle(el / clear / toDataURL / isEmpty) - loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁 +- relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值 一行用法: diff --git a/llms.txt b/llms.txt index 839a35f..64f1b64 100644 --- a/llms.txt +++ b/llms.txt @@ -131,6 +131,7 @@ - codeBlock(props):代码块,等宽多行 + 行号列 + 标题行 + 一键复制原文,只展示不高亮 - signaturePad(props):手写签名板,指针绘制 + DPR 位图,返回 handle(el / clear / toDataURL / isEmpty) - loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁 + - relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值 - 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')` - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index b37887b..7d68973 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -82,3 +82,4 @@ export { diffView } from './diffView.js'; export { codeBlock } from './codeBlock.js'; export { signaturePad } from './signaturePad.js'; export { loading } from './loading.js'; +export { relativeTime } from './relativeTime.js'; diff --git a/packages/skin/ui/src/relativeTime.ts b/packages/skin/ui/src/relativeTime.ts new file mode 100644 index 0000000..c71e562 --- /dev/null +++ b/packages/skin/ui/src/relativeTime.ts @@ -0,0 +1,58 @@ +import { h } from '@ui-arch/frame'; +import type { RelativeTimeProps } from './types.js'; + +// 缺省刷新间隔与切换绝对日期的阈值:30s 够用又不吵,7 天后再用「N 天前」已不如日期直观 +const TICK = 30_000; +const MAX_AGO = 7 * 86_400; + +/*** 目标时间归一成毫秒时间戳;非法输入给 NaN,调用方按「未知」处理 ***/ +function toMs(value: string | number | Date): number { + if (value instanceof Date) return value.getTime(); + if (typeof value === 'number') return value < 1e12 ? value * 1000 : value; // 10 位按秒、13 位按毫秒 + return new Date(value).getTime(); +} + +/*** 相对文案:刚刚 / N 分钟前 / N 小时前 / N 天前;未来时间反过来说「还有」 ***/ +function phrase(diffSec: number): string { + const future = diffSec < 0; + const s = Math.abs(diffSec); + const unit = (n: number, name: string): string => (future ? `还有 ${n} ${name}` : `${n} ${name}前`); + if (s < 60) return future ? '即将' : '刚刚'; + if (s < 3600) return unit(Math.floor(s / 60), '分钟'); + if (s < 86400) return unit(Math.floor(s / 3600), '小时'); + return unit(Math.floor(s / 86400), '天'); +} + +/*** 补零:绝对日期要定宽,列表里才不会一行一个宽度 ***/ +const pad = (n: number): string => String(n).padStart(2, '0'); + +/*** 绝对日期:超出阈值后显示,同时作悬停提示(精确到分钟就够,秒是噪音) ***/ +function absolute(ms: number): string { + const d = new Date(ms); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +/*** 相对时间:把时间戳渲染成「3 分钟前」并按间隔自动刷新;超阈值改显示绝对日期,title 始终给精确值 ***/ +export function relativeTime(props: RelativeTimeProps): HTMLElement { + const ms = toMs(props.value); // 目标时间(毫秒) + const tick = props.refresh ?? TICK; // 自动刷新间隔;传 0 关闭 + const maxAgo = props.threshold ?? MAX_AGO; // 超过多少秒改用绝对日期 + const el = h('time', { class: 'ua-reltime', datetime: Number.isNaN(ms) ? '' : new Date(ms).toISOString() }) as HTMLTimeElement; + if (Number.isNaN(ms)) { el.textContent = props.fallback ?? '—'; return el; } // 非法时间:不猜,给占位(缺省破折号) + el.title = absolute(ms); // 悬停即精确时间:相对文案永远不需要「点开看」 + if (props.ariaLabel) el.setAttribute('aria-label', props.ariaLabel); + + /*** 一次重绘:算差值 → 超阈值用绝对日期,否则相对文案 ***/ + const paint = (): void => { + const diffSec = Math.round((Date.now() - ms) / 1000); + el.textContent = Math.abs(diffSec) > maxAgo ? absolute(ms) : phrase(diffSec); + }; + paint(); + if (tick > 0) { + const timer = window.setInterval(() => { + if (!el.isConnected) { window.clearInterval(timer); return; } // 已卸载:自断,避免列表反复挂载累积计时器 + paint(); + }, tick); + } + return el; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 4f81da0..8818e72 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -1078,3 +1078,12 @@ export interface LoadingProps { spinnerSize?: number; // 转圈直径(px);缺省 20 minHeight?: number; // 最小高度(px);内容为空时兜住遮罩高度 } + +/*** 相对时间入参 ***/ +export interface RelativeTimeProps { + value: string | number | Date; // 目标时间:ISO 串 / 时间戳(10 位按秒、13 位按毫秒)/ Date + refresh?: number; // 自动刷新间隔(ms);缺省 30000,传 0 不刷新 + threshold?: number; // 超过该秒数改用绝对日期;缺省 7 天 + fallback?: string; // 时间非法时的占位文案;缺省「—」 + ariaLabel?: string; // 可访问名(如「下单时间」),让读屏知道这个时间是什么 +} diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index ed5e457..3e1cee8 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -96,7 +96,8 @@ "diffView(props):文本差异对比,行级 LCS 对齐 + 增删着色 + 双行号列,顶部带 +N / −N 统计", "codeBlock(props):代码块,等宽多行 + 行号列 + 标题行 + 一键复制原文,只展示不高亮", "signaturePad(props):手写签名板,指针绘制 + DPR 位图,返回 handle(el / clear / toDataURL / isEmpty)", - "loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁" + "loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁", + "relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值" ], "keyTypes": [ "ButtonProps", @@ -4259,6 +4260,46 @@ "遮罩文案写明在等什么(「正在刷新订单」),读屏同一句也会播报;只要转圈就省略 text", "内容可能为空时给 minHeight,否则高度塌成 0 遮罩无处可盖" ] + }, + { + "id": "relativeTime", + "title": "相对时间 RelativeTime", + "demo": "sec-reltime", + "props": [ + { + "n": "value", + "t": "string | number | Date", + "d": "目标时间:ISO 串 / 时间戳(10 位按秒、13 位按毫秒)/ Date" + }, + { + "n": "refresh", + "t": "number", + "d": "自动刷新间隔(ms);缺省 30000,传 0 不刷新" + }, + { + "n": "threshold", + "t": "number", + "d": "超过该秒数改用绝对日期;缺省 7 天" + }, + { + "n": "fallback", + "t": "string", + "d": "时间非法时的占位文案;缺省「—」" + }, + { + "n": "ariaLabel", + "t": "string", + "d": "可访问名(如「下单时间」),让读屏知道这个时间是什么" + } + ], + "example": "relativeTime({ value: row.createdAt, ariaLabel: '下单时间' })", + "steps": [ + "把后端给的时间原样交进来:ISO 串、10 位秒级戳、13 位毫秒戳、Date 都认,不用自己 new Date", + "默认每 30 秒刷新一次文案(「3 分钟前」会在不刷新页面的情况下变「4 分钟前」);列表里条数极多时可把 refresh 调大或传 0", + "超过 7 天自动改显示绝对日期(YYYY-MM-DD HH:mm),想更早切换就调小 threshold", + "精确时间放在 title:鼠标悬停可看,不必为「想知道具体哪天」再点开详情", + "时间非法(空串 / 格式错)不猜也不抛,显示 fallback(缺省「—」),列表不会因一条脏数据空白" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 5c8456d..decf119 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -5779,3 +5779,10 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } color: var(--ua-color-text-secondary); font-size: var(--ua-font-size-sm); } + +/* ---- 相对时间:文字色与正文一致,靠 title 提供精确值,不做下划线等「可点」暗示(它不可点) ---- */ +.ua-reltime { + color: inherit; + font-variant-numeric: tabular-nums; /* 等宽数字:列表里时间列不左右抖动 */ + white-space: nowrap; /* 「3 分钟前」不该被折成两行 */ +}