搜索 K
Appearance
Appearance
本篇回答三个问题:菜单怎么变成页面?列表 / 表单 / 详情这三类页面从哪来?为什么刷新列表页、分享链接,筛选和翻页状态都还在?
前端的路由表是固定且通用的——它不为每个业务实体写一条路由,而是用带参数的路由模板兜住所有实体:
// router/index.ts(节选)
{ path: 'm/:module', name: 'module', component: ModuleHome },
{ path: 'm/:module/:menu', name: 'list', component: ResourceListView },
{ path: 'm/:module/:menu/new', name: 'create', component: ResourceFormView, props: { mode: 'create' } },
{ path: 'm/:module/:menu/:id/edit', name: 'edit', component: ResourceFormView, props: { mode: 'edit' } },
{ path: 'm/:module/:menu/:id', name: 'detail', component: ResourceDetailView },:module 是模块编码(如 biz),:menu 是菜单编码(如 biz:productList)。同一套五条路由服务所有模型——这正是元数据驱动的体现:路由不认识「商品」,只认识「某模块下的某菜单」。
每类页面都是「路由壳视图 → 资源容器组件 → 元数据组件 → 渲染引擎」四层结构。壳视图管页头与返回,容器组件管「拉 schema + 拉数据 + 提交」,元数据组件管「按 schema 布局」,渲染引擎管「每个字段用什么组件」。
| 页面 | 路由壳视图 | 容器组件 | 元数据组件 | 渲染引擎 |
|---|---|---|---|---|
| 列表 | ResourceListView | (直接组合) | MetaTable / MetaSearch / StatusTabs | FieldCell |
| 表单 | ResourceFormView | ResourceForm | MetaForm | FieldInput |
| 详情 | ResourceDetailView | ResourceDetail | MetaDetail | FieldCell |
列表页与表单/详情页的一个差异
表单与详情把「拉数据 + 提交」抽到了 ResourceForm / ResourceDetail 容器组件,所以它们能同时被整页路由和列表页抽屉复用——列表页里点「编辑」可以不跳转、就地弹抽屉,靠的就是同一个 ResourceForm。列表页本身则把表格、搜索、分页直接组合在 ResourceListView 里。
ResourceListView 进入后先 resolve():加载菜单树 → 校验菜单有 model → 拉 PageSchema,然后渲染五个区域:
StatusTabs:schema.tabs 存在才渲染;切页签时按 schema.tabField 做等值过滤(如商品的「在售/已售/下架」)。MetaSearch:关键词框(keywordPlaceholder)+ 按 schema.searchItems 渲染的高级搜索字段,每个搜索字段也走 FieldInput 渲染。MetaTable:列来自 schema.columns(为空则取 list: true 的字段),每格用 FieldCell 渲染;行动作来自 schema.actions.row。MetaForm 按 schema.formGroups 分组(无分组则所有 form: true 字段单组),每个字段用 FieldInput 渲染,并据字段 schema 自动生成校验规则:
// 校验规则由字段 schema 推导(节选)
if (f.required) rules.push({ required: true, message: `请填写${f.label}` })
if (f.length && (f.component === 'text' || f.component === 'textarea'))
rules.push({ max: f.length, message: `${f.label}不超过 ${f.length} 字` })textarea 字段占整行(span 24),其余占半行(span 12)。
MetaDetail 按 schema.detailSections 分区,用 antd 的 a-descriptions 两列布局,每个字段用 FieldCell 渲染(与列表单元格同一套组件)。
列表页的翻页、关键词、排序、高级筛选、状态页签全部编码进 URL query,不另存组件内部状态。这意味着:刷新页面、复制链接发给同事、浏览器前进后退,列表状态都能完整还原。
整个循环是单向的:用户操作不直接改数据,而是改 URL;URL 变化触发重新解析、重新请求。URL 是唯一的真相来源。
URL query 的编码约定:
| query 键 | 含义 | 示例 |
|---|---|---|
page / size | 分页(默认 1 / 10,等于默认值时不写进 URL,保持干净) | ?page=2&size=20 |
kw | 关键词 | ?kw=钢笔 |
sort | 单列排序,格式 字段.方向 | ?sort=price.desc |
filters | 高级搜索值,JSON 串(urlencoded) | ?filters=%7B%22status%22%3A1%7D |
tab | 状态页签(非「全部」才写) | ?tab=1 |
为什么不把状态存在组件里
存组件里,刷新即丢、链接不可分享、前进后退失效。把状态放 URL,浏览器天然帮你做了持久化、分享和历史。代价是每次操作要编解码一次 query,但 listQuery.ts 已把 parseListState / toRouteQuery / buildQueryRequest 封好,业务无感。设计动机详见设计文档 · 网络层与状态。
列表的工具栏/行/批量动作,以及表单的提交,都不是写死的,而是按 ActionSchema.behavior 分发到四种行为:
| behavior | 含义 | 典型动作 |
|---|---|---|
ROUTE | 整页跳转(据 target 拼路由) | 新建、编辑、详情 |
DRAWER | 就地弹抽屉(不离开列表) | 抽屉式编辑/详情 |
DOWNLOAD | 触发下载 | 导出 |
API | 直接按 endpoint 发请求 | 删除、审核等自定义动作 |
behavior 缺省时按内置 key 兜底(create/edit/detail→ROUTE,export→DOWNLOAD,其余→API)。自定义动作通过 endpoint(形如 POST /v1/data/biz/product/{id}/approve)发请求,{id} 自动替换。这套机制让「内置 CRUD 绑自定义 Controller」也能生效,详见设计文档 · 请求时序。