aidopMenuDisplay.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /**
  2. * Ai-DOP 侧栏一级目录显示名(与 ai-dop-platform/tools/gen_aidop_menu.py 中模块编码一致)。
  3. * 在登录菜单树写入路由前覆盖 meta.title,避免库中仍为旧标题时侧栏不更新。
  4. */
  5. export const AIDOP_DIRECTORY_TITLES: Record<string, string> = {
  6. aidopDirS0: 'S0 运营建模',
  7. aidopDirS1: 'S1 产销协同',
  8. aidopDirS2: 'S2 制造协同',
  9. aidopDirS3: 'S3 供应协同',
  10. aidopDirS4: 'S4 采购执行',
  11. aidopDirS5: 'S5 物料仓储',
  12. aidopDirS6: 'S6 生产执行',
  13. aidopDirS7: 'S7 成品仓储',
  14. aidopDirS8: 'S8 异常监控',
  15. aidopDirS9: 'S9 运营指标',
  16. // 兼容旧种子路由名(数据库未迁移到新 Path/Name 时)
  17. aidopDirM01: 'S0 运营建模',
  18. aidopDirM02: 'S1 产销协同',
  19. aidopDirM03: 'S2 制造协同',
  20. aidopDirM04: 'S3 供应协同',
  21. aidopDirM05: 'S4 采购执行',
  22. aidopDirM06: 'S5 物料仓储',
  23. aidopDirM07: 'S6 生产执行',
  24. aidopDirM08: 'S7 成品仓储',
  25. aidopDirM09: 'S8 异常监控',
  26. aidopDirM10: 'S9 运营指标',
  27. };
  28. /**
  29. * 兼容库中旧菜单 component:
  30. * 例如 M16/系统首页 仍指向占位页时,前端在菜单树注入阶段改为新首页组件。
  31. */
  32. const AIDOP_COMPONENT_OVERRIDES: Record<string, string> = {
  33. '/aidop/m16/001': '/dashboard/home',
  34. };
  35. type AMenu = Record<string, any>;
  36. /** 侧栏「智慧运营看板」下 10 个看板(九宫格+S1~S9);统一查询条件仅应出现在这些 component 对应页面 */
  37. /** 工单排产子页(新标签打开,侧栏隐藏) */
  38. const PRODUCTION_AUX_ROUTES: Array<{ path: string; title: string; component: string; name: string }> = [
  39. { path: '/aidop/production/work-order-trace', title: '工单执行追踪', component: '/aidop/production/workOrderSchedulingTraceForm', name: 'aidopProductionWorkOrderTrace' },
  40. { path: '/aidop/production/work-order-materials', title: '工单物料明细', component: '/aidop/production/workOrderMaterialDetailList', name: 'aidopProductionWorkOrderMaterials' },
  41. { path: '/aidop/production/work-order-routings', title: '工单工序明细', component: '/aidop/production/workOrderRoutingDetailList', name: 'aidopProductionWorkOrderRoutings' },
  42. ];
  43. const SMART_OPS_CHILDREN: Array<{ path: string; title: string; component: string; name: string }> = [
  44. { path: '/aidop/smart-ops/grid', title: '九宫格智慧运营看板', component: '/dashboard/home', name: 'aidopSmartOpsGrid' },
  45. { path: '/aidop/smart-ops/modeling', title: '运营指标建模', component: '/aidop/kanban/s0', name: 'aidopSmartOpsModeling' },
  46. { path: '/aidop/smart-ops/dashboard-config', title: '详情看板配置', component: '/aidop/kanban/dashboardConfig', name: 'aidopSmartOpsDashboardConfig' },
  47. { path: '/aidop/smart-ops/s1', title: 'S1产销协同看板', component: '/aidop/kanban/s1', name: 'aidopSmartOpsS1' },
  48. { path: '/aidop/smart-ops/s2', title: 'S2制造协同看板', component: '/aidop/kanban/s2', name: 'aidopSmartOpsS2' },
  49. { path: '/aidop/smart-ops/s3', title: 'S3供应协同看板', component: '/aidop/kanban/s3', name: 'aidopSmartOpsS3' },
  50. { path: '/aidop/smart-ops/s4', title: 'S4采购执行看板', component: '/aidop/kanban/s4', name: 'aidopSmartOpsS4' },
  51. { path: '/aidop/smart-ops/s5', title: 'S5物料仓储看板', component: '/aidop/kanban/s5', name: 'aidopSmartOpsS5' },
  52. { path: '/aidop/smart-ops/s6', title: 'S6生产执行看板', component: '/aidop/kanban/s6', name: 'aidopSmartOpsS6' },
  53. { path: '/aidop/smart-ops/s7', title: 'S7成品仓储看板', component: '/aidop/kanban/s7', name: 'aidopSmartOpsS7' },
  54. { path: '/aidop/smart-ops/s8', title: 'S8异常监控看板', component: '/aidop/kanban/s8', name: 'aidopSmartOpsS8' },
  55. { path: '/aidop/smart-ops/s9', title: 'S9运营指标看板', component: '/aidop/kanban/s9', name: 'aidopSmartOpsS9' },
  56. ];
  57. /**
  58. * S8 异常监控大屏下的可见子菜单兜底(visible,非 hidden)。
  59. * 用于 SysMenuSeedData 已新增但库尚未刷新的过渡期,避免侧栏不显示「订单执行档案」。
  60. * 数据库刷新后由 upsertMenu 合并 component/meta,行为幂等。
  61. */
  62. const S8_MONITORING_VISIBLE_ROUTES: Array<{ path: string; title: string; component: string; name: string; icon: string }> = [
  63. {
  64. path: '/aidop/s8/monitoring/order-execution',
  65. title: '订单执行档案',
  66. component: '/aidop/s8/monitoring/SoOrderExecutionDashboardPage',
  67. name: 'aidopS8OrderExecution',
  68. icon: 'ele-Tickets',
  69. },
  70. ];
  71. /** S8 配置中心中的隐藏详情页;库中若缺少授权或未同步新种子,前端补齐后可避免点卡片进入 404 */
  72. const S8_CONFIG_HIDDEN_ROUTES: Array<{ path: string; title: string; component: string; name: string }> = [
  73. {
  74. path: '/aidop/s8/config/exception-types',
  75. title: '异常类型配置',
  76. component: '/aidop/s8/config/S8ExceptionTypeConfigPage',
  77. name: 'aidopS8ExceptionTypeConfig',
  78. },
  79. {
  80. path: '/aidop/s8/config/dashboard-cells',
  81. title: '大屏卡片配置',
  82. component: '/aidop/s8/config/S8DashboardCellConfigPage',
  83. name: 'aidopS8DashboardCellConfig',
  84. },
  85. {
  86. path: '/aidop/s8/config/notifications',
  87. title: '通知分层配置',
  88. component: '/aidop/s8/config/S8NotificationLayerPage',
  89. name: 'aidopS8NotificationLayerConfig',
  90. },
  91. ];
  92. function collectPathsUnder(node: AMenu, acc: Set<string>): void {
  93. const p = node.path as string | undefined;
  94. if (p) acc.add(p);
  95. for (const c of node.children ?? []) collectPathsUnder(c as AMenu, acc);
  96. }
  97. function upsertMenu(parent: AMenu, menu: AMenu): void {
  98. parent.children = parent.children ?? [];
  99. const idx = parent.children.findIndex((x: AMenu) => x.path === menu.path);
  100. if (idx >= 0) {
  101. parent.children[idx] = { ...parent.children[idx], ...menu, meta: { ...(parent.children[idx].meta ?? {}), ...(menu.meta ?? {}) } };
  102. } else {
  103. parent.children.unshift(menu);
  104. }
  105. }
  106. /**
  107. * 用户已有 /aidop 根菜单时,patchAidopCustomMenusIfMissing 会补全智慧运营等子路由;
  108. * 个性化首页校验须认可这些 path(避免库中子项 path 与绝对 path 不一致时误判回退)。
  109. */
  110. export function augmentAllowedPathsForAidopHomepage(menuTree: any[] | undefined, allowed: Set<string>): void {
  111. if (!menuTree?.length) return;
  112. const aidopRoot = menuTree.find((x: AMenu) => x.path === '/aidop' || x.name === 'aidopRoot');
  113. if (!aidopRoot) return;
  114. for (const c of SMART_OPS_CHILDREN) allowed.add(c.path);
  115. for (const c of PRODUCTION_AUX_ROUTES) allowed.add(c.path);
  116. allowed.add('/aidop/smart-ops');
  117. allowed.add('/aidop/smart-diagnosis');
  118. }
  119. /** 仅当接口返回的树里尚不存在对应 path 时补节点,避免与 sys_menu 重复;库已同步时等价于 no-op。 */
  120. function patchAidopCustomMenusIfMissing(routes: AMenu[] | undefined): void {
  121. if (!routes?.length) return;
  122. const aidopRoot = routes.find((x) => x.path === '/aidop' || x.name === 'aidopRoot');
  123. if (!aidopRoot) return;
  124. const existing = new Set<string>();
  125. collectPathsUnder(aidopRoot, existing);
  126. if (!existing.has('/aidop/smart-ops')) {
  127. upsertMenu(aidopRoot, {
  128. path: '/aidop/smart-ops',
  129. name: 'aidopSmartOpsRoot',
  130. component: 'Layout',
  131. meta: { title: '智慧运营看板', icon: 'ele-DataBoard' },
  132. });
  133. existing.add('/aidop/smart-ops');
  134. }
  135. const smartOps = aidopRoot.children.find((x: AMenu) => x.path === '/aidop/smart-ops');
  136. if (smartOps) {
  137. for (const child of SMART_OPS_CHILDREN) {
  138. if (existing.has(child.path)) continue;
  139. upsertMenu(smartOps, {
  140. path: child.path,
  141. name: child.name,
  142. component: child.component,
  143. meta: { title: child.title, icon: 'ele-DataAnalysis' },
  144. });
  145. existing.add(child.path);
  146. }
  147. const orderMap = new Map(SMART_OPS_CHILDREN.map((c, i) => [c.path, i]));
  148. smartOps.children = (smartOps.children ?? []).sort((a: AMenu, b: AMenu) => {
  149. const ai = orderMap.has(a.path) ? (orderMap.get(a.path) as number) : 9999;
  150. const bi = orderMap.has(b.path) ? (orderMap.get(b.path) as number) : 9999;
  151. return ai - bi;
  152. });
  153. }
  154. if (!existing.has('/aidop/smart-diagnosis')) {
  155. upsertMenu(aidopRoot, {
  156. path: '/aidop/smart-diagnosis',
  157. name: 'aidopSmartDiagnosis',
  158. component: '/aidop/diagnosis/index',
  159. meta: { title: '智慧诊断', icon: 'ele-TrendCharts' },
  160. });
  161. }
  162. for (const child of PRODUCTION_AUX_ROUTES) {
  163. if (existing.has(child.path)) continue;
  164. upsertMenu(aidopRoot, {
  165. path: child.path,
  166. name: child.name,
  167. component: child.component,
  168. meta: { title: child.title, isHide: true, icon: 'ele-Document' },
  169. });
  170. existing.add(child.path);
  171. }
  172. }
  173. /** 为 S8 异常监控目录补可见子路由(如「订单执行档案」),兼容库未刷新到最新菜单的过渡期。 */
  174. function patchS8MonitoringVisibleRoutesIfMissing(routes: AMenu[] | undefined): void {
  175. if (!routes?.length) return;
  176. const aidopRoot = routes.find((x) => x.path === '/aidop' || x.name === 'aidopRoot');
  177. if (!aidopRoot?.children?.length) return;
  178. const s8Root = aidopRoot.children.find(
  179. (x: AMenu) => x.path === '/aidop/s8' || x.name === 'aidopDirS8' || x.name === 'aidopDirM09',
  180. );
  181. if (!s8Root?.children?.length) return;
  182. const monitoringDir = s8Root.children.find(
  183. (x: AMenu) => x.path === '/aidop/s8/monitoring' || x.name === 'aidopS8MonitoringDir',
  184. );
  185. if (!monitoringDir) return;
  186. for (const route of S8_MONITORING_VISIBLE_ROUTES) {
  187. upsertMenu(monitoringDir, {
  188. path: route.path,
  189. name: route.name,
  190. component: route.component,
  191. meta: { title: route.title, icon: route.icon },
  192. });
  193. }
  194. }
  195. /** 为 S8 配置中心补隐藏子路由,兼容数据库未刷新到最新菜单/角色授权时的直达 404 */
  196. function patchS8ConfigHiddenRoutesIfMissing(routes: AMenu[] | undefined): void {
  197. if (!routes?.length) return;
  198. const aidopRoot = routes.find((x) => x.path === '/aidop' || x.name === 'aidopRoot');
  199. if (!aidopRoot?.children?.length) return;
  200. const s8Root = aidopRoot.children.find((x: AMenu) => x.path === '/aidop/s8' || x.name === 'aidopDirS8' || x.name === 'aidopDirM09');
  201. if (!s8Root) return;
  202. const existing = new Set<string>();
  203. collectPathsUnder(s8Root, existing);
  204. for (const route of S8_CONFIG_HIDDEN_ROUTES) {
  205. upsertMenu(s8Root, {
  206. path: route.path,
  207. name: route.name,
  208. component: route.component,
  209. meta: { title: route.title, icon: 'ele-Document', isHide: true },
  210. });
  211. existing.add(route.path);
  212. }
  213. }
  214. /**
  215. * 递归覆盖 Ai-DOP 一级目录的 meta.title,供侧栏 / tagsView / 菜单搜索使用。
  216. * 智慧诊断、智慧运营看板以 sys_menu 为准;若租户/角色未关联到库中菜单,则仅补缺失 path,避免双份。
  217. */
  218. /** 强制智慧运营子菜单 component,避免库中仍为占位/错误路径时前端仍加载旧页 */
  219. function patchSmartOpsRouteComponents(nodes: AMenu[] | undefined): void {
  220. if (!nodes?.length) return;
  221. const byPath = Object.fromEntries(SMART_OPS_CHILDREN.map((c) => [c.path, c.component])) as Record<string, string>;
  222. for (const item of nodes) {
  223. const raw = item.path as string | undefined;
  224. const p = raw ? (raw.startsWith('/') ? raw : `/${raw}`) : '';
  225. const comp = p ? byPath[p] : undefined;
  226. if (comp) item.component = comp;
  227. if (item.children?.length) patchSmartOpsRouteComponents(item.children as AMenu[]);
  228. }
  229. }
  230. function patchS8ConfigRouteComponents(nodes: AMenu[] | undefined): void {
  231. if (!nodes?.length) return;
  232. const byPath = Object.fromEntries(S8_CONFIG_HIDDEN_ROUTES.map((c) => [c.path, c.component])) as Record<string, string>;
  233. for (const item of nodes) {
  234. const raw = item.path as string | undefined;
  235. const p = raw ? (raw.startsWith('/') ? raw : `/${raw}`) : '';
  236. const comp = p ? byPath[p] : undefined;
  237. if (comp) item.component = comp;
  238. if (item.children?.length) patchS8ConfigRouteComponents(item.children as AMenu[]);
  239. }
  240. }
  241. export function patchAidopMenuTitles(routes: any[] | undefined): void {
  242. if (!routes?.length) return;
  243. patchAidopCustomMenusIfMissing(routes as AMenu[]);
  244. patchS8MonitoringVisibleRoutesIfMissing(routes as AMenu[]);
  245. patchS8ConfigHiddenRoutesIfMissing(routes as AMenu[]);
  246. patchSmartOpsRouteComponents(routes as AMenu[]);
  247. patchS8ConfigRouteComponents(routes as AMenu[]);
  248. for (const item of routes) {
  249. const name = item.name as string | undefined;
  250. if (name && AIDOP_DIRECTORY_TITLES[name]) {
  251. item.meta = item.meta ?? {};
  252. item.meta.title = AIDOP_DIRECTORY_TITLES[name];
  253. }
  254. const path = item.path as string | undefined;
  255. if (path && AIDOP_COMPONENT_OVERRIDES[path]) {
  256. item.component = AIDOP_COMPONENT_OVERRIDES[path];
  257. }
  258. if (item.children?.length) patchAidopMenuTitles(item.children);
  259. }
  260. }