@epxing/core 5.3.0-beta.2 → 5.3.0-beta.4

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 (100) hide show
  1. package/API.md +715 -714
  2. package/README.md +3 -1
  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 +6 -0
  5. package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts.min.js +49 -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/role_select-DTqRlFNz.webp +0 -0
  9. package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/jsencrypt@3.5.4/_vendor/jsencrypt/bin/jsencrypt.min.js +1 -1
  10. package/dist/src/RootApp.js +54 -0
  11. package/dist/src/api/createHttp.js +6 -0
  12. package/dist/src/app-ui-settings.js +49 -0
  13. package/dist/src/axios.js +10 -15
  14. package/dist/src/chart-runtime-assets.js +19 -0
  15. package/dist/src/chart-runtime-sources.js +8 -0
  16. package/dist/src/create-app.js +3 -6
  17. package/dist/src/index.js +2 -0
  18. package/dist/src/loadComponent.js +2 -2
  19. package/dist/src/menu-route.js +11 -0
  20. package/dist/src/mixins/component.js +3 -0
  21. package/dist/src/navigation-context.js +47 -0
  22. package/dist/src/navigation.js +43 -12
  23. package/dist/src/package-install.js +3 -2
  24. package/dist/src/plugins/element-plus.js +8 -8
  25. package/dist/src/public-api.js +4 -2
  26. package/dist/src/route-components.js +9 -0
  27. package/dist/src/router/config.js +7 -8
  28. package/dist/src/runtime-services.js +1 -1
  29. package/dist/src/runtime-utils/meta.js +3 -0
  30. package/dist/src/runtime-utils/pdf.viewer.js +31 -15
  31. package/dist/src/runtime-utils/util.js +28 -5
  32. package/dist/src/shared-packages.js +1 -3
  33. package/dist/src/store/modules/list.js +25 -3
  34. package/dist/src/store/modules/meta.js +49 -7
  35. package/dist/src/style-presets.js +4 -4
  36. package/dist/src/ui-settings.js +90 -18
  37. package/dist/src/views/App.js +48 -0
  38. package/dist/src/views/AppRoleSelect.js +233 -0
  39. package/dist/src/views/Error.js +210 -0
  40. package/dist/src/views/Module.js +173 -0
  41. package/dist/style.css +251 -0
  42. package/package.json +4 -4
  43. package/src/RootApp.vue +75 -0
  44. package/src/api/createHttp.js +7 -0
  45. package/src/api/index.js +47 -47
  46. package/src/app-ui-settings.js +31 -0
  47. package/src/axios.js +311 -318
  48. package/src/chart-runtime-assets.js +13 -0
  49. package/src/chart-runtime-sources.js +5 -0
  50. package/src/create-app.js +3 -7
  51. package/src/index.js +28 -28
  52. package/src/install.js +157 -157
  53. package/src/loadComponent.js +187 -187
  54. package/src/loadLibrary.js +99 -99
  55. package/src/menu-route.js +12 -1
  56. package/src/mixins/component.js +1464 -1461
  57. package/src/navigation-context.js +49 -0
  58. package/src/navigation.js +208 -176
  59. package/src/package-install.js +2 -1
  60. package/src/plugins/element-plus.js +302 -302
  61. package/src/plugins/frame.js +178 -178
  62. package/src/public-api.js +1 -0
  63. package/src/route-components.js +7 -0
  64. package/src/router/config.js +861 -864
  65. package/src/router/public.js +1 -1
  66. package/src/runtime-services.js +1 -1
  67. package/src/runtime-utils/fieldFactory.js +179 -179
  68. package/src/runtime-utils/meta.js +399 -378
  69. package/src/runtime-utils/pdf.viewer.js +672 -644
  70. package/src/runtime-utils/util.js +1359 -1330
  71. package/src/shared-packages.js +1 -3
  72. package/src/store/modules/config.js +351 -351
  73. package/src/store/modules/list.js +30 -3
  74. package/src/store/modules/locale.js +236 -236
  75. package/src/store/modules/meta.js +314 -265
  76. package/src/style-presets.js +4 -4
  77. package/src/ui-settings.js +84 -23
  78. package/src/views/App.vue +53 -0
  79. package/src/views/AppRoleSelect.vue +334 -0
  80. package/src/views/Error.vue +63 -0
  81. package/src/views/Module.vue +210 -0
  82. package/src/xss.js +26 -26
  83. package/types/app.d.ts +10 -10
  84. package/types/axios.d.ts +1 -1
  85. package/types/component-mixin.d.ts +1 -1
  86. package/types/field-factory.d.ts +1 -1
  87. package/types/global-mixin.d.ts +1 -1
  88. package/types/http.d.ts +1 -1
  89. package/types/index.d.ts +3 -0
  90. package/types/meta.d.ts +1 -1
  91. package/types/navigation.d.ts +21 -21
  92. package/types/pdf-viewer.d.ts +1 -1
  93. package/types/runtime.d.ts +13 -13
  94. package/types/store.d.ts +1 -1
  95. package/types/util.d.ts +2 -2
  96. /package/dist/{build → .cache/route-chart-release/frontend/frame/build}/stubs/empty-components.js +0 -0
  97. /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-icons.names.js +0 -0
  98. /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-plus.lazy.js +0 -0
  99. /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
  100. /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
package/src/axios.js CHANGED
@@ -1,318 +1,311 @@
1
- import axios from 'axios'
2
- import util from '@epxing/core/src/runtime-utils/util'
3
- import { getXsrfToken, setXsrfToken } from '@epxing/utils/src/storage'
4
- import { consumeUrlToken, getStoredToken, setStoredToken, removeStoredToken } from '@epxing/utils/src/token-storage'
5
- import { ElMessage } from 'element-plus/es/components/message/index.mjs'
6
- // 菜单直达路由(appMenu/hallMenu)角色兜底:纯函数层,无依赖不成环
7
- import { MENU_ROUTE_ALIAS, roleStorage } from '@epxing/core/src/menu-route'
8
-
9
- /* global __EPX_VUE2_COMPAT__ */
10
-
11
- let message = null
12
- let timer = null
13
- const normalizeBearerToken = authorization => {
14
- if (typeof authorization !== 'string') return ''
15
- return authorization.replace(/^Bearer\s+/i, '').trim()
16
- }
17
-
18
- const requestAuthorization = config => {
19
- if (!config || !config.headers) return ''
20
- return normalizeBearerToken(config.headers.Authorization || config.headers.authorization)
21
- }
22
-
23
- const tokenContext = (config, fallbackClientCode = '') => {
24
- if (config && config.epxingTokenContext) return config.epxingTokenContext
25
- const clientCode = fallbackClientCode || ''
26
- return {
27
- clientCode,
28
- storageKey: clientCode + '-token',
29
- requestToken: requestAuthorization(config)
30
- }
31
- }
32
-
33
- // 一次登录失效响应,只有在它对应的正是"当前生效"的那个 token 时才能清;
34
- // 否则就是一条晚到的旧请求,把刚登录换来的新 token 误杀
35
- const canApplyTokenResponse = context => {
36
- if (!context.requestToken) return true
37
- return getStoredToken(context.storageKey) === context.requestToken
38
- }
39
-
40
- const isStaleTokenResponse = context => {
41
- const currentToken = getStoredToken(context.storageKey)
42
- return !!(context.requestToken && currentToken && currentToken !== context.requestToken)
43
- }
44
-
45
- const delayToLogin = function (msg) {
46
- if (timer) clearTimeout(timer)
47
- timer = setTimeout(() => {
48
- if (message) message.close()
49
- message = ElMessage({
50
- message: msg || '登录失效,请重新登录',
51
- type: 'error'
52
- })
53
- // 存储上一次未登录页面
54
- if (util.toLogin) {
55
- util.toLogin()
56
- }
57
- }, 100)
58
- }
59
-
60
- // ===== 混合加密(RSA + AES)=====
61
- // 对称密钥不在 server→client 明文传输。
62
- // AES key 由前端本地随机生成,用服务器 RSA 公钥加密后随请求上送;
63
- // 服务器用 RSA 私钥解出该 key,再用它加密响应,前端用本地 key/iv 解密。
64
- let cryptoReadyPromise = null
65
- let sessionAesKey = null // 本会话本地随机生成的 AES key(CryptoJS WordArray,16 字节 = AES-128)
66
- let encryptedAesKey = null // RSA 公钥加密后的 AES key(Base64),随请求头上送
67
-
68
- // 懒加载加密依赖、获取公钥、本地生成并 RSA 加密 AES key(整个会话仅执行一次)
69
- const ensureCrypto = () => {
70
- if (cryptoReadyPromise) return cryptoReadyPromise
71
- cryptoReadyPromise = (async () => {
72
- const [CryptoJS, jsEncryptModule] = await Promise.all([
73
- import('crypto-js'),
74
- import('jsencrypt/bin/jsencrypt.min')
75
- ])
76
- const JSEncrypt = jsEncryptModule.default
77
- // 取 RSA 公钥(v2 只返回 publicKey,不再下发 aesKey,见 store/modules/config.js GET_DECRYPT_KEY)
78
- let rsaKey = window.store.state.config.decryptKey.rsaKey
79
- if (!rsaKey) {
80
- const decryptKey = await window.store.dispatch('GET_DECRYPT_KEY')
81
- rsaKey = decryptKey.rsaKey
82
- }
83
- // 本地随机生成 AES key(不再从服务器获取、不全局复用明文)
84
- sessionAesKey = CryptoJS.lib.WordArray.random(16)
85
- // RSA 公钥加密 AES key,上送 Base64
86
- const encrypt = new JSEncrypt()
87
- encrypt.setPublicKey(rsaKey)
88
- encryptedAesKey = encrypt.encrypt(CryptoJS.enc.Base64.stringify(sessionAesKey))
89
- return CryptoJS
90
- })()
91
- return cryptoReadyPromise
92
- }
93
-
94
- const init = () => {
95
- const instance = axios.create({
96
- timeout: 1200 * 1000,
97
- baseURL: ''
98
- })
99
- instance.defaults.withCredentials = !!window.WITH_CREDENTIALS
100
- // 站点不使用 Cookie:关闭 axios 内置的「读 XSRF-TOKEN cookie 回填请求头」逻辑,
101
- // 防 CSRF 的 xsrf_token 改由响应头下发、存 sessionStorage(见下方响应拦截器)
102
- instance.defaults.xsrfCookieName = null
103
- // 直接 import axios 的零散调用(如登录页取图形验证码)也走全局默认,一并关掉
104
- axios.defaults.xsrfCookieName = null
105
- // 添加请求拦截器
106
- instance.interceptors.request.use(async config => {
107
- // 仅旧产品令牌走懒加载垫片,新令牌和 AbortSignal 保持原契约。
108
- const legacyCancelToken = config.cancelToken
109
- if ((typeof __EPX_VUE2_COMPAT__ === 'undefined' || __EPX_VUE2_COMPAT__) &&
110
- window.__EPX_VUE2_COMPAT__ !== false && legacyCancelToken &&
111
- typeof legacyCancelToken.subscribe !== 'function' && typeof legacyCancelToken.promise?.then === 'function') {
112
- const { adaptLegacyCancelToken } = await import('./vue2-cancel-token-compat')
113
- config = adaptLegacyCancelToken(config)
114
- }
115
- // 在发送请求之前做些什么
116
- if (window.IP) {
117
- config.baseURL = window.IP
118
- }
119
- let name = null
120
- let params = {}
121
- if (window.router) {
122
- params = window.router.currentRoute.value.params
123
- name = window.router.currentRoute.value.name
124
- }
125
- const locationUrl = window.location.hash.split('?')[0]
126
- var reg = /(app\/role\/select|hall\/role\/select|app|hall\/login|hall|login|system|client|service\/setting|recruit\/register)\/(\w+)\/?([\w-]+)?\/(\w+)\/?([\w-_]+)?/
127
- var regResult = reg.exec(locationUrl)
128
- const regClientCode = regResult ? RegExp.$2 === 'login' ? RegExp.$3 : RegExp.$2 : null
129
- const regSystemCode = regResult ? RegExp.$3 : null
130
- const regAppCode = regResult ? RegExp.$1 === 'hall' ? 'FM_SERVICE_HALL' : RegExp.$4 : null
131
- const regRoleCode = regResult ? RegExp.$1 === 'hall' ? RegExp.$3 : RegExp.$1 === 'system' ? RegExp.$4 : RegExp.$5 : null
132
- // 根据sysId获取请求服务
133
- const { clientConfig = {}, appConfig = {}, temporaryRole } = window.store.state.config
134
- const { CODE: CLIENT_CODE = '' } = clientConfig
135
- const { CODE: APP_CODE = '', EXTEND_OF_COMPETENCE } = appConfig
136
- const clientCode = params.client || regClientCode || CLIENT_CODE
137
- const appCode = params.app || APP_CODE || regAppCode
138
- let roleCode = params.role || regRoleCode || ''
139
- // 菜单直达路由(appMenu/hallMenu)URL 末段是 menuKey 不是 role——上面的 regRoleCode 会
140
- // 误取 menuKey,导致 Role 头带上 32 位 hex 垃圾值,后端角色过滤失效(菜单不按角色过滤,
141
- // 冒出本应隐藏的菜单)。这类路由改从持久化角色(roleStorage)取,与守卫/菜单加载同源。
142
- if (MENU_ROUTE_ALIAS[name]) {
143
- roleCode = params.role || roleStorage.get(regClientCode, regSystemCode) || ''
144
- }
145
- if (name === 'Config') roleCode = 'SYSTEM'
146
- const token = consumeUrlToken(clientCode ? clientCode + '-token' : '')
147
- if (window.IP && token) {
148
- config.headers.Authorization = `Bearer ${token}`
149
- }
150
- config.epxingTokenContext = {
151
- clientCode,
152
- storageKey: clientCode + '-token',
153
- requestToken: token || ''
154
- }
155
- config.headers.Language = config.headers.Language || util.getCurrentLanguage()
156
- if (name === 'manage' || locationUrl.startsWith('#/manage')) config.headers.Mode = 'manage'
157
- config.headers.Client = clientCode
158
- config.headers['App-Agent'] = appCode
159
- if (params.system || regSystemCode) {
160
- config.headers.System = params.system || regSystemCode
161
- }
162
- if ((EXTEND_OF_COMPETENCE !== 'allRole' && roleCode) || temporaryRole) {
163
- config.headers.Role = temporaryRole?.code || roleCode
164
- }
165
- const xsrfToken = getXsrfToken()
166
- if (xsrfToken) {
167
- config.headers.xsrf_token = xsrfToken
168
- }
169
- config.params = {
170
- ...config.params,
171
- nonceStr: util.uuid().replaceAll('-', ''),
172
- timestamp: new Date().getTime()
173
- }
174
- // 混合加密:随请求上送「RSA 加密后的 AES key」+「本次请求随机 IV」,
175
- // 供服务器在 respEncrypt=YES 时用其加密响应。公钥接口本身不参与,避免递归。
176
- if (!config.url || !/\/api\/v1\/rsa\/public/.test(config.url)) {
177
- try {
178
- const CryptoJS = await ensureCrypto()
179
- const iv = CryptoJS.lib.WordArray.random(16) // 每次请求新生成 IV,不复用、不等于 key
180
- config.headers['X-Encrypted-Key'] = encryptedAesKey
181
- config.headers['X-Encrypt-Iv'] = CryptoJS.enc.Base64.stringify(iv)
182
- // 暂存本次请求的 key / iv,供响应拦截器解密使用
183
- config.encryptAesKey = sessionAesKey
184
- config.encryptIv = iv
185
- } catch (e) {
186
- // 加密初始化失败(如公钥获取失败)时不阻断请求,按明文发送
187
- console.error('混合加密初始化失败', e)
188
- }
189
- }
190
- return config
191
- })
192
-
193
- instance.interceptors.response.use(async function (response) {
194
- let params = {}
195
- if (window.router) {
196
- params = window.router.currentRoute.value.params
197
- }
198
- const reg = /(login|hall\/login|hall|app\/role\/select|app|system|client|service\/setting)\/(\w+)/
199
- reg.exec(window.location.hash)
200
- // 根据sysId获取请求服务
201
- const { 'CLIENT|CODE': CLIENT_CODE = '' } = window.store.state.config.appConfig
202
- const clientCode = params.client || RegExp.$2 || CLIENT_CODE
203
- const context = tokenContext(response.config, clientCode)
204
- const authorization = response.headers.authorization
205
- if (authorization) {
206
- const token = normalizeBearerToken(authorization)
207
- if (token && canApplyTokenResponse(context)) {
208
- setStoredToken(context.storageKey, token)
209
- }
210
- }
211
- // 后端若通过响应头下发防 CSRF 令牌(xsrf_token / X-XSRF-TOKEN),存 sessionStorage,
212
- // 后续请求由请求拦截器回填到 xsrf_token 请求头;不再依赖服务端 Set-Cookie
213
- const xsrfHeader = response.headers.xsrf_token || response.headers['x-xsrf-token']
214
- if (xsrfHeader) setXsrfToken(xsrfHeader)
215
- const { data = {} } = response
216
- if (data instanceof Blob) {
217
- const content = response.headers['content-disposition'] || ''
218
- const filename = content.replace(/(.+)filename="(.+)"/, '$2')
219
- return Promise.resolve({ blob: data, filename: decodeURIComponent(filename) })
220
- } else if (data.code === '0') {
221
- // 处理成功
222
- if (data.responseEncrypt) {
223
- // aes 解密:使用本次请求本地生成、并随请求上送给服务器的 key / iv,
224
- // 服务器用同一把 key/iv 加密响应。密钥不再由服务器明文下发。
225
- const key = response.config.encryptAesKey
226
- const iv = response.config.encryptIv
227
- return import('crypto-js').then(CryptoJS => {
228
- const decrypted = CryptoJS.AES.decrypt(data.data, key, {
229
- iv,
230
- mode: CryptoJS.mode.CBC,
231
- padding: CryptoJS.pad.Pkcs7
232
- })
233
- const decryptedData = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
234
- return Promise.resolve(decryptedData)
235
- })
236
- } else {
237
- return Promise.resolve(data.data)
238
- }
239
- } else {
240
- // 处理失败
241
- if (data.code === 'error.nologin' || data.code === 'login.expired' || data.code === '401') {
242
- if (isStaleTokenResponse(context)) {
243
- data.message = data.msg
244
- return Promise.reject(data)
245
- }
246
- removeStoredToken(context.storageKey)
247
- // 单点登录
248
- if (data.urlToRedirectTo) {
249
- window.location.href = data.urlToRedirectTo
250
- } else {
251
- delayToLogin(data.msg)
252
- }
253
- } else {
254
- data.message = data.msg
255
- return Promise.reject(data)
256
- }
257
- }
258
- }, function (error) {
259
- // T34 axios 0.19→1.x:取消请求的错误对象从 Cancel(toString()==='Cancel')变成
260
- // CanceledError(toString()==='CanceledError: canceled')—— 字符串比对会失配,
261
- // 取消的请求会掉进下面的错误分支弹「处理失败」。改用 axios.isCancel(两个版本都有)。
262
- if (axios.isCancel(error)) {
263
- // 取消请求
264
- return
265
- }
266
- const {
267
- data = {
268
- code: '500',
269
- msg: '处理失败'
270
- }, status
271
- } = error.response || {}
272
- if (status === 304) {
273
- return Promise.resolve({
274
- notModified: true,
275
- etag: error.response.headers.etag || ''
276
- })
277
- } else if (data instanceof Blob) {
278
- const fileReader = new FileReader()
279
- fileReader.readAsText(data, 'utf-8')
280
- return new Promise((resolve, reject) => {
281
- fileReader.onload = function () {
282
- const errors = JSON.parse(fileReader.result)
283
- // eslint-disable-next-line prefer-promise-reject-errors
284
- reject({ message: errors.msg, ...errors })
285
- }
286
- })
287
- } else if (data.code === 'error.nologin' || data.code === 'login.expired' || data.code === '401' || status === 401) {
288
- let params = {}
289
- if (window.router) {
290
- params = window.router.currentRoute.value.params
291
- }
292
- const reg = /(login|hall\/login|hall|app\/role\/select|app|system|client|service\/setting)\/(\w+)/
293
- reg.exec(window.location.hash)
294
- // 根据sysId获取请求服务
295
- const { 'CLIENT|CODE': CLIENT_CODE = '' } = window.store.state.config.appConfig
296
- const clientCode = params.client || RegExp.$2 || CLIENT_CODE
297
- const context = tokenContext(error.config, clientCode)
298
- if (isStaleTokenResponse(context)) {
299
- if (data.msg) data.message = data.msg
300
- return Promise.reject(data)
301
- }
302
- removeStoredToken(context.storageKey)
303
- // 单点登录
304
- if (data.urlToRedirectTo) {
305
- window.location.href = data.urlToRedirectTo
306
- } else {
307
- delayToLogin(data.msg)
308
- }
309
- } else {
310
- if (data.msg) data.message = data.msg
311
- return Promise.reject(data)
312
- }
313
- })
314
-
315
- return instance
316
- }
317
-
318
- export default init()
1
+ import axios from 'axios'
2
+ import util from '@epxing/core/src/runtime-utils/util'
3
+ import { getXsrfToken, setXsrfToken } from '@epxing/utils/src/storage'
4
+ import { consumeUrlToken, getStoredToken, setStoredToken, removeStoredToken } from '@epxing/utils/src/token-storage'
5
+ import { ElMessage } from 'element-plus/es/components/message/index.mjs'
6
+ import { resolveRouteRole } from '@epxing/core/src/menu-route'
7
+
8
+ /* global __EPX_VUE2_COMPAT__ */
9
+
10
+ let message = null
11
+ let timer = null
12
+ const normalizeBearerToken = authorization => {
13
+ if (typeof authorization !== 'string') return ''
14
+ return authorization.replace(/^Bearer\s+/i, '').trim()
15
+ }
16
+
17
+ const requestAuthorization = config => {
18
+ if (!config || !config.headers) return ''
19
+ return normalizeBearerToken(config.headers.Authorization || config.headers.authorization)
20
+ }
21
+
22
+ const tokenContext = (config, fallbackClientCode = '') => {
23
+ if (config && config.epxingTokenContext) return config.epxingTokenContext
24
+ const clientCode = fallbackClientCode || ''
25
+ return {
26
+ clientCode,
27
+ storageKey: clientCode + '-token',
28
+ requestToken: requestAuthorization(config)
29
+ }
30
+ }
31
+
32
+ // 一次登录失效响应,只有在它对应的正是"当前生效"的那个 token 时才能清;
33
+ // 否则就是一条晚到的旧请求,把刚登录换来的新 token 误杀
34
+ const canApplyTokenResponse = context => {
35
+ if (!context.requestToken) return true
36
+ return getStoredToken(context.storageKey) === context.requestToken
37
+ }
38
+
39
+ const isStaleTokenResponse = context => {
40
+ const currentToken = getStoredToken(context.storageKey)
41
+ return !!(context.requestToken && currentToken && currentToken !== context.requestToken)
42
+ }
43
+
44
+ const delayToLogin = function (msg) {
45
+ if (timer) clearTimeout(timer)
46
+ timer = setTimeout(() => {
47
+ if (message) message.close()
48
+ message = ElMessage({
49
+ message: msg || '登录失效,请重新登录',
50
+ type: 'error'
51
+ })
52
+ // 存储上一次未登录页面
53
+ if (util.toLogin) {
54
+ util.toLogin()
55
+ }
56
+ }, 100)
57
+ }
58
+
59
+ // ===== 混合加密(RSA + AES)=====
60
+ // 对称密钥不在 server→client 明文传输。
61
+ // AES key 由前端本地随机生成,用服务器 RSA 公钥加密后随请求上送;
62
+ // 服务器用 RSA 私钥解出该 key,再用它加密响应,前端用本地 key/iv 解密。
63
+ let cryptoReadyPromise = null
64
+ let sessionAesKey = null // 本会话本地随机生成的 AES key(CryptoJS WordArray,16 字节 = AES-128)
65
+ let encryptedAesKey = null // RSA 公钥加密后的 AES key(Base64),随请求头上送
66
+
67
+ // 懒加载加密依赖、获取公钥、本地生成并 RSA 加密 AES key(整个会话仅执行一次)
68
+ const ensureCrypto = () => {
69
+ if (cryptoReadyPromise) return cryptoReadyPromise
70
+ cryptoReadyPromise = (async () => {
71
+ const [cryptoModule, jsEncryptModule] = await Promise.all([
72
+ import('crypto-js'),
73
+ import('jsencrypt/bin/jsencrypt.min')
74
+ ])
75
+ const CryptoJS = 'default' in cryptoModule ? cryptoModule.default : cryptoModule
76
+ const JSEncrypt = jsEncryptModule.default
77
+ // 取 RSA 公钥(v2 只返回 publicKey,不再下发 aesKey,见 store/modules/config.js GET_DECRYPT_KEY)
78
+ let rsaKey = window.store.state.config.decryptKey.rsaKey
79
+ if (!rsaKey) {
80
+ const decryptKey = await window.store.dispatch('GET_DECRYPT_KEY')
81
+ rsaKey = decryptKey.rsaKey
82
+ }
83
+ // 本地随机生成 AES key(不再从服务器获取、不全局复用明文)
84
+ sessionAesKey = CryptoJS.lib.WordArray.random(16)
85
+ // RSA 公钥加密 AES key,上送 Base64
86
+ const encrypt = new JSEncrypt()
87
+ encrypt.setPublicKey(rsaKey)
88
+ encryptedAesKey = encrypt.encrypt(CryptoJS.enc.Base64.stringify(sessionAesKey))
89
+ return CryptoJS
90
+ })()
91
+ return cryptoReadyPromise
92
+ }
93
+
94
+ const init = () => {
95
+ const instance = axios.create({
96
+ timeout: 1200 * 1000,
97
+ baseURL: ''
98
+ })
99
+ instance.defaults.withCredentials = !!window.WITH_CREDENTIALS
100
+ // 站点不使用 Cookie:关闭 axios 内置的「读 XSRF-TOKEN cookie 回填请求头」逻辑,
101
+ // 防 CSRF 的 xsrf_token 改由响应头下发、存 sessionStorage(见下方响应拦截器)
102
+ instance.defaults.xsrfCookieName = null
103
+ // 直接 import axios 的零散调用(如登录页取图形验证码)也走全局默认,一并关掉
104
+ axios.defaults.xsrfCookieName = null
105
+ // 添加请求拦截器
106
+ instance.interceptors.request.use(async config => {
107
+ // 仅旧产品令牌走懒加载垫片,新令牌和 AbortSignal 保持原契约。
108
+ const legacyCancelToken = config.cancelToken
109
+ if ((typeof __EPX_VUE2_COMPAT__ === 'undefined' || __EPX_VUE2_COMPAT__) &&
110
+ window.__EPX_VUE2_COMPAT__ !== false && legacyCancelToken &&
111
+ typeof legacyCancelToken.subscribe !== 'function' && typeof legacyCancelToken.promise?.then === 'function') {
112
+ const { adaptLegacyCancelToken } = await import('./vue2-cancel-token-compat')
113
+ config = adaptLegacyCancelToken(config)
114
+ }
115
+ // 在发送请求之前做些什么
116
+ if (window.IP) {
117
+ config.baseURL = window.IP
118
+ }
119
+ const locationUrl = window.location.hash.split('?')[0]
120
+ const currentRoute = window.router?.currentRoute?.value
121
+ // 首轮导航尚未确认时也按路由定义解析命名参数,不能将菜单路径段猜成角色。
122
+ const route = currentRoute?.name ? currentRoute : window.router?.resolve?.(locationUrl.slice(1)) || currentRoute
123
+ const { name = null, params = {} } = route || {}
124
+ var reg = /(app\/role\/select|hall\/role\/select|app|hall\/login|hall|login|system|client|service\/setting|recruit\/register)\/(\w+)\/?([\w-]+)?\/(\w+)\/?([\w-_]+)?/
125
+ var regResult = reg.exec(locationUrl)
126
+ const regClientCode = regResult ? RegExp.$2 === 'login' ? RegExp.$3 : RegExp.$2 : null
127
+ const regSystemCode = regResult ? RegExp.$3 : null
128
+ const regAppCode = regResult ? RegExp.$1 === 'hall' ? 'FM_SERVICE_HALL' : RegExp.$4 : null
129
+ // 根据sysId获取请求服务
130
+ const { clientConfig = {}, appConfig = {}, temporaryRole } = window.store.state.config
131
+ const { CODE: CLIENT_CODE = '' } = clientConfig
132
+ const { CODE: APP_CODE = '', EXTEND_OF_COMPETENCE } = appConfig
133
+ const clientCode = params.client || regClientCode || CLIENT_CODE
134
+ const appCode = params.app || APP_CODE || regAppCode
135
+ // 角色与导航使用同一来源;大厅没有系统段,不能拿菜单或角色段作为存储作用域。
136
+ let roleCode = resolveRouteRole({ params: { ...params, client: clientCode, system: params.client ? params.system : regSystemCode } })
137
+ if (name === 'Config') roleCode = 'SYSTEM'
138
+ const token = consumeUrlToken(clientCode ? clientCode + '-token' : '')
139
+ if (window.IP && token) {
140
+ config.headers.Authorization = `Bearer ${token}`
141
+ }
142
+ config.epxingTokenContext = {
143
+ clientCode,
144
+ storageKey: clientCode + '-token',
145
+ requestToken: token || ''
146
+ }
147
+ config.headers.Language = config.headers.Language || util.getCurrentLanguage()
148
+ if (name === 'manage' || locationUrl.startsWith('#/manage')) config.headers.Mode = 'manage'
149
+ config.headers.Client = clientCode
150
+ config.headers['App-Agent'] = appCode
151
+ if (params.system || regSystemCode) {
152
+ config.headers.System = params.system || regSystemCode
153
+ }
154
+ if ((EXTEND_OF_COMPETENCE !== 'allRole' && roleCode) || temporaryRole) {
155
+ config.headers.Role = temporaryRole?.code || roleCode
156
+ }
157
+ const xsrfToken = getXsrfToken()
158
+ if (xsrfToken) {
159
+ config.headers.xsrf_token = xsrfToken
160
+ }
161
+ config.params = {
162
+ ...config.params,
163
+ nonceStr: util.uuid().replaceAll('-', ''),
164
+ timestamp: new Date().getTime()
165
+ }
166
+ // 混合加密:随请求上送「RSA 加密后的 AES key」+「本次请求随机 IV」,
167
+ // 供服务器在 respEncrypt=YES 时用其加密响应。公钥接口本身不参与,避免递归。
168
+ if (!config.url || !/\/api\/v1\/rsa\/public/.test(config.url)) {
169
+ try {
170
+ const CryptoJS = await ensureCrypto()
171
+ const iv = CryptoJS.lib.WordArray.random(16) // 每次请求新生成 IV,不复用、不等于 key
172
+ config.headers['X-Encrypted-Key'] = encryptedAesKey
173
+ config.headers['X-Encrypt-Iv'] = CryptoJS.enc.Base64.stringify(iv)
174
+ // 暂存本次请求的 key / iv,供响应拦截器解密使用
175
+ config.encryptAesKey = sessionAesKey
176
+ config.encryptIv = iv
177
+ } catch (e) {
178
+ // 加密初始化失败(如公钥获取失败)时不阻断请求,按明文发送
179
+ console.error('混合加密初始化失败', e)
180
+ }
181
+ }
182
+ return config
183
+ })
184
+
185
+ instance.interceptors.response.use(async function (response) {
186
+ let params = {}
187
+ if (window.router) {
188
+ params = window.router.currentRoute.value.params
189
+ }
190
+ const reg = /(login|hall\/login|hall|app\/role\/select|app|system|client|service\/setting)\/(\w+)/
191
+ reg.exec(window.location.hash)
192
+ // 根据sysId获取请求服务
193
+ const { 'CLIENT|CODE': CLIENT_CODE = '' } = window.store.state.config.appConfig
194
+ const clientCode = params.client || RegExp.$2 || CLIENT_CODE
195
+ const context = tokenContext(response.config, clientCode)
196
+ const authorization = response.headers.authorization
197
+ if (authorization) {
198
+ const token = normalizeBearerToken(authorization)
199
+ if (token && canApplyTokenResponse(context)) {
200
+ setStoredToken(context.storageKey, token)
201
+ }
202
+ }
203
+ // 后端若通过响应头下发防 CSRF 令牌(xsrf_token / X-XSRF-TOKEN),存 sessionStorage,
204
+ // 后续请求由请求拦截器回填到 xsrf_token 请求头;不再依赖服务端 Set-Cookie
205
+ const xsrfHeader = response.headers.xsrf_token || response.headers['x-xsrf-token']
206
+ if (xsrfHeader) setXsrfToken(xsrfHeader)
207
+ const { data = {} } = response
208
+ if (data instanceof Blob) {
209
+ const content = response.headers['content-disposition'] || ''
210
+ const filename = content.replace(/(.+)filename="(.+)"/, '$2')
211
+ return Promise.resolve({ blob: data, filename: decodeURIComponent(filename) })
212
+ } else if (data.code === '0') {
213
+ // 处理成功
214
+ if (data.responseEncrypt) {
215
+ // aes 解密:使用本次请求本地生成、并随请求上送给服务器的 key / iv,
216
+ // 服务器用同一把 key/iv 加密响应。密钥不再由服务器明文下发。
217
+ const key = response.config.encryptAesKey
218
+ const iv = response.config.encryptIv
219
+ return import('crypto-js').then(cryptoModule => {
220
+ const CryptoJS = 'default' in cryptoModule ? cryptoModule.default : cryptoModule
221
+ const decrypted = CryptoJS.AES.decrypt(data.data, key, {
222
+ iv,
223
+ mode: CryptoJS.mode.CBC,
224
+ padding: CryptoJS.pad.Pkcs7
225
+ })
226
+ const decryptedData = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
227
+ return Promise.resolve(decryptedData)
228
+ })
229
+ } else {
230
+ return Promise.resolve(data.data)
231
+ }
232
+ } else {
233
+ // 处理失败
234
+ if (data.code === 'error.nologin' || data.code === 'login.expired' || data.code === '401') {
235
+ if (isStaleTokenResponse(context)) {
236
+ data.message = data.msg
237
+ return Promise.reject(data)
238
+ }
239
+ removeStoredToken(context.storageKey)
240
+ // 单点登录
241
+ if (data.urlToRedirectTo) {
242
+ window.location.href = data.urlToRedirectTo
243
+ } else {
244
+ delayToLogin(data.msg)
245
+ }
246
+ } else {
247
+ data.message = data.msg
248
+ return Promise.reject(data)
249
+ }
250
+ }
251
+ }, function (error) {
252
+ // T34 axios 0.19→1.x:取消请求的错误对象从 Cancel(toString()==='Cancel')变成
253
+ // CanceledError(toString()==='CanceledError: canceled')—— 字符串比对会失配,
254
+ // 取消的请求会掉进下面的错误分支弹「处理失败」。改用 axios.isCancel(两个版本都有)。
255
+ if (axios.isCancel(error)) {
256
+ // 取消请求
257
+ return
258
+ }
259
+ const {
260
+ data = {
261
+ code: '500',
262
+ msg: '处理失败'
263
+ }, status
264
+ } = error.response || {}
265
+ if (status === 304) {
266
+ return Promise.resolve({
267
+ notModified: true,
268
+ etag: error.response.headers.etag || ''
269
+ })
270
+ } else if (data instanceof Blob) {
271
+ const fileReader = new FileReader()
272
+ fileReader.readAsText(data, 'utf-8')
273
+ return new Promise((resolve, reject) => {
274
+ fileReader.onload = function () {
275
+ const errors = JSON.parse(fileReader.result)
276
+ // eslint-disable-next-line prefer-promise-reject-errors
277
+ reject({ message: errors.msg, ...errors })
278
+ }
279
+ })
280
+ } else if (data.code === 'error.nologin' || data.code === 'login.expired' || data.code === '401' || status === 401) {
281
+ let params = {}
282
+ if (window.router) {
283
+ params = window.router.currentRoute.value.params
284
+ }
285
+ const reg = /(login|hall\/login|hall|app\/role\/select|app|system|client|service\/setting)\/(\w+)/
286
+ reg.exec(window.location.hash)
287
+ // 根据sysId获取请求服务
288
+ const { 'CLIENT|CODE': CLIENT_CODE = '' } = window.store.state.config.appConfig
289
+ const clientCode = params.client || RegExp.$2 || CLIENT_CODE
290
+ const context = tokenContext(error.config, clientCode)
291
+ if (isStaleTokenResponse(context)) {
292
+ if (data.msg) data.message = data.msg
293
+ return Promise.reject(data)
294
+ }
295
+ removeStoredToken(context.storageKey)
296
+ // 单点登录
297
+ if (data.urlToRedirectTo) {
298
+ window.location.href = data.urlToRedirectTo
299
+ } else {
300
+ delayToLogin(data.msg)
301
+ }
302
+ } else {
303
+ if (data.msg) data.message = data.msg
304
+ return Promise.reject(data)
305
+ }
306
+ })
307
+
308
+ return instance
309
+ }
310
+
311
+ export default init()