iter(g167): 弹窗支持内置页脚与异步确认
This commit is contained in:
+20
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T08:12:34.257Z",
|
"generatedAt": "2026-10-26T08:13:59.371Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -1720,7 +1720,23 @@
|
|||||||
{
|
{
|
||||||
"n": "footer",
|
"n": "footer",
|
||||||
"t": "unknown",
|
"t": "unknown",
|
||||||
"d": "底部操作区内容(常见为按钮组,右对齐);缺省不渲染"
|
"d": "底部操作区内容(常见为按钮组,右对齐);缺省不渲染;给了它就用自定义页脚,内置按钮组不再渲染"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "confirmText",
|
||||||
|
"t": "string",
|
||||||
|
"d": "内置页脚:确认按钮文案;缺省「确定」(不传 footer 且有 onConfirm 时也渲染内置页脚)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "cancelText",
|
||||||
|
"t": "string",
|
||||||
|
"d": "内置页脚:取消按钮文案;缺省「取消」,点击走 close 流程"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onConfirm",
|
||||||
|
"t": "() => void | Promise<void>",
|
||||||
|
"k": 1,
|
||||||
|
"d": "内置页脚:确认回调;返回 Promise 时按钮变「处理中…」并禁用,成功自动收窗、失败留在窗口重试"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "closeOnOverlay",
|
"n": "closeOnOverlay",
|
||||||
@@ -1746,7 +1762,8 @@
|
|||||||
"打开:open(true);关闭:open(false),点遮罩、× 或 Esc 会自动写回 false;多弹层叠加时 Esc 只关最上层(后开先关)",
|
"打开:open(true);关闭:open(false),点遮罩、× 或 Esc 会自动写回 false;多弹层叠加时 Esc 只关最上层(后开先关)",
|
||||||
"入场自带遮罩淡入 + 弹窗上浮动画",
|
"入场自带遮罩淡入 + 弹窗上浮动画",
|
||||||
"底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'",
|
"底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'",
|
||||||
"打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素"
|
"打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素",
|
||||||
|
"普通确认框不必手拼 footer:传 confirmText / onConfirm 即得「取消 + 确认」按钮组;onConfirm 写成 async,组件会在等待期间把按钮置灰改文案,resolve 后自己收窗、reject 时保持打开供重试"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+6
-9
@@ -576,19 +576,16 @@ const content = h(
|
|||||||
),
|
),
|
||||||
modal(
|
modal(
|
||||||
{
|
{
|
||||||
title: '确认操作',
|
title: '确认操作(内置页脚 · 异步确认)',
|
||||||
open: modalOpen,
|
open: modalOpen,
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
|
confirmText: '确认提交',
|
||||||
|
cancelText: '再想想',
|
||||||
onClose: () => modalOpen(false),
|
onClose: () => modalOpen(false),
|
||||||
footer: h(
|
onConfirm: async () => { await new Promise((r) => setTimeout(r, 900)); toast.success('已确认,窗口自动关闭'); }, // 成功后组件自己收窗
|
||||||
'div',
|
|
||||||
{ class: 'row' },
|
|
||||||
button({ label: '取消', variant: 'ghost', onClick: () => modalOpen(false) }),
|
|
||||||
button({ label: '确定', onClick: () => { modalOpen(false); toast.success('已确认'); } }),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
h('p', null, '这是一个由信号驱动的弹窗组件(size: lg + footer 操作区)。'),
|
h('p', null, '不传 footer 时走内置按钮组:onConfirm 返回 Promise 期间按钮变「处理中…」并禁用,成功才收窗,失败留在窗口重试(提示交给调用方的 toast)。'),
|
||||||
h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'footer', variant: 'default' })),
|
h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'onConfirm', variant: 'default' })),
|
||||||
select({ options: [{ value: 'a', label: '优先处理' }, { value: 'b', label: '排队等待' }, { value: 'c', label: '转交他人' }], value: modalPick }),
|
select({ options: [{ value: 'a', label: '优先处理' }, { value: 'b', label: '排队等待' }, { value: 'c', label: '转交他人' }], value: modalPick }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T08:12:34.257Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T08:13:59.371Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -879,7 +879,10 @@ const page = signal(1); pagination({ page, pageSize: 3, total: 24, onChange: (p)
|
|||||||
- open(必填):Signal<boolean> —— 开关信号:true 显示 / false 隐藏
|
- open(必填):Signal<boolean> —— 开关信号:true 显示 / false 隐藏
|
||||||
- title(必填):string —— 标题
|
- title(必填):string —— 标题
|
||||||
- size(默认 md):'sm' | 'md' | 'lg' —— 宽度档位:sm 340px / md 420px / lg 640px;缺省 md
|
- size(默认 md):'sm' | 'md' | 'lg' —— 宽度档位:sm 340px / md 420px / lg 640px;缺省 md
|
||||||
- footer:unknown —— 底部操作区内容(常见为按钮组,右对齐);缺省不渲染
|
- footer:unknown —— 底部操作区内容(常见为按钮组,右对齐);缺省不渲染;给了它就用自定义页脚,内置按钮组不再渲染
|
||||||
|
- confirmText:string —— 内置页脚:确认按钮文案;缺省「确定」(不传 footer 且有 onConfirm 时也渲染内置页脚)
|
||||||
|
- cancelText:string —— 内置页脚:取消按钮文案;缺省「取消」,点击走 close 流程
|
||||||
|
- onConfirm(回调):() => void | Promise<void> —— 内置页脚:确认回调;返回 Promise 时按钮变「处理中…」并禁用,成功自动收窗、失败留在窗口重试
|
||||||
- closeOnOverlay:boolean —— 点遮罩是否关闭弹窗;缺省 true,表单类可传 false 防误关(× 与 Esc 仍可用)
|
- closeOnOverlay:boolean —— 点遮罩是否关闭弹窗;缺省 true,表单类可传 false 防误关(× 与 Esc 仍可用)
|
||||||
- onClose(回调):() => void —— 关闭回调(点遮罩、× 或 Esc 触发)
|
- onClose(回调):() => void —— 关闭回调(点遮罩、× 或 Esc 触发)
|
||||||
- ...children:unknown[] —— 弹窗内容子节点
|
- ...children:unknown[] —— 弹窗内容子节点
|
||||||
@@ -898,6 +901,7 @@ const open = signal(false); modal({ title: '确认操作', open, onClose: () =>
|
|||||||
4. 入场自带遮罩淡入 + 弹窗上浮动画
|
4. 入场自带遮罩淡入 + 弹窗上浮动画
|
||||||
5. 底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'
|
5. 底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'
|
||||||
6. 打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素
|
6. 打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素
|
||||||
|
7. 普通确认框不必手拼 footer:传 confirmText / onConfirm 即得「取消 + 确认」按钮组;onConfirm 写成 async,组件会在等待期间把按钮置灰改文案,resolve 后自己收窗、reject 时保持打开供重试
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-tabs-modal
|
现场演示:apps/docs/demo.html#sec-tabs-modal
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { ModalProps } from './types.js';
|
import type { ModalProps } from './types.js';
|
||||||
import { lockScroll, pushOverlay, trapFocus } from './overlay.js';
|
import { lockScroll, pushOverlay, trapFocus } from './overlay.js';
|
||||||
|
import { button } from './button.js';
|
||||||
|
|
||||||
let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby 关联
|
let seq = 0; // 实例序号:给标题生成唯一 id,供 aria-labelledby 关联
|
||||||
|
|
||||||
@@ -12,6 +13,23 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
|||||||
props.open(false); // 写回信号触发隐藏
|
props.open(false); // 写回信号触发隐藏
|
||||||
props.onClose?.();
|
props.onClose?.();
|
||||||
};
|
};
|
||||||
|
/*** 内置页脚:取消直接关;确认若返回 Promise,则按钮转「处理中…」并禁用,成功才收窗、失败留在窗口重试 ***/
|
||||||
|
const builtinFoot = (): HTMLElement => {
|
||||||
|
const ok = button({ label: props.confirmText ?? '确定' }) as HTMLButtonElement;
|
||||||
|
const cancel = button({ label: props.cancelText ?? '取消', variant: 'ghost', onClick: close }) as HTMLButtonElement;
|
||||||
|
const idle = ok.textContent; // 记下常态文案,异步结束后还原
|
||||||
|
ok.addEventListener('click', () => {
|
||||||
|
const out = props.onConfirm?.();
|
||||||
|
if (!(out instanceof Promise)) { close(); return; } // 同步确认:直接收窗
|
||||||
|
ok.disabled = true;
|
||||||
|
ok.textContent = '处理中…';
|
||||||
|
out.then(() => { if (props.open()) close(); }) // 成功才关;onConfirm 里已手动关掉就不重复触发
|
||||||
|
.catch(() => { /* 失败什么都不做:保持窗口打开让用户重试,提示交给调用方的 toast */ })
|
||||||
|
.finally(() => { ok.disabled = false; ok.textContent = idle; });
|
||||||
|
});
|
||||||
|
return h('div', { class: 'ua-modal-actions' }, cancel, ok);
|
||||||
|
};
|
||||||
|
const foot = props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : props.confirmText || props.onConfirm ? h('div', { class: 'ua-modal-foot' }, builtinFoot()) : null; // footer 优先,其次内置按钮组
|
||||||
const overlay = h('div', {
|
const overlay = h('div', {
|
||||||
class: 'ua-modal-overlay',
|
class: 'ua-modal-overlay',
|
||||||
onClick: (e: Event) => {
|
onClick: (e: Event) => {
|
||||||
@@ -25,7 +43,7 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
|||||||
{ class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId }, // 对话框语义 + 标题关联
|
{ class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId }, // 对话框语义 + 标题关联
|
||||||
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title', id: titleId }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')),
|
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title', id: titleId }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')),
|
||||||
h('div', { class: 'ua-modal-body' }, ...children),
|
h('div', { class: 'ua-modal-body' }, ...children),
|
||||||
props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染
|
foot, // 底部操作区:自定义 footer 或内置按钮组,都没有则不渲染
|
||||||
);
|
);
|
||||||
overlay.append(box);
|
overlay.append(box);
|
||||||
let lastFocus: HTMLElement | null = null; // 打开前焦点宿主,关闭时归还
|
let lastFocus: HTMLElement | null = null; // 打开前焦点宿主,关闭时归还
|
||||||
|
|||||||
@@ -363,6 +363,9 @@ export interface ModalProps {
|
|||||||
open: Signal<boolean>; // 开关信号:true 显示 / false 隐藏
|
open: Signal<boolean>; // 开关信号:true 显示 / false 隐藏
|
||||||
size?: 'sm' | 'md' | 'lg'; // 宽度档位;缺省 md(420px),sm 340px / lg 640px
|
size?: 'sm' | 'md' | 'lg'; // 宽度档位;缺省 md(420px),sm 340px / lg 640px
|
||||||
footer?: unknown; // 底部操作区内容(常见为按钮组);缺省不渲染
|
footer?: unknown; // 底部操作区内容(常见为按钮组);缺省不渲染
|
||||||
|
confirmText?: string; // 内置页脚:确认按钮文案;与 footer 二选一(footer 优先),缺省「确定」
|
||||||
|
cancelText?: string; // 内置页脚:取消按钮文案;缺省「取消」
|
||||||
|
onConfirm?: () => void | Promise<void>; // 内置页脚:确认回调;返回 Promise 时装 loading 禁用按钮,成功才收窗、失败留在窗口重试
|
||||||
closeOnOverlay?: boolean; // 点遮罩是否关闭;缺省 true(表单类弹窗可传 false 防误关,× 与 Esc 仍可用)
|
closeOnOverlay?: boolean; // 点遮罩是否关闭;缺省 true(表单类弹窗可传 false 防误关,× 与 Esc 仍可用)
|
||||||
onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发)
|
onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1591,7 +1591,23 @@
|
|||||||
{
|
{
|
||||||
"n": "footer",
|
"n": "footer",
|
||||||
"t": "unknown",
|
"t": "unknown",
|
||||||
"d": "底部操作区内容(常见为按钮组,右对齐);缺省不渲染"
|
"d": "底部操作区内容(常见为按钮组,右对齐);缺省不渲染;给了它就用自定义页脚,内置按钮组不再渲染"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "confirmText",
|
||||||
|
"t": "string",
|
||||||
|
"d": "内置页脚:确认按钮文案;缺省「确定」(不传 footer 且有 onConfirm 时也渲染内置页脚)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "cancelText",
|
||||||
|
"t": "string",
|
||||||
|
"d": "内置页脚:取消按钮文案;缺省「取消」,点击走 close 流程"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onConfirm",
|
||||||
|
"t": "() => void | Promise<void>",
|
||||||
|
"k": 1,
|
||||||
|
"d": "内置页脚:确认回调;返回 Promise 时按钮变「处理中…」并禁用,成功自动收窗、失败留在窗口重试"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "closeOnOverlay",
|
"n": "closeOnOverlay",
|
||||||
@@ -1617,7 +1633,8 @@
|
|||||||
"打开:open(true);关闭:open(false),点遮罩、× 或 Esc 会自动写回 false;多弹层叠加时 Esc 只关最上层(后开先关)",
|
"打开:open(true);关闭:open(false),点遮罩、× 或 Esc 会自动写回 false;多弹层叠加时 Esc 只关最上层(后开先关)",
|
||||||
"入场自带遮罩淡入 + 弹窗上浮动画",
|
"入场自带遮罩淡入 + 弹窗上浮动画",
|
||||||
"底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'",
|
"底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'",
|
||||||
"打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素"
|
"打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素",
|
||||||
|
"普通确认框不必手拼 footer:传 confirmText / onConfirm 即得「取消 + 确认」按钮组;onConfirm 写成 async,组件会在等待期间把按钮置灰改文案,resolve 后自己收窗、reject 时保持打开供重试"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1823,6 +1823,11 @@ button.ua-step-dot:hover {
|
|||||||
.ua-modal-lg {
|
.ua-modal-lg {
|
||||||
width: min(640px, 92vw);
|
width: min(640px, 92vw);
|
||||||
}
|
}
|
||||||
|
.ua-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--ua-space-2);
|
||||||
|
margin-left: auto; /* 内置页脚:按钮组靠右,与自定义 footer 的排布一致 */
|
||||||
|
}
|
||||||
.ua-modal-foot {
|
.ua-modal-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
Reference in New Issue
Block a user