通用组件与交互约定¶
业务页面尽量复用 frontend/src/components,不要重复实现表格、弹窗、文件选择与提示。这一页列出手头可用的组件和推荐用法。
1. 组件总览¶
| 组件 | 作用 | 典型场景 |
|---|---|---|
PageHeader |
页面标题 + 副标题 + 右侧操作 | 每个页面顶部 |
DataTable |
列表表格:列设置、密度、刷新、分页、移动端卡片 | 所有列表页 |
MobileRecordList |
手机卡片列表 | 由 DataTable 自动调用 |
FormModal |
弹窗 + antd Form | 新增/编辑 |
FilePicker |
文件库选择弹窗 | 选文件、上传 |
FileSelect |
表单内文件字段 | 头像、附件、图片 |
StatusTag / MethodTag |
状态/方法标签 | 用户状态、日志方法 |
Icon |
统一图标 | 导航、按钮 |
Loading 系列 |
页面/表格/骨架屏 | 加载态 |
RequireAuth |
登录/权限路由守卫 | 路由层 |
useToast |
统一 message/notification | 所有操作反馈 |
布局组件(components/layout):AppLayout、Rail、ContextBar、CommandBar、TabBar、KeepAliveOutlet、LockScreen、NotificationCenter。
2. DataTable¶
<DataTable<User>
storageKey="users"
rowKey="id"
data={query.data?.list ?? []}
loading={query.isFetching}
total={query.data?.total ?? 0}
page={page}
pageSize={pageSize}
onPageChange={(p, size) => { setPage(p); setPageSize(size) }}
onRefresh={() => void query.refetch()}
columns={columns}
filters={<Input placeholder="搜索用户名" />}
toolbar={<Button type="primary">新建用户</Button>}
selection={rowSelection}
scroll={{ x: 'max-content' }}
expandable={{ ... }}
/>
Props:
| Prop | 说明 |
|---|---|
columns |
antd TableColumnsType,同一份定义同时驱动桌面/手机 |
rowKey |
字符串或函数 |
storageKey |
列显隐偏好的持久化 key;同一页面保持稳定 |
loading |
首屏无数据时显示骨架,已有数据时只显示轻量遮罩 |
total/page/pageSize/onPageChange |
服务端分页 |
toolbar / filters |
左侧业务按钮 / 筛选区;手机端筛选默认收起 |
selection/expandable/onRow |
透传 antd Table |
mobile |
覆盖手机端主标题列、隐藏列、操作列 |
行为:
- 列设置、密度、刷新按钮固定在工具栏右侧。
- 列偏好写入
useUi.columns[storageKey],刷新后保留;列被删除时自动清理脏 key。 - 紧凑视口下自动换成
MobileRecordList:首列作标题,其余列转标签/值,fixed: 'right'或key: 'actions'固定底部。 - 空数据文案用
emptyText,首屏加载用骨架屏。
约定:页面不要自己判断 window.innerWidth,交给 DataTable。
3. MobileRecordList¶
一般不需要直接用,DataTable 会调用。需要自定义手机列表时:
<MobileRecordList
columns={columns}
data={rows}
rowKey="id"
loading={loading}
pagination={{ current, pageSize, total, onChange }}
mobile={{ titleKey: 'name', hiddenKeys: ['id'], actionKeys: ['actions'] }}
/>
- 支持选择、树形展开、加载态、空状态、分页。
- 操作列固定在卡片底部;宽列自动换行。
- 与桌面表格共享
utils/table.ts的列 key/渲染逻辑。
4. FormModal¶
const [form] = Form.useForm<FormValues>()
<FormModal
open={open}
title={editing ? '编辑参数' : '新建参数'}
form={form}
onCancel={() => setOpen(false)}
onSubmit={(values) => save.mutate(values)}
confirmLoading={save.isPending}
>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</FormModal>
- 确定按钮触发
form.submit(),再由onSubmit提交。 destroyOnHidden会卸载表单,避免残留状态;打开前用form.setFieldsValue回填。- 默认宽度 520,复杂表单传
width。
5. PageHeader¶
<PageHeader
title="参数配置"
sub={`${total} 个参数 · 公共参数可被未登录页面读取`}
actions={can('system:param:create') && <Button>新建参数</Button>}
/>
统一标题、副标题与右侧操作区,不要页面自己写 h1。
6. 文件选择¶
6.1 FileSelect(表单字段)¶
<Form.Item name="avatar" label="头像">
<FileSelect types="image" />
</Form.Item>
<Form.Item name="attachments" label="附件">
<FileSelect types={['document', 'archive']} multiple limit={5} />
</Form.Item>
Props:
| Prop | 说明 |
|---|---|
value/onChange |
URL 或 URL[],可直接接 Form.Item |
types |
image/video/audio/document/archive/other,可单值或数组 |
multiple |
是否多选 |
limit |
多选上限 |
disabled |
只读 |
placeholder/title |
文案 |
upload |
是否允许在弹窗内上传(默认受权限控制) |
只回传 URL,保持表单值可序列化;已有 URL 会被包装成文件卡片展示,可单独移除。
6.2 FilePicker(弹窗)¶
<FilePicker
open={open}
types="image"
multiple={false}
value={selected}
onCancel={() => setOpen(false)}
onConfirm={(files) => { save(files[0]); setOpen(false) }}
/>
- 支持类型过滤、搜索、分页、跨页保留已选、清除单项。
- 内置上传入口(受
system:file:upload权限控制),上传后立即选中。 upload={false}可强制隐藏上传按钮。
7. 状态与反馈¶
7.1 StatusTag / MethodTag¶
7.2 useToast¶
const toast = useToast()
toast.success('保存成功')
toast.fail(error) // 自动提取错误消息
await toast.promise(task, {
loading: '正在导出…',
success: '导出完成',
error: '导出失败',
})
toast.notify({ type: 'success', title: '导入完成', description: '成功 3 条,失败 1 条' })
- 成功/失败/警告用
success/warning/error;长请求用promise;多行结果用notify。 - 位置、时长、最大条数由
ThemeProvider的AntdApp统一配置,页面不要单独设置top/duration。 - 错误优先展示后端
message,不要自己拼「操作失败」。
8. 布局与导航组件¶
| 组件 | 说明 |
|---|---|
Rail |
一级分组图标轨,选中态动画 |
ContextBar |
二级导航;单页模块自动隐藏 |
CommandBar |
⌘K 命令面板、主题切换、通知、用户菜单 |
TabBar |
多页签,右键菜单支持刷新/关闭其他/左/右/全部 |
KeepAliveOutlet |
页签保活,关闭页签释放页面 |
NotificationCenter |
未读角标、全部/未读、单条/全部已读 |
LockScreen |
闲置锁屏,密码/Passkey 解锁 |
Ambient |
极光背景(纯色材质关闭) |
这些组件依赖 AppLayout 的上下文,页面不要单独渲染。
9. 图标¶
name是内部图标表的 key(users、shield、tree、folder、clock、activity…)。- 菜单的
icon字段与Icon的 key 对应;新增图标在components/Icon.tsx注册。 - 品牌标记用
BrandMark,不要用普通 Icon 代替。
10. 组件编写约定¶
- 组件只关心展示与交互,数据请求放页面或 hook。
- Props 用显式 interface,避免
any;受控组件同时支持value/onChange。 - 样式用 class + 现有 partial,不写行内大段样式。
- 加载态复用
Loading系列;空状态复用 antdEmpty或DataTable.emptyText。 - 无障碍:按钮有可读名称,图标按钮加
aria-label,弹窗标题明确。 - 动效遵循
prefers-reduced-motion,统一用 motion 的MotionConfig reducedMotion="user"。 - 组件测试放在同目录
*.test.tsx,用@testing-library/react。
11. 常见问题¶
| 现象 | 原因 |
|---|---|
| 手机端表格错位 | 列定义用了自定义 render 但没给 key/dataIndex,tableColumnKey 无法稳定识别 |
| 列设置刷新后丢失 | storageKey 变了,或列 key 不稳定 |
FileSelect 表单值为空 |
没有接 Form.Item 的 value/onChange,或用了非受控 defaultValue |
| Toast 被导航遮挡 | 页面自己设置了 top,应使用全局 useToast |
| 弹窗关闭后表单还有值 | 没设 destroyOnHidden 或没 form.resetFields() |
| 移动端筛选看不到 | 筛选器默认收起,点工具栏滑杆按钮展开 |