Files
UI/packages/tools/cli/recipes.md
T
2026-10-26 13:13:08 +08:00

105 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 组合配方(Recipes)
> 单个组件的参数表说明「怎么用」,这些配方说明「怎么拼」。所有配方都只消费组件对外 API,不依赖内部文件。
## 配方 1:表单校验 + 提交反馈
```ts
const name = signal('');
const email = signal('');
const formRef = form({ onSubmit: (ok) => (ok ? result({ status: 'success', title: '已保存' }) : toast.error('请检查表单')) }, [
formItem({ label: '名称', value: name, required: true, rules: [{ min: 2 }] }, textField({ value: name })),
formItem({ label: '邮箱', value: email, required: true, rules: [{ pattern: /^\S+@\S+$/ }] }, textField({ value: email })),
]);
```
要点:
- 校验规则挂在 `formItem`,值信号只有一个来源(`value` 同时给 `formItem` 与控件),避免两处各存一份。
- 提交结果用 `toast`(轻量、瞬时)或 `result`(整页终态)表达,不要两者同时弹。
## 配方 2:危险操作二次确认
```ts
const open = signal(false);
const dangerOpen = (key: string) => {
open(true);
return modal({
title: `确认删除 ${key}?`,
open,
size: 'sm',
onClose: () => open(false),
footer: h('div', { class: 'row' },
button({ label: '取消', variant: 'ghost', onClick: () => open(false) }),
button({ label: '删除', variant: 'danger', onClick: () => { open(false); toast.success('已删除'); } })),
}, h('p', null, '删除后不可恢复。'));
};
```
要点:
- `modal` 自带焦点陷阱与 Esc 逐层关闭,不要在业务层再挂 Esc 监听。
- 确认按钮用 `variant: 'danger'`,取消用 `ghost`,主次分明。
## 配方 3:表格筛选 + 分页 + 批量操作
```ts
const picked = signal<string[]>([]);
const page = signal(1);
dataTable({
data: rows,
columns: [{ key: 'name', header: '名称', sortable: true }, { key: 'city', header: '城市' }],
pageSize: 10,
page, // 同一 page 信号也给 pagination,二者自动联动
selectable: true,
selected: picked,
loading: saving,
});
pagination({ page, pageSize: 10, total: rows.length, jumpable: true, onChange: (p) => page(p) });
```
要点:
- `page` 信号是表格与分页器的唯一共享状态;外部改页码前自己钳制范围(core 的 `setPage` 不做钳制)。
- 选中一律存 `rowKey`,翻页/筛选后仍然稳定;全选只作用于当前可见行。
- 批量操作按钮在 `picked().length === 0` 时禁用,避免空提交。
## 配方 4:长列表数据加载与空态
```ts
const loading = signal(true);
const items = signal<ListItem[]>([]);
list({ items: items(), bordered: true, emptyText: loading() ? '加载中…' : '暂无数据' });
```
要点:
- 空态文案随加载态切换;列表本身会在 `items` 为空时渲染 `empty`,不需要业务层再判断一次。
- 需要骨架屏就换成 `skeleton`,加载完成再 `renderList` 成真实行。
## 配方 5:数据表 + 独立分页器 + 导出
```ts
const page = signal(1);
const table = dataTable({
data: rows,
columns: [
{ key: 'name', header: '姓名', sortable: true, width: 160 },
{ key: 'city', header: '城市', align: 'center', width: 'flex' },
],
page, // 受控页码:与外部 pagination 双向联动
selectable: true,
exportable: true, // 导出筛选 + 排序后的全量行(不是当前页)
exportName: '用户列表.csv',
});
const pager = pagination({ page, pageSize: 5, total: rows.length, jumpable: true, pageSizes: [5, 10, 20] });
```
要点:
- 页码只保留一个来源:`dataTable` 与 `pagination` 共传同一个 `page` 信号,翻页由内核写回,桥接层会钳制越界值。
- `exportable` 导出的是「筛选 + 排序后、未分页」的全量行,列序与表头一致、单元格走 `format`;想导出当前页请自行读 `getVisibleRows()`。
- 列宽拖拽只对 `width` 给数字的列生效(`'flex'` 列交给浏览器自适应);双击手柄回到声明宽度。
- 行选择一律按 rowKey 记账,筛选换掉可见行后已选仍在,批量操作前用已选计数确认。
## 通用约定
- 状态一律放信号(`signal` / `computed`),组件只读写信号,不持有业务数据副本。
- 反馈分层:`toast` 右上角瞬时、`notification` 右下角可长读、`result` 整页终态、`alert` 页内常驻。
- 样式不要写颜色与间距字面值,先跑 `ua tokens` 查 `--ua-*`(verify 会拦截未定义令牌与 `#hex`)。