815 lines
36 KiB
TypeScript
815 lines
36 KiB
TypeScript
import { computed, h, mount, renderList, signal } from '@ui-arch/frame';
|
||
|
||
/*** 演示用内联 SVG 图片(data URI,离线可用) ***/
|
||
const IMG_DEMO = 'data:image/svg+xml;utf8,' + encodeURIComponent(
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320">' +
|
||
'<rect width="480" height="320" fill="#185fa5"/>' +
|
||
'<circle cx="240" cy="160" r="90" fill="#ffffff" opacity="0.85"/>' +
|
||
'<text x="240" y="170" font-size="28" text-anchor="middle" fill="#185fa5">IMAGE DEMO</text></svg>',
|
||
);
|
||
import {
|
||
accordion,
|
||
alert,
|
||
avatar,
|
||
badge,
|
||
breadcrumb,
|
||
button,
|
||
card,
|
||
checkbox,
|
||
dataTable,
|
||
descriptions,
|
||
divider,
|
||
drawer,
|
||
empty,
|
||
image,
|
||
form,
|
||
formItem,
|
||
inputNumber,
|
||
list,
|
||
carousel,
|
||
tree,
|
||
notification,
|
||
result,
|
||
upload,
|
||
menu,
|
||
modal,
|
||
pagination,
|
||
popover,
|
||
progress,
|
||
radioGroup,
|
||
rate,
|
||
segmented,
|
||
select,
|
||
showToast,
|
||
skeleton,
|
||
slider,
|
||
spinner,
|
||
stepper,
|
||
statistic,
|
||
switchButton,
|
||
tabs,
|
||
tag,
|
||
textarea,
|
||
textField,
|
||
timeline,
|
||
toast,
|
||
tooltip,
|
||
} from '@ui-arch/ui';
|
||
import type { RowKey } from '@ui-arch/ui';
|
||
|
||
/*** 侧边目录:挂载后从 .sec 反推锚点,IntersectionObserver 高亮视口内区块(清单改动自动跟随);顶部搜索框按标题过滤 ***/
|
||
function buildToc(toc: HTMLElement, content: HTMLElement): void {
|
||
const secs = Array.from(content.querySelectorAll<HTMLElement>('section.sec'));
|
||
if (!secs.length) return; // 卫语句:无区块不建目录
|
||
const links = secs.map((sec) =>
|
||
h('a', { class: 'toc-link', href: `#${sec.id}`, 'data-for': sec.id }, sec.querySelector('h2')?.textContent ?? sec.id),
|
||
);
|
||
const search = h('input', { class: 'toc-search', type: 'search', placeholder: '过滤组件…', 'aria-label': '过滤组件目录' }) as HTMLInputElement;
|
||
const empty = h('p', { class: 'toc-empty' }, '无匹配项'); // 过滤后一条不剩时的提示
|
||
empty.hidden = true;
|
||
/*** 按标题子串过滤目录项;空关键字恢复全量 ***/
|
||
const apply = (): void => {
|
||
const q = search.value.trim().toLowerCase();
|
||
let hit = 0;
|
||
for (const link of links) {
|
||
const on = !q || (link.textContent ?? '').toLowerCase().includes(q);
|
||
link.hidden = !on; // 隐藏项同时退出 Tab 序(hidden 自带)
|
||
if (on) hit++;
|
||
}
|
||
empty.hidden = hit > 0;
|
||
};
|
||
search.addEventListener('input', apply);
|
||
search.addEventListener('keydown', (e: KeyboardEvent) => {
|
||
if (e.key !== 'Escape' || !search.value) return; // 卫语句:无内容时 Esc 交给默认行为
|
||
search.value = '';
|
||
apply();
|
||
});
|
||
toc.append(search, ...links, empty);
|
||
const io = new IntersectionObserver(
|
||
(entries) => {
|
||
for (const en of entries) {
|
||
if (!en.isIntersecting) continue; // 只处理刚进入视口的区块
|
||
const id = (en.target as HTMLElement).id;
|
||
for (const link of links) link.classList.toggle('is-active', link.getAttribute('data-for') === id);
|
||
}
|
||
},
|
||
{ rootMargin: '-10% 0px -80% 0px' }, // 顶部窄条判定,滚动时高亮更跟手
|
||
);
|
||
for (const sec of secs) io.observe(sec);
|
||
}
|
||
|
||
/*** 生成演示区块;id 供文档站 iframe 锚点定位 ***/
|
||
function section(id: string, title: string, desc: string, ...body: unknown[]): HTMLElement {
|
||
return h(
|
||
'section',
|
||
{ class: 'sec', id },
|
||
h('h2', null, title),
|
||
h('p', { class: 'desc' }, desc),
|
||
h('div', { class: 'stage' }, ...body),
|
||
);
|
||
}
|
||
|
||
// —— 演示数据 ——
|
||
const cities = ['北京', '上海', '深圳', '杭州'];
|
||
const users = [
|
||
'Alice', 'Bob', 'Charlie', 'Dave', 'Eve', 'Frank', 'Grace', 'Hank',
|
||
'Ivy', 'Jack', 'Kate', 'Liam', 'Mia', 'Noah', 'Olivia', 'Paul',
|
||
'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] }));
|
||
|
||
// —— 响应式状态演示 ——
|
||
const count = signal(0);
|
||
|
||
// —— 双向绑定演示 ——
|
||
const name = signal('');
|
||
|
||
// —— 表单控件状态集中声明 ——
|
||
const agreed = signal(false);
|
||
const nightMode = signal(true);
|
||
const city = signal('');
|
||
const size = signal('m');
|
||
const bio = signal('');
|
||
|
||
// —— 反馈组件状态 ——
|
||
const percent = signal(0);
|
||
setInterval(() => percent((percent() + 5) % 105), 400); // 自动推进演示进度
|
||
|
||
// —— 标签页 / 弹窗状态 ——
|
||
const activeTab = signal('a');
|
||
const modalOpen = signal(false);
|
||
const modalPick = signal('a'); // 弹窗内下拉的取值,用于演示 Esc 逐层关闭
|
||
|
||
// —— 表单校验状态集中声明 ——
|
||
const username = signal('');
|
||
const email = signal('');
|
||
const formResult = signal('');
|
||
|
||
// —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 ——
|
||
const tablePage = signal(1);
|
||
|
||
// —— 抽屉开关 ——
|
||
const drawerOpen = signal(false);
|
||
|
||
// —— 标签列表:关闭即写回信号,容器按信号重渲染 ——
|
||
const tags = signal(['设计', '前端', '组件库']);
|
||
|
||
// —— 滑块:音量与禁用态演示 ——
|
||
const volume = signal(40);
|
||
|
||
// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 ——
|
||
const qty = signal(1);
|
||
const price = signal('9.9'); // 演示 suffix 附加块
|
||
const amount = signal(2); // 演示 inputNumber 前后缀
|
||
|
||
// —— 步骤条:下单流程演示,按钮推进、点已完成步骤回跳 ——
|
||
const stepIdx = signal(0);
|
||
|
||
// —— 评分:满意度演示,与只读实例共用同一信号 ——
|
||
const score = signal(0);
|
||
|
||
// —— 空状态:搜索无结果演示 ——
|
||
const noResult = signal(true);
|
||
|
||
// —— 受控选中:dataTable 内部勾选实时写回,外部可清空 ——
|
||
const picked = signal<RowKey[]>([]);
|
||
|
||
// —— 表格加载态:切换后数据行变骨架行,筛选与翻页临时禁用 ——
|
||
const tableLoading = signal(false);
|
||
|
||
// —— 菜单选中结果:演示 onSelect 回调 ——
|
||
const menuPick = signal('');
|
||
const listPick = signal(''); // 列表点击选中的条目标题
|
||
const slideIndex = signal(0); // 轮播当前下标(外部受控演示)
|
||
const treePick = signal('core'); // 树形选中节点 key
|
||
const cityPick = signal(''); // 可搜索下拉的选中城市
|
||
const pickedFiles = signal(''); // 文件选择结果(名称串)
|
||
|
||
// —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled ——
|
||
const locked = signal(true);
|
||
|
||
// —— 统计数值:两个指标共用同一组按钮刷新,演示滚动过渡 ——
|
||
const gmv = signal(128000);
|
||
const orders = signal(2048);
|
||
|
||
// —— 分段控制器:两个实例共用同一信号,演示信号驱动同步与整组禁用 ——
|
||
const range = signal('week');
|
||
|
||
// —— 时间线:false 时末尾显示「进行中」虚线节点,true 表示流程已结束 ——
|
||
const flowDone = signal(false);
|
||
|
||
const content = h(
|
||
'div',
|
||
{ class: 'page' },
|
||
h(
|
||
'header',
|
||
null,
|
||
h('h1', null, '@ui-arch 组件库 · 现场演示'),
|
||
h('p', { class: 'sub' }, '驱动:@ui-arch/frame(自研微框架) + @ui-arch/ui(组件库) + @ui-arch/table-core(表格内核)'),
|
||
),
|
||
section(
|
||
'sec-counter',
|
||
'响应式状态',
|
||
'signal 驱动:按钮修改信号,文本自动更新,无虚拟 DOM。',
|
||
button({ label: 'count +1', onClick: () => count(count() + 1) }),
|
||
button({ label: '重置', variant: 'ghost', onClick: () => count(0) }),
|
||
h('span', { class: 'out' }, () => `count = ${count()}`),
|
||
),
|
||
section(
|
||
'sec-bind',
|
||
'双向绑定',
|
||
'textField 与信号双向同步:输入即回显;prefix/suffix 渲染为输入框内侧附加块;clearable 开启后有值即显一键清空。',
|
||
textField({ placeholder: '输入名字…', value: name }),
|
||
textField({ placeholder: '搜索…', value: name, prefix: '🔍', clearable: true }),
|
||
textField({ value: price, suffix: '元', clearable: true }),
|
||
h('span', { class: 'out' }, () => (name() ? `你好,${name()}!` : '等待输入…')),
|
||
),
|
||
section(
|
||
'sec-form',
|
||
'表单控件',
|
||
'checkbox / switch / select / radioGroup / textarea:状态一律由调用方信号持有;select 弹层在视口下方空间不足时自动翻到上方,展开后键入字母可快跳选项;textarea 传 maxCount 显示实时字数。',
|
||
h('div', { class: 'row' }, checkbox({ label: '同意条款', checked: agreed }), h('span', { class: 'out' }, () => (agreed() ? '已同意' : '未同意'))),
|
||
h('div', { class: 'row' }, switchButton({ label: '夜间模式', checked: nightMode }), h('span', { class: 'out' }, () => (nightMode() ? '开' : '关'))),
|
||
h('div', { class: 'row' }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '选择城市…' }), h('span', { class: 'out' }, () => (city() ? `城市:${city()}` : '未选择'))),
|
||
h('div', { class: 'row' }, radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }, { value: 'l', label: '大' }], value: size })),
|
||
textarea({ placeholder: '一句话介绍…', value: bio, rows: 2, maxCount: 30 }),
|
||
h('span', { class: 'out' }, () => `简介 ${bio().length} 字`),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '禁用按钮', variant: 'ghost', disabled: true }),
|
||
checkbox({ label: '信号禁用的勾选', disabled: locked }),
|
||
switchButton({ label: '锁定', checked: locked }), // 关掉开关即解锁上面的勾选框与输入框
|
||
textField({ placeholder: '锁定后不可输入…', disabled: locked }),
|
||
),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '信号禁用的下拉…', disabled: locked }),
|
||
radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }], value: size, disabled: locked }),
|
||
radioGroup({ options: [{ value: 'x', label: '静态禁用' }], value: size, disabled: true }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-drawer',
|
||
'抽屉',
|
||
'drawer:open 信号驱动侧滑面板,保留主视图上下文;遮罩 / × / Esc 关闭。',
|
||
button({ label: '打开抽屉', onClick: () => drawerOpen(true) }),
|
||
drawer(
|
||
{
|
||
title: '订单详情',
|
||
open: drawerOpen,
|
||
width: 360,
|
||
onClose: () => drawerOpen(false),
|
||
footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => drawerOpen(false) })),
|
||
},
|
||
h('p', null, '这里是抽屉内容,可放任意 DOM。'),
|
||
h('div', { class: 'row' }, tag({ label: '#A1024', variant: 'info' }), badge({ label: '已发货', variant: 'success' })),
|
||
),
|
||
),
|
||
section(
|
||
'sec-tag',
|
||
'标签',
|
||
'tag:可关闭标签(badge 的交互版),关闭回调里改信号即从列表移除。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
tag({ label: '纯展示', variant: 'default' }),
|
||
tag({ label: '通过', variant: 'success' }),
|
||
tag({ label: '待处理', variant: 'warning' }),
|
||
tag({ label: '已失效', variant: 'danger' }),
|
||
),
|
||
(() => {
|
||
const box = h('div', { class: 'row' });
|
||
renderList(box, () => tags().map((t) => tag({ label: t, variant: 'info', closable: true, onClose: () => tags(tags().filter((x) => x !== t)) })));
|
||
return box;
|
||
})(),
|
||
),
|
||
section(
|
||
'sec-avatar',
|
||
'头像',
|
||
'avatar:有 src 走图片(alt 用名称),无 src 用首字占位;size 与语义底色都可调。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
avatar({ name: '张三' }),
|
||
avatar({ name: 'Alice', size: 40, variant: 'success' }),
|
||
avatar({ name: 'Bob', size: 24, variant: 'warning' }),
|
||
avatar({ name: '李四', size: 36, variant: 'danger' }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-slider',
|
||
'滑块',
|
||
'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
slider({ value: volume, min: 0, max: 100, step: 5, onChange: (v) => toast.info(`音量 ${v}`) }),
|
||
slider({ value: volume, disabled: locked }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-inputnumber',
|
||
'数字输入',
|
||
'inputNumber:−/+ 按钮与键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取;长按按钮可连发步进。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => toast.info(`数量 ${v}`) }),
|
||
inputNumber({ value: qty, disabled: locked }),
|
||
inputNumber({ value: amount, min: 0, prefix: '¥', suffix: '件' }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-feedback',
|
||
'反馈与提示',
|
||
'badge / alert / spinner / progress:语义色全部来自令牌,暗色自动切换。',
|
||
badge({ label: '默认' }),
|
||
badge({ label: '成功', variant: 'success' }),
|
||
badge({ label: '警告', variant: 'warning' }),
|
||
badge({ label: '危险', variant: 'danger' }),
|
||
alert({ message: '信息提示:这是一条 info 提示条。' }),
|
||
alert({ message: '警告提示:操作可能存在风险。', variant: 'warning' }),
|
||
h('div', { class: 'row' }, spinner(), spinner({ size: 26 })),
|
||
progress({ value: percent }),
|
||
h('div', { class: 'row' }, h('span', { class: 'out' }, '不确定态:'), progress()),
|
||
),
|
||
section(
|
||
'sec-alert',
|
||
'提示条',
|
||
'alert:四档语义色 + 可选标题与关闭按钮,role=alert 由读屏播报。',
|
||
alert({ message: '这是一条中性提示(无标题)', variant: 'info' }),
|
||
alert({ title: '保存失败', message: '网络波动,请稍后重试;本次修改已保留在本地草稿。', variant: 'danger', closable: true, onClose: () => toast.warning('提示已关闭') }),
|
||
),
|
||
section(
|
||
'sec-tooltip',
|
||
'悬浮提示',
|
||
'tooltip 包裹触发器:hover / 聚焦延迟 150ms 出场,移出、失焦或 Esc 收起,气泡不抢指针;视口一侧空间不足时气泡自动翻到另一侧。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
tooltip({ content: '保存后立即生效' }, button({ label: '保存' })),
|
||
tooltip({ content: '该操作不可撤销', placement: 'bottom' }, button({ label: '删除', variant: 'danger' })),
|
||
tooltip({ content: '聚焦输入框同样会显示' }, textField({ placeholder: '聚焦我…' })),
|
||
),
|
||
),
|
||
section(
|
||
'sec-toast',
|
||
'消息提示',
|
||
'toast 右上角堆叠弹出,默认 3s 自动淡出移除(悬停可暂停倒计时);语义色四档,出入场微动画。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '成功', onClick: () => toast.success('保存成功') }),
|
||
button({ label: '错误', variant: 'danger', onClick: () => toast.error('网络请求失败') }),
|
||
button({ label: '警告', onClick: () => toast.warning('磁盘空间不足') }),
|
||
button({ label: '信息(1.5s)', onClick: () => toast.info('已同步云端', 1500) }),
|
||
button({ label: '可关闭', variant: 'ghost', onClick: () => showToast({ message: '这条可以手动关掉', variant: 'warning', closable: true }) }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-skeleton',
|
||
'骨架屏',
|
||
'skeleton 占位:text 多行段落 / rect 块 / circle 圆,扫光微动画,reduced-motion 自动静态化。',
|
||
card(
|
||
{ title: '加载中的用户资料' },
|
||
h('div', { class: 'row' }, skeleton({ variant: 'circle' }), skeleton({ variant: 'text', rows: 2 })),
|
||
skeleton({ variant: 'rect' }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-formlab',
|
||
'表单校验',
|
||
'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。',
|
||
form(
|
||
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) },
|
||
[
|
||
formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })),
|
||
formItem({ label: '邮箱', required: true, value: email, rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })),
|
||
formItem({ label: '城市', required: true, value: city }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '请选择城市…' })),
|
||
],
|
||
),
|
||
h('span', { class: 'out' }, () => formResult() || '等待提交…'),
|
||
),
|
||
section(
|
||
'sec-tabs-modal',
|
||
'标签页与弹窗',
|
||
'tabs 滑动指示条 + 方向键切换(←→),禁用页签置灰;modal 支持 Esc / 遮罩 / × 关闭,弹窗内的下拉展开时 Esc 先收下拉、再按才关弹窗(逐层关闭)。',
|
||
tabs({ tabs: [{ value: 'a', label: '概览' }, { value: 'b', label: '明细' }, { value: 'c', label: '设置' }, { value: 'd', label: '已停用', disabled: true }], active: activeTab }),
|
||
h('span', { class: 'out' }, () => `当前页签:${activeTab()}`),
|
||
button({ label: '打开弹窗', onClick: () => modalOpen(true) }),
|
||
modal(
|
||
{
|
||
title: '确认操作',
|
||
open: modalOpen,
|
||
size: 'lg',
|
||
onClose: () => modalOpen(false),
|
||
footer: h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '取消', variant: 'ghost', onClick: () => modalOpen(false) }),
|
||
button({ label: '确定', onClick: () => { modalOpen(false); toast.success('已确认'); } }),
|
||
),
|
||
},
|
||
h('p', null, '这是一个由信号驱动的弹窗组件(size: lg + footer 操作区)。'),
|
||
h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'footer', variant: 'default' })),
|
||
select({ options: [{ value: 'a', label: '优先处理' }, { value: 'b', label: '排队等待' }, { value: 'c', label: '转交他人' }], value: modalPick }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-stepper',
|
||
'步骤条',
|
||
'stepper:done/current/upcoming 三态随 active 信号流转,clickable 时点已完成步骤回跳。',
|
||
stepper({
|
||
steps: [{ label: '确认订单', description: '收货信息与商品清单' }, { label: '支付', description: '选择支付方式' }, { label: '完成', description: '等待发货' }],
|
||
active: stepIdx,
|
||
clickable: true,
|
||
onChange: (i) => toast.info(`回到第 ${i + 1} 步`),
|
||
}),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '上一步', variant: 'ghost', disabled: computed(() => stepIdx() === 0), onClick: () => stepIdx(Math.max(0, stepIdx() - 1)) }),
|
||
button({ label: '下一步', disabled: computed(() => stepIdx() === 2), onClick: () => stepIdx(Math.min(2, stepIdx() + 1)) }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-rate',
|
||
'评分',
|
||
'rate:点选星标打分,悬停预览移出还原,键盘 →/↑ 加分 ←/↓ 减分,disabled 只读展示。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
rate({ value: score, onChange: (v) => toast.info(`评分 ${v} 星`) }),
|
||
h('span', { class: 'out' }, () => `当前:${score() || '未评分'}`),
|
||
),
|
||
h('div', { class: 'row' }, rate({ value: score, disabled: true })),
|
||
),
|
||
section(
|
||
'sec-image',
|
||
'图片',
|
||
'image:缩略图点击 / 回车打开遮罩大图,Esc 或点击遮罩关闭,焦点归还缩略图;加载失败自动落虚线占位(第三张故意给错地址)。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
image({ src: IMG_DEMO, alt: '演示图一', width: 180 }),
|
||
image({ src: IMG_DEMO, alt: '演示图二', width: 120 }),
|
||
image({ src: './no-such-image.png', alt: '演示图三(地址无效)', width: 120, fallback: '图片走丢了' }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-empty',
|
||
'空状态',
|
||
'empty:无数据 / 无结果的统一占位,action 放引导按钮,点击按钮切回有数据态可对比。',
|
||
h('div', { class: 'row' }, h('span', { class: 'out' }, () => (noResult() ? '当前:无结果' : '当前:有数据'))),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '切换数据态', onClick: () => noResult(!noResult()) }),
|
||
),
|
||
() => (noResult()
|
||
? empty({ title: '暂无匹配结果', description: '试试放宽筛选条件,或清空关键词重新搜索。', action: button({ label: '重置筛选', variant: 'ghost', onClick: () => noResult(false) }) })
|
||
: card({ title: '搜索结果' }, h('p', null, '这里展示命中的数据列表。'))),
|
||
),
|
||
section(
|
||
'sec-divider',
|
||
'分隔线',
|
||
'divider:水平线可带居中文案,dashed 切虚线;vertical 行内竖线用于工具栏。',
|
||
divider({ label: '更多信息' }),
|
||
h('p', null, '分割线上下的内容各自成组。'),
|
||
divider({ dashed: true }),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '保存', variant: 'ghost' }),
|
||
divider({ vertical: true }),
|
||
button({ label: '另存为', variant: 'ghost' }),
|
||
divider({ vertical: true, dashed: true }),
|
||
button({ label: '导出', variant: 'ghost' }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-statistic',
|
||
'统计数值',
|
||
'statistic:大号等宽数字 + 前后缀 + 环比趋势;value 变化时 400ms 缓出滚动(减弱动效偏好下直落终值)。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
statistic({ title: '本月 GMV', value: gmv, prefix: '¥', precision: 2, trend: 'up' }),
|
||
statistic({ title: '订单数', value: orders, suffix: '单', trend: 'down' }),
|
||
),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '刷新数据', variant: 'ghost', onClick: () => {
|
||
gmv(gmv() + Math.round(Math.random() * 20000 - 8000)); // 随机涨跌,演示双向滚动
|
||
orders(orders() + Math.round(Math.random() * 200 - 80));
|
||
} }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-descriptions',
|
||
'描述列表',
|
||
'descriptions:label : value 网格,column 定列数(窄屏自动单列);bordered 切表格形态,span 让长字段占整行。',
|
||
descriptions({
|
||
title: '订单信息(纯网格 · 3 列)',
|
||
column: 3,
|
||
items: [
|
||
{ label: '订单号', value: 'RB-2048' },
|
||
{ label: '下单人', value: '王小明' },
|
||
{ label: '金额', value: '¥1,280.00' },
|
||
{ label: '状态', value: tag({ label: '待复核', variant: 'warning' }) },
|
||
{ label: '备注', value: '需要开专用发票,抬头与税号见附件。', span: 2 },
|
||
],
|
||
}),
|
||
divider({ dashed: true }),
|
||
descriptions({
|
||
title: '物流信息(表格形态 · 2 列)',
|
||
column: 2,
|
||
bordered: true,
|
||
size: 'sm',
|
||
items: [
|
||
{ label: '承运商', value: '顺丰速运' },
|
||
{ label: '运单号', value: 'SF1234567890' },
|
||
{ label: '收件地址', value: '浙江省杭州市余杭区文一西路 969 号', span: 2 },
|
||
],
|
||
}),
|
||
),
|
||
section(
|
||
'sec-segmented',
|
||
'分段控制器',
|
||
'segmented:胶囊分段 + 滑动滑块,value 信号双向绑定;两个实例共用信号即自动同步,整组禁用走信号。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
segmented({
|
||
options: [
|
||
{ value: 'day', label: '日' },
|
||
{ value: 'week', label: '周' },
|
||
{ value: 'month', label: '月' },
|
||
{ value: 'year', label: '年', disabled: true },
|
||
],
|
||
value: range,
|
||
}),
|
||
h('span', { class: 'out' }, () => `当前粒度:${range()}`),
|
||
),
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }, { value: 'month', label: '月' }], value: range, size: 'sm' }),
|
||
select({ options: [{ value: 'bj', label: '北京' }, { value: 'sh', label: '上海' }, { value: 'gz', label: '广州' }, { value: 'sz', label: '深圳' }, { value: 'hz', label: '杭州' }, { value: 'cd', label: '成都' }], value: cityPick, searchable: true, placeholder: '可搜索城市' }),
|
||
segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range, size: 'sm', disabled: locked }),
|
||
h('span', { class: 'out' }, () => (locked() ? '第二个分段已禁用(同上「锁定」开关)' : '第二个分段可用')),
|
||
),
|
||
),
|
||
section(
|
||
'sec-timeline',
|
||
'时间线',
|
||
'timeline:items 自左向右排事件流,variant 给节点语义色;pending 追加虚线「进行中」节点,点按钮切换。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '流程继续/结束', variant: 'ghost', onClick: () => flowDone(!flowDone()) }),
|
||
),
|
||
() => timeline({
|
||
items: [
|
||
{ title: '提交审批', time: '09:12', content: '王小明提交了报销单 RB-2048', variant: 'success' },
|
||
{ title: '财务复核', time: '10:30', content: '金额与发票一致,待张工确认', variant: 'warning' },
|
||
{ title: '打款失败', time: '14:05', content: '收款账户信息有误,已退回修改', variant: 'danger' },
|
||
],
|
||
pending: !flowDone(),
|
||
}),
|
||
),
|
||
section(
|
||
'sec-list',
|
||
'列表',
|
||
'list:header/footer 外壳 + 条目 avatar/title/description/extra;传 onItemClick 后条目可点击并有 hover 反馈,items 为空自动回落空状态。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
list({
|
||
header: '审批待办(3)',
|
||
footer: '共 3 条,点击条目查看详情',
|
||
bordered: true,
|
||
items: [
|
||
{ title: '王小明 · 报销单 RB-2048', description: '差旅费 ¥1,280,等待财务复核', avatar: avatar({ name: '王' }), extra: tag({ label: '待审', variant: 'warning' }) },
|
||
{ title: '李静 · 采购申请 CG-112', description: '显示器 2 台,预算 ¥3,600', avatar: avatar({ name: '李', variant: 'success' }), extra: tag({ label: '已通过', variant: 'success' }) },
|
||
{ title: '张伟 · 加班调休', description: '10-24 加班 3 小时,申请调休', avatar: avatar({ name: '张', variant: 'danger' }), extra: tag({ label: '已驳回', variant: 'danger' }) },
|
||
],
|
||
onItemClick: (item) => listPick(item.title), // 点击回传整条数据
|
||
}),
|
||
h(
|
||
'div',
|
||
{ class: 'stack' },
|
||
h('span', { class: 'out' }, () => (listPick() ? '已选:' + listPick() : '未点击条目')),
|
||
list({ items: [], bordered: true, emptyText: '暂无待办事项' }),
|
||
),
|
||
),
|
||
),
|
||
section(
|
||
'sec-carousel',
|
||
'轮播',
|
||
'carousel:items 横排位移,指示点 / 箭头 / ←→ 键切换,autoplay 毫秒自动前进且悬停暂停;index 传信号即可外部受控。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
carousel({
|
||
height: 150,
|
||
autoplay: 4000,
|
||
index: slideIndex,
|
||
onChange: (i) => slideIndex(i),
|
||
items: [
|
||
h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;height:100%;font-size:15px;background:var(--ua-color-surface);color:var(--ua-color-text)' }, '① 令牌驱动的幻灯片 —— 悬停自动暂停'),
|
||
h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;height:100%;font-size:15px;background:color-mix(in srgb, var(--ua-color-accent) 12%, transparent);color:var(--ua-color-text)' }, '② 指示点与箭头均可点击,←→ 也能切'),
|
||
h('div', { style: 'flex:1;display:flex;align-items:center;justify-content:center;height:100%;font-size:15px;background:color-mix(in srgb, var(--ua-color-success) 14%, transparent);color:var(--ua-color-text)' }, '③ 环形回绕:末张的下一张回到首张'),
|
||
],
|
||
}),
|
||
h('span', { class: 'out' }, () => `第 ${slideIndex() + 1} / 3 张`),
|
||
),
|
||
),
|
||
section(
|
||
'sec-tree',
|
||
'树形',
|
||
'tree:递归节点,箭头展开/收起、点标签选中;键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
tree({
|
||
nodes: [
|
||
{ key: 'src', label: 'src', children: [
|
||
{ key: 'core', label: 'core', children: [{ key: 'frame', label: 'frame' }, { key: 'table', label: 'table-core' }] },
|
||
{ key: 'skin', label: 'skin', children: [{ key: 'ui', label: 'ui' }, { key: 'css', label: 'ui.css' }] },
|
||
] },
|
||
{ key: 'apps', label: 'apps', children: [{ key: 'demo', label: 'demo' }, { key: 'docs', label: 'docs' }] },
|
||
{ key: 'lock', label: 'package-lock.json(禁用示例)', disabled: true },
|
||
],
|
||
selected: treePick,
|
||
onSelect: (n) => treePick(n.key),
|
||
}),
|
||
h('span', { class: 'out' }, () => (treePick() ? `已选节点:${treePick()}` : '未选中节点')),
|
||
),
|
||
),
|
||
section(
|
||
'sec-upload',
|
||
'文件选择',
|
||
'upload:点击或拖拽入列,清单可逐个移除;只负责选文件并回传 onChange,上传请求交给调用方。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
upload({ multiple: true, hint: '点击或拖拽多个文件到此处', onChange: (fs2) => pickedFiles(fs2.map((f) => f.name).join('、')) }),
|
||
h('span', { class: 'out' }, () => (pickedFiles() ? `已选:${pickedFiles()}` : '未选择文件')),
|
||
),
|
||
h('div', { class: 'row' }, upload({ accept: 'image/*', hint: '仅图片(accept: image/*)' }), upload({ disabled: true, hint: '禁用态' })),
|
||
),
|
||
section(
|
||
'sec-notification',
|
||
'通知卡片',
|
||
'notification:右下角堆叠(与右上角 toast 分区),标题 + 描述 + 操作区,缺省常驻需手动关闭;duration 传毫秒可自动消失。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
button({ label: '常驻通知', variant: 'primary', onClick: () => notification({ title: '有 3 条待审报销', description: '王小明 / 李静 / 张伟提交的报销单等待复核', action: button({ label: '去处理', onClick: () => toast.success('已跳转待办') }) }) }),
|
||
button({ label: '成功(4s 自动关)', onClick: () => notification({ title: '导出完成', description: 'report-2026-10.csv 已开始下载', variant: 'success', duration: 4000 }) }),
|
||
button({ label: '警告(常驻)', variant: 'ghost', onClick: () => notification({ title: '连接不稳定', description: '正在重试第 2 次,请检查网络', variant: 'warning' }) }),
|
||
button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-result',
|
||
'结果页',
|
||
'result:终态页(成功 / 警告 / 失败 / 中性),状态符号 + 标题 + 说明 + 操作区,适合提交完成、权限不足等场景。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
result({ status: 'success', title: '提交成功', description: '报销单 RB-2049 已进入财务复核,预计 2 个工作日内出结果。', extra: h('div', { class: 'row' }, button({ label: '查看单据', variant: 'primary', onClick: () => toast.success('打开单据') }), button({ label: '继续提交', variant: 'ghost', onClick: () => toast.info('已新建草稿') })) }),
|
||
result({ status: 'danger', title: '校验未通过', description: '发票金额与报销金额不一致,请核对后重新提交。' }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-menu',
|
||
'下拉菜单',
|
||
'menu 触发器 + 自绘弹层:条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部或 Esc 收起;视口下方空间不足时弹层自动翻到上方,展开后键入字母可快跳菜单项。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
menu({
|
||
label: '文件操作',
|
||
items: [
|
||
{ value: 'rename', label: '重命名', shortcut: 'F2' },
|
||
{ value: 'copy', label: '创建副本', shortcut: '⌘D' },
|
||
{ value: 'archive', label: '归档', disabled: true },
|
||
{ divider: true, value: '', label: '' },
|
||
{ value: 'delete', label: '删除', danger: true },
|
||
],
|
||
onSelect: (v) => menuPick(v),
|
||
}),
|
||
menu({
|
||
label: '右对齐',
|
||
align: 'end',
|
||
variant: 'primary',
|
||
items: [
|
||
{ value: 'profile', label: '个人资料' },
|
||
{ value: 'settings', label: '偏好设置' },
|
||
],
|
||
onSelect: (v) => menuPick(v),
|
||
}),
|
||
h('span', { class: 'out' }, () => (menuPick() ? `已选:${menuPick()}` : '未选择')),
|
||
),
|
||
),
|
||
section(
|
||
'sec-popover',
|
||
'气泡卡片',
|
||
'popover:可交互弹层(tooltip 只展示,popover 里能点),点外部 / Esc 收起;视口下方空间不足时弹层自动翻到上方。',
|
||
h(
|
||
'div',
|
||
{ class: 'row' },
|
||
popover(
|
||
{
|
||
title: '筛选条件',
|
||
content: h(
|
||
'div',
|
||
null,
|
||
h('div', { class: 'row' }, checkbox({ label: '只看我的', checked: agreed })),
|
||
h('div', { class: 'row' }, button({ label: '应用', onClick: () => toast.success('已应用筛选') })),
|
||
),
|
||
},
|
||
button({ label: '打开气泡卡片' }),
|
||
),
|
||
popover({ placement: 'top', trigger: 'hover', content: '悬停出场,移出收起(可放链接)' }, badge({ label: 'hover 触发' })),
|
||
),
|
||
),
|
||
section(
|
||
'sec-breadcrumb',
|
||
'面包屑',
|
||
'breadcrumb:nav + ol 语义结构,末项自动标为当前页,无 href 的条目走 onNavigate。',
|
||
breadcrumb({
|
||
items: [{ label: '首页', href: '#sec-counter' }, { label: '组件库' }, { label: '导航类' }, { label: '面包屑' }],
|
||
separator: '›',
|
||
onNavigate: (item) => toast.info(`跳转到 ${item.label}`),
|
||
}),
|
||
),
|
||
section(
|
||
'sec-accordion',
|
||
'折叠面板',
|
||
'accordion:标题按钮 + grid 行高过渡,默认手风琴单开,multiple 可多开,支持禁用面板。',
|
||
accordion({
|
||
multiple: true,
|
||
items: [
|
||
{ value: 'a', title: '账户设置', content: '修改昵称、头像与绑定邮箱。' },
|
||
{ value: 'b', title: '通知偏好', content: alert({ message: '通知偏好:每日摘要已开启', variant: 'info' }) },
|
||
{ value: 'c', title: '危险操作', content: '删除账号后数据不可恢复。' },
|
||
{ value: 'd', title: '企业版专属(禁用)', content: '升级后可配置 SSO。', disabled: true },
|
||
],
|
||
}),
|
||
),
|
||
section(
|
||
'sec-table',
|
||
'数据表格',
|
||
'粘性表头、斑马纹、行选择(全选三态 + 已选计数,选中行低视觉比重)、点表头排序、工具栏筛选、底部分页。',
|
||
card({ title: '用户表' }, dataTable({
|
||
data: users,
|
||
selectable: true,
|
||
loading: tableLoading,
|
||
columns: [
|
||
{ key: 'name', header: '姓名', sortable: true, width: 160 },
|
||
{ key: 'age', header: '年龄', sortable: true, align: 'right', width: 100, format: (v) => `${v} 岁` },
|
||
{ key: 'city', header: '城市', align: 'center', width: 'flex' },
|
||
],
|
||
pageSize: 5,
|
||
selected: picked,
|
||
}), h(
|
||
'div',
|
||
{ class: 'row' },
|
||
switchButton({ label: '加载态', checked: tableLoading }),
|
||
badge({ label: '受控选中', variant: 'default' }),
|
||
h('span', { class: 'out' }, () => `已选 ${picked().length} 行`),
|
||
button({ label: '清空选择', variant: 'ghost', onClick: () => picked([]) }),
|
||
)),
|
||
card(
|
||
{ title: '独立分页器联动(pageSize 3,共用页码信号)' },
|
||
dataTable({
|
||
data: users,
|
||
columns: [
|
||
{ key: 'name', header: '姓名' },
|
||
{ key: 'city', header: '城市' },
|
||
],
|
||
pageSize: 3,
|
||
page: tablePage,
|
||
}),
|
||
pagination({ page: tablePage, pageSize: 3, total: users.length, jumpable: true, onChange: (p) => tablePage(p) }),
|
||
),
|
||
),
|
||
section(
|
||
'sec-card',
|
||
'卡片容器',
|
||
'card 组件:标题 + 任意子内容,样式全部来自设计令牌。',
|
||
card({ title: '令牌驱动' }, h('p', { class: 'desc' }, '这里没有硬编码颜色 —— 全部消费 --ua-* CSS 变量,暗色模式自动切换。')),
|
||
),
|
||
);
|
||
|
||
const toc = h('aside', { class: 'toc' }, h('div', { class: 'toc-title' }, '目录')); // 目录项由 buildToc 填充
|
||
const app = h('div', { class: 'shell' }, toc, content);
|
||
|
||
mount('#app', () => app);
|
||
buildToc(toc, content);
|