diff --git a/.env.dev b/.env.dev index 16fb7bd..efc7859 100644 --- a/.env.dev +++ b/.env.dev @@ -6,5 +6,5 @@ VITE_OTHER_SERVICE_BASE_URL= `{ "demo": "http://localhost:9528" }` -# 鏄惁鍦ㄥ紑鍙戠幆澧冨惎鐢?Vue DevTools 娴姩鍏ュ彛 +# 是否在开发环境启用 Vue DevTools 浮动入口 VITE_DEVTOOLS_ENABLED=N diff --git a/.gitattributes b/.gitattributes index 9553ccb..3204a8d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ "*.md" eol=lf "*.yaml" eol=lf "*.yml" eol=lf +".*" text eol=lf diff --git a/.gitignore b/.gitignore index d13bb21..c844a81 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ yarn.lock # Docs /docs/* !/docs/frontend-page-resource-manifest.json + +# Temp +/codeTemp/* diff --git a/AGENTS.md b/AGENTS.md index 0dd7087..138abb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,15 @@ - 常量路由维护入口优先是 `build/plugins/router.ts` 和 `src/router/routes/custom-routes.ts`,不要把常量路由散落到业务页面逻辑里。 - 菜单图标约定属于路由契约的一部分:`meta.icon` 表示 Iconify 图标,`meta.localIcon` 表示本地 SVG 图标;不要混用字段语义。 +### 对象上下文业务域入口页口径 + +- `product`、`project` 这类对象上下文业务域的入口页,按 `docs/rdms/rdms-object-context-navigation-implementation-notes.md` 口径,本来就是“先进入业务域入口页,再选择对象建立上下文”;不要把“入口页是可点击菜单”误判成配置错误。 +- 对象上下文业务域的“入口态”页面,例如 `product_list -> /product/list -> view.product_list`,可以作为左侧一级入口菜单实际命中的页面;这不等于已经进入对象上下文态。 +- 不要为了修复“点击入口页后只剩内容页、布局壳消失”的问题,直接要求把对象域入口菜单从“菜单”改成“目录”。先检查当前是不是动态权限路由模式,以及后端 `get-user-routes` 返回是否缺少业务域根路由。 +- 在 `VITE_AUTH_ROUTE_MODE=dynamic` 下,如果后端只返回对象域入口叶子页,而没有返回本地静态骨架中的业务域根路由,例如缺少 `product -> layout.base`、只返回 `product_list -> view.product_list`,前端必须在动态路由归一化阶段补回本地业务域骨架,而不是让入口页直接裸挂成顶层 `view.*` 路由。 +- 对象上下文业务域的稳定来源仍应是本地路由骨架:业务域根路由负责 `layout.base`,入口页负责对象列表或对象选择,真正的对象功能页继续挂在该业务域下。动态路由兼容逻辑只能做“补骨架”和“对齐入口”,不要反过来推翻这层结构。 +- 后续新增新的对象上下文业务域时,至少同步检查这几处是否闭环:本地静态路由骨架、`src/constants/object-context.ts` 中的 `domainKey / entryRouteKey / entryRoutePath / fallbackDefaultRouteKey`、动态路由归一化逻辑、对象上下文 store 与头部菜单切换逻辑。 + ## 分层职责约束 ### `src/views` @@ -158,6 +167,40 @@ - 涉及路由、菜单、权限的改动时,同时检查 `build/plugins/router.ts`、`src/router/routes/*`、`src/store/modules/route/*` 和相关文档。 - 对于可再生的路由产物,优先修改源配置并执行 `pnpm gen-route`,不要把手工修补生成文件当成常规方案。 +## 运行时字典使用口径 + +- 运行时字典统一由 `src/store/modules/dict/index.ts` 管理,登录后通过 `/system/dict-data/frontend-cache` 初始化;不要在业务页面重复直调字典接口。 +- 字典编码常量优先收敛在 `src/constants/dict.ts`,不要在页面里散落硬编码 `dictType`。 +- 不要猜测字典编码。新增某个业务字段对应的字典前,先从“后端接口文档、后端字段契约、系统字典管理页”确认真实 `dictType`,再写入 `src/constants/dict.ts`。 +- `src/constants/dict.ts` 中每个导出的字典常量,尽量补中文注释,至少说明两件事:对应哪个业务字段、这个编码是从哪里确认出来的。 +- 如果后端实际 `dictType` 带有历史命名痕迹,例如当前对象方向仍叫 `rdms_product_direction`,前端常量名优先按真实业务语义命名,不要继续把历史误导扩散到页面代码里。 +- 表单下拉优先使用 `src/components/custom/dict-select.vue`。 +- 普通文案回显优先使用 `src/components/custom/dict-text.vue`。 +- 需要标签态回显时优先使用 `src/components/custom/dict-tag.vue`,标签颜色仍由业务页面自己决定。 +- 在 `script setup`、TSX 列格式化、复杂判断里,优先使用 `src/hooks/business/dict.ts` 提供的 `useDict(dictCode)`,常用能力包括 `dictOptions`、`getItem`、`getLabel`、`getLabels`、`hasValue`。 +- `DictSelect` 默认只展示启用项;确实需要包含禁用项时,显式传 `:only-enabled="false"`。 + +简单示例: + +```vue + + + +``` + +```ts +const { getLabel, getLabels } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE); + +const directionLabel = getLabel(row.directionCode); +const directionLabels = getLabels(row.directionCodes, { separator: ',' }); +``` + +确认字典编码的典型方式: + +- 后端接口文档直接写明字段使用哪个字典,例如产品 `directionCode -> rdms_product_direction`;如果该编码只是历史命名,前端常量名仍按通用业务语义命名。 +- 当前系统已有页面或接口已经稳定使用某个字典,例如用户所属公司 `company -> system_user_company`。 +- 如果以上两种都没有,就先让后端或业务明确 `dictType`,不要前端自己命名。 + ## 页面资源与菜单目录约束 - 页面组件键、页面资源、菜单目录是三层不同概念,不要把它们当成同一个值。 @@ -170,6 +213,15 @@ - 优先使用现有别名导入(`@/...`、`~/...`),避免过长的相对路径。 - 保持与 TypeScript 严格模式兼容。 +- 后端返回的主键 ID、用户 ID、对象 ID、雪花 ID、Long ID 等,一律优先按 `string` 在前端接收和传递,不要默认写成 `number`。 +- 这是强约束,不是建议项。原因包括:JavaScript `number` 无法稳定承载长整型精度、接口序列化后可能出现精度丢失、运行时还容易出现 `number/string` 键不一致,最终导致回显、筛选、映射、路由参数、对象上下文等逻辑异常。 +- 这条约束要落实到所有层:`typings`、API 返回类型、页面表单 `model`、组件 `props` / `emits`、`ElSelect` 的 `value`、路由参数、查询参数、`Map` 键、筛选条件、store 状态,一律优先使用 `string` / `string[]`。 +- 明确禁止把 ID 当成普通数值处理。禁止写法包括但不限于:`Number(id)`、`+id`、`parseInt(id)`、`parseFloat(id)`、`Math.floor(id)`,以及任何“为了比较、传参、回填、提交而把 ID 转成 number”的做法。 +- 比较、映射、筛选 ID 时,默认按字符串语义处理,例如 `id === targetId`、`Map`、`Set`,不要混用 `number/string` 双口径。 +- 如果后端当前接口暂时还返回数值型 ID,前端也必须尽可能在 `typings`、API 适配层或进入业务层前转成 `string`,不要把 ID 按 `number` 扩散到页面、store 和组件里。 +- 但要注意:如果后端把超出 JS 安全整数范围的 Long 直接作为 JSON 数字返回,前端在业务层再 `String(number)` 只能得到“已经丢精度后的错误字符串”。这种情况必须明确记为接口契约风险,不要误判为“前端已安全处理”。 +- 因此,新增或改造接口时,最稳妥的契约仍然是:后端长整型 ID 直接按字符串返回;前端全链路按字符串接收和传递。若后端暂未改,前端侧也不得新增 `number` 口径 ID 用法。 +- 对仓库中的历史代码,原则是“不再新增 number 口径 ID,当前任务触达相关链路时优先顺手矫正”;不要继续复制历史写法。 - 遵循仓库现有的 Vue SFC 风格:`script setup`、类型化 store、职责单一的小型 composable/helper。 - 修改界面时优先延续 `src/layouts` 和 `src/theme` 中已有的 UI 模式,不要平行引入另一套设计体系。 - 注释保持克制,只在代码本身不够直观时补充必要说明。 diff --git a/build/plugins/router.ts b/build/plugins/router.ts index 8a885d1..22ce16d 100644 --- a/build/plugins/router.ts +++ b/build/plugins/router.ts @@ -29,6 +29,27 @@ export function setupElegantRouter() { const constantRoutes: RouteKey[] = ['login', '403', '404', '500']; const routeMetaMap: Partial>> = { + product: { + icon: 'carbon:product', + order: 4 + }, + product_list: { + icon: 'material-symbols:view-list-outline-rounded', + order: 1, + keepAlive: true + }, + product_dashboard: { + hideInMenu: true, + activeMenu: 'product_list' + }, + product_requirement: { + hideInMenu: true, + activeMenu: 'product_list' + }, + product_setting: { + hideInMenu: true, + activeMenu: 'product_list' + }, system: { icon: 'carbon:cloud-service-management', order: 9, diff --git a/docs/frontend-page-resource-manifest.json b/docs/frontend-page-resource-manifest.json index 6d28a90..1b2b4fd 100644 --- a/docs/frontend-page-resource-manifest.json +++ b/docs/frontend-page-resource-manifest.json @@ -1,13 +1,46 @@ { - "generatedAt": "2026-03-27T05:39:32.467Z", + "generatedAt": "2026-04-20T11:27:02.190Z", "description": "Frontend visible page resource whitelist for backend route/menu configuration.", "rules": { "directoryComponent": "layout.base", "pageComponentPattern": "view.", "singlePageComponentPattern": "layout.$view." }, - "total": 6, + "total": 7, "items": [ + { + "name": "product_list", + "path": "/product/list", + "component": "view.product_list", + "title": "产品列表", + "routeTitle": "product_list", + "i18nKey": "route.product_list", + "icon": "material-symbols:view-list-outline-rounded", + "localIcon": null, + "order": 1, + "hideInMenu": false, + "keepAlive": true, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "产品列表", + "i18nKey": "route.product_list", + "icon": "material-symbols:view-list-outline-rounded", + "localIcon": null, + "order": 1, + "keepAlive": true, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "product", + "pageType": "leaf", + "source": "generated" + }, { "name": "system_user", "path": "/system/user", @@ -174,12 +207,12 @@ "source": "generated" }, { - "name": "system_user_management_relation", + "name": "system_user-management-relation", "path": "/system/user-management-relation", "component": "view.system_user-management-relation", "title": "管理链路", - "routeTitle": "system-user-management-relation", - "i18nKey": "", + "routeTitle": "system_user-management-relation", + "i18nKey": "route.system_user-management-relation", "icon": null, "localIcon": null, "order": null, @@ -192,7 +225,7 @@ "props": null, "meta": { "title": "管理链路", - "i18nKey": "", + "i18nKey": "route.system_user-management-relation", "icon": null, "localIcon": null, "order": null, diff --git a/route.json b/route.json deleted file mode 100644 index 3175188..0000000 --- a/route.json +++ /dev/null @@ -1,393 +0,0 @@ -{ - "code": 0, - "msg": "", - "data": { - "routes": [ - { - "id": "900000", - "name": "system", - "path": "/system", - "component": "layout.base", - "redirect": "/system/user", - "props": null, - "meta": { - "title": "权限中心", - "i18nKey": null, - "icon": "carbon:cloud-service-management", - "localIcon": null, - "order": 9, - "keepAlive": false, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": [ - { - "id": "900001", - "name": "system_user", - "path": "/system/user", - "component": "view.system_user", - "redirect": null, - "props": null, - "meta": { - "title": "用户管理", - "i18nKey": null, - "icon": "ic:round-manage-accounts", - "localIcon": null, - "order": 1, - "keepAlive": false, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "900002", - "name": "system_role", - "path": "/system/role", - "component": "view.system_role", - "redirect": null, - "props": null, - "meta": { - "title": "角色管理", - "i18nKey": null, - "icon": "carbon:user-role", - "localIcon": null, - "order": 2, - "keepAlive": false, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "2036723586735665153", - "name": "system_post", - "path": "/system/post", - "component": "view.system_post", - "redirect": null, - "props": null, - "meta": { - "title": "岗位管理", - "i18nKey": null, - "icon": "mdi:account-group-outline", - "localIcon": null, - "order": 3, - "keepAlive": false, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "900003", - "name": "system_menu", - "path": "/system/menu", - "component": "view.system_menu", - "redirect": null, - "props": null, - "meta": { - "title": "菜单管理", - "i18nKey": null, - "icon": "material-symbols:route", - "localIcon": null, - "order": 4, - "keepAlive": false, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - } - ] - }, - { - "id": "1", - "name": "legacy-system", - "path": "/legacy-system", - "component": "layout.base", - "redirect": "/legacy-system/dict", - "props": null, - "meta": { - "title": "系统管理", - "i18nKey": null, - "icon": "ep:tools", - "localIcon": null, - "order": 10, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": [ - { - "id": "900004", - "name": "system_dict", - "path": "/legacy-system/dict", - "component": "view.system_dict", - "redirect": null, - "props": null, - "meta": { - "title": "字典管理", - "i18nKey": null, - "icon": "mdi:book-open-page-variant-outline", - "localIcon": null, - "order": 4, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "108", - "name": "legacy_system_log", - "path": "/legacy-system/log", - "component": "layout.base", - "redirect": "/legacy-system/log/operate-log", - "props": null, - "meta": { - "title": "审计日志", - "i18nKey": null, - "icon": "ep:document-copy", - "localIcon": null, - "order": 9, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": [ - { - "id": "500", - "name": "SystemOperateLog", - "path": "/legacy-system/log/operate-log", - "component": "system/operatelog/index", - "redirect": null, - "props": null, - "meta": { - "title": "操作日志", - "i18nKey": null, - "icon": "ep:position", - "localIcon": null, - "order": 1, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "501", - "name": "SystemLoginLog", - "path": "/legacy-system/log/login-log", - "component": "system/loginlog/index", - "redirect": null, - "props": null, - "meta": { - "title": "登录日志", - "i18nKey": null, - "icon": "ep:promotion", - "localIcon": null, - "order": 2, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - } - ] - } - ] - }, - { - "id": "2", - "name": "infra", - "path": "/infra", - "component": "layout.base", - "redirect": "/infra/log", - "props": null, - "meta": { - "title": "基础设施", - "i18nKey": null, - "icon": "ep:monitor", - "localIcon": null, - "order": 20, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": [ - { - "id": "1083", - "name": "infra_log", - "path": "/infra/log", - "component": "layout.base", - "redirect": "/infra/log/api-access-log", - "props": null, - "meta": { - "title": "API 日志", - "i18nKey": null, - "icon": "fa:tasks", - "localIcon": null, - "order": 4, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": [ - { - "id": "1078", - "name": "InfraApiAccessLog", - "path": "/infra/log/api-access-log", - "component": "infra/apiAccessLog/index", - "redirect": null, - "props": null, - "meta": { - "title": "访问日志", - "i18nKey": null, - "icon": "ep:place", - "localIcon": null, - "order": 1, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "1084", - "name": "InfraApiErrorLog", - "path": "/infra/log/api-error-log", - "component": "infra/apiErrorLog/index", - "redirect": null, - "props": null, - "meta": { - "title": "错误日志", - "i18nKey": null, - "icon": "ep:warning-filled", - "localIcon": null, - "order": 2, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - } - ] - }, - { - "id": "1243", - "name": "infra_file", - "path": "/infra/file", - "component": "layout.base", - "redirect": "/infra/file/file-config", - "props": null, - "meta": { - "title": "文件管理", - "i18nKey": null, - "icon": "ep:files", - "localIcon": null, - "order": 6, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": [ - { - "id": "1237", - "name": "InfraFileConfig", - "path": "/infra/file/file-config", - "component": "infra/fileConfig/index", - "redirect": null, - "props": null, - "meta": { - "title": "文件配置", - "i18nKey": null, - "icon": "fa-solid:file-signature", - "localIcon": null, - "order": 0, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - }, - { - "id": "1090", - "name": "InfraFile", - "path": "/infra/file/file", - "component": "infra/file/index", - "redirect": null, - "props": null, - "meta": { - "title": "文件列表", - "i18nKey": null, - "icon": "ep:upload-filled", - "localIcon": null, - "order": 5, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - } - ] - }, - { - "id": "106", - "name": "InfraConfig", - "path": "/infra/config", - "component": "infra/config/index", - "redirect": null, - "props": null, - "meta": { - "title": "配置管理", - "i18nKey": null, - "icon": "fa:connectdevelop", - "localIcon": null, - "order": 8, - "keepAlive": true, - "hideInMenu": false, - "activeMenu": null, - "multiTab": null, - "fixedIndexInTab": null - }, - "children": null - } - ] - } - ], - "home": "system_user" - } -} diff --git a/src/components/custom/dict-select.vue b/src/components/custom/dict-select.vue new file mode 100644 index 0000000..ded9e89 --- /dev/null +++ b/src/components/custom/dict-select.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/custom/dict-tag.vue b/src/components/custom/dict-tag.vue new file mode 100644 index 0000000..94b4ba4 --- /dev/null +++ b/src/components/custom/dict-tag.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/custom/dict-text.vue b/src/components/custom/dict-text.vue new file mode 100644 index 0000000..adc3261 --- /dev/null +++ b/src/components/custom/dict-text.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/constants/business.ts b/src/constants/business.ts index 83e8a93..1618ca8 100644 --- a/src/constants/business.ts +++ b/src/constants/business.ts @@ -17,6 +17,20 @@ export const commonStatusOptions = [ { value: 1, label: commonStatusRecord[1] } ] satisfies CommonType.Option[]; +export const scopeTypeRecord: Record = { + global: 'page.system.common.scopeType.global', + object: 'page.system.common.scopeType.object' +}; + +export const scopeTypeOptions = transformRecordToOption(scopeTypeRecord); + +export const objectTypeRecord: Record = { + product: 'page.system.common.objectType.product', + project: 'page.system.common.objectType.project' +}; + +export const objectTypeOptions = transformRecordToOption(objectTypeRecord); + export const dictStatusRecord: Record<'0' | '1', App.I18n.I18nKey> = { '0': 'page.system.common.status.enable', '1': 'page.system.common.status.disable' diff --git a/src/constants/dict.ts b/src/constants/dict.ts new file mode 100644 index 0000000..ab60a20 --- /dev/null +++ b/src/constants/dict.ts @@ -0,0 +1,37 @@ +/** + * 运行时字典编码常量 + * + * 约定: + * 1. 不要在业务页面硬编码 dictType。 + * 2. 新增字典编码前,先从“后端接口文档 / 后端字段契约 / 系统字典管理页”确认真实 dictType。 + * 3. 确认后再收敛到本文件,并补上中文注释说明“这个编码对应哪个业务字段”。 + */ + +/** + * 对象方向字典编码 + * + * 对应业务字段:产品、项目及后续其他对象中的 directionCode / direction + * 来源口径: + * 1. 方向类业务语义已经纠正为“对象通用方向” + * 2. 后端字典编码已准备切到更准确的 rdms_object_direction + * + * 说明: + * 前端页面统一使用本常量,不再继续使用带 product 痕迹的旧命名。 + */ +export const RDMS_OBJECT_DIRECTION_DICT_CODE = 'rdms_object_direction'; + +/** + * 对象方向历史字典编码 + * + * 用途: + * 仅用于前后端切换期间兼容旧数据,不允许新页面直接使用。 + */ +export const RDMS_OBJECT_DIRECTION_LEGACY_DICT_CODE = 'rdms_product_direction'; + +/** + * 用户所属公司字典编码 + * + * 对应业务字段:用户相关接口和页面中的 company + * 来源口径:当前系统“用户管理”页面按系统字典 system_user_company 做下拉和文案回显 + */ +export const SYSTEM_USER_COMPANY_DICT_CODE = 'system_user_company'; diff --git a/src/constants/object-context.ts b/src/constants/object-context.ts new file mode 100644 index 0000000..940fa3d --- /dev/null +++ b/src/constants/object-context.ts @@ -0,0 +1,59 @@ +import { WEB_SERVICE_PREFIX } from './service'; + +export const OBJECT_CONTEXT_QUERY_KEY = 'objectId' as const; + +export const objectContextDomainConfigs: App.ObjectContext.DomainConfig[] = [ + { + domainKey: 'project', + mode: 'object-context', + objectType: 'project', + routePathPrefixes: ['/project'], + entryRouteKey: 'project_list', + entryRoutePath: '/project/list', + fallbackDefaultRouteKey: 'project_dashboard', + fallbackDefaultRoutePath: '/project/dashboard', + contextApiPath: `${WEB_SERVICE_PREFIX}/project/context`, + contextApiObjectIdParamKey: 'projectId', + contextApiObjectIdPlacement: 'query', + objectIdQueryKey: OBJECT_CONTEXT_QUERY_KEY + }, + { + domainKey: 'product', + mode: 'object-context', + objectType: 'product', + routePathPrefixes: ['/product'], + entryRouteKey: 'product_list', + entryRoutePath: '/product/list', + fallbackDefaultRouteKey: 'product_dashboard', + fallbackDefaultRoutePath: '/product/dashboard', + contextApiPath: `${WEB_SERVICE_PREFIX}/project/product/{id}/context`, + contextApiObjectIdParamKey: 'id', + contextApiObjectIdPlacement: 'path', + objectIdQueryKey: OBJECT_CONTEXT_QUERY_KEY + } +]; + +function normalizePath(path: string) { + if (!path) { + return '/'; + } + + return path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path; +} + +function isPathMatchedByPrefix(path: string, prefix: string) { + const normalizedPath = normalizePath(path); + const normalizedPrefix = normalizePath(prefix); + + return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`); +} + +export function getObjectContextDomainConfigByPath(path: string) { + return objectContextDomainConfigs.find(config => + config.routePathPrefixes.some(prefix => isPathMatchedByPrefix(path, prefix)) + ); +} + +export function isObjectContextEntryPath(path: string, config: App.ObjectContext.DomainConfig) { + return normalizePath(path) === normalizePath(config.entryRoutePath); +} diff --git a/src/constants/product-demo.ts b/src/constants/product-demo.ts new file mode 100644 index 0000000..9c09297 --- /dev/null +++ b/src/constants/product-demo.ts @@ -0,0 +1,455 @@ +export interface DemoProductRequirement { + id: string; + title: string; + status: '待评审' | '设计中' | '开发中' | '验证中' | '已完成'; + priority: 'P0' | 'P1' | 'P2'; + owner: string; + module: string; + updatedAt: string; +} + +export interface DemoProductRoadmapItem { + id: string; + title: string; + window: string; + status: '已排期' | '推进中' | '风险关注'; + summary: string; +} + +export type DemoProductManageStatus = '启用产品' | '归档产品' | '暂停产品' | '废弃产品'; + +export interface DemoProduct { + id: string; + name: string; + code: string; + owner: string; + department: string; + status: '规划中' | '研发中' | '稳定运营'; + manageStatus: DemoProductManageStatus; + stage: '探索' | '增长' | '平台化'; + version: string; + releaseTarget: string; + updatedAt: string; + health: '健康' | '关注' | '加速'; + summary: string; + tags: string[]; + teamCount: number; + requirementCount: number; + bugCount: number; + focus: string[]; + requirements: DemoProductRequirement[]; + roadmap: DemoProductRoadmapItem[]; +} + +export const demoProducts: DemoProduct[] = [ + { + id: 'product-alpha', + name: '产品中台 Alpha', + code: 'ALPHA', + owner: '林语辰', + department: '平台产品部', + status: '研发中', + manageStatus: '启用产品', + stage: '平台化', + version: 'v2.8.0', + releaseTarget: '2026-05-10', + updatedAt: '2026-04-16', + health: '健康', + summary: '面向多业务线复用的产品主数据与流程配置中台,当前重点在规则编排和版本发布节奏收口。', + tags: ['平台能力', '规则编排', '统一发布'], + teamCount: 14, + requirementCount: 26, + bugCount: 5, + focus: ['统一配置台账', '版本灰度策略', '对象权限接入'], + requirements: [ + { + id: 'REQ-101', + title: '支持产品对象上下文的头部导航切换', + status: '开发中', + priority: 'P0', + owner: '赵明远', + module: '工作台', + updatedAt: '2026-04-15' + }, + { + id: 'REQ-108', + title: '接入对象成员角色模板的快捷查看', + status: '设计中', + priority: 'P1', + owner: '姜知夏', + module: '权限', + updatedAt: '2026-04-13' + }, + { + id: 'REQ-112', + title: '支持发布包差异对比摘要', + status: '待评审', + priority: 'P1', + owner: '周承安', + module: '发布', + updatedAt: '2026-04-11' + } + ], + roadmap: [ + { + id: 'RM-1', + title: '对象上下文导航试点', + window: '2026 Q2', + status: '推进中', + summary: '先在产品域打通对象入口、头部导航、按钮权限隔离。' + }, + { + id: 'RM-2', + title: '规则编排配置台账', + window: '2026 Q2', + status: '已排期', + summary: '把历史分散配置统一归档到产品规则台账。' + }, + { + id: 'RM-3', + title: '发布治理看板', + window: '2026 Q3', + status: '风险关注', + summary: '依赖后端事件流与测试数据沉淀,排期受联调进度影响。' + } + ] + }, + { + id: 'product-orbit', + name: 'Orbit 客户协同端', + code: 'ORBIT', + owner: '程清和', + department: '客户体验部', + status: '稳定运营', + manageStatus: '归档产品', + stage: '增长', + version: 'v1.9.3', + releaseTarget: '2026-04-28', + updatedAt: '2026-04-14', + health: '关注', + summary: '围绕客户协同与交付反馈的门户产品,近期重点是降低工单回流和优化首屏转化链路。', + tags: ['客户协同', '交付门户', '反馈闭环'], + teamCount: 10, + requirementCount: 18, + bugCount: 9, + focus: ['首屏引导改版', '交付看板合并', '通知触达回收'], + requirements: [ + { + id: 'REQ-203', + title: '重构客户交付看板首页信息密度', + status: '验证中', + priority: 'P0', + owner: '顾思远', + module: '门户', + updatedAt: '2026-04-15' + }, + { + id: 'REQ-217', + title: '补充客户联系人生命周期标签', + status: '开发中', + priority: 'P1', + owner: '何嘉宁', + module: '客户画像', + updatedAt: '2026-04-12' + } + ], + roadmap: [ + { + id: 'RM-4', + title: '客户首页分群策略升级', + window: '2026 Q2', + status: '推进中', + summary: '把静态首页切分为按客户阶段动态呈现的版本。' + }, + { + id: 'RM-5', + title: '交付反馈闭环自动催办', + window: '2026 Q3', + status: '已排期', + summary: '通过规则任务减少人工跟进成本。' + } + ] + }, + { + id: 'product-lighthouse', + name: 'Lighthouse 经营驾驶舱', + code: 'LIGHT', + owner: '宋知序', + department: '商业产品部', + status: '稳定运营', + manageStatus: '启用产品', + stage: '增长', + version: 'v3.2.1', + releaseTarget: '2026-05-22', + updatedAt: '2026-04-15', + health: '健康', + summary: '承接经营看板、指标订阅和异常播报的统一产品驾驶舱,当前聚焦跨部门指标口径收敛与高频场景提效。', + tags: ['经营分析', '指标订阅', '统一驾驶舱'], + teamCount: 12, + requirementCount: 21, + bugCount: 3, + focus: ['指标口径治理', '异常订阅编排', '高层驾驶舱视图'], + requirements: [ + { + id: 'REQ-221', + title: '支持核心经营指标的口径版本管理', + status: '开发中', + priority: 'P0', + owner: '孟之遥', + module: '指标中心', + updatedAt: '2026-04-14' + }, + { + id: 'REQ-228', + title: '补齐驾驶舱异常波动播报模板', + status: '待评审', + priority: 'P1', + owner: '韩屿川', + module: '播报', + updatedAt: '2026-04-11' + } + ], + roadmap: [ + { + id: 'RM-8', + title: '经营指标主题化看板升级', + window: '2026 Q2', + status: '推进中', + summary: '将现有指标页按经营主题重组,减少跨页面跳转成本。' + } + ] + }, + { + id: 'product-pulse', + name: 'Pulse 消息协同台', + code: 'PULSE', + owner: '许闻洲', + department: '协同平台部', + status: '规划中', + manageStatus: '启用产品', + stage: '探索', + version: 'v0.6.4', + releaseTarget: '2026-05-30', + updatedAt: '2026-04-13', + health: '关注', + summary: '统一承接站内消息、流程通知与消息编排的试点产品,当前重点是通知模板复用与多渠道触达一致性。', + tags: ['消息编排', '流程通知', '多渠道触达'], + teamCount: 7, + requirementCount: 13, + bugCount: 2, + focus: ['模板复用', '渠道一致性', '消息审计留痕'], + requirements: [ + { + id: 'REQ-331', + title: '梳理流程类通知的统一模板规范', + status: '设计中', + priority: 'P1', + owner: '丁和畅', + module: '模板中心', + updatedAt: '2026-04-12' + } + ], + roadmap: [ + { + id: 'RM-9', + title: '流程消息中心试点', + window: '2026 Q2', + status: '已排期', + summary: '先打通审批、告警两条主链路,验证模板与渠道编排能力。' + } + ] + }, + { + id: 'product-nova', + name: 'Nova 数据服务台', + code: 'NOVA', + owner: '陆闻笙', + department: '数据中台部', + status: '研发中', + manageStatus: '暂停产品', + stage: '探索', + version: 'v0.9.0', + releaseTarget: '2026-06-18', + updatedAt: '2026-04-12', + health: '加速', + summary: '承接跨系统数据接入、数据模型装配与查询服务的试点产品,当前仍在能力边界探索阶段。', + tags: ['数据服务', '模型装配', '试点产品'], + teamCount: 8, + requirementCount: 11, + bugCount: 4, + focus: ['接入链路模板化', '查询 SLA 监控', '多租户样例沉淀'], + requirements: [ + { + id: 'REQ-301', + title: '沉淀数据接入模板库', + status: '开发中', + priority: 'P0', + owner: '沈南舟', + module: '接入', + updatedAt: '2026-04-16' + }, + { + id: 'REQ-306', + title: '接入失败告警卡片化展示', + status: '设计中', + priority: 'P2', + owner: '夏安宁', + module: '监控', + updatedAt: '2026-04-10' + } + ], + roadmap: [ + { + id: 'RM-6', + title: '试点租户接入扩容', + window: '2026 Q2', + status: '推进中', + summary: '把当前 2 个试点租户扩到 6 个,验证模型复用率。' + } + ] + }, + { + id: 'product-atlas', + name: 'Atlas 组织配置台', + code: 'ATLAS', + owner: '冯见山', + department: '企业应用部', + status: '稳定运营', + manageStatus: '归档产品', + stage: '平台化', + version: 'v2.6.8', + releaseTarget: '2026-02-28', + updatedAt: '2026-04-09', + health: '健康', + summary: '曾用于统一组织架构、岗位映射和通讯录同步的配置平台,现已完成能力迁移,仅作为历史归档保留。', + tags: ['组织配置', '历史归档', '同步映射'], + teamCount: 6, + requirementCount: 8, + bugCount: 0, + focus: ['历史配置追溯', '迁移审计', '只读查询'], + requirements: [ + { + id: 'REQ-510', + title: '补充组织配置迁移后的审计说明', + status: '已完成', + priority: 'P2', + owner: '罗听雪', + module: '审计', + updatedAt: '2026-04-06' + } + ], + roadmap: [ + { + id: 'RM-10', + title: '归档访问范围收口', + window: '2026 Q2', + status: '已排期', + summary: '控制仅审计角色可访问历史配置详情,普通角色只看摘要。' + } + ] + }, + { + id: 'product-sprint', + name: 'Sprint 交付排期台', + code: 'SPRINT', + owner: '魏书言', + department: '交付效能部', + status: '研发中', + manageStatus: '暂停产品', + stage: '增长', + version: 'v1.3.0', + releaseTarget: '2026-06-08', + updatedAt: '2026-04-08', + health: '关注', + summary: '面向交付里程碑排期、风险跟踪和协作节奏对齐的产品,当前因上游流程调整进入阶段性暂停。', + tags: ['交付排期', '风险跟踪', '协作节奏'], + teamCount: 9, + requirementCount: 15, + bugCount: 6, + focus: ['排期模板统一', '跨团队风险同步', '里程碑预警'], + requirements: [ + { + id: 'REQ-612', + title: '梳理暂停期间保留的风险同步能力范围', + status: '待评审', + priority: 'P1', + owner: '徐青禾', + module: '风险中心', + updatedAt: '2026-04-07' + } + ], + roadmap: [ + { + id: 'RM-11', + title: '暂停期能力边界梳理', + window: '2026 Q2', + status: '风险关注', + summary: '待交付流程新方案确定后,再决定是否恢复后续迭代。' + } + ] + }, + { + id: 'product-legacy', + name: 'Legacy 营销活动台', + code: 'LEGACY', + owner: '陈念初', + department: '增长运营部', + status: '稳定运营', + manageStatus: '废弃产品', + stage: '增长', + version: 'v3.4.1', + releaseTarget: '2026-03-18', + updatedAt: '2026-03-25', + health: '关注', + summary: '面向历史营销活动配置与投放归档的旧产品,目前仅保留数据查询和审计访问能力,不再纳入持续建设计划。', + tags: ['历史归档', '活动投放', '审计留痕'], + teamCount: 5, + requirementCount: 6, + bugCount: 1, + focus: ['历史活动追溯', '旧投放数据迁移', '权限范围收敛'], + requirements: [ + { + id: 'REQ-401', + title: '补充历史活动包的只读访问说明', + status: '已完成', + priority: 'P2', + owner: '白昭宁', + module: '审计', + updatedAt: '2026-03-20' + } + ], + roadmap: [ + { + id: 'RM-7', + title: '历史活动数据归档收尾', + window: '2026 Q1', + status: '已排期', + summary: '只保留审计查询链路,后续不再承接新的活动能力建设。' + } + ] + } +]; + +export function getDemoProductById(productId: string) { + return demoProducts.find(item => item.id === productId) || null; +} + +export function getProductStatusType(status: DemoProduct['status']) { + const statusTypeMap: Record = { + 规划中: 'info', + 研发中: 'warning', + 稳定运营: 'success' + }; + + return statusTypeMap[status]; +} + +export function getProductHealthType(health: DemoProduct['health']) { + const healthTypeMap: Record = { + 健康: 'success', + 关注: 'warning', + 加速: 'danger' + }; + + return healthTypeMap[health]; +} diff --git a/src/directives/auth-shared.ts b/src/directives/auth-shared.ts new file mode 100644 index 0000000..fb6f595 --- /dev/null +++ b/src/directives/auth-shared.ts @@ -0,0 +1,46 @@ +export type AuthSource = 'global' | 'object' | 'both'; + +export interface AuthDirectiveBindingValue { + code: string | string[]; + source?: AuthSource; +} + +export interface AuthDirectiveCodeSource { + globalButtonCodes: string[]; + objectButtonCodes: string[]; +} + +function normalizeCodes(codes: string | string[]) { + return Array.isArray(codes) ? codes : [codes]; +} + +function includesAny(sourceCodes: string[], targetCodes: string[]) { + return targetCodes.some(code => sourceCodes.includes(code)); +} + +export function resolveAuthVisible( + bindingValue: string | AuthDirectiveBindingValue, + codeSource: AuthDirectiveCodeSource +) { + const resolvedBinding = + typeof bindingValue === 'string' + ? { + code: bindingValue, + source: 'global' as const + } + : bindingValue; + + const targetCodes = normalizeCodes(resolvedBinding.code); + const hasGlobal = includesAny(codeSource.globalButtonCodes, targetCodes); + const hasObject = includesAny(codeSource.objectButtonCodes, targetCodes); + + if (resolvedBinding.source === 'object') { + return hasObject; + } + + if (resolvedBinding.source === 'both') { + return hasGlobal || hasObject; + } + + return hasGlobal; +} diff --git a/src/directives/auth.ts b/src/directives/auth.ts new file mode 100644 index 0000000..4df212d --- /dev/null +++ b/src/directives/auth.ts @@ -0,0 +1,44 @@ +import { watchEffect } from 'vue'; +import type { Directive } from 'vue'; +import { useAuthStore } from '@/store/modules/auth'; +import { useObjectContextStore } from '@/store/modules/object-context'; +import { type AuthDirectiveBindingValue, resolveAuthVisible } from './auth-shared'; + +type AuthDirectiveElement = HTMLElement & { + authStopHandle?: (() => void) | null; +}; + +function toggleElementVisible(el: HTMLElement, visible: boolean) { + el.style.display = visible ? '' : 'none'; +} + +function getVisible(bindingValue: string | AuthDirectiveBindingValue) { + const authStore = useAuthStore(); + const objectContextStore = useObjectContextStore(); + + return resolveAuthVisible(bindingValue, { + globalButtonCodes: authStore.userInfo.buttons, + objectButtonCodes: objectContextStore.buttonCodes + }); +} + +function bindAuthEffect(el: AuthDirectiveElement, bindingValue: string | AuthDirectiveBindingValue) { + el.authStopHandle?.(); + + el.authStopHandle = watchEffect(() => { + toggleElementVisible(el, getVisible(bindingValue)); + }); +} + +export const authDirective: Directive = { + mounted(el, binding) { + bindAuthEffect(el, binding.value); + }, + updated(el, binding) { + bindAuthEffect(el, binding.value); + }, + unmounted(el) { + el.authStopHandle?.(); + el.authStopHandle = null; + } +}; diff --git a/src/directives/index.ts b/src/directives/index.ts new file mode 100644 index 0000000..96b17b9 --- /dev/null +++ b/src/directives/index.ts @@ -0,0 +1,6 @@ +import type { App } from 'vue'; +import { authDirective } from './auth'; + +export function setupDirectives(app: App) { + app.directive('auth', authDirective); +} diff --git a/src/enum/index.ts b/src/enum/index.ts index 2739b3a..843f1c3 100644 --- a/src/enum/index.ts +++ b/src/enum/index.ts @@ -2,6 +2,8 @@ export enum SetupStoreId { App = 'app-store', Theme = 'theme-store', Auth = 'auth-store', + Dict = 'dict-store', Route = 'route-store', - Tab = 'tab-store' + Tab = 'tab-store', + ObjectContext = 'object-context-store' } diff --git a/src/hooks/business/auth.ts b/src/hooks/business/auth.ts index f8fc749..01c48ca 100644 --- a/src/hooks/business/auth.ts +++ b/src/hooks/business/auth.ts @@ -1,7 +1,9 @@ import { useAuthStore } from '@/store/modules/auth'; +import { useObjectContextStore } from '@/store/modules/object-context'; export function useAuth() { const authStore = useAuthStore(); + const objectContextStore = useObjectContextStore(); function hasAuth(codes: string | string[]) { if (!authStore.isLogin) { @@ -15,7 +17,14 @@ export function useAuth() { return codes.some(code => authStore.userInfo.buttons.includes(code)); } + function hasObjectAuth(codes: string | string[]) { + const targetCodes = typeof codes === 'string' ? [codes] : codes; + + return targetCodes.some(code => objectContextStore.buttonCodes.includes(code)); + } + return { - hasAuth + hasAuth, + hasObjectAuth }; } diff --git a/src/hooks/business/dict.ts b/src/hooks/business/dict.ts new file mode 100644 index 0000000..9bcda7a --- /dev/null +++ b/src/hooks/business/dict.ts @@ -0,0 +1,86 @@ +import { computed, toValue } from 'vue'; +import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue'; +import { useDictStore } from '@/store/modules/dict'; + +type DictCode = string | Ref | ComputedRef; +type DictValue = string | number | null | undefined; +type DictValueList = Array | null | undefined; +type DictFilterOptions = { + onlyEnabled?: boolean; +}; +type DictLabelOptions = string | (DictFilterOptions & { fallback?: string }); +type DictLabelsOptions = string | (DictFilterOptions & { fallback?: string; separator?: string }); + +function normalizeLabelOptions(options?: DictLabelOptions) { + if (typeof options === 'string') { + return { + fallback: options, + onlyEnabled: false + }; + } + + return { + fallback: options?.fallback ?? '--', + onlyEnabled: options?.onlyEnabled ?? false + }; +} + +function normalizeLabelsOptions(options?: DictLabelsOptions) { + if (typeof options === 'string') { + return { + fallback: options, + separator: ' / ', + onlyEnabled: false + }; + } + + return { + fallback: options?.fallback ?? '--', + separator: options?.separator ?? ' / ', + onlyEnabled: options?.onlyEnabled ?? false + }; +} + +export function useDict(dictCode: DictCode | MaybeRefOrGetter) { + const dictStore = useDictStore(); + + const currentDictCode = computed(() => toValue(dictCode)); + + const dictData = computed(() => dictStore.getDictData(currentDictCode.value)); + const enabledDictData = computed(() => dictStore.getDictData(currentDictCode.value, true)); + const dictOptions = computed(() => dictStore.getDictOptions(currentDictCode.value)); + const dictMap = computed(() => new Map(dictData.value.map(item => [item.value, item]))); + const enabledDictMap = computed(() => new Map(enabledDictData.value.map(item => [item.value, item]))); + + function getItem(value?: DictValue, options: DictFilterOptions = {}) { + return dictStore.getDictItem(currentDictCode.value, value, options); + } + + function getLabel(value?: DictValue, options?: DictLabelOptions) { + const normalizedOptions = normalizeLabelOptions(options); + + return dictStore.getDictLabel(currentDictCode.value, value, normalizedOptions); + } + + function getLabels(values?: DictValueList, options?: DictLabelsOptions) { + const normalizedOptions = normalizeLabelsOptions(options); + + return dictStore.getDictLabels(currentDictCode.value, values, normalizedOptions); + } + + function hasValue(value?: DictValue, options: DictFilterOptions = {}) { + return dictStore.hasDictValue(currentDictCode.value, value, options); + } + + return { + dictData, + enabledDictData, + dictOptions, + dictMap, + enabledDictMap, + getItem, + getLabel, + getLabels, + hasValue + }; +} diff --git a/src/hooks/common/router.ts b/src/hooks/common/router.ts index 4ab21ea..4b71d76 100644 --- a/src/hooks/common/router.ts +++ b/src/hooks/common/router.ts @@ -1,7 +1,7 @@ import { useRouter } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router'; import type { RouteKey } from '@elegant-router/types'; -import { router as globalRouter } from '@/router'; +import { getGlobalRouter } from '@/router/instance'; /** * Router push @@ -11,6 +11,7 @@ import { router as globalRouter } from '@/router'; * @param inSetup Whether is in vue script setup */ export function useRouterPush(inSetup = true) { + const globalRouter = getGlobalRouter(); const router = inSetup ? useRouter() : globalRouter; const route = globalRouter.currentRoute; diff --git a/src/layouts/context/index.ts b/src/layouts/context/index.ts index 56ff229..10e889e 100644 --- a/src/layouts/context/index.ts +++ b/src/layouts/context/index.ts @@ -2,17 +2,31 @@ import { computed, nextTick, ref, watch } from 'vue'; import { useRoute } from 'vue-router'; import { useContext } from '@sa/hooks'; import type { RouteKey } from '@elegant-router/types'; +import { getObjectContextDomainConfigByPath } from '@/constants/object-context'; import { useRouteStore } from '@/store/modules/route'; +import { useObjectContextStore } from '@/store/modules/object-context'; import { useRouterPush } from '@/hooks/common/router'; export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu); +function isMenuMatchedByPath(path: string, menuPath: string) { + if (!menuPath) { + return false; + } + + return path === menuPath || path.startsWith(`${menuPath}/`); +} + function useMixMenu() { + const route = useRoute(); const routeStore = useRouteStore(); + const objectContextStore = useObjectContextStore(); const { selectedKey } = useMenu(); const activeFirstLevelMenuKey = ref(''); + const allMenus = computed(() => routeStore.menus); + function setActiveFirstLevelMenuKey(key: string) { activeFirstLevelMenuKey.value = key; } @@ -20,10 +34,16 @@ function useMixMenu() { function getActiveFirstLevelMenuKey() { const [firstLevelMenuKey = ''] = routeStore.getSelectedMenuKeyPath(selectedKey.value); - setActiveFirstLevelMenuKey(firstLevelMenuKey); - } + if (firstLevelMenuKey) { + setActiveFirstLevelMenuKey(firstLevelMenuKey); + return; + } - const allMenus = computed(() => routeStore.menus); + const fallbackFirstLevelMenuKey = + allMenus.value.find(menu => isMenuMatchedByPath(route.path, menu.routePath))?.key || ''; + + setActiveFirstLevelMenuKey(fallbackFirstLevelMenuKey); + } const firstLevelMenus = computed(() => routeStore.menus.map(menu => { @@ -37,6 +57,25 @@ function useMixMenu() { () => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || [] ); + const currentObjectContextDomain = computed(() => getObjectContextDomainConfigByPath(route.path) || null); + + const headerMenuMode = computed<'global' | 'object-context'>(() => + currentObjectContextDomain.value && objectContextStore.hasContext ? 'object-context' : 'global' + ); + + const headerMenus = computed<(App.Global.Menu | App.ObjectContext.Menu)[]>(() => { + if (headerMenuMode.value === 'object-context') { + return objectContextStore.contextScopedMenus; + } + + // 对象型业务域处于入口态时,头部只保留业务域锚点,不继续投影全局子菜单。 + if (currentObjectContextDomain.value) { + return []; + } + + return childLevelMenus.value; + }); + const isActiveFirstLevelMenuHasChildren = computed(() => { if (!activeFirstLevelMenuKey.value) { return false; @@ -48,7 +87,7 @@ function useMixMenu() { }); watch( - [selectedKey, allMenus], + [selectedKey, allMenus, () => route.path], () => { getActiveFirstLevelMenuKey(); }, @@ -59,6 +98,9 @@ function useMixMenu() { allMenus, firstLevelMenus, childLevelMenus, + headerMenuMode, + headerMenus, + currentObjectContextDomain, isActiveFirstLevelMenuHasChildren, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey, diff --git a/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue b/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue index 8d80ec0..dae7519 100644 --- a/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue +++ b/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue @@ -1,8 +1,10 @@ @@ -60,9 +103,19 @@ function isMenuActive(menu: App.Global.Menu) { {{ activeFirstLevelMenu.label }} -
-
-