iter(g35): 新增 notification 通知卡片组件
This commit is contained in:
@@ -18,6 +18,7 @@ export { avatar } from './avatar.js';
|
||||
export { image } from './image.js';
|
||||
export { alert } from './alert.js';
|
||||
export { toast, showToast } from './toast.js';
|
||||
export { notification } from './notification.js';
|
||||
export { tooltip } from './tooltip.js';
|
||||
export { popover } from './popover.js';
|
||||
export { flip } from './position.js';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { h } from '@ui-arch/frame';
|
||||
import type { NotificationProps } from './types.js';
|
||||
|
||||
let host: HTMLElement | null = null; // 右下角堆叠容器:首条通知出现时懒创建
|
||||
const MAX_STACK = 3; // 同屏最多条数,超出挤掉最早一条
|
||||
|
||||
/*** 确保堆叠容器存在:fixed 挂 body 末尾,与右上角 toast 分区不打架 ***/
|
||||
function ensureHost(): HTMLElement {
|
||||
if (host) return host;
|
||||
host = h('div', { class: 'ua-notice-host' });
|
||||
document.body.appendChild(host);
|
||||
return host;
|
||||
}
|
||||
|
||||
/*** 退场并移除:摘 ua-open 触发过渡,transitionend 与 320ms 超时二选一兜底 ***/
|
||||
function dismiss(item: HTMLElement): void {
|
||||
item.classList.remove('ua-open');
|
||||
item.addEventListener('transitionend', () => item.remove(), { once: true });
|
||||
window.setTimeout(() => item.isConnected && item.remove(), 320); // reduced-motion 下不触发 transition
|
||||
}
|
||||
|
||||
/*** 通知卡片:标题 + 描述 + 可选操作区,返回 { el, close } 句柄;duration 为 0 表示常驻 ***/
|
||||
export function notification(props: NotificationProps): { el: HTMLElement; close: () => void } {
|
||||
// —— 状态集中声明 ——
|
||||
const duration = props.duration ?? 0; // 缺省常驻:通知通常需要用户读完
|
||||
const closable = props.closable ?? true; // 缺省带关闭按钮
|
||||
let timer = 0; // 自动关闭计时器句柄
|
||||
const closeBtn = h('button', { type: 'button', class: 'ua-notice-close', 'aria-label': '关闭通知' }, '×') as HTMLButtonElement; // 关闭按钮,回调在下方绑定
|
||||
const el = h(
|
||||
'div',
|
||||
{ class: `ua-notice ua-notice-${props.variant ?? 'info'}`, role: 'alert' },
|
||||
h(
|
||||
'div',
|
||||
{ class: 'ua-notice-head' },
|
||||
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;
|
||||
|
||||
/*** 关闭:清计时器后走退场动画 ***/
|
||||
const close = () => {
|
||||
window.clearTimeout(timer);
|
||||
dismiss(el);
|
||||
};
|
||||
closeBtn.addEventListener('click', () => close());
|
||||
|
||||
const stack = ensureHost();
|
||||
if (stack.childElementCount >= MAX_STACK) stack.firstElementChild?.remove(); // 超出上限挤掉最早一条
|
||||
stack.appendChild(el);
|
||||
requestAnimationFrame(() => el.classList.add('ua-open')); // 下一帧入场,保证过渡生效
|
||||
if (duration > 0) timer = window.setTimeout(() => dismiss(el), duration); // 常驻(0)不设定时器
|
||||
return { el, close };
|
||||
}
|
||||
@@ -420,6 +420,16 @@ export interface ListProps {
|
||||
onItemClick?: (item: ListItem, index: number) => void; // 条目点击回调;提供后条目可点击并带 hover 反馈
|
||||
}
|
||||
|
||||
/*** 通知入参 ***/
|
||||
export interface NotificationProps {
|
||||
title: string; // 通知标题(必填)
|
||||
description?: string; // 补充说明;缺省不渲染
|
||||
variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info
|
||||
duration?: number; // 自动关闭毫秒;缺省 0 = 常驻(通知通常要读完)
|
||||
closable?: boolean; // 是否显示关闭按钮;缺省 true
|
||||
action?: unknown; // 操作区内容(如按钮);缺省不渲染
|
||||
}
|
||||
|
||||
/*** 文件选择入参 ***/
|
||||
export interface UploadProps {
|
||||
accept?: string; // 接受的类型(同原生 accept),如 'image/*,.pdf'
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"list(props):列表,header/footer 外壳 + 条目 avatar/title/description/extra,onItemClick 让条目可点击,items 为空回落空状态",
|
||||
"carousel(props):轮播,items 横排位移,指示点/箭头/←→ 键切换,autoplay 毫秒自动前进且悬停暂停,index 信号可外部受控",
|
||||
"tree(props):树形,递归节点 + 箭头展开/收起 + 点选节点,键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应",
|
||||
"upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)"
|
||||
"upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)",
|
||||
"notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭"
|
||||
],
|
||||
"keyTypes": [
|
||||
"ButtonProps",
|
||||
@@ -1817,6 +1818,52 @@
|
||||
"键盘:区域可 Tab 聚焦,Enter/空格 打开选择器;拖拽时区域高亮,拖出恢复",
|
||||
"disabled 时不可点选、不接受拖拽;已选清单的 × 会触发 onChange 回传剩余文件"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "notification",
|
||||
"title": "通知卡片 Notification",
|
||||
"demo": "sec-notification",
|
||||
"props": [
|
||||
{
|
||||
"n": "title",
|
||||
"t": "string",
|
||||
"r": 1,
|
||||
"d": "通知标题"
|
||||
},
|
||||
{
|
||||
"n": "description",
|
||||
"t": "string",
|
||||
"d": "补充说明;缺省不渲染"
|
||||
},
|
||||
{
|
||||
"n": "variant",
|
||||
"t": "'info' | 'success' | 'warning' | 'danger'",
|
||||
"d": "语义色(左侧条与圆点);缺省 info"
|
||||
},
|
||||
{
|
||||
"n": "duration",
|
||||
"t": "number",
|
||||
"d": "自动关闭毫秒;缺省 0 = 常驻不自动关"
|
||||
},
|
||||
{
|
||||
"n": "closable",
|
||||
"t": "boolean",
|
||||
"d": "是否显示关闭按钮;缺省 true"
|
||||
},
|
||||
{
|
||||
"n": "action",
|
||||
"t": "unknown",
|
||||
"d": "操作区内容(如按钮);缺省不渲染"
|
||||
}
|
||||
],
|
||||
"example": "const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭",
|
||||
"steps": [
|
||||
"调用即入列:右下角堆叠,同屏最多 3 条,超出自动挤掉最早一条",
|
||||
"缺省常驻(duration 0),需要自动消失就传毫秒数,如 duration: 4000",
|
||||
"返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el",
|
||||
"action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色",
|
||||
"与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2282,3 +2282,84 @@ button.ua-step-dot:hover {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* —— 通知卡片 —— */
|
||||
.ua-notice-host {
|
||||
position: fixed;
|
||||
right: var(--ua-space-4);
|
||||
bottom: var(--ua-space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ua-space-2);
|
||||
z-index: 60;
|
||||
pointer-events: none; /* 容器不挡点击,卡片自身恢复 */
|
||||
}
|
||||
.ua-notice {
|
||||
width: 300px;
|
||||
padding: var(--ua-space-3);
|
||||
border: 1px solid var(--ua-color-border);
|
||||
border-left: 3px solid var(--ua-color-accent);
|
||||
border-radius: var(--ua-radius-md);
|
||||
background: var(--ua-color-bg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.ua-notice-open {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
.ua-notice-success { border-left-color: var(--ua-color-success); }
|
||||
.ua-notice-warning { border-left-color: var(--ua-color-warning); }
|
||||
.ua-notice-danger { border-left-color: var(--ua-color-danger); }
|
||||
.ua-notice-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ua-space-1);
|
||||
}
|
||||
.ua-notice-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--ua-color-accent);
|
||||
flex: none;
|
||||
}
|
||||
.ua-notice-success .ua-notice-dot { background: var(--ua-color-success); }
|
||||
.ua-notice-warning .ua-notice-dot { background: var(--ua-color-warning); }
|
||||
.ua-notice-danger .ua-notice-dot { background: var(--ua-color-danger); }
|
||||
.ua-notice-title {
|
||||
flex: 1;
|
||||
font-size: var(--ua-font-size-md);
|
||||
font-weight: 600;
|
||||
color: var(--ua-color-text);
|
||||
}
|
||||
.ua-notice-close {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--ua-color-text-muted);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.ua-notice-close:hover {
|
||||
color: var(--ua-color-text);
|
||||
}
|
||||
.ua-notice-desc {
|
||||
margin-top: var(--ua-space-1);
|
||||
font-size: var(--ua-font-size-sm);
|
||||
color: var(--ua-color-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ua-notice-action {
|
||||
margin-top: var(--ua-space-2);
|
||||
display: flex;
|
||||
gap: var(--ua-space-1);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ua-notice {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user