iter(g161): 通知支持同 key 原地更新
This commit is contained in:
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T08:03:40.841Z",
|
"generatedAt": "2026-10-26T08:04:47.351Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -2419,6 +2419,11 @@
|
|||||||
"t": "'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'",
|
"t": "'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'",
|
||||||
"df": "bottom-right",
|
"df": "bottom-right",
|
||||||
"d": "停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧"
|
"d": "停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "key",
|
||||||
|
"t": "string",
|
||||||
|
"d": "同 key 复用同一张卡:再次调用只原地改标题 / 描述 / 语义色,不再堆新条(返回的句柄带 update 方法可主动改)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭",
|
"example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭",
|
||||||
@@ -2428,7 +2433,8 @@
|
|||||||
"返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el",
|
"返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el",
|
||||||
"action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色",
|
"action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色",
|
||||||
"与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作",
|
"与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作",
|
||||||
"停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让"
|
"停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让",
|
||||||
|
"进度类通知传 key: 'sync-job':第一次返回句柄,后续 job.update({ title, description, variant }) 就地把同一条卡改成 60% / 完成,不会像以前那样堆出三条;job.close() 收掉时同时释放 key 登记"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-1
@@ -872,7 +872,7 @@ const content = h(
|
|||||||
section(
|
section(
|
||||||
'sec-notification',
|
'sec-notification',
|
||||||
'通知卡片',
|
'通知卡片',
|
||||||
'notification:右下角堆叠(与右上角 toast 分区),标题 + 描述 + 操作区,缺省常驻需手动关闭;duration 传毫秒可自动消失,悬停或聚焦会暂停倒计时、离开按剩余时间续计。',
|
'notification:右下角堆叠(与右上角 toast 分区),标题 + 描述 + 操作区,缺省常驻需手动关闭;duration 传毫秒可自动消失,悬停或聚焦会暂停倒计时、离开按剩余时间续计;带 key 的第二次调用原地改同一张卡(进度刷新不再堆条)。',
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'row' },
|
{ class: 'row' },
|
||||||
@@ -881,6 +881,12 @@ const content = h(
|
|||||||
button({ label: '警告(常驻)', variant: 'ghost', onClick: () => notification({ title: '连接不稳定', description: '正在重试第 2 次,请检查网络', variant: 'warning' }) }),
|
button({ label: '警告(常驻)', variant: 'ghost', onClick: () => notification({ title: '连接不稳定', description: '正在重试第 2 次,请检查网络', variant: 'warning' }) }),
|
||||||
button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }),
|
button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }),
|
||||||
button({ label: '左上角通知', variant: 'ghost', onClick: () => notification({ title: '后台任务已排队', description: '四个角各自独立堆叠,这条去左上角', placement: 'top-left' }) }),
|
button({ label: '左上角通知', variant: 'ghost', onClick: () => notification({ title: '后台任务已排队', description: '四个角各自独立堆叠,这条去左上角', placement: 'top-left' }) }),
|
||||||
|
button({ label: '同 key 原地更新', variant: 'ghost', onClick: () => { // 三拍演示:一条卡原地改三次,不会堆成三条
|
||||||
|
const job = notification({ key: 'sync-job', title: '同步中 0%', description: '正在上传附件' });
|
||||||
|
window.setTimeout(() => job.update({ title: '同步中 60%', description: '已上传 3 / 5 个文件' }), 900);
|
||||||
|
window.setTimeout(() => job.update({ title: '同步完成', description: '5 个文件已全部就绪', variant: 'success' }), 1800);
|
||||||
|
window.setTimeout(() => job.close(), 4000);
|
||||||
|
} }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
section(
|
section(
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T08:03:40.841Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T08:04:47.351Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -1246,6 +1246,7 @@ upload({ multiple: true, accept: 'image/*', hint: '拖拽图片到此处', onCha
|
|||||||
- closable:boolean —— 是否显示关闭按钮;缺省 true
|
- closable:boolean —— 是否显示关闭按钮;缺省 true
|
||||||
- action:unknown —— 操作区内容(如按钮);缺省不渲染
|
- action:unknown —— 操作区内容(如按钮);缺省不渲染
|
||||||
- placement(默认 bottom-right):'top-right' | 'bottom-right' | 'top-left' | 'bottom-left' —— 停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧
|
- placement(默认 bottom-right):'top-right' | 'bottom-right' | 'top-left' | 'bottom-left' —— 停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧
|
||||||
|
- key:string —— 同 key 复用同一张卡:再次调用只原地改标题 / 描述 / 语义色,不再堆新条(返回的句柄带 update 方法可主动改)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -1261,6 +1262,7 @@ const { close } = notification({ title: '导出完成', description: 'report.csv
|
|||||||
4. action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色
|
4. action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色
|
||||||
5. 与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作
|
5. 与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作
|
||||||
6. 停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让
|
6. 停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让
|
||||||
|
7. 进度类通知传 key: 'sync-job':第一次返回句柄,后续 job.update({ title, description, variant }) 就地把同一条卡改成 60% / 完成,不会像以前那样堆出三条;job.close() 收掉时同时释放 key 登记
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-notification
|
现场演示:apps/docs/demo.html#sec-notification
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { h } from '@ui-arch/frame';
|
import { h } from '@ui-arch/frame';
|
||||||
import type { NotificationProps } from './types.js';
|
import type { NotificationHandle, NotificationProps } from './types.js';
|
||||||
|
|
||||||
const hosts = new Map<string, HTMLElement>(); // 每个角落一个堆叠容器,首次用到该角落时懒创建
|
const hosts = new Map<string, HTMLElement>(); // 每个角落一个堆叠容器,首次用到该角落时懒创建
|
||||||
|
const byKey = new Map<string, NotificationHandle>(); // 同 key 只留一张卡,二次调用走原地更新
|
||||||
const MAX_STACK = 3; // 同一角落最多条数,超出挤掉最早一条
|
const MAX_STACK = 3; // 同一角落最多条数,超出挤掉最早一条
|
||||||
|
|
||||||
/*** 确保指定角落的堆叠容器存在:fixed 挂 body 末尾,与 toast 的右上角分区不打架 ***/
|
/*** 确保指定角落的堆叠容器存在:fixed 挂 body 末尾,与 toast 的右上角分区不打架 ***/
|
||||||
@@ -21,8 +22,11 @@ function dismiss(item: HTMLElement): void {
|
|||||||
window.setTimeout(() => item.isConnected && item.remove(), 320); // reduced-motion 下不触发 transition
|
window.setTimeout(() => item.isConnected && item.remove(), 320); // reduced-motion 下不触发 transition
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 通知卡片:标题 + 描述 + 可选操作区,返回 { el, close } 句柄;duration 为 0 表示常驻,非 0 时悬停 / 聚焦自动暂停计时 ***/
|
/*** 通知卡片:标题 + 描述 + 可选操作区,返回 { el, close, update } 句柄;duration 为 0 表示常驻,非 0 时悬停 / 聚焦自动暂停计时;带 key 时二次调用原地更新同一张卡 ***/
|
||||||
export function notification(props: NotificationProps): { el: HTMLElement; close: () => void } {
|
export function notification(props: NotificationProps): NotificationHandle {
|
||||||
|
const dup = props.key ? byKey.get(props.key) : undefined;
|
||||||
|
if (dup) { dup.update(props); return dup; } // 同 key:只更新内容,不新增一条
|
||||||
|
|
||||||
// —— 状态集中声明 ——
|
// —— 状态集中声明 ——
|
||||||
const duration = props.duration ?? 0; // 缺省常驻:通知通常需要用户读完
|
const duration = props.duration ?? 0; // 缺省常驻:通知通常需要用户读完
|
||||||
const closable = props.closable ?? true; // 缺省带关闭按钮
|
const closable = props.closable ?? true; // 缺省带关闭按钮
|
||||||
@@ -31,27 +35,43 @@ export function notification(props: NotificationProps): { el: HTMLElement; close
|
|||||||
let startedAt = 0; // 本轮计时起点,用于折算暂停前已流逝的时间
|
let startedAt = 0; // 本轮计时起点,用于折算暂停前已流逝的时间
|
||||||
let paused = false; // 是否因悬停 / 聚焦而暂停
|
let paused = false; // 是否因悬停 / 聚焦而暂停
|
||||||
const closeBtn = h('button', { type: 'button', class: 'ua-notice-close', 'aria-label': '关闭通知' }, '×') as HTMLButtonElement; // 关闭按钮,回调在下方绑定
|
const closeBtn = h('button', { type: 'button', class: 'ua-notice-close', 'aria-label': '关闭通知' }, '×') as HTMLButtonElement; // 关闭按钮,回调在下方绑定
|
||||||
|
const titleEl = h('span', { class: 'ua-notice-title' }, props.title) as HTMLElement;
|
||||||
|
const actionEl = props.action ? h('div', { class: 'ua-notice-action' }, props.action) : null;
|
||||||
|
let descEl = props.description ? h('div', { class: 'ua-notice-desc' }, props.description) : null;
|
||||||
|
|
||||||
|
/*** 换语义色:整串重设 class,保住已挂上的入场态 ***/
|
||||||
|
const paint = (variant: string): void => {
|
||||||
|
el.className = `ua-notice ua-notice-${variant}${el.classList.contains('ua-open') ? ' ua-open' : ''}`;
|
||||||
|
};
|
||||||
const el = h(
|
const el = h(
|
||||||
'div',
|
'div',
|
||||||
{ class: `ua-notice ua-notice-${props.variant ?? 'info'}`, role: 'alert' },
|
{ class: `ua-notice ua-notice-${props.variant ?? 'info'}`, role: 'alert' },
|
||||||
h(
|
h('div', { class: 'ua-notice-head' }, h('span', { class: 'ua-notice-dot', 'aria-hidden': 'true' }), titleEl, closable ? closeBtn : null),
|
||||||
'div',
|
descEl,
|
||||||
{ class: 'ua-notice-head' },
|
actionEl,
|
||||||
h('span', { class: 'ua-notice-dot', 'aria-hidden': 'true' }),
|
|
||||||
h('span', { class: 'ua-notice-title' }, props.title),
|
|
||||||
closable ? closeBtn : null,
|
|
||||||
),
|
|
||||||
props.description ? h('div', { class: 'ua-notice-desc' }, props.description) : null,
|
|
||||||
props.action ? h('div', { class: 'ua-notice-action' }, props.action) : null,
|
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
/*** 关闭:清计时器后走退场动画 ***/
|
/*** 关闭:清计时器后走退场动画,顺手释放 key 登记 ***/
|
||||||
const close = () => {
|
const close = () => {
|
||||||
window.clearTimeout(timer);
|
window.clearTimeout(timer);
|
||||||
|
if (props.key && byKey.get(props.key) === handle) byKey.delete(props.key);
|
||||||
dismiss(el);
|
dismiss(el);
|
||||||
};
|
};
|
||||||
closeBtn.addEventListener('click', () => close());
|
closeBtn.addEventListener('click', () => close());
|
||||||
|
|
||||||
|
/*** 原地更新:改标题 / 描述 / 语义色;描述按需插入到操作区之前 ***/
|
||||||
|
const update = (next: Partial<NotificationProps>): void => {
|
||||||
|
if (next.title != null) titleEl.textContent = next.title;
|
||||||
|
if (next.variant) paint(next.variant);
|
||||||
|
if (next.description != null) {
|
||||||
|
if (!descEl) { // 首次补描述:插在操作区之前,没有操作区就追加
|
||||||
|
descEl = h('div', { class: 'ua-notice-desc' });
|
||||||
|
if (actionEl) el.insertBefore(descEl, actionEl); else el.appendChild(descEl);
|
||||||
|
}
|
||||||
|
descEl.textContent = next.description;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/*** 起表:按剩余毫秒设定时器 ***/
|
/*** 起表:按剩余毫秒设定时器 ***/
|
||||||
const arm = () => { if (remain <= 0) return; startedAt = Date.now(); timer = window.setTimeout(() => dismiss(el), remain); };
|
const arm = () => { if (remain <= 0) return; startedAt = Date.now(); timer = window.setTimeout(() => dismiss(el), remain); };
|
||||||
/*** 暂停计时:扣掉已流逝部分,剩余时间留给续计 ***/
|
/*** 暂停计时:扣掉已流逝部分,剩余时间留给续计 ***/
|
||||||
@@ -66,7 +86,9 @@ export function notification(props: NotificationProps): { el: HTMLElement; close
|
|||||||
const stack = ensureHost(props.placement ?? 'bottom-right'); // 缺省右下,与右上角 toast 错开
|
const stack = ensureHost(props.placement ?? 'bottom-right'); // 缺省右下,与右上角 toast 错开
|
||||||
if (stack.childElementCount >= MAX_STACK) stack.firstElementChild?.remove(); // 超出上限挤掉最早一条
|
if (stack.childElementCount >= MAX_STACK) stack.firstElementChild?.remove(); // 超出上限挤掉最早一条
|
||||||
stack.appendChild(el);
|
stack.appendChild(el);
|
||||||
|
const handle: NotificationHandle = { el, close, update };
|
||||||
|
if (props.key) byKey.set(props.key, handle);
|
||||||
requestAnimationFrame(() => el.classList.add('ua-open')); // 下一帧入场,保证过渡生效
|
requestAnimationFrame(() => el.classList.add('ua-open')); // 下一帧入场,保证过渡生效
|
||||||
if (duration > 0) arm(); // 常驻(0)不设定时器
|
if (duration > 0) arm(); // 常驻(0)不设定时器
|
||||||
return { el, close };
|
return handle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,6 +493,14 @@ export interface NotificationProps {
|
|||||||
closable?: boolean; // 是否显示关闭按钮;缺省 true
|
closable?: boolean; // 是否显示关闭按钮;缺省 true
|
||||||
action?: unknown; // 操作区内容(如按钮);缺省不渲染
|
action?: unknown; // 操作区内容(如按钮);缺省不渲染
|
||||||
placement?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'; // 停靠角落;缺省 bottom-right(避开右上角 toast)
|
placement?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'; // 停靠角落;缺省 bottom-right(避开右上角 toast)
|
||||||
|
key?: string; // 同 key 复用同一张卡:再次调用会原地改标题/描述/语义色,不再堆新条
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 通知句柄:拿到卡片节点、关闭函数与原地更新入口 ***/
|
||||||
|
export interface NotificationHandle {
|
||||||
|
el: HTMLElement; // 卡片根节点
|
||||||
|
close: () => void; // 关闭(走退场动画);同 key 登记随之释放
|
||||||
|
update: (next: Partial<NotificationProps>) => void; // 原地更新标题 / 描述 / 语义色
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 文件选择入参 ***/
|
/*** 文件选择入参 ***/
|
||||||
|
|||||||
@@ -2290,6 +2290,11 @@
|
|||||||
"t": "'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'",
|
"t": "'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'",
|
||||||
"df": "bottom-right",
|
"df": "bottom-right",
|
||||||
"d": "停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧"
|
"d": "停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "key",
|
||||||
|
"t": "string",
|
||||||
|
"d": "同 key 复用同一张卡:再次调用只原地改标题 / 描述 / 语义色,不再堆新条(返回的句柄带 update 方法可主动改)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭",
|
"example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭",
|
||||||
@@ -2299,7 +2304,8 @@
|
|||||||
"返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el",
|
"返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el",
|
||||||
"action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色",
|
"action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色",
|
||||||
"与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作",
|
"与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作",
|
||||||
"停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让"
|
"停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让",
|
||||||
|
"进度类通知传 key: 'sync-job':第一次返回句柄,后续 job.update({ title, description, variant }) 就地把同一条卡改成 60% / 完成,不会像以前那样堆出三条;job.close() 收掉时同时释放 key 登记"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user