Skip to content

页面与路由

本篇回答三个问题:菜单怎么变成页面?列表 / 表单 / 详情这三类页面从哪来?为什么刷新列表页、分享链接,筛选和翻页状态都还在?

菜单驱动路由

前端的路由表是固定且通用的——它不为每个业务实体写一条路由,而是用带参数的路由模板兜住所有实体:

ts
// 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 / StatusTabsFieldCell
表单ResourceFormViewResourceFormMetaFormFieldInput
详情ResourceDetailViewResourceDetailMetaDetailFieldCell

列表页与表单/详情页的一个差异

表单与详情把「拉数据 + 提交」抽到了 ResourceForm / ResourceDetail 容器组件,所以它们能同时被整页路由和列表页抽屉复用——列表页里点「编辑」可以不跳转、就地弹抽屉,靠的就是同一个 ResourceForm。列表页本身则把表格、搜索、分页直接组合在 ResourceListView 里。

列表页:一份 schema,五个区域

ResourceListView 进入后先 resolve():加载菜单树 → 校验菜单有 model → 拉 PageSchema,然后渲染五个区域:

  • 页签 StatusTabsschema.tabs 存在才渲染;切页签时按 schema.tabField 做等值过滤(如商品的「在售/已售/下架」)。
  • 搜索 MetaSearch:关键词框(keywordPlaceholder)+ 按 schema.searchItems 渲染的高级搜索字段,每个搜索字段也走 FieldInput 渲染。
  • 表格 MetaTable:列来自 schema.columns(为空则取 list: true 的字段),每格用 FieldCell 渲染;行动作来自 schema.actions.row

表单页:分组 + 校验

MetaFormschema.formGroups 分组(无分组则所有 form: true 字段单组),每个字段用 FieldInput 渲染,并据字段 schema 自动生成校验规则:

ts
// 校验规则由字段 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)。

详情页:分区展示

MetaDetailschema.detailSections 分区,用 antd 的 a-descriptions 两列布局,每个字段用 FieldCell 渲染(与列表单元格同一套组件)。

URL 即列表状态(单一事实源)

列表页的翻页、关键词、排序、高级筛选、状态页签全部编码进 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」也能生效,详见设计文档 · 请求时序

下一步

  • 字段渲染:上面反复出现的 FieldInput / FieldCell 如何为一个字段选到组件。
  • 自定义与覆盖:给某个字段/页面换成你自己的组件。