@epxing/core 5.3.0-beta.1 → 5.3.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/API.md +30 -27
  2. package/README.md +5 -4
  3. package/dist/.cache/route-chart-release/frontend/frame/public/js/ecStat.min.js +4 -0
  4. package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts-wordcloud.min.js +4 -0
  5. package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts.min.js +4 -0
  6. package/dist/_virtual/_plugin-vue_export-helper.js +10 -0
  7. package/dist/_virtual/jsencrypt.min.js +1 -1
  8. package/dist/assets/ecStat.min-BrTYPOXX.js +1 -0
  9. package/dist/assets/echarts-wordcloud.min-nWR4Hzny.js +3 -0
  10. package/dist/assets/echarts.min-DROS9E5X.js +45 -0
  11. package/dist/assets/role_select-DTqRlFNz.webp +0 -0
  12. package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/jsencrypt@3.5.4/_vendor/jsencrypt/bin/jsencrypt.min.js +1 -1
  13. package/dist/src/Container.js +1 -0
  14. package/dist/src/RootApp.js +54 -0
  15. package/dist/src/api/config.js +1 -0
  16. package/dist/src/api/createHttp.js +7 -0
  17. package/dist/src/api/index.js +1 -0
  18. package/dist/src/app-layout.js +1 -0
  19. package/dist/src/app-ui-settings.js +49 -0
  20. package/dist/src/app.js +1 -0
  21. package/dist/src/axios.js +11 -15
  22. package/dist/src/capture-ref.js +1 -0
  23. package/dist/src/chart-runtime-assets.js +9 -0
  24. package/dist/src/components/draggable/index.js +1 -0
  25. package/dist/src/create-app.js +4 -6
  26. package/dist/src/directives/clickoutside.js +1 -0
  27. package/dist/src/directives/global.js +1 -0
  28. package/dist/src/index.js +3 -0
  29. package/dist/src/lazy-library.js +1 -0
  30. package/dist/src/loadComponent.js +3 -2
  31. package/dist/src/loadLibrary.js +1 -0
  32. package/dist/src/menu-route.js +12 -0
  33. package/dist/src/mixins/component.js +4 -0
  34. package/dist/src/mixins/custom-option.js +1 -0
  35. package/dist/src/mixins/el-locale.js +1 -0
  36. package/dist/src/mixins/global.js +1 -0
  37. package/dist/src/mixins/index.js +1 -0
  38. package/dist/src/mixins/methods-to-prototype.js +1 -0
  39. package/dist/src/mixins/optimized-global.js +1 -0
  40. package/dist/src/mixins/popper.js +1 -0
  41. package/dist/src/navigation-context.js +47 -0
  42. package/dist/src/navigation.js +44 -12
  43. package/dist/src/package-install.js +4 -2
  44. package/dist/src/plugins/element-plus.js +9 -8
  45. package/dist/src/plugins/i18n.js +1 -0
  46. package/dist/src/plugins/library-locale.js +1 -0
  47. package/dist/src/progress-bar.js +1 -0
  48. package/dist/src/public-api.js +5 -2
  49. package/dist/src/register-components.js +1 -0
  50. package/dist/src/remote-deps.js +1 -0
  51. package/dist/src/route-components.js +9 -0
  52. package/dist/src/router/config.js +8 -8
  53. package/dist/src/router/index.js +1 -0
  54. package/dist/src/router/public.js +1 -0
  55. package/dist/src/runtime-services.js +2 -1
  56. package/dist/src/runtime-utils/fieldFactory.js +1 -0
  57. package/dist/src/runtime-utils/meta.js +4 -0
  58. package/dist/src/runtime-utils/pdf.viewer.js +32 -15
  59. package/dist/src/runtime-utils/util.js +30 -5
  60. package/dist/src/shallow-same.js +1 -0
  61. package/dist/src/shared-packages.js +2 -3
  62. package/dist/src/store/actions.js +1 -0
  63. package/dist/src/store/create-store.js +1 -0
  64. package/dist/src/store/index.js +1 -0
  65. package/dist/src/store/lru.js +1 -0
  66. package/dist/src/store/modules/config.js +1 -0
  67. package/dist/src/store/modules/file.js +1 -0
  68. package/dist/src/store/modules/index.js +1 -0
  69. package/dist/src/store/modules/list.js +26 -3
  70. package/dist/src/store/modules/locale.js +1 -0
  71. package/dist/src/store/modules/meta.js +50 -7
  72. package/dist/src/store/modules/page.js +1 -0
  73. package/dist/src/store/modules/router.js +1 -0
  74. package/dist/src/store/modules/tabs.js +1 -0
  75. package/dist/src/store/modules/user.js +1 -0
  76. package/dist/src/style-presets.js +5 -4
  77. package/dist/src/ui-settings.js +91 -18
  78. package/dist/src/viewport.js +1 -0
  79. package/dist/src/views/App.js +48 -0
  80. package/dist/src/views/AppRoleSelect.js +233 -0
  81. package/dist/src/views/Error.js +210 -0
  82. package/dist/src/views/Module.js +173 -0
  83. package/dist/src/vue.bus.js +1 -0
  84. package/dist/src/vue2-cancel-token-compat.js +1 -0
  85. package/dist/src/vue2-remote-shim.js +1 -0
  86. package/dist/src/vue2-render-compat.js +1 -0
  87. package/dist/src/vue2-select-style-compat.js +1 -0
  88. package/dist/src/watermark.js +1 -0
  89. package/dist/src/xss.js +1 -0
  90. package/dist/src/z-index.js +1 -0
  91. package/dist/style.css +251 -0
  92. package/package.json +6 -5
  93. package/src/RootApp.vue +75 -0
  94. package/src/api/createHttp.js +7 -0
  95. package/src/api/index.js +47 -47
  96. package/src/app-ui-settings.js +31 -0
  97. package/src/axios.js +311 -318
  98. package/src/chart-runtime-assets.js +7 -0
  99. package/src/create-app.js +3 -7
  100. package/src/index.js +28 -28
  101. package/src/install.js +157 -157
  102. package/src/loadComponent.js +187 -187
  103. package/src/loadLibrary.js +99 -99
  104. package/src/menu-route.js +12 -1
  105. package/src/mixins/component.js +1464 -1461
  106. package/src/navigation-context.js +49 -0
  107. package/src/navigation.js +208 -176
  108. package/src/package-install.js +2 -1
  109. package/src/plugins/element-plus.js +302 -302
  110. package/src/plugins/frame.js +178 -178
  111. package/src/public-api.js +1 -0
  112. package/src/route-components.js +7 -0
  113. package/src/router/config.js +861 -864
  114. package/src/router/public.js +1 -1
  115. package/src/runtime-services.js +1 -1
  116. package/src/runtime-utils/fieldFactory.js +179 -179
  117. package/src/runtime-utils/meta.js +399 -378
  118. package/src/runtime-utils/pdf.viewer.js +672 -644
  119. package/src/runtime-utils/util.js +1357 -1330
  120. package/src/shared-packages.js +1 -3
  121. package/src/store/modules/config.js +351 -351
  122. package/src/store/modules/list.js +30 -3
  123. package/src/store/modules/locale.js +236 -236
  124. package/src/store/modules/meta.js +314 -265
  125. package/src/style-presets.js +4 -4
  126. package/src/ui-settings.js +84 -23
  127. package/src/views/App.vue +53 -0
  128. package/src/views/AppRoleSelect.vue +334 -0
  129. package/src/views/Error.vue +63 -0
  130. package/src/views/Module.vue +210 -0
  131. package/src/xss.js +26 -26
  132. package/types/app.d.ts +10 -10
  133. package/types/axios.d.ts +1 -1
  134. package/types/component-mixin.d.ts +1 -1
  135. package/types/field-factory.d.ts +1 -1
  136. package/types/global-mixin.d.ts +1 -1
  137. package/types/http.d.ts +1 -1
  138. package/types/index.d.ts +3 -0
  139. package/types/meta.d.ts +1 -1
  140. package/types/navigation.d.ts +21 -21
  141. package/types/pdf-viewer.d.ts +1 -1
  142. package/types/runtime.d.ts +13 -13
  143. package/types/store.d.ts +1 -1
  144. package/types/util.d.ts +2 -2
  145. /package/dist/{build → .cache/route-chart-release/frontend/frame/build}/stubs/empty-components.js +0 -0
  146. /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-icons.names.js +0 -0
  147. /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-plus.lazy.js +0 -0
  148. /package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/element-plus@2.14.3_vue@3.5.39_typescript@5.9.3_/_vendor/element-plus/dist/locale/en.js +0 -0
  149. /package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/element-plus@2.14.3_vue@3.5.39_typescript@5.9.3_/_vendor/element-plus/dist/locale/zh-cn.js +0 -0
@@ -1,864 +1,861 @@
1
- import { runtimeRouteComponent } from '../runtime-services'
2
- const Page = runtimeRouteComponent('Page')
3
- const Login = runtimeRouteComponent('Login')
4
- const QyWechat = runtimeRouteComponent('QyWechat')
5
- const App = runtimeRouteComponent('App')
6
- const WikiSitePage = runtimeRouteComponent('WikiSitePage')
7
- const Module = runtimeRouteComponent('Module')
8
- const AppRoleSelect = runtimeRouteComponent('AppRoleSelect')
9
- const Task = runtimeRouteComponent('Task')
10
- const Config = runtimeRouteComponent('Config')
11
- const Error = runtimeRouteComponent('Error')
12
- const HelpDocSet = runtimeRouteComponent('HelpDocSet')
13
- import { isEmpty } from 'lodash-es'
14
- import { runtimeStore as store, getRuntimeService } from '../runtime-services'
15
- import util from '@epxing/core/src/runtime-utils/util'
16
- import http from '@epxing/core/src/api'
17
- import loadLibrary from '@epxing/core/src/loadLibrary'
18
- import { GET_USER, GET_APP_CONFIG, GET_COMPONENT_GLOBAL_CONFIG, DELETE_USER } from '@epxing/core/src/store/actions'
19
- import { ElMessage } from 'element-plus/es/components/message/index.mjs'
20
- import { setElementPlusLocale } from '@epxing/core/src/plugins/element-plus'
21
- // 菜单直达路由的纯函数层(别改引 navigation.js——那边引 router 单例会成环)
22
- import { MENU_ROUTE_ALIAS, effectiveRouteName, roleStorage } from '@epxing/core/src/menu-route'
23
- window.isLoadLibrary = false
24
- let isVisitedService = false
25
- // Task 19 / C2:**同一次用户导航内**的登录校验结果交接(不是「本会话只校验一次」)。
26
- //
27
- // 现象:守卫自己发起的二次导航(补默认角色 `next({...role})`、跳角色选择页、剥 URL 上的
28
- // token)会让 beforeEach 完整重跑一遍。重跑时 appConfig 有 `isEmpty` 门挡着、loadLibrary 有
29
- // `window.isLoadLibrary` 门挡着,唯独 loginPromise 没有任何跨遍复用 ⇒ 一次 app1 冷启动发两次
30
- // `FM_USER/validLogin`(Task 20 实测,两次相差 1.27s)。
31
- //
32
- // ⚠️ 刻意**不**做成 judgeIsLogin 那种「置一次、永不复检」的布尔标记(那个半成品的字面语义正是
33
- // 如此,见下面 willCheckLogin 处的说明):登录态**会过期**,永久标记会让中途失效的 token 再也
34
- // 检不出来,用户带着废会话继续操作,直到某个业务请求 401 才被踢——「validLogin 2→1」这个判据
35
- // 会满足,但改动是错的。
36
- //
37
- // 复用作用域 = **同一条重定向链**,三个条件同时成立才继承(修复轮 1,评审 C-1):
38
- // ① 本遍守卫以 `next(location)` 自我重定向收尾 —— 由 runBeforeEach 里那层 next 包装**显式
39
- // 挂载**。注意这是「默认不泄漏」:不再像第一版那样无条件挂上、指望函数尾部清掉(那种形状
40
- // 在**守卫抛错**这条出口上会把交接原样留在模块变量里,评审已造出现场:manage 分支
41
- // `hasMangeService` 请求失败 ⇒ 守卫抛错 ⇒ 残留被下一次用户导航捡走)。外层 beforeEach
42
- // 还包了一层 try/catch 兜底,抛错必清。
43
- // ② 本遍是 vue-router 标记的「重定向续跑」(`to.redirectedFrom` 有值)。
44
- // ③ **链根一致**:交接绑定在重定向链根的 fullPath 上。只有 ② 是不够的 —— 路由表 `redirect:`
45
- // (本文件 /m/:menuKey 两条兼容链接)会让一次**纯用户导航**的 redirectedFrom 天然为真,
46
- // 锁 ② 对它形同虚设。依据:vue-router 4.6 pushWithRedirect 对路由表 redirect 与守卫
47
- // 重定向两条递归都传 `redirectedFrom || (targetLocation|toLocation)`(vue-router.mjs
48
- // :1293/:1314)⇒ 整条链上 redirectedFrom 恒等于链根,用它当身份在多跳链上也精确。
49
- // 任何一次真正由用户发起的导航(redirectedFrom 为空,或链根对不上)都会照常重新发 validLogin。
50
- //
51
- // 注意:单纯「复用 in-flight promise」在这里是**空操作**——二次导航发生在 `await loginPromise`
52
- // **之后**(见下面各分支:先 await 拿到 isLogin,再 await GET_USER,最后才 next({...role})),
53
- // 第二遍开始时第一遍那个 promise 早就 settled 了。省掉请求靠的是复用同一个**已 settled 的
54
- // promise 对象**,不是靠请求还在飞。
55
- let handoff = null // { promise, key: 重定向链根 fullPath, at: 挂载时刻 }
56
- // 陈旧上界(保险丝,不是去重机制本身)。挂载点是 `next(location)` 那一刻,消费点是紧接着重跑的
57
- // 那一遍守卫的第一行,中间只隔 vue-router 的微任务、**没有网络**,实测同一 tick 内完成;这个
58
- // 常数只是给「redirect 目标与当前路由相同被判 NAVIGATION_DUPLICATED、守卫压根没重跑」这类
59
- // 病态路径封一个顶,免得残留在模块变量里躺成用户步调。取值比正常间隔大三个数量级,判据不会
60
- // 挂在它身上。
61
- const HANDOFF_MAX_AGE_MS = 10000
62
- // Task 13 review v2(round 3:注释前提已改,见下):改方案后(发起点挪到 GET_APP_CONFIG
63
- // resolve 之后,见 beforeEach),本函数与 loadLibrary 不再需要显式穿 client/domainContext。
64
- // 撑住这一点的**不是**"appConfig/clientConfig 必然一起就绪"——这个前提是假的:
65
- // GET_APP_CONFIG action(store/modules/config.js)有三条分支 clientConfig 从未提交或提交空
66
- // 对象(`data` 为假 / 请求 reject / 提交的 `client` 本身是 `data.client.data || {}`)。
67
- // ⚠️ 2026-09 更新:GET_APP_CONFIG 失败的 catch 现在有 return 了(跳错误页后不再往下走,
68
- // 见 dedupe 相关改动),下面这条不变量此后只需要对"resolve 但 clientConfig 为空/不完整"
69
- // 这几条分支成立,不用再覆盖"请求 reject"这一种——reject 已经在此之前 return 掉,走不到这里。
70
- // 真正承重的是更弱、因而更可靠的不变量:从这里到"改动前的原发起位置"之间,只隔着
71
- // `await GET_COMPONENT_GLOBAL_CONFIG` 这一步,它只 commit `componentGlobalConfig`/
72
- // `systemComponentGlobalConfig`——axios.js 的头部解析链完全不读这两个 key。所以**不管
73
- // clientConfig 这次到底有没有就绪**,本函数在这里发起时解析出的请求头,与"改动前挪到原位置
74
- // 发起"逐字节相同(唯一例外是 localStorage 里的 token,若窗口内先返回的响应带了
75
- // authorization 刷新头会改写它——但这不是新增的不确定性,基线里 `getI18nMessages` 发出的
76
- // `FM_MESSAGE` 请求本来就不 await、早已横跨同一段窗口)。已回退到 Task 13 之前的原始实现,
77
- // 本函数与 loadLibrary.js 均不再有 Task 13 相关改动。
78
- /**
79
- * 引导期接口去重(develop 6ce3678ca 系列移植)
80
- *
81
- * 同一次页面加载里路由守卫会被重复触发:产品库加载完成后,远程库自己的 install() 里会
82
- * `router.addRoute(...)` + `router.replace(当前路径)`(见 loadLibrary.js 头部注释、
83
- * lazy-library.js 的 "util.loadJs 的 onload 重跑一遍 useVuePlugin → app.use → install()")。
84
- * 这是**全新的一次导航**,不是守卫内部 `next(location)` 产生的重定向 ——
85
- * ⚠️ 之前这里的注释说 validLogin 的去重"已经由 handoff 单独承担",是错的:handoff 的
86
- * `inheritedLoginPromise` 第一个条件就是 `to.redirectedFrom` 有值(见文件头部 ②、
87
- * runBeforeEach 里 `chainRootKey`/`inheritedLoginPromise` 那几行),只覆盖守卫**自己发起**
88
- * 的重定向(补默认角色、剥 token、跳角色选择页);`router.replace(当前路径)` 是外部代码发起
89
- * 的全新导航,`redirectedFrom` 恒为空,`inheritedLoginPromise` 摸不到这条路径,第二遍守卫
90
- * 照样会再发一次 `FM_USER/validLogin`。所以 isLoginFn 仍要包一层这里的 dedupe——
91
- * 两者不冲突:`inheritedLoginPromise` 命中时走 `||` 短路,压根不会调到 isLoginFn(见下方
92
- * `loginPromise = willCheckLogin ? (inheritedLoginPromise || isLoginFn()) : null`)。
93
- * GET_USER / GET_COMPONENT_GLOBAL_CONFIG 同样没有 handoff 这类机制,`isEmpty(user)` /
94
- * `!componentGlobalConfig` 这类先查后写的判据在守卫重跑时仍可能撞见"上一遍还没提交"的窗口
95
- * 而重复发起,也包一层。
96
- * GET_APP_CONFIG 目前的发起顺序严格排在 loadLibrary 之前(Task 13 review),触发重跑的
97
- * addRoute+replace 发生时它必然已经 resolve,理论上摸不到这个窗口——仍然包一层纯粹是让四个
98
- * 引导请求统一走同一套去重外壳,不改变现有时序。
99
- *
100
- * ttl 为 0 时只复用进行中的请求,大于 0 时结果也复用 ttl 毫秒(引导期够用,之后仍会重新校验)。
101
- */
102
- function dedupe (fn, { key = () => '', ttl = 0 } = {}) {
103
- const cache = new Map()
104
- const wrapped = function (...args) {
105
- const cacheKey = key(...args)
106
- const hit = cache.get(cacheKey)
107
- if (hit && (hit.pending || (ttl > 0 && Date.now() - hit.doneAt < ttl))) return hit.promise
108
- const record = { pending: true, doneAt: 0 }
109
- record.promise = Promise.resolve()
110
- .then(() => fn(...args))
111
- .then(data => {
112
- record.pending = false
113
- record.doneAt = Date.now()
114
- return data
115
- })
116
- .catch(err => {
117
- cache.delete(cacheKey)
118
- return Promise.reject(err)
119
- })
120
- cache.set(cacheKey, record)
121
- return record.promise
122
- }
123
- wrapped.clear = () => cache.clear()
124
- return wrapped
125
- }
126
- // 引导期内复用登录态校验结果,避免守卫重跑时重复请求(BOOT_TTL 之后仍会重新校验,
127
- // 不是"本会话只查一次"的永久标记——登录态会过期,见文件头部对 judgeIsLogin 反模式的警告)
128
- const BOOT_TTL = 3000
129
- const isLoginFn = dedupe(() => {
130
- return http.get({
131
- action: 'validLogin',
132
- entityId: 'FM_USER'
133
- })
134
- .then(data => {
135
- return data
136
- })
137
- .catch(() => false)
138
- }, { ttl: BOOT_TTL })
139
- const getUserOnce = dedupe(() => store.dispatch({ type: GET_USER }))
140
- const getAppConfigOnce = dedupe(payload => store.dispatch(payload), {
141
- key: payload => [payload.client, payload.system, payload.app, payload.id].join('|')
142
- })
143
- const getComponentGlobalConfigOnce = dedupe(payload => store.dispatch(payload), {
144
- key: payload => [payload.client, payload.system].join('|')
145
- })
146
- // 公共 API 可以在应用创建前导入;首次运行守卫时才订阅实际注入的 store。
147
- const subscribedStores = new WeakSet()
148
- function subscribeToLogout () {
149
- const currentStore = getRuntimeService('store')
150
- if (subscribedStores.has(currentStore)) return
151
- currentStore.subscribe(mutation => {
152
- if (mutation.type === DELETE_USER) {
153
- isLoginFn.clear()
154
- getUserOnce.clear()
155
- }
156
- })
157
- subscribedStores.add(currentStore)
158
- }
159
-
160
- function getDefaultRole (user, appId) {
161
- const { serviceRoles = {} } = user
162
- const roleList = serviceRoles[appId] || []
163
- let defaultRole = null
164
- if (roleList.length === 1) {
165
- defaultRole = roleList[0].code + (roleList[0].type === 'client' ? '-client' : '')
166
- } else {
167
- const defaultRoleMap = roleList.find(item => item.isDefault === true)
168
- if (defaultRoleMap) defaultRole = defaultRoleMap.code + (defaultRoleMap.type === 'client' ? '-client' : '')
169
- }
170
- return defaultRole
171
- }
172
-
173
- function isHasRole (user, appId, role) {
174
- const { serviceRoles = {} } = user
175
- const roleList = serviceRoles[appId] || []
176
- let roleCode = role
177
- if (role.endsWith('-client')) {
178
- roleCode = role.replace(/-client$/, '')
179
- // 从通用角色获取
180
- return roleList.find(item => item.code === roleCode && item.type === 'client')
181
- } else {
182
- return roleList.find(item => item.code === roleCode && item.type !== 'client')
183
- }
184
- }
185
-
186
- function getI18nMessages (system) {
187
- // 国际化设置
188
- const { clientConfig } = store.state.config || {}
189
- const { CODE: clientCode, LANGUAGE_DEFAULT: defaultLanguage } = clientConfig
190
- const currentLanguage = localStorage.getItem(`${clientCode}-language`) || defaultLanguage
191
- // T33:element-ui 的 `locale.use()` → EP 的 reactive config(见 plugins/element-plus.js)
192
- setElementPlusLocale(currentLanguage)
193
- http.list({
194
- entityId: 'FM_MESSAGE',
195
- scopeParams: JSON.stringify([
196
- {
197
- field: 'TYPE',
198
- values: 'page'
199
- },
200
- {
201
- lb: '(',
202
- field: 'SCOPE',
203
- values: 'frame',
204
- link: 'or'
205
- },
206
- {
207
- field: 'SYSTEM',
208
- values: system,
209
- rb: ')'
210
- }
211
- ])
212
- })
213
- .then(data => {
214
- const messages = {}
215
- data.entitys.forEach(item => {
216
- let name = item.NAME
217
- let content = item.CONTENT
218
- if (data.encrypt === '1') {
219
- name = util.utf8to16(window.atob(name))
220
- content = util.utf8to16(window.atob(content))
221
- }
222
- messages[name.trim()] = content
223
- })
224
- globalThis.i18n.setMessage(messages)
225
- })
226
- }
227
- function visitService (serviceId) {
228
- if (isVisitedService) return
229
- isVisitedService = true
230
- http.get({
231
- action: 'visitService',
232
- entityId: 'FM_USER_SERVICE_VISIT_RECORD',
233
- serviceId
234
- })
235
- }
236
-
237
- function closeLoading () {
238
- const loadingElem = document.querySelector('#loading')
239
- if (loadingElem) loadingElem.style.display = 'none'
240
- }
241
-
242
- // appConfig.NAME 可能只有多语言字段的翻译值 appConfig['NAME#'](`#` 后缀是本平台代码表/字段
243
- // 显示值的通用约定),只认 NAME 会在这种情况下把标题落空
244
- function setAppDocumentTitle (appConfig = {}) {
245
- const title = appConfig.NAME || appConfig['NAME#']
246
- if (title) document.title = title
247
- return title
248
- }
249
-
250
- export default [
251
- {
252
- path: '/login/:client/:system/:app?',
253
- name: 'login',
254
- component: Login,
255
- meta: { nav: false },
256
- props: (route) => ({ query: route.query })
257
- },
258
- {
259
- path: '/hall/login/:client',
260
- name: 'hallLogin',
261
- component: Login,
262
- meta: { nav: false },
263
- props: (route) => ({ query: route.query })
264
- },
265
- {
266
- path: '/app/role/select/:client/:system?/:app?',
267
- name: 'appRoleSelect',
268
- component: AppRoleSelect
269
- },
270
- {
271
- path: '/hall/role/select/:client/:app?',
272
- name: 'hallRoleSelect',
273
- component: AppRoleSelect
274
- },
275
- {
276
- path: '/',
277
- name: 'defaultApp',
278
- component: App,
279
- props: route => ({ query: route.query })
280
- },
281
- {
282
- path: '/app/:client/:system/:app?/:role?',
283
- name: 'app1',
284
- component: App,
285
- meta: { refresh: false }, // 菜单直达在本路由族内切换 URL,根 router-view 不因 path 变化重挂(防导航闪烁/卡顿)
286
- props: route => ({ query: route.query })
287
- },
288
- {
289
- // 菜单直达(menuKey=菜单编号 MODULE_CODE,没配编号用 ID)。不带 role 段——role 走
290
- // localStorage(menu-route.js roleStorage)。与 app1 无歧义:本路由比 app1 多吃两段。
291
- path: '/app/:client/:system/:app/:menuKey([0-9A-Fa-f]{32})',
292
- name: 'appMenu',
293
- component: App,
294
- meta: { refresh: false },
295
- props: route => ({ query: route.query })
296
- },
297
- {
298
- path: '/app/:id?/:roleId?',
299
- name: 'app',
300
- component: App,
301
- props: (route) => ({ query: route.query })
302
- },
303
- {
304
- // Wiki 站点:#/wiki/{客户}/{系统}/{空间编码}/{页路径}
305
- //
306
- // 页路径用通配段而不是单段参数——Wiki 的路径本来就带层级
307
- // (implementation/05-sop),单段参数会把斜杠当分隔符,深层页永远打不开。
308
- // 客户与系统段与其它业务路由保持同一形状:它们是权限边界的一部分,
309
- // 少一段就没法在进页面前判断这个人能不能看这个空间。
310
- path: '/wiki/:client/:system/:wiki/:path*',
311
- name: 'wikiSite',
312
- component: WikiSitePage,
313
- props: route => ({ query: route.query })
314
- },
315
- {
316
- // 引用直达:#/wiki/view?w=空间&p=页&rev=版本&h=锚点
317
- // 后端铸造的引用地址用这一条。它不带客户/系统段,因为引用是从答案里点进来的,
318
- // 上下文已经在登录态里;权限仍由接口按空间的 VIEW_ROLES 判定。
319
- path: '/wiki/view',
320
- name: 'wikiView',
321
- component: WikiSitePage,
322
- props: route => ({ query: route.query })
323
- },
324
- {
325
- path: '/service/setting/:client/:system/:app/:role?',
326
- name: 'appSetting',
327
- component: App,
328
- props: route => ({ query: route.query })
329
- },
330
- {
331
- path: '/manage/:client/:system/:app/:role?',
332
- name: 'manage',
333
- component: App,
334
- props: route => ({ query: route.query })
335
- },
336
- {
337
- path: '/system/:client/:system/:role?',
338
- name: 'system',
339
- component: App,
340
- props: route => ({ query: route.query })
341
- },
342
- {
343
- path: '/client/:client/:role?',
344
- name: 'client',
345
- component: App,
346
- props: route => ({ query: route.query })
347
- },
348
- {
349
- path: '/hall/:client/:role?',
350
- name: 'hall',
351
- component: App,
352
- meta: { refresh: false },
353
- props: route => ({ query: route.query })
354
- },
355
- {
356
- // 服务大厅的菜单直达(同 appMenu 语义)
357
- path: '/hall/:client/:menuKey([0-9A-Fa-f]{32})',
358
- name: 'hallMenu',
359
- component: App,
360
- meta: { refresh: false },
361
- props: route => ({ query: route.query })
362
- },
363
- {
364
- path: '/task/:client/:system/:taskId',
365
- name: 'Task',
366
- component: Task,
367
- props: (route) => ({ query: route.query })
368
- },
369
- {
370
- path: '/config/:mode/:client/:system/:configId',
371
- name: 'Config',
372
- component: Config,
373
- props: (route) => ({ query: route.query })
374
- },
375
- {
376
- path: '/feishuTask/:client/:system/:taskId',
377
- name: 'feishuTask',
378
- component: Task,
379
- props: (route) => ({ query: route.query })
380
- },
381
- {
382
- path: '/help/:client/:system/:docset/:docId?',
383
- name: 'help',
384
- component: HelpDocSet,
385
- meta: {
386
- title: '帮助中心',
387
- loadAppConfig: false,
388
- refresh: false
389
- }
390
- },
391
- {
392
- path: '/module',
393
- name: 'module',
394
- component: Module,
395
- props: (route) => ({ query: route.query })
396
- },
397
- {
398
- path: '/page/:client/:system/:app/:role?',
399
- name: 'Page',
400
- component: Page,
401
- props: (route) => ({ query: route.query })
402
- },
403
- {
404
- path: '/page',
405
- name: 'Page1',
406
- component: Page,
407
- props: (route) => ({ query: route.query })
408
- },
409
- {
410
- path: '/s/:link',
411
- name: 'shorturl',
412
- component: App,
413
- props: (route) => ({ query: route.query })
414
- },
415
- {
416
- path: '/qywechat',
417
- name: 'qywechat',
418
- component: QyWechat
419
- },
420
- {
421
- // 旧形态 /m/:menuKey 链接兼容:重定向到无 /m/ 的新形态
422
- path: '/app/:client/:system/:app/m/:menuKey',
423
- redirect: to => ({ name: 'appMenu', params: to.params, query: to.query })
424
- },
425
- {
426
- path: '/hall/:client/m/:menuKey',
427
- redirect: to => ({ name: 'hallMenu', params: to.params, query: to.query })
428
- },
429
- {
430
- path: '/error',
431
- name: 'error',
432
- component: Error
433
- },
434
- {
435
- path: '/:id?',
436
- name: 'app2',
437
- component: App,
438
- props: (route) => ({ query: route.query })
439
- }
440
- ]
441
-
442
- /**
443
- * 路由守卫(对外入口)。守卫体是下面的 runBeforeEach,这层薄壳只做一件事:**任何抛错都保证
444
- * 登录校验交接被清掉**。守卫体里 await 的请求(GET_APP_CONFIG、hasMangeService…)都可能
445
- * reject,而守卫一抛错导航就中止、此后不会再有任何守卫运行,残留会一直躺到用户下次导航
446
- * (评审 C-1 现场)。这是第二道保险;第一道是 runBeforeEach 里「只在自我重定向出口显式挂载」。
447
- * ⚠️ 形参必须保持 3 个:vue-router 用 `guard.length < 3` 判断是不是回调式守卫。
448
- */
449
- export const beforeEach = async (to, from, next) => {
450
- try {
451
- subscribeToLogout()
452
- return await runBeforeEach(to, from, next)
453
- } catch (err) {
454
- handoff = null
455
- throw err
456
- }
457
- }
458
-
459
- const runBeforeEach = async (to, from, rawNext) => {
460
- // 重定向链根的 fullPath —— 交接的身份(三个条件见文件头部)。必须在下面 isMenuRoute 改写
461
- // to 之前取(那里的 `to = {...to, …}` 是浅拷贝,其实也带得走,但别依赖它)。
462
- const chainRootKey = to.redirectedFrom?.fullPath || to.fullPath
463
- // 交接**一次性消费**:读完立刻无条件清空。这条无条件清空同时保证了「交接最多只能被紧接着的
464
- // 那一遍守卫捡走」——中间只要再跑过任何一遍守卫(任何导航),残留就没了。
465
- const inheritedLoginPromise = (to.redirectedFrom && handoff && handoff.key === chainRootKey &&
466
- Date.now() - handoff.at <= HANDOFF_MAX_AGE_MS) ? handoff.promise : null
467
- handoff = null
468
- // 本遍自己的登录校验结果,只有本遍以 next(location) 自我重定向收尾时才交接出去。
469
- // ⚠️ 必须是独立变量、且在函数最开头初始化,不能让下面那层 next 包装直接去读 `let loginPromise`:
470
- // shorturl 长链接分支的 `return`(见下)排在 `let libPromise, loginPromise` **之前**,它在
471
- // .then 里异步调 next 时那行 let 根本没执行过,读 loginPromise 会撞 TDZ ReferenceError。
472
- let ownLoginPromise = null
473
- let nextCalled = false
474
- // 把「挂载交接」从**无条件挂上、指望函数尾部清掉**倒过来成**只在自我重定向这个出口显式挂上**。
475
- // 包在 next 上是为了让守卫体里那十几处 next(...) 调用点一行都不用改。
476
- const next = (location) => {
477
- // 只认第一次调用(与 vue-router 一致)。本文件末尾就有一处既有的 next 双调用,记账必须跟着
478
- // 第一次走,否则剥 token 那条重定向刚挂上的交接会被紧随其后的 next() 清掉。调用本身照常
479
- // 原样转发,不改动 router 行为。
480
- if (!nextCalled) {
481
- nextCalled = true
482
- // 自我重定向 = 传了一个 location 对象。next()/next(false)/next(Error) 都不是;
483
- // next('/error?…') 那种字符串也不算——那条路径上 ownLoginPromise 还是 null,且「不挂交接」
484
- // 最多多发一次 validLogin,是安全方向。
485
- // ⚠️ 必须写 globalThis.Error:本文件顶上 `import Error from '@epxing/business/src/views/Error'`
486
- // 把全局 Error 构造器**在整个模块作用域里遮蔽**成了一个 Vue 组件对象,直接写
487
- // `x instanceof Error` 会抛 `TypeError: Right-hand side of 'instanceof' is not callable`
488
- // ——这条已在本轮实测中真的炸过一次(守卫抛错 ⇒ 补默认角色的重定向整个不发生)。
489
- handoff = (ownLoginPromise && location && typeof location === 'object' && !(location instanceof globalThis.Error))
490
- ? { promise: ownLoginPromise, key: chainRootKey, at: Date.now() }
491
- : null
492
- }
493
- return rawNext(location)
494
- }
495
- // 菜单直达路由(appMenu/hallMenu)归一成旧路由处理:name 换成别名(app1/hall),
496
- // role 从 localStorage 补进 params(这些路由不带 role 段)。只改本函数内的局部视图,
497
- // vue-router 实际导航目标不变(URL 保持 /m/:menuKey)。
498
- const isMenuRoute = !!MENU_ROUTE_ALIAS[to.name]
499
- if (isMenuRoute) {
500
- const storedRole = roleStorage.get(to.params.client, to.params.system)
501
- to = { ...to, name: effectiveRouteName(to.name), params: { ...to.params, role: to.params.role || storedRole || undefined } }
502
- }
503
- if (MENU_ROUTE_ALIAS[from.name]) {
504
- from = { ...from, name: effectiveRouteName(from.name) }
505
- }
506
- // 访问其他应用刷新页面
507
- if ((to.name === 'app' || to.name === 'appRoleSelect') && (from.name === 'app' || from.name === 'appRoleSelect')) {
508
- if (to.params.id !== from.params.id || to.params.app !== from.params.app) {
509
- setTimeout(() => {
510
- window.location.reload()
511
- }, 200)
512
- }
513
- }
514
- if ((to.name === 'app1' || to.name === 'hall') && (from.name === 'app1' || from.name === 'hall')) {
515
- if (to.params.app !== from.params.app) {
516
- setTimeout(() => {
517
- window.location.reload()
518
- }, 200)
519
- }
520
- }
521
- let { id, appId, client, system, app, roleId, role } = to.params
522
- // 带 role 的旧式 URL 进来顺手持久化(供菜单直达路由/无 role URL 复用同一角色)
523
- if (role && client) roleStorage.set(client, system, role)
524
- if (to.path === '/') {
525
- client = window.BASE_CLIENT_ID
526
- system = window.BASE_SYSTEM_ID
527
- app = window.BASE_APP_ID
528
- }
529
- if (to.name === 'Config') {
530
- app = 'FM_SERVICE_SYSTEM'
531
- }
532
- // Wiki 引用直达(#/wiki/view?w=…)刻意不带客户/系统段:它是从答案或空间列表
533
- // 点进来的,上下文已在登录态里。但刷新时 to.params 是空的,getAppConfig 会拿着
534
- // undefined 去查,最后跳 /error——现象是「点开能看,刷一下就报错,像把租户和系统弄丢了」。
535
- // 退回平台默认上下文,与 to.path === '/' 同一套;空间能不能看仍由接口按 VIEW_ROLES 判。
536
- if (to.name === 'wikiView') {
537
- client = client || window.BASE_CLIENT_ID
538
- system = system || window.BASE_SYSTEM_ID
539
- app = app || window.BASE_APP_ID
540
- }
541
- let belongToClient = client
542
- let belongToSystem = system
543
- if (/\/s\/([\w]+)/.test(to.path)) {
544
- if (!to.params.pageId && !to.params.flag && !to.params.app) {
545
- http.get({
546
- action: 'getLongUrl',
547
- entityId: 'FM_URL_MAP',
548
- url: to.params.link
549
- }, {
550
- domainContext: window.BASE_PROJECT_NAME,
551
- client: client,
552
- system: window.BASE_SYSTEM_ID
553
- })
554
- .then(data => {
555
- const fullPath = window.ROUTER_MODE === 'history' ? data : data.split('#')[1]
556
- const path = fullPath.split('?')[0]
557
- const search = fullPath.split('?')[1]
558
- const params = {
559
- ...to.params
560
- }
561
- if (search) {
562
- const searchArr = search.split('&')
563
- searchArr.forEach(item => {
564
- const arr = item.split('=')
565
- params[arr[0]] = arr[1]
566
- })
567
- }
568
-
569
- /\/(app|recruit\/register)\/([\w-]+)\/([\w-]+)\/([\w-]+)/.test(path)
570
- params.client = RegExp.$2
571
- params.system = RegExp.$3
572
- params.app = RegExp.$4
573
- next({ ...to, params })
574
- })
575
- return
576
- }
577
- } else if (to.name === 'client' || to.name === 'system' || to.name === 'hall' || to.name === 'hallRoleSelect' || to.name === 'hallLogin' || to.name === 'appSetting') {
578
- belongToSystem = 'epxing-frame'
579
- app = to.name === 'appSetting' ? 'FM_SERVICE_SETTING' : to.name === 'client' ? 'FM_SERVICE_CLIENT' : to.name === 'system' ? 'FM_SERVICE_SYSTEM' : 'FM_SERVICE_HALL'
580
- } else if (to.name === 'wikiSite' || to.name === 'wikiView') {
581
- // Wiki 站点是独立页面,不挂在任何业务应用下,沿用服务大厅的上下文加载配置。
582
- //
583
- // 不给这条分支的话 client / system / app 全是空,getAppConfig 拿不到配置,
584
- // 直接输地址或刷新就打不开——而在 SPA 内 router.push 过去却是好的。
585
- // 这个差别很要命:站点地址是设计成可分享的,别人拿到链接一打开就是白屏,
586
- // 自己在页面里点却一切正常,最容易被当成「偶发」放过去。
587
- //
588
- // wikiView(引用直达)地址上没有客户段,回落到部署的默认客户端;
589
- // wikiSite 的地址里带,优先用它自己的。
590
- belongToClient = client || window.BASE_CLIENT_ID
591
- belongToSystem = 'epxing-frame'
592
- app = 'FM_SERVICE_HALL'
593
- } else if (app === 'FM_SERVICE_SYSTEM') {
594
- belongToSystem = 'epxing-frame'
595
- } else if (/\/(recruit\/register)\/([\w-]+)\/([\w-]+)\/([\w-]+)/.test(window.location.hash)) {
596
- belongToClient = RegExp.$2
597
- belongToSystem = RegExp.$3
598
- app = RegExp.$4
599
- }
600
- // 在任何路由请求或参数清理前接收一次 URL Token;localStorage 受限时会回落到当前标签页内存。
601
- if (belongToClient) util.consumeUrlToken(belongToClient)
602
- if (to.meta?.title) document.title = to.meta.title
603
- let { appConfig, componentGlobalConfig } = store.state.config || {}
604
- // libPromise/loginPromise 声明在 if 外层(函数作用域):isLoginFn 的 await 消费点散落在
605
- // 本函数后面第二个独立的 if-else 链(shorturl/app1/app/Task 等分支,与本 if 是兄弟块),
606
- // 块级作用域的 const 够不到那里。未进下面这个 if 时两者保持 undefined 且不会被 await 到
607
- // ——现有分支结构保证了这点('error' 路由/loadAppConfig:false 路由不落进第二条链的任何分支)。
608
- let libPromise, loginPromise
609
- if (to.name !== 'error' && to.meta?.loadAppConfig !== false) {
610
- if (isEmpty(appConfig)) {
611
- try {
612
- appConfig = await getAppConfigOnce({ type: GET_APP_CONFIG, client: belongToClient, system: belongToSystem, app, id: id || appId, routeClient: client }) || {}
613
- } catch (err) {
614
- console.error(err)
615
- // 已经跳错误页,后面的引导逻辑不用再跑(继续跑会拿着空 appConfig 发一堆没有意义的请求,
616
- // 还会二次调用 next)
617
- return next('/error?code=' + (err.code || '') + '&url=' + window.location.hash)
618
- }
619
- getI18nMessages(appConfig['SYSTEM|ID'] || appConfig.SYSTEM)
620
- }
621
- // Task 13 / C1 v2(评审换方案后,设计文档 §7.2;round 3:下面这段的前提改过,见理由):
622
- // 只把 loadLibrary 与 isLoginFn 的"发起"提前到这里——GET_APP_CONFIG 已经 resolve。
623
- // 撑住"不需要显式穿 client/domainContext"这一点的**不是**"appConfig/clientConfig 必然
624
- // 一起就绪":GET_APP_CONFIG action 有三条分支 clientConfig 从未提交或提交空对象
625
- // (`data` 为假 / 请求 reject / 提交的 `client` 本身是空对象,见 store/modules/config.js)。
626
- // ⚠️ 2026-09 更新:上面的 catch 现在有 return 了,"请求 reject"这一种已经在此之前跳错误页
627
- // 退出,走不到这里;下面这条不变量只需要对"resolve 但 clientConfig 为空/不完整"的其余两条
628
- // 分支成立。真正承重的是更弱、因而更可靠的不变量:从这里到"改动前的原发起位置"之间,只隔着
629
- // 这一步,它只 commit `componentGlobalConfig`/`systemComponentGlobalConfig`——axios.js
630
- // 的头部解析链完全不读这两个 key。所以不管 clientConfig 这次到底有没有就绪,这里发起的
631
- // 请求头都和"改动前挪到原位置发起"逐字节相同(唯一例外见下方 isLoginFn 定义处的 token
632
- // 刷新头注释)。不需要碰 axios.js/createHttp.js/loadLibrary.js——evaluation round 2
633
- // 发现在"appConfig 就绪前"提前发起会导致隐式链落空丢 Authorization 头(C-1,且波及比最初
634
- // 判断更多的路由族),而给 createHttp.js 的 options.client 打通到 axios.js 又会撞上全仓
635
- // 15 处既有调用点的既有语义(N-1,新 Critical);挪到这个位置后两个问题都不存在,故两轮
636
- // 相关改动已从 axios.js/createHttp.js/loadLibrary.js/isLoginFn 上完全回退。与紧接着的
637
- // GET_COMPONENT_GLOBAL_CONFIG 请求重叠等待网络时间;下面 await 的位置一行不动(仍是原来
638
- // loadLibrary 那一行、以及后面各分支里原来 isLoginFn 那几行)。
639
- // ⚠️ 但"副作用发生顺序完全不变"不成立——validLogin 响应侧的副作用(未登录/过期路径触发的
640
- // delayToLogin→util.toLogin→router.push)现在可能在本次导航自身的 next() 之前、从一个
641
- // 仍处于 pending 的导航内部发出,不再像改动前那样严格排在原分支代码之后。终态大概率相同,
642
- // 是任何并行化都躲不掉的固有代价,不是"顺序不变"——详见 task-13-report.md。
643
- const libParam = { busiClientCode: client || belongToClient, busiSystemCode: system || belongToSystem }
644
- libPromise = window.isLoadLibrary ? null : loadLibrary(libParam)
645
- // review I-1(评审确认收窄不削减并行化收益,原样保留):只在原本就会消费 loginPromise
646
- // 的路由族上才发起,镜像下面第二条 if-else 链的分支条件(含内层 isEmpty(user)/
647
- // !judgeIsLogin 判据),否则 login/hallLogin/appRoleSelect/Config/Page/qywechat 等公开
648
- // 路由会凭空多发一次 validLogin:匿名访问必然拿到 nologin 类响应,被 axios.js 的 401
649
- // 拦截器当场弹 toast 并踢去登录页——对着登录页本身踢登录页,是本条改动最容易被忽略的
650
- // 副作用。
651
- // Task 19 / C2:原来这里还有一个 `&& !judgeIsLogin` 的门(下面 Task/feishuTask 分支里也有
652
- // 一个 `if (!judgeIsLogin)`),已一并删除。理由:`store.state.config.judgeIsLogin` 在全仓
653
- // **只被读、从未被写**,config 模块的 state 里也没有声明它,且该模块所有 mutation 只往具名
654
- // 子对象(systemConfig/appConfig/clientConfig/...)里铺数据,既没有 `Object.assign(state, …)`
655
- // 也没有 `state[动态键] =`,后端也没法把它下发注入到 state.config 根上 ⇒ 它恒为 undefined,
656
- // `!judgeIsLogin` 恒为 true,两处判据都是死重量,删掉**不改变任何现有行为**。
657
- // 之所以是删掉而不是「补上写入语义」:它注释里的意图(「是否校验过用户已登录」)就是一个
658
- // 会话级永久标记,而登录态会过期——真按那个字面语义补写入,等于亲手做出上面文件头部警告的
659
- // 那个错误修法。去重该做在「同一条重定向链内」这个粒度上,已由 handoff 承担(见文件头部)。
660
- const { user } = store.state.user
661
- const willCheckLogin = to.name === 'shorturl' ||
662
- to.path === '/' || to.name === 'app1' || to.name === 'client' || to.name === 'hall' ||
663
- to.name === 'system' || to.name === 'appSetting' || to.name === 'manage' ||
664
- (to.name === 'app' && isEmpty(user)) ||
665
- to.name === 'Task' || to.name === 'feishuTask'
666
- // 本遍是「自我重定向的续跑」就直接复用上一遍那个已 settled 的 promise,不再发请求;
667
- // 否则照常发。inheritedLoginPromise 的产生/失效条件见文件头部注释。
668
- loginPromise = willCheckLogin ? (inheritedLoginPromise || isLoginFn()) : null
669
- // 只登记「本遍可交接的结果是什么」。真正的挂载在上面那层 next 包装里,且只发生在自我重定向
670
- // 这个出口 —— 走别的出口(正常 next()、next(false)、守卫抛错)都不会留下任何东西。
671
- ownLoginPromise = loginPromise
672
- // 立即挂 catch 占位,否则在被 await 之前 reject 会产生 unhandled rejection。
673
- // catch 返回的是新 promise,不影响原 promise 的 reject 传播,真正的错误处理仍在原 await 处。
674
- if (libPromise) libPromise.catch(() => {})
675
- if (loginPromise) loginPromise.catch(() => {})
676
-
677
- if (!componentGlobalConfig) {
678
- await getComponentGlobalConfigOnce({ type: GET_COMPONENT_GLOBAL_CONFIG, client: appConfig.CLIENT, system: appConfig['SYSTEM|ID'] || appConfig.SYSTEM })
679
- }
680
- setAppDocumentTitle(appConfig)
681
- if (!window.componentLibrary) window.componentLibrary = {}
682
- if (!window.isLoadLibrary) {
683
- await libPromise
684
- window.isLoadLibrary = true
685
- console.log('----加载完成----')
686
- }
687
- }
688
- const nextTo = { ...to }
689
- if (to.name === 'shorturl') {
690
- let { user } = store.state.user
691
- if (setAppDocumentTitle(appConfig)) {
692
- if (!app) app = appConfig.CODE
693
- }
694
- // 判断用户是否登录
695
- const isLogin = await loginPromise
696
- if (isLogin) {
697
- if (isEmpty(user)) {
698
- try {
699
- user = await getUserOnce()
700
- } catch (error) {
701
- ElMessage.error(error.message)
702
- util.toLogin()
703
- }
704
- }
705
- visitService(appConfig.ID)
706
- closeLoading()
707
- // 判断是否需要角色
708
- if (appConfig.EXTEND_OF_COMPETENCE !== 'allRole' && (!roleId && (!role || !isHasRole(user, appConfig.ID, role)))) {
709
- const defaultRole = getDefaultRole(user, appConfig.ID)
710
- if (defaultRole) {
711
- return next({
712
- name: to.name,
713
- params: { ...to.params, role: defaultRole },
714
- query: { ...to.query }
715
- })
716
- } else {
717
- return next({
718
- name: 'appRoleSelect',
719
- params: { ...to.params, name: to.name },
720
- query: { ...to.query },
721
- replace: true
722
- })
723
- }
724
- }
725
- }
726
- } else if (to.path === '/' || to.name === 'app1' || to.name === 'client' || to.name === 'hall' || to.name === 'system' || to.name === 'appSetting' || to.name === 'manage') {
727
- let { user } = store.state.user
728
- if (setAppDocumentTitle(appConfig)) {
729
- if (!app) app = appConfig.CODE
730
- }
731
- // 判断用户是否登录
732
- const isLogin = await loginPromise
733
- if (isLogin) {
734
- if (isEmpty(user)) {
735
- try {
736
- user = await getUserOnce()
737
- } catch (error) {
738
- ElMessage.error(error.message)
739
- util.toLogin()
740
- }
741
- }
742
- visitService(appConfig.ID)
743
- closeLoading()
744
- if (to.name === 'manage') {
745
- // 获取权限
746
- const manageRight = await http.get({
747
- action: 'hasMangeService',
748
- entityId: 'FM_USER',
749
- querySystem: appConfig.SYSTEM
750
- })
751
- if (manageRight.systemAdmin !== 'true') {
752
- return next({
753
- name: 'app1',
754
- params: { id: appId || '', client, system, app },
755
- query: { ...to.query },
756
- replace: true
757
- })
758
- }
759
- } else if (appConfig.EXTEND_OF_COMPETENCE !== 'allRole' && (!roleId && (!role || !isHasRole(user, appConfig.ID, role)))) {
760
- const defaultRole = getDefaultRole(user, appConfig.ID)
761
- if (defaultRole) {
762
- if (isMenuRoute) {
763
- // 菜单直达路由不做补 role 的 URL 重定向(会丢 /m/:menuKey):默认角色落
764
- // localStorage,取数点经 resolveRouteRole 读取,导航照常放行
765
- roleStorage.set(client, system, defaultRole)
766
- } else {
767
- return next({
768
- name: to.name,
769
- params: { id: appId || '', client, system, app, role: defaultRole },
770
- query: { ...to.query },
771
- replace: true
772
- })
773
- }
774
- } else {
775
- return next({
776
- name: to.name === 'hall' ? 'hallRoleSelect' : 'appRoleSelect',
777
- // backMenu:菜单直达进来的,选完角色回到原菜单(AppRoleSelect 消费)
778
- params: { system: system || '', client, app, appId, name: to.name === 'defaultApp' ? to.name : '' },
779
- query: { ...to.query, ...(isMenuRoute ? { backMenu: to.params.menuKey } : {}) },
780
- replace: true
781
- })
782
- }
783
- }
784
- }
785
- } else if (to.name === 'app') {
786
- const { user } = store.state.user
787
- if (setAppDocumentTitle(appConfig)) {
788
- appId = appConfig.ID
789
- }
790
- if (isEmpty(user)) {
791
- // 判断用户是否登录
792
- const isLogin = await loginPromise
793
- if (isLogin) {
794
- // 没有角色
795
- let user = null
796
- try {
797
- user = await getUserOnce()
798
- } catch (error) {
799
- ElMessage.error(error.message)
800
- util.toLogin()
801
- }
802
- visitService(appConfig.ID)
803
- closeLoading()
804
- if (!to.params.roleId && to.name !== 'appRoleSelect') {
805
- const { serviceRoles = {} } = user
806
- const roleList = serviceRoles[appId] || []
807
- let defaultRole = null
808
- if (roleList.length === 1) {
809
- defaultRole = roleList[0].id
810
- } else {
811
- const defaultRoleMap = roleList.find(item => item.isDefault === true) || {}
812
- defaultRole = defaultRoleMap.id
813
- }
814
- if (defaultRole) {
815
- return next({
816
- name: 'app',
817
- params: { id: appId || '', roleId: defaultRole },
818
- query: { ...to.query }
819
- })
820
- } else {
821
- return next({
822
- name: 'appRoleSelect',
823
- params: { id: appId || '' },
824
- query: { ...to.query }
825
- })
826
- }
827
- }
828
- }
829
- }
830
- } else if (/\/(recruit\/register)\/([\w-]+)\/([\w-]+)\/([\w-]+)/.test(window.location.hash) && !to.name) {
831
- // 同样是重定向,必须 return,否则末尾还会再调一次 next
832
- return next({ ...nextTo })
833
- } else if (to.name === 'Task' || to.name === 'feishuTask') {
834
- let { user } = store.state.user
835
- // Task 19 / C2:原来这里包着一层 `if (!judgeIsLogin)`,judgeIsLogin 恒为 undefined ⇒ 恒进入,
836
- // 已连同上面 willCheckLogin 里的同一个死判据一起删除(理由见那里)。行为不变。
837
- // 判断用户是否登录
838
- const isLogin = await loginPromise
839
- if (isLogin) {
840
- if (isEmpty(user)) {
841
- try {
842
- user = await getUserOnce()
843
- } catch (error) {
844
- ElMessage.error(error.message)
845
- util.toLogin()
846
- }
847
- }
848
- }
849
- }
850
- closeLoading()
851
- if (nextTo.query.token || nextTo.query._token) {
852
- const query = { ...nextTo.query }
853
- delete query.token
854
- delete query._token
855
- delete query.code
856
- // 剥 token 的重定向仍属同一次用户导航 ⇒ 交接由上面那层 next 包装自动保留(传的是 location
857
- // 对象)。(下面那句 next() 是既有的重复调用——vue-router 与本文件的记账都只认第一次调用,
858
- // 这里不顺手改它。)
859
- next({ ...nextTo, query })
860
- }
861
- // 走到这里而没有自我重定向的,什么都没挂过:登录校验结果**绝不跨用户导航复用**,否则就退化成
862
- // 「本会话只校验一次」,token 中途过期再也检不出来(见文件头部注释)。
863
- next()
864
- }
1
+ import { restoreChatWikiRoute } from '@epxing/utils/src/wiki-path'
2
+ import { runtimeRouteComponent, runtimeStore as store, getRuntimeService } from '../runtime-services'
3
+ import { isEmpty } from 'lodash-es'
4
+ import util from '@epxing/core/src/runtime-utils/util'
5
+ import http from '@epxing/core/src/api'
6
+ import loadLibrary from '@epxing/core/src/loadLibrary'
7
+ import { GET_USER, GET_APP_CONFIG, GET_COMPONENT_GLOBAL_CONFIG, DELETE_USER } from '@epxing/core/src/store/actions'
8
+ import { ElMessage } from 'element-plus/es/components/message/index.mjs'
9
+ import { setElementPlusLocale } from '@epxing/core/src/plugins/element-plus'
10
+ // 菜单直达路由的纯函数层(别改引 navigation.js——那边引 router 单例会成环)
11
+ import { MENU_ROUTE_ALIAS, effectiveRouteName, roleStorage } from '@epxing/core/src/menu-route'
12
+ const Page = runtimeRouteComponent('Page')
13
+ const Login = runtimeRouteComponent('Login')
14
+ const App = runtimeRouteComponent('App')
15
+ const WikiSitePage = runtimeRouteComponent('WikiSitePage')
16
+ const Module = runtimeRouteComponent('Module')
17
+ const AppRoleSelect = runtimeRouteComponent('AppRoleSelect')
18
+ const Task = runtimeRouteComponent('Task')
19
+ const Config = runtimeRouteComponent('Config')
20
+ const Error = runtimeRouteComponent('Error')
21
+ const HelpDocSet = runtimeRouteComponent('HelpDocSet')
22
+ window.isLoadLibrary = false
23
+ let isVisitedService = false
24
+ // Task 19 / C2:**同一次用户导航内**的登录校验结果交接(不是「本会话只校验一次」)。
25
+ //
26
+ // 现象:守卫自己发起的二次导航(补默认角色 `next({...role})`、跳角色选择页、剥 URL 上的
27
+ // token)会让 beforeEach 完整重跑一遍。重跑时 appConfig 有 `isEmpty` 门挡着、loadLibrary 有
28
+ // `window.isLoadLibrary` 门挡着,唯独 loginPromise 没有任何跨遍复用 ⇒ 一次 app1 冷启动发两次
29
+ // `FM_USER/validLogin`(Task 20 实测,两次相差 1.27s)。
30
+ //
31
+ // ⚠️ 刻意**不**做成 judgeIsLogin 那种「置一次、永不复检」的布尔标记(那个半成品的字面语义正是
32
+ // 如此,见下面 willCheckLogin 处的说明):登录态**会过期**,永久标记会让中途失效的 token 再也
33
+ // 检不出来,用户带着废会话继续操作,直到某个业务请求 401 才被踢——「validLogin 2→1」这个判据
34
+ // 会满足,但改动是错的。
35
+ //
36
+ // 复用作用域 = **同一条重定向链**,三个条件同时成立才继承(修复轮 1,评审 C-1):
37
+ // ① 本遍守卫以 `next(location)` 自我重定向收尾 —— 由 runBeforeEach 里那层 next 包装**显式
38
+ // 挂载**。注意这是「默认不泄漏」:不再像第一版那样无条件挂上、指望函数尾部清掉(那种形状
39
+ // 在**守卫抛错**这条出口上会把交接原样留在模块变量里,评审已造出现场:manage 分支
40
+ // `hasMangeService` 请求失败 ⇒ 守卫抛错 ⇒ 残留被下一次用户导航捡走)。外层 beforeEach
41
+ // 还包了一层 try/catch 兜底,抛错必清。
42
+ // ② 本遍是 vue-router 标记的「重定向续跑」(`to.redirectedFrom` 有值)。
43
+ // ③ **链根一致**:交接绑定在重定向链根的 fullPath 上。只有 ② 是不够的 —— 路由表 `redirect:`
44
+ // (本文件 /m/:menuKey 两条兼容链接)会让一次**纯用户导航**的 redirectedFrom 天然为真,
45
+ // 锁 ② 对它形同虚设。依据:vue-router 4.6 pushWithRedirect 对路由表 redirect 与守卫
46
+ // 重定向两条递归都传 `redirectedFrom || (targetLocation|toLocation)`(vue-router.mjs
47
+ // :1293/:1314)⇒ 整条链上 redirectedFrom 恒等于链根,用它当身份在多跳链上也精确。
48
+ // 任何一次真正由用户发起的导航(redirectedFrom 为空,或链根对不上)都会照常重新发 validLogin。
49
+ //
50
+ // 注意:单纯「复用 in-flight promise」在这里是**空操作**——二次导航发生在 `await loginPromise`
51
+ // **之后**(见下面各分支:先 await 拿到 isLogin,再 await GET_USER,最后才 next({...role})),
52
+ // 第二遍开始时第一遍那个 promise 早就 settled 了。省掉请求靠的是复用同一个**已 settled 的
53
+ // promise 对象**,不是靠请求还在飞。
54
+ let handoff = null // { promise, key: 重定向链根 fullPath, at: 挂载时刻 }
55
+ // 陈旧上界(保险丝,不是去重机制本身)。挂载点是 `next(location)` 那一刻,消费点是紧接着重跑的
56
+ // 那一遍守卫的第一行,中间只隔 vue-router 的微任务、**没有网络**,实测同一 tick 内完成;这个
57
+ // 常数只是给「redirect 目标与当前路由相同被判 NAVIGATION_DUPLICATED、守卫压根没重跑」这类
58
+ // 病态路径封一个顶,免得残留在模块变量里躺成用户步调。取值比正常间隔大三个数量级,判据不会
59
+ // 挂在它身上。
60
+ const HANDOFF_MAX_AGE_MS = 10000
61
+ // Task 13 review v2(round 3:注释前提已改,见下):改方案后(发起点挪到 GET_APP_CONFIG
62
+ // resolve 之后,见 beforeEach),本函数与 loadLibrary 不再需要显式穿 client/domainContext。
63
+ // 撑住这一点的**不是**"appConfig/clientConfig 必然一起就绪"——这个前提是假的:
64
+ // GET_APP_CONFIG action(store/modules/config.js)有三条分支 clientConfig 从未提交或提交空
65
+ // 对象(`data` 为假 / 请求 reject / 提交的 `client` 本身是 `data.client.data || {}`)。
66
+ // ⚠️ 2026-09 更新:GET_APP_CONFIG 失败的 catch 现在有 return 了(跳错误页后不再往下走,
67
+ // 见 dedupe 相关改动),下面这条不变量此后只需要对"resolve 但 clientConfig 为空/不完整"
68
+ // 这几条分支成立,不用再覆盖"请求 reject"这一种——reject 已经在此之前 return 掉,走不到这里。
69
+ // 真正承重的是更弱、因而更可靠的不变量:从这里到"改动前的原发起位置"之间,只隔着
70
+ // `await GET_COMPONENT_GLOBAL_CONFIG` 这一步,它只 commit `componentGlobalConfig`/
71
+ // `systemComponentGlobalConfig`——axios.js 的头部解析链完全不读这两个 key。所以**不管
72
+ // clientConfig 这次到底有没有就绪**,本函数在这里发起时解析出的请求头,与"改动前挪到原位置
73
+ // 发起"逐字节相同(唯一例外是 localStorage 里的 token,若窗口内先返回的响应带了
74
+ // authorization 刷新头会改写它——但这不是新增的不确定性,基线里 `getI18nMessages` 发出的
75
+ // `FM_MESSAGE` 请求本来就不 await、早已横跨同一段窗口)。已回退到 Task 13 之前的原始实现,
76
+ // 本函数与 loadLibrary.js 均不再有 Task 13 相关改动。
77
+ /**
78
+ * 引导期接口去重(develop 6ce3678ca 系列移植)
79
+ *
80
+ * 同一次页面加载里路由守卫会被重复触发:产品库加载完成后,远程库自己的 install() 里会
81
+ * `router.addRoute(...)` + `router.replace(当前路径)`(见 loadLibrary.js 头部注释、
82
+ * lazy-library.js 的 "util.loadJs 的 onload 重跑一遍 useVuePlugin → app.use → install()")。
83
+ * 这是**全新的一次导航**,不是守卫内部 `next(location)` 产生的重定向 ——
84
+ * ⚠️ 之前这里的注释说 validLogin 的去重"已经由 handoff 单独承担",是错的:handoff 的
85
+ * `inheritedLoginPromise` 第一个条件就是 `to.redirectedFrom` 有值(见文件头部 ②、
86
+ * runBeforeEach 里 `chainRootKey`/`inheritedLoginPromise` 那几行),只覆盖守卫**自己发起**
87
+ * 的重定向(补默认角色、剥 token、跳角色选择页);`router.replace(当前路径)` 是外部代码发起
88
+ * 的全新导航,`redirectedFrom` 恒为空,`inheritedLoginPromise` 摸不到这条路径,第二遍守卫
89
+ * 照样会再发一次 `FM_USER/validLogin`。所以 isLoginFn 仍要包一层这里的 dedupe——
90
+ * 两者不冲突:`inheritedLoginPromise` 命中时走 `||` 短路,压根不会调到 isLoginFn(见下方
91
+ * `loginPromise = willCheckLogin ? (inheritedLoginPromise || isLoginFn()) : null`)。
92
+ * GET_USER / GET_COMPONENT_GLOBAL_CONFIG 同样没有 handoff 这类机制,`isEmpty(user)` /
93
+ * `!componentGlobalConfig` 这类先查后写的判据在守卫重跑时仍可能撞见"上一遍还没提交"的窗口
94
+ * 而重复发起,也包一层。
95
+ * GET_APP_CONFIG 目前的发起顺序严格排在 loadLibrary 之前(Task 13 review),触发重跑的
96
+ * addRoute+replace 发生时它必然已经 resolve,理论上摸不到这个窗口——仍然包一层纯粹是让四个
97
+ * 引导请求统一走同一套去重外壳,不改变现有时序。
98
+ *
99
+ * ttl 为 0 时只复用进行中的请求,大于 0 时结果也复用 ttl 毫秒(引导期够用,之后仍会重新校验)。
100
+ */
101
+ function dedupe (fn, { key = () => '', ttl = 0 } = {}) {
102
+ const cache = new Map()
103
+ const wrapped = function (...args) {
104
+ const cacheKey = key(...args)
105
+ const hit = cache.get(cacheKey)
106
+ if (hit && (hit.pending || (ttl > 0 && Date.now() - hit.doneAt < ttl))) return hit.promise
107
+ const record = { pending: true, doneAt: 0 }
108
+ record.promise = Promise.resolve()
109
+ .then(() => fn(...args))
110
+ .then(data => {
111
+ record.pending = false
112
+ record.doneAt = Date.now()
113
+ return data
114
+ })
115
+ .catch(err => {
116
+ cache.delete(cacheKey)
117
+ return Promise.reject(err)
118
+ })
119
+ cache.set(cacheKey, record)
120
+ return record.promise
121
+ }
122
+ wrapped.clear = () => cache.clear()
123
+ return wrapped
124
+ }
125
+ // 引导期内复用登录态校验结果,避免守卫重跑时重复请求(BOOT_TTL 之后仍会重新校验,
126
+ // 不是"本会话只查一次"的永久标记——登录态会过期,见文件头部对 judgeIsLogin 反模式的警告)
127
+ const BOOT_TTL = 3000
128
+ const isLoginFn = dedupe(() => {
129
+ return http.get({
130
+ action: 'validLogin',
131
+ entityId: 'FM_USER'
132
+ })
133
+ .then(data => {
134
+ return data
135
+ })
136
+ .catch(() => false)
137
+ }, { ttl: BOOT_TTL })
138
+ const getUserOnce = dedupe(() => store.dispatch({ type: GET_USER }))
139
+ const getAppConfigOnce = dedupe(payload => store.dispatch(payload), {
140
+ key: payload => [payload.client, payload.system, payload.app, payload.id].join('|')
141
+ })
142
+ const getComponentGlobalConfigOnce = dedupe(payload => store.dispatch(payload), {
143
+ key: payload => [payload.client, payload.system].join('|')
144
+ })
145
+ // 公共 API 可以在应用创建前导入;首次运行守卫时才订阅实际注入的 store。
146
+ const subscribedStores = new WeakSet()
147
+ function subscribeToLogout () {
148
+ const currentStore = getRuntimeService('store')
149
+ if (subscribedStores.has(currentStore)) return
150
+ currentStore.subscribe(mutation => {
151
+ if (mutation.type === DELETE_USER) {
152
+ isLoginFn.clear()
153
+ getUserOnce.clear()
154
+ }
155
+ })
156
+ subscribedStores.add(currentStore)
157
+ }
158
+
159
+ function getDefaultRole (user, appId) {
160
+ const { serviceRoles = {} } = user
161
+ const roleList = serviceRoles[appId] || []
162
+ let defaultRole = null
163
+ if (roleList.length === 1) {
164
+ defaultRole = roleList[0].code + (roleList[0].type === 'client' ? '-client' : '')
165
+ } else {
166
+ const defaultRoleMap = roleList.find(item => item.isDefault === true)
167
+ if (defaultRoleMap) defaultRole = defaultRoleMap.code + (defaultRoleMap.type === 'client' ? '-client' : '')
168
+ }
169
+ return defaultRole
170
+ }
171
+
172
+ function isHasRole (user, appId, role) {
173
+ const { serviceRoles = {} } = user
174
+ const roleList = serviceRoles[appId] || []
175
+ let roleCode = role
176
+ if (role.endsWith('-client')) {
177
+ roleCode = role.replace(/-client$/, '')
178
+ // 从通用角色获取
179
+ return roleList.find(item => item.code === roleCode && item.type === 'client')
180
+ } else {
181
+ return roleList.find(item => item.code === roleCode && item.type !== 'client')
182
+ }
183
+ }
184
+
185
+ function getI18nMessages (system) {
186
+ // 国际化设置
187
+ const { clientConfig } = store.state.config || {}
188
+ const { CODE: clientCode, LANGUAGE_DEFAULT: defaultLanguage } = clientConfig
189
+ const currentLanguage = localStorage.getItem(`${clientCode}-language`) || defaultLanguage
190
+ // T33:element-ui 的 `locale.use()` → EP 的 reactive config(见 plugins/element-plus.js)
191
+ setElementPlusLocale(currentLanguage)
192
+ http.list({
193
+ entityId: 'FM_MESSAGE',
194
+ scopeParams: JSON.stringify([
195
+ {
196
+ field: 'TYPE',
197
+ values: 'page'
198
+ },
199
+ {
200
+ lb: '(',
201
+ field: 'SCOPE',
202
+ values: 'frame',
203
+ link: 'or'
204
+ },
205
+ {
206
+ field: 'SYSTEM',
207
+ values: system,
208
+ rb: ')'
209
+ }
210
+ ])
211
+ })
212
+ .then(data => {
213
+ const messages = {}
214
+ data.entitys.forEach(item => {
215
+ let name = item.NAME
216
+ let content = item.CONTENT
217
+ if (data.encrypt === '1') {
218
+ name = util.utf8to16(window.atob(name))
219
+ content = util.utf8to16(window.atob(content))
220
+ }
221
+ messages[name.trim()] = content
222
+ })
223
+ globalThis.i18n.setMessage(messages)
224
+ })
225
+ }
226
+ function visitService (serviceId) {
227
+ if (isVisitedService) return
228
+ isVisitedService = true
229
+ http.get({
230
+ action: 'visitService',
231
+ entityId: 'FM_USER_SERVICE_VISIT_RECORD',
232
+ serviceId
233
+ })
234
+ }
235
+
236
+ function closeLoading () {
237
+ const loadingElem = document.querySelector('#loading')
238
+ if (loadingElem) loadingElem.style.display = 'none'
239
+ }
240
+
241
+ // appConfig.NAME 可能只有多语言字段的翻译值 appConfig['NAME#'](`#` 后缀是本平台代码表/字段
242
+ // 显示值的通用约定),只认 NAME 会在这种情况下把标题落空
243
+ function setAppDocumentTitle (appConfig = {}) {
244
+ const title = appConfig.NAME || appConfig['NAME#']
245
+ if (title) document.title = title
246
+ return title
247
+ }
248
+
249
+ export default [
250
+ {
251
+ path: '/login/:client/:system/:app?',
252
+ name: 'login',
253
+ component: Login,
254
+ meta: { nav: false },
255
+ props: (route) => ({ query: route.query })
256
+ },
257
+ {
258
+ path: '/hall/login/:client',
259
+ name: 'hallLogin',
260
+ component: Login,
261
+ meta: { nav: false },
262
+ props: (route) => ({ query: route.query })
263
+ },
264
+ {
265
+ path: '/app/role/select/:client/:system?/:app?',
266
+ name: 'appRoleSelect',
267
+ component: AppRoleSelect
268
+ },
269
+ {
270
+ path: '/hall/role/select/:client/:app?',
271
+ name: 'hallRoleSelect',
272
+ component: AppRoleSelect
273
+ },
274
+ {
275
+ path: '/',
276
+ name: 'defaultApp',
277
+ component: App,
278
+ props: route => ({ query: route.query })
279
+ },
280
+ {
281
+ path: '/app/:client/:system/:app?/:role?',
282
+ name: 'app1',
283
+ component: App,
284
+ meta: { refresh: false }, // 菜单直达在本路由族内切换 URL,根 router-view 不因 path 变化重挂(防导航闪烁/卡顿)
285
+ props: route => ({ query: route.query })
286
+ },
287
+ {
288
+ // 菜单直达(menuKey=菜单编号 MODULE_CODE,没配编号用 ID)。不带 role 段——role 走
289
+ // localStorage(menu-route.js roleStorage)。与 app1 无歧义:本路由比 app1 多吃两段。
290
+ path: '/app/:client/:system/:app/:menuKey([0-9A-Fa-f]{32})',
291
+ name: 'appMenu',
292
+ component: App,
293
+ meta: { refresh: false },
294
+ props: route => ({ query: route.query })
295
+ },
296
+ {
297
+ path: '/app/:id?/:roleId?',
298
+ name: 'app',
299
+ component: App,
300
+ props: (route) => ({ query: route.query })
301
+ },
302
+ {
303
+ // Wiki 站点:#/wiki/{客户}/{系统}/{空间编码}/{页路径}
304
+ //
305
+ // 页路径用通配段而不是单段参数——Wiki 的路径本来就带层级
306
+ // (implementation/05-sop),单段参数会把斜杠当分隔符,深层页永远打不开。
307
+ // 客户与系统段与其它业务路由保持同一形状:它们是权限边界的一部分,
308
+ // 少一段就没法在进页面前判断这个人能不能看这个空间。
309
+ path: '/wiki/:client/:system/:wiki/:path*',
310
+ name: 'wikiSite',
311
+ component: WikiSitePage,
312
+ props: route => ({ query: route.query })
313
+ },
314
+ {
315
+ // 引用直达:#/wiki/view?w=空间&p=页&rev=版本&h=锚点
316
+ // 后端铸造的引用地址用这一条。它不带客户/系统段,因为引用是从答案里点进来的,
317
+ // 上下文已经在登录态里;权限仍由接口按空间的 VIEW_ROLES 判定。
318
+ path: '/wiki/view',
319
+ name: 'wikiView',
320
+ component: WikiSitePage,
321
+ props: route => ({ query: route.query })
322
+ },
323
+ {
324
+ path: '/service/setting/:client/:system/:app/:role?',
325
+ name: 'appSetting',
326
+ component: App,
327
+ props: route => ({ query: route.query })
328
+ },
329
+ {
330
+ path: '/manage/:client/:system/:app/:role?',
331
+ name: 'manage',
332
+ component: App,
333
+ props: route => ({ query: route.query })
334
+ },
335
+ {
336
+ // 32 位菜单 ID 与旧角色编号消歧;旧的 /system/.../SYSTEM 地址仍可使用。
337
+ path: '/system/:client/:system/:menuKey([0-9A-Fa-f]{32})?/:role?',
338
+ name: 'system',
339
+ component: App,
340
+ meta: { refresh: false },
341
+ props: route => ({ query: route.query })
342
+ },
343
+ {
344
+ path: '/client/:client/:role?',
345
+ name: 'client',
346
+ component: App,
347
+ props: route => ({ query: route.query })
348
+ },
349
+ {
350
+ path: '/hall/:client/:role?',
351
+ name: 'hall',
352
+ component: App,
353
+ meta: { refresh: false },
354
+ props: route => ({ query: route.query })
355
+ },
356
+ {
357
+ // 服务大厅的菜单直达(同 appMenu 语义)
358
+ path: '/hall/:client/:menuKey([0-9A-Fa-f]{32})',
359
+ name: 'hallMenu',
360
+ component: App,
361
+ meta: { refresh: false },
362
+ props: route => ({ query: route.query })
363
+ },
364
+ {
365
+ path: '/task/:client/:system/:taskId',
366
+ name: 'Task',
367
+ component: Task,
368
+ props: (route) => ({ query: route.query })
369
+ },
370
+ {
371
+ path: '/config/:mode/:client/:system/:configId',
372
+ name: 'Config',
373
+ component: Config,
374
+ props: (route) => ({ query: route.query })
375
+ },
376
+ {
377
+ path: '/feishuTask/:client/:system/:taskId',
378
+ name: 'feishuTask',
379
+ component: Task,
380
+ props: (route) => ({ query: route.query })
381
+ },
382
+ {
383
+ path: '/help/:client/:system/:docset/:docId?',
384
+ name: 'help',
385
+ component: HelpDocSet,
386
+ meta: {
387
+ title: '帮助中心',
388
+ loadAppConfig: false,
389
+ refresh: false
390
+ }
391
+ },
392
+ {
393
+ path: '/module',
394
+ name: 'module',
395
+ component: Module,
396
+ props: (route) => ({ query: route.query })
397
+ },
398
+ {
399
+ path: '/page/:client/:system/:app/:role?',
400
+ name: 'Page',
401
+ component: Page,
402
+ props: (route) => ({ query: route.query })
403
+ },
404
+ {
405
+ path: '/page',
406
+ name: 'Page1',
407
+ component: Page,
408
+ props: (route) => ({ query: route.query })
409
+ },
410
+ {
411
+ path: '/s/:link',
412
+ name: 'shorturl',
413
+ component: App,
414
+ props: (route) => ({ query: route.query })
415
+ },
416
+ {
417
+ // 旧形态 /m/:menuKey 链接兼容:重定向到无 /m/ 的新形态
418
+ path: '/app/:client/:system/:app/m/:menuKey',
419
+ redirect: to => ({ name: 'appMenu', params: to.params, query: to.query })
420
+ },
421
+ {
422
+ path: '/hall/:client/m/:menuKey',
423
+ redirect: to => ({ name: 'hallMenu', params: to.params, query: to.query })
424
+ },
425
+ {
426
+ path: '/error',
427
+ name: 'error',
428
+ component: Error
429
+ },
430
+ {
431
+ path: '/:id?',
432
+ name: 'app2',
433
+ component: App,
434
+ props: (route) => ({ query: route.query })
435
+ }
436
+ ]
437
+
438
+ /**
439
+ * 路由守卫(对外入口)。守卫体是下面的 runBeforeEach,这层薄壳只做一件事:**任何抛错都保证
440
+ * 登录校验交接被清掉**。守卫体里 await 的请求(GET_APP_CONFIG、hasMangeService…)都可能
441
+ * reject,而守卫一抛错导航就中止、此后不会再有任何守卫运行,残留会一直躺到用户下次导航
442
+ * (评审 C-1 现场)。这是第二道保险;第一道是 runBeforeEach 里「只在自我重定向出口显式挂载」。
443
+ * ⚠️ 形参必须保持 3 个:vue-router 用 `guard.length < 3` 判断是不是回调式守卫。
444
+ */
445
+ export const beforeEach = async (to, from, next) => {
446
+ const restoredWikiRoute = restoreChatWikiRoute(to)
447
+ if (restoredWikiRoute) return next(restoredWikiRoute)
448
+ try {
449
+ subscribeToLogout()
450
+ return await runBeforeEach(to, from, next)
451
+ } catch (err) {
452
+ handoff = null
453
+ throw err
454
+ }
455
+ }
456
+
457
+ const runBeforeEach = async (to, from, rawNext) => {
458
+ // 重定向链根的 fullPath —— 交接的身份(三个条件见文件头部)。必须在下面 isMenuRoute 改写
459
+ // to 之前取(那里的 `to = {...to, …}` 是浅拷贝,其实也带得走,但别依赖它)。
460
+ const chainRootKey = to.redirectedFrom?.fullPath || to.fullPath
461
+ // 交接**一次性消费**:读完立刻无条件清空。这条无条件清空同时保证了「交接最多只能被紧接着的
462
+ // 那一遍守卫捡走」——中间只要再跑过任何一遍守卫(任何导航),残留就没了。
463
+ const inheritedLoginPromise = (to.redirectedFrom && handoff && handoff.key === chainRootKey &&
464
+ Date.now() - handoff.at <= HANDOFF_MAX_AGE_MS) ? handoff.promise : null
465
+ handoff = null
466
+ // 本遍自己的登录校验结果,只有本遍以 next(location) 自我重定向收尾时才交接出去。
467
+ // ⚠️ 必须是独立变量、且在函数最开头初始化,不能让下面那层 next 包装直接去读 `let loginPromise`:
468
+ // shorturl 长链接分支的 `return`(见下)排在 `let libPromise, loginPromise` **之前**,它在
469
+ // .then 里异步调 next 时那行 let 根本没执行过,读 loginPromise 会撞 TDZ ReferenceError。
470
+ let ownLoginPromise = null
471
+ let nextCalled = false
472
+ // 把「挂载交接」从**无条件挂上、指望函数尾部清掉**倒过来成**只在自我重定向这个出口显式挂上**。
473
+ // 包在 next 上是为了让守卫体里那十几处 next(...) 调用点一行都不用改。
474
+ const next = (location) => {
475
+ // 只认第一次调用(与 vue-router 一致)。本文件末尾就有一处既有的 next 双调用,记账必须跟着
476
+ // 第一次走,否则剥 token 那条重定向刚挂上的交接会被紧随其后的 next() 清掉。调用本身照常
477
+ // 原样转发,不改动 router 行为。
478
+ if (!nextCalled) {
479
+ nextCalled = true
480
+ // 自我重定向 = 传了一个 location 对象。next()/next(false)/next(Error) 都不是;
481
+ // next('/error?…') 那种字符串也不算——那条路径上 ownLoginPromise 还是 null,且「不挂交接」
482
+ // 最多多发一次 validLogin,是安全方向。
483
+ // ⚠️ 必须写 globalThis.Error:本文件顶上 `import Error from '@epxing/business/src/views/Error'`
484
+ // 把全局 Error 构造器**在整个模块作用域里遮蔽**成了一个 Vue 组件对象,直接写
485
+ // `x instanceof Error` 会抛 `TypeError: Right-hand side of 'instanceof' is not callable`
486
+ // ——这条已在本轮实测中真的炸过一次(守卫抛错 ⇒ 补默认角色的重定向整个不发生)。
487
+ handoff = (ownLoginPromise && location && typeof location === 'object' && !(location instanceof globalThis.Error))
488
+ ? { promise: ownLoginPromise, key: chainRootKey, at: Date.now() }
489
+ : null
490
+ }
491
+ return rawNext(location)
492
+ }
493
+ // 应用与大厅的菜单直达归一到旧路由名,配置中心保留 system;角色从持久化状态恢复。
494
+ // 只修改守卫内的局部视图,浏览器地址仍保留菜单路径段。
495
+ const isMenuRoute = !!MENU_ROUTE_ALIAS[to.name] || (to.name === 'system' && !!to.params.menuKey)
496
+ if (isMenuRoute) {
497
+ const storedRole = roleStorage.get(to.params.client, to.params.system)
498
+ to = { ...to, name: effectiveRouteName(to.name), params: { ...to.params, role: to.params.role || storedRole || undefined } }
499
+ }
500
+ if (MENU_ROUTE_ALIAS[from.name]) {
501
+ from = { ...from, name: effectiveRouteName(from.name) }
502
+ }
503
+ // 访问其他应用刷新页面
504
+ if ((to.name === 'app' || to.name === 'appRoleSelect') && (from.name === 'app' || from.name === 'appRoleSelect')) {
505
+ if (to.params.id !== from.params.id || to.params.app !== from.params.app) {
506
+ setTimeout(() => {
507
+ window.location.reload()
508
+ }, 200)
509
+ }
510
+ }
511
+ if ((to.name === 'app1' || to.name === 'hall') && (from.name === 'app1' || from.name === 'hall')) {
512
+ if (to.params.app !== from.params.app) {
513
+ setTimeout(() => {
514
+ window.location.reload()
515
+ }, 200)
516
+ }
517
+ }
518
+ let { id, appId, client, system, app, roleId, role } = to.params
519
+ // 带 role 的旧式 URL 进来顺手持久化(供菜单直达路由/无 role URL 复用同一角色)
520
+ if (role && client) roleStorage.set(client, system, role)
521
+ if (to.path === '/') {
522
+ client = window.BASE_CLIENT_ID
523
+ system = window.BASE_SYSTEM_ID
524
+ app = window.BASE_APP_ID
525
+ }
526
+ if (to.name === 'Config') {
527
+ app = 'FM_SERVICE_SYSTEM'
528
+ }
529
+ // Wiki 引用直达(#/wiki/view?w=…)刻意不带客户/系统段:它是从答案或空间列表
530
+ // 点进来的,上下文已在登录态里。但刷新时 to.params 是空的,getAppConfig 会拿着
531
+ // undefined 去查,最后跳 /error——现象是「点开能看,刷一下就报错,像把租户和系统弄丢了」。
532
+ // 退回平台默认上下文,与 to.path === '/' 同一套;空间能不能看仍由接口按 VIEW_ROLES 判。
533
+ if (to.name === 'wikiView') {
534
+ client = client || window.BASE_CLIENT_ID
535
+ system = system || window.BASE_SYSTEM_ID
536
+ app = app || window.BASE_APP_ID
537
+ }
538
+ let belongToClient = client
539
+ let belongToSystem = system
540
+ if (/\/s\/([\w]+)/.test(to.path)) {
541
+ if (!to.params.pageId && !to.params.flag && !to.params.app) {
542
+ http.get({
543
+ action: 'getLongUrl',
544
+ entityId: 'FM_URL_MAP',
545
+ url: to.params.link
546
+ }, {
547
+ domainContext: window.BASE_PROJECT_NAME,
548
+ client: client,
549
+ system: window.BASE_SYSTEM_ID
550
+ })
551
+ .then(data => {
552
+ const fullPath = window.ROUTER_MODE === 'history' ? data : data.split('#')[1]
553
+ const path = fullPath.split('?')[0]
554
+ const search = fullPath.split('?')[1]
555
+ const params = {
556
+ ...to.params
557
+ }
558
+ if (search) {
559
+ const searchArr = search.split('&')
560
+ searchArr.forEach(item => {
561
+ const arr = item.split('=')
562
+ params[arr[0]] = arr[1]
563
+ })
564
+ }
565
+
566
+ /\/(app|recruit\/register)\/([\w-]+)\/([\w-]+)\/([\w-]+)/.test(path)
567
+ params.client = RegExp.$2
568
+ params.system = RegExp.$3
569
+ params.app = RegExp.$4
570
+ next({ ...to, params })
571
+ })
572
+ return
573
+ }
574
+ } else if (to.name === 'client' || to.name === 'system' || to.name === 'hall' || to.name === 'hallRoleSelect' || to.name === 'hallLogin' || to.name === 'appSetting') {
575
+ belongToSystem = 'epxing-frame'
576
+ app = to.name === 'appSetting' ? 'FM_SERVICE_SETTING' : to.name === 'client' ? 'FM_SERVICE_CLIENT' : to.name === 'system' ? 'FM_SERVICE_SYSTEM' : 'FM_SERVICE_HALL'
577
+ } else if (to.name === 'wikiSite' || to.name === 'wikiView') {
578
+ // Wiki 站点是独立页面,不挂在任何业务应用下,沿用服务大厅的上下文加载配置。
579
+ //
580
+ // 不给这条分支的话 client / system / app 全是空,getAppConfig 拿不到配置,
581
+ // 直接输地址或刷新就打不开——而在 SPA 内 router.push 过去却是好的。
582
+ // 这个差别很要命:站点地址是设计成可分享的,别人拿到链接一打开就是白屏,
583
+ // 自己在页面里点却一切正常,最容易被当成「偶发」放过去。
584
+ //
585
+ // wikiView(引用直达)地址上没有客户段,回落到部署的默认客户端;
586
+ // wikiSite 的地址里带,优先用它自己的。
587
+ belongToClient = client || window.BASE_CLIENT_ID
588
+ belongToSystem = 'epxing-frame'
589
+ app = 'FM_SERVICE_HALL'
590
+ } else if (app === 'FM_SERVICE_SYSTEM') {
591
+ belongToSystem = 'epxing-frame'
592
+ } else if (/\/(recruit\/register)\/([\w-]+)\/([\w-]+)\/([\w-]+)/.test(window.location.hash)) {
593
+ belongToClient = RegExp.$2
594
+ belongToSystem = RegExp.$3
595
+ app = RegExp.$4
596
+ }
597
+ // 在任何路由请求或参数清理前接收一次 URL Token;localStorage 受限时会回落到当前标签页内存。
598
+ if (belongToClient) util.consumeUrlToken(belongToClient)
599
+ if (to.meta?.title) document.title = to.meta.title
600
+ let { appConfig, componentGlobalConfig } = store.state.config || {}
601
+ // libPromise/loginPromise 声明在 if 外层(函数作用域):isLoginFn 的 await 消费点散落在
602
+ // 本函数后面第二个独立的 if-else 链(shorturl/app1/app/Task 等分支,与本 if 是兄弟块),
603
+ // 块级作用域的 const 够不到那里。未进下面这个 if 时两者保持 undefined 且不会被 await 到
604
+ // ——现有分支结构保证了这点('error' 路由/loadAppConfig:false 路由不落进第二条链的任何分支)。
605
+ let libPromise, loginPromise
606
+ if (to.name !== 'error' && to.meta?.loadAppConfig !== false) {
607
+ if (isEmpty(appConfig)) {
608
+ try {
609
+ appConfig = await getAppConfigOnce({ type: GET_APP_CONFIG, client: belongToClient, system: belongToSystem, app, id: id || appId, routeClient: client }) || {}
610
+ } catch (err) {
611
+ console.error(err)
612
+ // 已经跳错误页,后面的引导逻辑不用再跑(继续跑会拿着空 appConfig 发一堆没有意义的请求,
613
+ // 还会二次调用 next)
614
+ return next('/error?code=' + (err.code || '') + '&url=' + window.location.hash)
615
+ }
616
+ getI18nMessages(appConfig['SYSTEM|ID'] || appConfig.SYSTEM)
617
+ }
618
+ // Task 13 / C1 v2(评审换方案后,设计文档 §7.2;round 3:下面这段的前提改过,见理由):
619
+ // 只把 loadLibrary 与 isLoginFn 的"发起"提前到这里——GET_APP_CONFIG 已经 resolve。
620
+ // 撑住"不需要显式穿 client/domainContext"这一点的**不是**"appConfig/clientConfig 必然
621
+ // 一起就绪":GET_APP_CONFIG action 有三条分支 clientConfig 从未提交或提交空对象
622
+ // (`data` 为假 / 请求 reject / 提交的 `client` 本身是空对象,见 store/modules/config.js)。
623
+ // ⚠️ 2026-09 更新:上面的 catch 现在有 return 了,"请求 reject"这一种已经在此之前跳错误页
624
+ // 退出,走不到这里;下面这条不变量只需要对"resolve 但 clientConfig 为空/不完整"的其余两条
625
+ // 分支成立。真正承重的是更弱、因而更可靠的不变量:从这里到"改动前的原发起位置"之间,只隔着
626
+ // 这一步,它只 commit `componentGlobalConfig`/`systemComponentGlobalConfig`——axios.js
627
+ // 的头部解析链完全不读这两个 key。所以不管 clientConfig 这次到底有没有就绪,这里发起的
628
+ // 请求头都和"改动前挪到原位置发起"逐字节相同(唯一例外见下方 isLoginFn 定义处的 token
629
+ // 刷新头注释)。不需要碰 axios.js/createHttp.js/loadLibrary.js——evaluation round 2
630
+ // 发现在"appConfig 就绪前"提前发起会导致隐式链落空丢 Authorization 头(C-1,且波及比最初
631
+ // 判断更多的路由族),而给 createHttp.js 的 options.client 打通到 axios.js 又会撞上全仓
632
+ // 15 处既有调用点的既有语义(N-1,新 Critical);挪到这个位置后两个问题都不存在,故两轮
633
+ // 相关改动已从 axios.js/createHttp.js/loadLibrary.js/isLoginFn 上完全回退。与紧接着的
634
+ // GET_COMPONENT_GLOBAL_CONFIG 请求重叠等待网络时间;下面 await 的位置一行不动(仍是原来
635
+ // loadLibrary 那一行、以及后面各分支里原来 isLoginFn 那几行)。
636
+ // ⚠️ 但"副作用发生顺序完全不变"不成立——validLogin 响应侧的副作用(未登录/过期路径触发的
637
+ // delayToLogin→util.toLogin→router.push)现在可能在本次导航自身的 next() 之前、从一个
638
+ // 仍处于 pending 的导航内部发出,不再像改动前那样严格排在原分支代码之后。终态大概率相同,
639
+ // 是任何并行化都躲不掉的固有代价,不是"顺序不变"——详见 task-13-report.md。
640
+ const libParam = { busiClientCode: client || belongToClient, busiSystemCode: system || belongToSystem }
641
+ libPromise = window.isLoadLibrary ? null : loadLibrary(libParam)
642
+ // review I-1(评审确认收窄不削减并行化收益,原样保留):只在原本就会消费 loginPromise
643
+ // 的路由族上才发起,镜像下面第二条 if-else 链的分支条件(含内层 isEmpty(user)/
644
+ // !judgeIsLogin 判据),否则 login/hallLogin/appRoleSelect/Config/Page/qywechat 等公开
645
+ // 路由会凭空多发一次 validLogin:匿名访问必然拿到 nologin 类响应,被 axios.js 的 401
646
+ // 拦截器当场弹 toast 并踢去登录页——对着登录页本身踢登录页,是本条改动最容易被忽略的
647
+ // 副作用。
648
+ // Task 19 / C2:原来这里还有一个 `&& !judgeIsLogin` 的门(下面 Task/feishuTask 分支里也有
649
+ // 一个 `if (!judgeIsLogin)`),已一并删除。理由:`store.state.config.judgeIsLogin` 在全仓
650
+ // **只被读、从未被写**,config 模块的 state 里也没有声明它,且该模块所有 mutation 只往具名
651
+ // 子对象(systemConfig/appConfig/clientConfig/...)里铺数据,既没有 `Object.assign(state, …)`
652
+ // 也没有 `state[动态键] =`,后端也没法把它下发注入到 state.config 根上 ⇒ 它恒为 undefined,
653
+ // `!judgeIsLogin` 恒为 true,两处判据都是死重量,删掉**不改变任何现有行为**。
654
+ // 之所以是删掉而不是「补上写入语义」:它注释里的意图(「是否校验过用户已登录」)就是一个
655
+ // 会话级永久标记,而登录态会过期——真按那个字面语义补写入,等于亲手做出上面文件头部警告的
656
+ // 那个错误修法。去重该做在「同一条重定向链内」这个粒度上,已由 handoff 承担(见文件头部)。
657
+ const { user } = store.state.user
658
+ const willCheckLogin = to.name === 'shorturl' ||
659
+ to.path === '/' || to.name === 'app1' || to.name === 'client' || to.name === 'hall' ||
660
+ to.name === 'system' || to.name === 'appSetting' || to.name === 'manage' ||
661
+ (to.name === 'app' && isEmpty(user)) ||
662
+ to.name === 'Task' || to.name === 'feishuTask'
663
+ // 本遍是「自我重定向的续跑」就直接复用上一遍那个已 settled 的 promise,不再发请求;
664
+ // 否则照常发。inheritedLoginPromise 的产生/失效条件见文件头部注释。
665
+ loginPromise = willCheckLogin ? (inheritedLoginPromise || isLoginFn()) : null
666
+ // 只登记「本遍可交接的结果是什么」。真正的挂载在上面那层 next 包装里,且只发生在自我重定向
667
+ // 这个出口 —— 走别的出口(正常 next()、next(false)、守卫抛错)都不会留下任何东西。
668
+ ownLoginPromise = loginPromise
669
+ // 立即挂 catch 占位,否则在被 await 之前 reject 会产生 unhandled rejection。
670
+ // catch 返回的是新 promise,不影响原 promise 的 reject 传播,真正的错误处理仍在原 await 处。
671
+ if (libPromise) libPromise.catch(() => {})
672
+ if (loginPromise) loginPromise.catch(() => {})
673
+
674
+ if (!componentGlobalConfig) {
675
+ await getComponentGlobalConfigOnce({ type: GET_COMPONENT_GLOBAL_CONFIG, client: appConfig.CLIENT, system: appConfig['SYSTEM|ID'] || appConfig.SYSTEM })
676
+ }
677
+ setAppDocumentTitle(appConfig)
678
+ if (!window.componentLibrary) window.componentLibrary = {}
679
+ if (!window.isLoadLibrary) {
680
+ await libPromise
681
+ window.isLoadLibrary = true
682
+ console.log('----加载完成----')
683
+ }
684
+ }
685
+ const nextTo = { ...to }
686
+ if (to.name === 'shorturl') {
687
+ let { user } = store.state.user
688
+ if (setAppDocumentTitle(appConfig)) {
689
+ if (!app) app = appConfig.CODE
690
+ }
691
+ // 判断用户是否登录
692
+ const isLogin = await loginPromise
693
+ if (isLogin) {
694
+ if (isEmpty(user)) {
695
+ try {
696
+ user = await getUserOnce()
697
+ } catch (error) {
698
+ ElMessage.error(error.message)
699
+ util.toLogin()
700
+ }
701
+ }
702
+ visitService(appConfig.ID)
703
+ closeLoading()
704
+ // 判断是否需要角色
705
+ if (appConfig.EXTEND_OF_COMPETENCE !== 'allRole' && (!roleId && (!role || !isHasRole(user, appConfig.ID, role)))) {
706
+ const defaultRole = getDefaultRole(user, appConfig.ID)
707
+ if (defaultRole) {
708
+ return next({
709
+ name: to.name,
710
+ params: { ...to.params, role: defaultRole },
711
+ query: { ...to.query }
712
+ })
713
+ } else {
714
+ return next({
715
+ name: 'appRoleSelect',
716
+ params: { ...to.params, name: to.name },
717
+ query: { ...to.query },
718
+ replace: true
719
+ })
720
+ }
721
+ }
722
+ }
723
+ } else if (to.path === '/' || to.name === 'app1' || to.name === 'client' || to.name === 'hall' || to.name === 'system' || to.name === 'appSetting' || to.name === 'manage') {
724
+ let { user } = store.state.user
725
+ if (setAppDocumentTitle(appConfig)) {
726
+ if (!app) app = appConfig.CODE
727
+ }
728
+ // 判断用户是否登录
729
+ const isLogin = await loginPromise
730
+ if (isLogin) {
731
+ if (isEmpty(user)) {
732
+ try {
733
+ user = await getUserOnce()
734
+ } catch (error) {
735
+ ElMessage.error(error.message)
736
+ util.toLogin()
737
+ }
738
+ }
739
+ visitService(appConfig.ID)
740
+ closeLoading()
741
+ if (to.name === 'manage') {
742
+ // 获取权限
743
+ const manageRight = await http.get({
744
+ action: 'hasMangeService',
745
+ entityId: 'FM_USER',
746
+ querySystem: appConfig.SYSTEM
747
+ })
748
+ if (manageRight.systemAdmin !== 'true') {
749
+ return next({
750
+ name: 'app1',
751
+ params: { id: appId || '', client, system, app },
752
+ query: { ...to.query },
753
+ replace: true
754
+ })
755
+ }
756
+ } else if (appConfig.EXTEND_OF_COMPETENCE !== 'allRole' && (!roleId && (!role || !isHasRole(user, appConfig.ID, role)))) {
757
+ const defaultRole = getDefaultRole(user, appConfig.ID)
758
+ if (defaultRole) {
759
+ if (isMenuRoute) {
760
+ // 菜单直达路由不做补 role 的 URL 重定向(会丢 /m/:menuKey):默认角色落
761
+ // localStorage,取数点经 resolveRouteRole 读取,导航照常放行
762
+ roleStorage.set(client, system, defaultRole)
763
+ } else {
764
+ return next({
765
+ name: to.name,
766
+ params: { id: appId || '', client, system, app, role: defaultRole },
767
+ query: { ...to.query },
768
+ replace: true
769
+ })
770
+ }
771
+ } else {
772
+ return next({
773
+ name: to.name === 'hall' ? 'hallRoleSelect' : 'appRoleSelect',
774
+ // backMenu:菜单直达进来的,选完角色回到原菜单(AppRoleSelect 消费)
775
+ params: { system: system || '', client, app, appId, name: to.name === 'defaultApp' ? to.name : '' },
776
+ query: { ...to.query, ...(isMenuRoute ? { backMenu: to.params.menuKey } : {}) },
777
+ replace: true
778
+ })
779
+ }
780
+ }
781
+ }
782
+ } else if (to.name === 'app') {
783
+ const { user } = store.state.user
784
+ if (setAppDocumentTitle(appConfig)) {
785
+ appId = appConfig.ID
786
+ }
787
+ if (isEmpty(user)) {
788
+ // 判断用户是否登录
789
+ const isLogin = await loginPromise
790
+ if (isLogin) {
791
+ // 没有角色
792
+ let user = null
793
+ try {
794
+ user = await getUserOnce()
795
+ } catch (error) {
796
+ ElMessage.error(error.message)
797
+ util.toLogin()
798
+ }
799
+ visitService(appConfig.ID)
800
+ closeLoading()
801
+ if (!to.params.roleId && to.name !== 'appRoleSelect') {
802
+ const { serviceRoles = {} } = user
803
+ const roleList = serviceRoles[appId] || []
804
+ let defaultRole = null
805
+ if (roleList.length === 1) {
806
+ defaultRole = roleList[0].id
807
+ } else {
808
+ const defaultRoleMap = roleList.find(item => item.isDefault === true) || {}
809
+ defaultRole = defaultRoleMap.id
810
+ }
811
+ if (defaultRole) {
812
+ return next({
813
+ name: 'app',
814
+ params: { id: appId || '', roleId: defaultRole },
815
+ query: { ...to.query }
816
+ })
817
+ } else {
818
+ return next({
819
+ name: 'appRoleSelect',
820
+ params: { id: appId || '' },
821
+ query: { ...to.query }
822
+ })
823
+ }
824
+ }
825
+ }
826
+ }
827
+ } else if (/\/(recruit\/register)\/([\w-]+)\/([\w-]+)\/([\w-]+)/.test(window.location.hash) && !to.name) {
828
+ // 同样是重定向,必须 return,否则末尾还会再调一次 next
829
+ return next({ ...nextTo })
830
+ } else if (to.name === 'Task' || to.name === 'feishuTask') {
831
+ let { user } = store.state.user
832
+ // Task 19 / C2:原来这里包着一层 `if (!judgeIsLogin)`,judgeIsLogin 恒为 undefined ⇒ 恒进入,
833
+ // 已连同上面 willCheckLogin 里的同一个死判据一起删除(理由见那里)。行为不变。
834
+ // 判断用户是否登录
835
+ const isLogin = await loginPromise
836
+ if (isLogin) {
837
+ if (isEmpty(user)) {
838
+ try {
839
+ user = await getUserOnce()
840
+ } catch (error) {
841
+ ElMessage.error(error.message)
842
+ util.toLogin()
843
+ }
844
+ }
845
+ }
846
+ }
847
+ closeLoading()
848
+ if (nextTo.query.token || nextTo.query._token) {
849
+ const query = { ...nextTo.query }
850
+ delete query.token
851
+ delete query._token
852
+ delete query.code
853
+ // 剥 token 的重定向仍属同一次用户导航 ⇒ 交接由上面那层 next 包装自动保留(传的是 location
854
+ // 对象)。(下面那句 next() 是既有的重复调用——vue-router 与本文件的记账都只认第一次调用,
855
+ // 这里不顺手改它。)
856
+ next({ ...nextTo, query })
857
+ }
858
+ // 走到这里而没有自我重定向的,什么都没挂过:登录校验结果**绝不跨用户导航复用**,否则就退化成
859
+ // 「本会话只校验一次」,token 中途过期再也检不出来(见文件头部注释)。
860
+ next()
861
+ }