菜单的类型:为什么「菜单」需要分类------结合驰骋 GPN_Menu 的设计
一、问题的起点:如果菜单只存「URL + 标题」会怎样
上一篇文章说到,菜单是「功能页面与操作者的交互面」。那么一个很自然的问题:菜单表里到底要存什么?
最朴素的做法是存两个字段:
菜单 = { 标题: "待办工作", URL: "/WF/GL/TodoList" }
这么做能跑,但很快会遇到几个绕不开的问题:
- 打开哪个页面、怎么解析? 待办列表和一张表单,长得完全不一样,光靠 URL 前端不知道拉哪套渲染逻辑。
- 权限怎么配? 「待办」和「薪资查询」的授权方式可能完全不同。
- 移动端怎么办? PC 页面和移动端页面是两套入口。
- 参数从哪来? 同一类列表,绑不同实体就是不同菜单,参数不能写死。
根本原因在于:菜单承载的不只是一个跳转,而是一类功能的行为约定。 这些约定需要一个显式的字段来表达------这就是「菜单类型」(MenuModel)。
二、驰骋的答案:MenuModel 菜单类型
在 Menu.ts 的实体定义里,MenuModel 是菜单的必填字段之一:
map.AddTBString('MenuModel', null, '菜单模式', true, true, 0, 50, 200);
而所有合法的类型值,集中定义在 MenuModel.ts 里。把这份常量表读一遍,会发现它不是随便堆的------它是按「功能形态」分组的。
下面结合 GPN_Menu 向导的分组,把菜单类型分类说明。
三、菜单类型的分类
MenuModel.ts 里有一个 menuCategory,把类型粗分为三档:
ts
const menuCategory = {
highCode: '高代码',
lowCode: '低代码',
oa: 'OA类菜单',
};
按这个骨架,再结合 GPN_Menu 的向导分组,菜单类型可以这样归类:
3.1 流程类
| 类型 | 说明 |
|---|---|
Start / StartSpecFlow |
发起流程(全部 / 指定流程) |
Todolist / TodolistSpecFlow |
待办(全部 / 指定流程) |
Runing / RuningSpecNo |
在途 |
CC / CCSpecFlow |
抄送 |
SearchFlow / SearchSpecFlow |
流程查询 |
特点 :带 SpecFlow / SpecNo 后缀的,是把「全部流程」收窄为「指定流程」。这就是为什么流程类要正反两种类型------避免所有流程挤在同一个待办入口里。
3.2 单据类(流水,偏「叙事」)
| 类型 | 说明 |
|---|---|
Bill / BillRef / BillRptWhite |
单据及其关联 |
SearchBill / FrmBill |
单据查询 / 单据表单 |
单据的定义在向导帮助里写得很直白:有单号、标题字段,记录事情的经过,具有时间、地点、任务,叙事文性质------报销单、出库单、出门单。
3.3 实体类(主数据,偏「说明」)
| 类型 | 说明 |
|---|---|
EntityNoName |
编号名称实体(资产、学生、车辆) |
Dict / DictTable / DictTableTree |
字典(键值对、树) |
EntityRptWhite / EnRptWhite |
基于实体的报表 |
FrmDtlView |
从表视图 |
实体的定义与单据相对:有编号、名称字段,描述事物的属性,说明文性质。
单据 vs 实体,是菜单类型里最容易被实施人员搞混的一对。向导的帮助文案(
GPN_Menu.ts的AddGroup描述)其实就是在教这件事:叙事文 vs 说明文。
3.4 视图类(不建表,只看数)
| 类型 | 说明 |
|---|---|
DBList |
分页视图 |
GenerList |
不分页视图 |
GanttDBView |
甘特图视图 |
FrmDtlView / FlowDtlView |
从表明细视图 |
视图的共同点是:用数据源配置「怎么看数据」,不新建业务表。
3.5 大屏 / 报表类
| 类型 | 说明 |
|---|---|
RptWhite |
白色大屏 |
RptBlue / RptBluePage |
蓝色大屏 |
Rpt2DLowCode / Rpt3DLowCode |
二维 / 三维报表 |
FlowRptWhite / BillRptWhite / EnRptWhite / EntityRptWhite |
分别基于流程 / 单据 / 实体 / 高代码的大屏 |
这里也能看到「基础形态 + 数据来源」的组合思路:白色大屏是通用形态,再按数据来源分化成四种。
3.6 高代码类
| 类型 | 说明 |
|---|---|
Search / Group / Tree |
查询 / 分组 / 树 |
TreeEns |
左树右表 |
GL |
通用列表 |
Tabs |
标签页容器 |
GPN / Ens / En / EnOnly |
新建 / 批量编辑 / 属性卡片 |
Data_V |
大屏 |
Self |
自定义 URL |
这些类型对应上一系列的「面向模式的编程」------每个类型背后是一个基类 + 解析页面 。选 GL,前端就走 GenerList.vue 解析;选 TreeEns,就走 TreeEns.vue。
3.7 OA 类
| 类型 | 说明 |
|---|---|
Calendar / Notepad / WorkRec |
日历 / 记事本 / 工作日志 |
KnowledgeManagement |
知识管理 |
Info / Message |
信息发布 / 消息 |
NetDisk |
网盘 |
Task |
任务 |
OA 类的特点是:功能已开发好,配上菜单即可使用,不需要再设计。
四、类型背后的三种「承载方式」
MenuModel.ts 只是类型清单,真正有意思的是 readme.md 里对功能页面属性如何存储的说明。它把菜单按「属性复杂程度」分成三类,称之为 A / B / C:
| 类别 | 适用场景 | 存储方式 |
|---|---|---|
| A 类:菜单承载通用方式 | 属性简单或没有属性(日志、日历) | 用 MenuGenerPage 类,属性存菜单表 |
| B 类:菜单承载个性方式 | 属性相对复杂,但不想单独建表 | 属性写进 GPM_Menu,与菜单属性共表 |
| C 类:独立实体方式 | 属性复杂,需要独立表 | 创建菜单时生成独立记录,如 Frm_XXXXX |
对应的代码写法(readme 原文):
A类: Menu.SetPara('EnName', 'TS.GPM.MenunGenerPage')
B类: Menu.SetPara('EnName', 'TS.XXX.MenuXX')
C类: Menu.SetPara('EnName', 'TS.XXX.XXXX'), Menu.SetPKVal('EnPKVal', 'Frm_XXXXX')
这是一个很务实的分级设计 :不是所有菜单都值得建一张表。日历这种「打开即用」的,共表就够了;表单这种属性复杂的,才给独立实体。复杂度按需分配,避免为简单功能制造冗余表。
五、为什么需要这样设计:优点
5.1 类型决定解析方式,前端不做 if-else 地狱
菜单存了 MenuModel,前端就能「按类型路由」:GL → GenerList.vue,TreeEns → TreeEns.vue。这和「面向模式编程」是同一套哲学------用类型分发,而不是用条件判断。
如果没有类型,前端就得根据 URL 猜、根据实体名猜,最终变成一堆 if...else。
5.2 类型是「工具箱的分类」,降低选错成本
GPN_Menu 向导不是让人从空白路由开始填,而是先选类型、再配置。类型本身就是一份「这类功能是什么、适合什么场景」的说明书:
- 单据:叙事文,有单号标题
- 实体:说明文,有编号名称
- 视图:不建表,只看数
实施人员选类型的过程,就是一次需求归类。选对了类型,后面的配置就是顺理成章。
5.3 类型是权限与移动端的挂载点
有了类型,才能做到:
- 权限按类型初始化:不同功能的默认按钮、数据范围不同;
- 移动端单独配置 :
Menu.MobileUrlExt字段让 PC 与移动端各走各的入口; - 参数化复用 :
UrlExt携带参数(如x?EnName=TS.Demo.Student),同一类型可生成 N 个菜单。
5.4 类型让「低代码」与「高代码」在同一棵树上共存
menuCategory 里明确区分了高代码与低代码。这不是文字游戏------它意味着:
- 低代码菜单:配置元数据即可用;
- 高代码菜单:挂一个 TS 子类,走解析页面。
同一个 GPM_Menu 体系,既能挂配置出来的功能,也能挂写代码出来的功能,而不是两套导航。这就是「标准能力配置化,例外能力代码化」在菜单层的体现。
5.5 类型让功能可以「按需收窄」
流程类里 Todolist(全部)与 TodolistSpecFlow(指定流程)并存,是很好的例子。它承认了一个现实:大系统里,不同部门关心的待办范围不一样。 用类型把「全部」和「指定」分开,就避免了「一个待办入口所有人共用」的混乱。
六、一张总表
| 类型分组 | 代表类型 | 解决的问题 | 解析去向 |
|---|---|---|---|
| 流程 | Start / Todolist / Runing / CC |
按节点推进的事务 | 流程列表解析页 |
| 单据 | Bill / SearchBill |
流水、事件、过程 | 单据解析页 |
| 实体 | EntityNoName / Dict |
主数据、属性说明 | 实体解析页 |
| 视图 | DBList / GenerList / GanttDBView |
不建表的展现 | 视图解析页 |
| 大屏报表 | RptWhite / RptBlue / Rpt2D/3D |
分析与可视化 | 大屏解析页 |
| 高代码 | GL / TreeEns / GPN / Self |
定制功能 | 模式解析页(GL_* 等) |
| OA | Calendar / Notepad / Task |
办公配套 | 已开发页面 |
七、结论
菜单需要分类,本质上是因为**「菜单」这个词承载了太多不同性质的功能**。把类型显式化,能同时解决四件事:
- 怎么打开(解析方式)
- 谁能用(权限初始化)
- 在哪用(PC / 移动端)
- 用什么参数(路由复用)
驰骋的做法是:用 MenuModel 定义类型、用 menuCategory 粗分高/低代码、用 A/B/C 三种承载方式控制属性复杂度,最后用 GPN_Menu 向导把「选类型」这一步变成「需求归类」。
回过头看,MenuModel.ts 那一长串常量并不只是枚举------它是这套低代码平台对「一个业务系统由哪些功能组成」的一份分类学。 分类清晰,堆积才不会乱。
附录:本文涉及的代码索引
| 主题 | 路径 |
|---|---|
| 菜单类型常量 | Vue3/src/CCFast/GPM/CCMenu/MenuModel.ts |
| 新建菜单向导 | Vue3/src/CCFast/GPM/CCMenu/GPN_Menu.ts |
| 菜单实体(MenuModel 字段) | Vue3/src/CCFast/GPM/CCMenu/Menu.ts |
| A/B/C 承载方式说明 | Vue3/src/CCFast/GPM/CCMenu/readme.md |
| 菜单类型 → 解析页面映射 | Vue3/src/CCFast/GPM/CCMenu/GPN_Menu.ts(ClassFactory 映射表) |
| 页面模式框架 | doc/低代码部分/面向模式编程思想-的高代码开发.md |
本文基于当前仓库源码静态分析整理,类型清单随版本迭代变化,具体以你所使用的版本为准。