@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,1330 +1,1357 @@
1
- // import _router from '@epxing/core/src/router'
2
- // 不 import store:本文件运行时全部经 window.store(store/index.js 挂载)访问。
3
- // 原 import 是死导入,且构成 utils→store 反向边(4 个循环依赖的共同主干,P4 拆包 @epxing/utils 必须为叶子)。
4
- import storage, { getXsrfToken } from '@epxing/utils/src/storage'
5
- import { consumeUrlToken as consumeStoredUrlToken, getStoredToken, setStoredToken, removeStoredToken } from '@epxing/utils/src/token-storage'
6
- import { isEqual, cloneDeep, isNil } from 'lodash-es'
7
- import * as formula from '@epxing/utils/src/formula'
8
- import { evalWith } from '@epxing/utils/src/eval-with'
9
- import { useVuePlugin, getApp } from '@epxing/core/src/app'
10
- // 纯函数层,零依赖(见 menu-route.js 头注释)——不会与 core 成环
11
- import { resolveRouteRole } from '@epxing/core/src/menu-route'
12
- // 安装旧库会等待兼容模块的动态 import;并发 script.onload 必须共享同一次安装。
13
- const installingLibraries = new Map()
14
- class Util {
15
- constructor () {
16
- this.sysId = ''
17
- this.loadPlugins = {}
18
- this.loadSystemLibrarys = {}
19
- }
20
-
21
- getSysId () {
22
- const searchSysId = this.search('FM_SYS_ID', window.location.search)
23
- const hashSysId = this.search('FM_SYS_ID')
24
- return hashSysId || searchSysId
25
- }
26
-
27
- setSysId (sysId) {
28
- const { CLIENT_ID = '' } = window.store.state.config
29
- storage.set('EPX_FM_SYS_ID-' + CLIENT_ID, sysId)
30
- this.sysId = sysId
31
- }
32
-
33
- getPublicPath () {
34
- return window.ROUTER_MODE === 'history' ? window.BASE_URL || '/' : './'
35
- }
36
-
37
- // 运行时动态加载的三方库(echarts/pdf/tinymce/signature_pad 等)所在目录,相对 publicPath。
38
- // 默认 'js/' —— 产品项目脚手架的 public/js 构建后就在 dist/js/,行为不变;
39
- // 本平台把静态资源归入 dist/assets/js/,由其 config.js 注入 window.STATIC_JS_DIR 覆盖。
40
- getStaticJsDir () {
41
- return window.STATIC_JS_DIR || 'js/'
42
- }
43
-
44
- getRootPath () {
45
- if (window.ROUTER_MODE === 'history') {
46
- const reg = /\/((app|hall|login|system|page|s|task|feishuTask)\/.+)/g
47
- return location.origin + (location.pathname === '/' ? '' : location.pathname.replace(reg, ''))
48
- }
49
- return location.origin + location.pathname + (location.pathname.endsWith('/') ? '#' : '/#')
50
- }
51
-
52
- getRoutePath () {
53
- const router = window.router
54
- const { fullPath } = router.currentRoute.value || {}
55
- let path = fullPath
56
- if (!path || path === '/') {
57
- path = window.ROUTER_MODE === 'history' ? location.pathname : location.hash
58
- const reg = /\/((app|hall|login|system|page|s|task|feishuTask)\/.+)/g
59
- const flag = reg.exec(path)
60
- if (flag) return RegExp.$1
61
- }
62
- return path
63
- }
64
-
65
- getClientCode () {
66
- // 1. 当前路由里取 2. clientConfig 2.当前路径中取- 判断是history还是hash
67
- const router = window.router
68
- const { params = {} } = router.currentRoute.value || {}
69
- const { config = {} } = window.store.state || {}
70
- const { clientConfig = {} } = config
71
- const { CODE } = clientConfig
72
- const clientCode = params.client || CODE
73
- if (!clientCode) {
74
- const path = this.getRoutePath()
75
- let reg = /app\/role\/select\/(\w+)/
76
- let regResult = reg.exec(path)
77
- if (!regResult) {
78
- reg = /[login|app|hall|system|client]\/(\w+)/
79
- regResult = reg.exec(path)
80
- }
81
- return RegExp.$1
82
- }
83
- return clientCode
84
- }
85
-
86
- setToken (v, routeClientCode) {
87
- const clientCode = routeClientCode || this.getClientCode()
88
- setStoredToken(`${clientCode}-token`, v)
89
- }
90
-
91
- getToken (routeClientCode) {
92
- const clientCode = routeClientCode || this.getClientCode()
93
- return getStoredToken(`${clientCode}-token`)
94
- }
95
-
96
- removeToken (routeClientCode) {
97
- const clientCode = routeClientCode || this.getClientCode()
98
- removeStoredToken(`${clientCode}-token`)
99
- }
100
-
101
- consumeUrlToken (routeClientCode) {
102
- const clientCode = routeClientCode || this.getClientCode()
103
- return consumeStoredUrlToken(clientCode ? `${clientCode}-token` : '')
104
- }
105
-
106
- search (name, path) {
107
- if (!name) {
108
- const searchStr = (path || window.location.hash || window.location.search).split('?')[1]
109
- if (!searchStr) return
110
- const searchArray = searchStr.split('&')
111
- const searchObj = {}
112
- searchArray.map(item => {
113
- const key = item.split('=')[0]
114
- const value = item.split('=')[1]
115
- searchObj[key] = value
116
- })
117
- return searchObj
118
- }
119
- const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
120
- const searchStr = '?' + (path || window.location.hash || window.location.search).split('?')[1]
121
- const r = searchStr.substr(1).match(reg)
122
- if (r != null) {
123
- return unescape(r[2])
124
- } else {
125
- return null
126
- }
127
- }
128
-
129
- getCurrentLanguage (routeClientCode) {
130
- const { config = {}, user = {} } = window.store.state || {}
131
- const { clientConfig = {} } = config
132
- const { userInfo = {} } = user
133
- const { locale = {} } = window.store.state || {}
134
- // 全仓字段名是 LANGUAGE_DEFAULT(20+ 处一致,ClientFormItem.vue 里它是真实的数据库
135
- // attriName),这里原来错写成 DEFAULT_LANGUAGE,兜底分支因此恒为 undefined,已改正
136
- const { CODE: configuredClientCode, LANGUAGE_DEFAULT: defaultLanguage } = clientConfig
137
- const { LANGUAGE: userLanguage } = userInfo
138
- const clientCode = routeClientCode || configuredClientCode
139
- // INITIALIZE_LOCALE(store/modules/locale.js)已落地当前语言时优先取它——它是唯一同时
140
- // 兼顾"客户是否启用多语言""用户/浏览器偏好""受支持语言集合"三者的权威来源
141
- if (locale.clientCode === clientCode && locale.currentLocale) return locale.currentLocale
142
- const storedLanguage = storage.get((clientCode || '') + '-language')
143
- // routeClientCode 指定了别的客户(如引导期还没切到当前客户)时,不能用本客户的
144
- // user/默认语言兜底——那两个值语义上属于 configuredClientCode,答非所问
145
- const canUseLoadedDefaults = !routeClientCode || routeClientCode === configuredClientCode
146
- return storedLanguage || (canUseLoadedDefaults ? userLanguage || defaultLanguage : '') || 'zh-CN'
147
- }
148
-
149
- setCurrentLanguage (language) {
150
- return window.store.dispatch('CHANGE_LOCALE', language)
151
- }
152
-
153
- isChineseLanguage (language = this.getCurrentLanguage()) {
154
- return language === '010' || /^zh(?:-|$)/i.test(language || '')
155
- }
156
-
157
- toPx (v) {
158
- if (v) {
159
- const str = String(v)
160
- return str.includes('px') ? str : str + 'px'
161
- }
162
- return ''
163
- }
164
-
165
- // 返回登陆页
166
- toLogin (clearPath) {
167
- const router = window.router
168
- if (window.openedPopup && window.openedPopup.length > 0) {
169
- window.openedPopup.forEach(item => item.close())
170
- window.openedPopup = []
171
- }
172
- // 获取当前应用信息
173
- const { config = {} } = window.store.state || {}
174
- const { appConfig = {} } = config
175
- const { fullPath, params = {} } = router.currentRoute.value || {}
176
- const clientCode = this.getClientCode()
177
- const system = params.system || appConfig['SYSTEM|CODE']
178
- const app = appConfig.CODE || params.app
179
-
180
- this.removeToken()
181
-
182
- window.store.commit('DELETE_USER')
183
- window.store.commit('DELETE_TABS')
184
- const path = this.getRoutePath()
185
- // 删除_token参数
186
- const pathname = path.split('?')[0]
187
- if (fullPath && !fullPath.includes('/login')) {
188
- const query = {}
189
- let _path = ''
190
- if (!(fullPath === '/' && pathname === '/')) {
191
- _path = fullPath === '/' ? path : fullPath
192
- }
193
- if (_path) {
194
- query.path = _path.replace(/(\?|&)(_token|client_code|client)=([\w|.]+)(&?)/g, '$1')
195
- }
196
- if (clearPath) delete query.path
197
- delete query._token
198
- router.push({
199
- name: (appConfig['SYSTEM|CODE'] === 'epxing-frame' && appConfig.CODE === 'FM_SERVICE_HALL') ? 'hallLogin' : window.IP ? 'login' : 'localLogin',
200
- params: {
201
- // ⚠️ 键名必须是 client —— 路由定义是 /login/:client/:system/:app?。
202
- // 写成 clientCode 会被 Vue Router 丢弃([Vue Router warn] Discarded invalid param(s)),
203
- // 平时看不出来是因为 Router 4 按 name 跳转时会从**当前路由继承** client;
204
- // 一旦当前路由没有 client(如落在 /#/),就变成 Missing required param "client" → 白屏。
205
- client: clientCode,
206
- system,
207
- app
208
- },
209
- query
210
- })
211
- }
212
- }
213
-
214
- urlToBase64 (url) {
215
- return new Promise((resolve, reject) => {
216
- const image = new Image()
217
- image.onload = function () {
218
- const canvas = document.createElement('canvas')
219
- canvas.width = this.width
220
- canvas.height = this.height
221
- // 将图片插入画布并开始绘制
222
- canvas.getContext('2d').drawImage(image, 0, 0)
223
- // result
224
- const result = canvas.toDataURL('image/png')
225
- resolve({ data: result, width: this.width, height: this.height })
226
- }
227
- image.setAttribute('crossOrigin', 'Anonymous')
228
- image.src = url
229
- // 图片加载失败的错误处理
230
- image.onerror = () => {
231
- reject(new Error('转换失败'))
232
- }
233
- })
234
- }
235
-
236
- getFileUrl (id, client, flag = true, useSignOrigin = false) {
237
- // 兼容:第二个参数可传对象 { client, flag, useSignOrigin },旧的位置参数写法 (id, client, flag, useSignOrigin) 仍然可用
238
- if (client !== null && typeof client === 'object') {
239
- const options = client
240
- client = options.client
241
- flag = options.flag !== undefined ? options.flag : true
242
- useSignOrigin = options.useSignOrigin || false
243
- }
244
- const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID, MINIO_IP, MINIO_FILE_URL } = window
245
- if (!id) return
246
- if (id.includes('http') || id.startsWith('data:image') || id.includes('//')) return id
247
-
248
- if (id.includes('/')) return `${MINIO_IP || IP}${MINIO_FILE_URL || 'file-server'}${id}`
249
- const { CODE: clientCode = BASE_CLIENT_ID } = window.store.state.config.clientConfig
250
- const { 'SYSTEM|DOMAIN|CONTEXT': domainContext, 'SYSTEM|CODE': sysCode = BASE_SYSTEM_ID, 'SYSTEM|SIGN_ORIGIN': signOrigin } = window.store.state.config.appConfig
251
- const projectName = domainContext || BASE_PROJECT_NAME
252
- const baseUrl = IP ? `${IP}${projectName}/` : ''
253
- const inter = `api/v1/${client || clientCode}/${sysCode}/download?id=${id}`
254
- let flieUrl = baseUrl + inter
255
-
256
- // 两处运算符优先级修复(vue3 侧与本次 token 续期修复各自独立发现同一处 bug):
257
- // ① `client || clientCode + '-token'` 里 `+` 比 `||` 紧,实际是 `client || (clientCode + '-token')`
258
- // —— 传了 client 实参时会去读 `<client>` 这个键(少了 `-token` 后缀),永远拿不到 token。
259
- // 目前全仓没有调用方传非空 client,所以这条是**潜伏**的;但一旦有人传就悄悄失效。
260
- // ② 结尾的 `|| ''` 同理会被 `+` 抢先而恒不生效;getStoredToken 内部已自带空串兜底
261
- // (取不到时返回 `''` 而非 undefined),改用它之后不用再套这层 `|| ''`。
262
- if (flag) flieUrl += '&EPXTID=' + btoa(getStoredToken((client || clientCode) + '-token')) + '&xsrf_token=' + getXsrfToken()
263
- // 当系统配置了签名地址 SIGN_ORIGIN 时,按需将文件地址的域名替换为该地址
264
- if (useSignOrigin && signOrigin) flieUrl = flieUrl.replace(/^https?:\/\/[^/]+/, signOrigin.replace(/\/+$/, ''))
265
- return flieUrl
266
- }
267
-
268
- getOfficeFileUrl (id, client, flag = true, useSignOrigin = false) {
269
- // 兼容:第二个参数可传对象 { client, flag, useSignOrigin },旧的位置参数写法 (id, client, flag, useSignOrigin) 仍然可用
270
- if (client !== null && typeof client === 'object') {
271
- const options = client
272
- client = options.client
273
- flag = options.flag !== undefined ? options.flag : true
274
- useSignOrigin = options.useSignOrigin || false
275
- }
276
- const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID } = window
277
- const { CODE: clientCode = BASE_CLIENT_ID } = window.store.state.config.clientConfig
278
- const { 'SYSTEM|DOMAIN|CONTEXT': domainContext, 'SYSTEM|CODE': sysCode = BASE_SYSTEM_ID, 'SYSTEM|SIGN_ORIGIN': signOrigin } = window.store.state.config.appConfig
279
- const projectName = domainContext || BASE_PROJECT_NAME
280
- const baseUrl = IP ? `${IP}${projectName}/` : ''
281
- const inter = `api/v1/${client || clientCode}/${sysCode}/onlineview?fileId=${id}`
282
- let flieUrl = baseUrl + inter
283
- // 同 getFileUrl 的两处优先级问题,改法一致(详见那里的注释)。
284
- if (flag) flieUrl += '&token=' + getStoredToken((client || clientCode) + '-token') + '&xsrf_token=' + getXsrfToken()
285
- // 当系统配置了签名地址 SIGN_ORIGIN 时,按需将文件地址的域名替换为该地址
286
- if (useSignOrigin && signOrigin) flieUrl = flieUrl.replace(/^https?:\/\/[^/]+/, signOrigin.replace(/\/+$/, ''))
287
- return flieUrl
288
- }
289
-
290
- // ONLYOFFICE Document Server 地址,来自 config.js 的 window.ONLYOFFICE_URL(未配置返回空串,调用方据此降级)
291
- // 建议配成与主站同源的反代路径(如 /onlyoffice),可同时避免 mixed content 与跨域问题
292
- getOfficeServerUrl () {
293
- const url = (window.ONLYOFFICE_URL || '').replace(/\/+$/, '')
294
- if (!url) return ''
295
- // 已是完整地址(含协议或 //)原样返回
296
- if (!url.startsWith('/') || url.startsWith('//')) return url
297
- // 相对路径:本地开发(localhost)返回相对,走 devServer 同源代理绕过跨域;
298
- // 线上拼主站 window.IP 直连(前端与 onlyoffice 同域名则同源)
299
- const host = window.location.hostname
300
- if (host === 'localhost' || host === '127.0.0.1') return url
301
- return (window.IP || '').replace(/\/+$/, '') + url
302
- }
303
-
304
- getAbsPath (path, params = {}) {
305
- let paramStr = ''
306
- for (const i in params) {
307
- paramStr += '&' + i + '=' + params[i]
308
- }
309
- const { IP, BASE_PROJECT_NAME } = window
310
- const { 'SYSTEM|DOMAIN|CONTEXT': domainContext } = window.store.state.config.appConfig
311
- const projectName = domainContext || BASE_PROJECT_NAME
312
-
313
- const { clientConfig = {} } = window.store.state.config
314
- return (IP ? (IP + projectName) : '') + path + `?client=${clientConfig.CODE}` + '&xsrf_token=' + getXsrfToken() + paramStr
315
- }
316
-
317
- getPlatformRequestUrl (params = {}) {
318
- const router = window.router
319
-
320
- const { params: routeParams = {} } = router.currentRoute.value || {}
321
- const { appConfig = {} } = window.store.state.config
322
- const client = routeParams.client || appConfig['CLIENT|CODE']
323
- const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID } = window
324
- const baseUrl = IP ? `${IP}${BASE_PROJECT_NAME}/` : ''
325
- const platformUrl = `api/v1/${BASE_CLIENT_ID}/${BASE_SYSTEM_ID}/${params.entityId}/${params.action}`
326
- let paramStr = ''
327
- for (const i in params) {
328
- if (i !== 'action' && i !== 'entityId') {
329
- paramStr += (paramStr.includes('?') ? '&' : '?') + i + '=' + params[i]
330
- }
331
- }
332
- // 结尾 `|| ''` 同样会被 `+` 抢先而恒不生效(这里 client 已在上面解析好,没有键名那个问题);
333
- // getStoredToken 内部已自带空串兜底,不用再套这层 `|| ''`。
334
- return baseUrl + platformUrl + paramStr + '&token=' + getStoredToken(client + '-token') + '&xsrf_token=' + getXsrfToken()
335
- }
336
-
337
- getRequestUrl (params = {}, { requestKey = 'api' } = {}) {
338
- const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID } = window
339
- const { CODE: clientCode = BASE_CLIENT_ID } = window.store.state.config.clientConfig
340
- const { 'SYSTEM|DOMAIN|CONTEXT': domainContext, 'SYSTEM|CODE': sysCode = BASE_SYSTEM_ID } = window.store.state.config.appConfig
341
- const projectName = domainContext || BASE_PROJECT_NAME
342
- const baseUrl = IP ? `${IP}${projectName}/` : ''
343
- const platformUrl = `${requestKey}/v1/${clientCode}/${sysCode}/${params.entityId}/${params.action}`
344
- let paramStr = ''
345
- const xsrfToken = getXsrfToken()
346
- if (xsrfToken) params.xsrf_token = xsrfToken
347
- for (const i in params) {
348
- if (i !== 'action' && i !== 'entityId') {
349
- paramStr += (paramStr.includes('?') ? '&' : '?') + i + '=' + params[i]
350
- }
351
- }
352
- return baseUrl + platformUrl + paramStr
353
- }
354
-
355
- getSystemMeta (metaCode) {
356
- const router = window.router
357
-
358
- const { name, params: routeParams = {} } = router.currentRoute.value || {}
359
- const { appConfig = {} } = window.store.state.config
360
- const client = routeParams.client || appConfig['CLIENT|CODE']
361
- const system = routeParams.system || appConfig['SYSTEM|CODE']
362
- return name === 'hall' ? `${metaCode}@system@${client}` : `${metaCode}@${system}@${client}`
363
- }
364
-
365
- isWeiXin () {
366
- var ua = window.navigator.userAgent.toLowerCase()
367
- // 修复:ua.match() 返回数组(或 null),永远不会 === 字符串 'micromessenger',
368
- // 原写法恒为 false——微信内嵌浏览器判断从未生效过。
369
- // 移动端、Windows/Mac 桌面版微信以及企业微信的内置浏览器 UA 里都带 micromessenger
370
- return ua.indexOf('micromessenger') !== -1
371
- }
372
-
373
- isFeishu () {
374
- var ua = window.navigator.userAgent.toLowerCase()
375
- if (ua.match(/Lark/i)) {
376
- return true
377
- } else {
378
- return false
379
- }
380
- }
381
-
382
- isDingTalk () {
383
- const ua = navigator.userAgent.toLowerCase()
384
- return ua.match(/dingtalk/i) !== null
385
- }
386
-
387
- isIos () {
388
- var ua = window.navigator.userAgent.toLowerCase()
389
- // eslint-disable-next-line
390
- return !!ua.match(/i[^;]+;( U;)? CPU.+Mac OS X/i)
391
- }
392
-
393
- // 浏览器能否在页面内直接渲染 PDF。不能的话,给 iframe / a 标签赋 PDF 地址
394
- // 会被当成文件下载(微信内置浏览器会直接弹「下载 / 用其他应用打开」)
395
- canPreviewPdfInline () {
396
- // 微信内置浏览器(含桌面版、企业微信)既不能内联渲染 PDF,也不支持网页发起打印
397
- if (this.isWeiXin()) return false
398
- if (typeof navigator.pdfViewerEnabled === 'boolean') return navigator.pdfViewerEnabled
399
- // 老浏览器没有 pdfViewerEnabled,退回看是否注册了 PDF 处理器
400
- const mimeTypes = navigator.mimeTypes || {}
401
- return !!(mimeTypes['application/pdf'] || mimeTypes['application/x-google-chrome-pdf'])
402
- }
403
-
404
- // 把一段 HTML 放进临时 iframe 打印,返回浏览器是否真的进入了打印流程。
405
- // 打印的是 HTML 而不是 PDF,所以不会触发「下载 / 用其他应用打开」弹窗
406
- printHtml (html) {
407
- return new Promise(resolve => {
408
- const frame = document.createElement('iframe')
409
- frame.setAttribute('aria-hidden', 'true')
410
- frame.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0'
411
- document.body.appendChild(frame)
412
- let removed = false
413
- const remove = () => {
414
- if (removed) return
415
- removed = true
416
- if (frame.parentNode) frame.parentNode.removeChild(frame)
417
- }
418
- const doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document)
419
- if (!doc) {
420
- remove()
421
- return resolve(false)
422
- }
423
- doc.open()
424
- doc.write(html)
425
- doc.close()
426
- // 图片是 dataURL,也要等它们解码完再打印,否则会打出空白页
427
- const images = Array.prototype.slice.call(doc.images || [])
428
- const loaded = images.map(img => img.complete
429
- ? Promise.resolve()
430
- : new Promise(resolve => { img.onload = img.onerror = resolve }))
431
- Promise.all(loaded).then(() => {
432
- const win = frame.contentWindow
433
- // print() 在有的浏览器里是异步的,过早移除 iframe 会打断打印
434
- try { win.addEventListener('afterprint', remove) } catch (e) {}
435
- const printed = this.printWindow(win)
436
- // 没进打印流程就直接收掉,留着只是白占内存(图片是 dataURL,可能有好几 MB)
437
- if (printed) setTimeout(remove, 60000)
438
- else remove()
439
- resolve(printed)
440
- })
441
- })
442
- }
443
-
444
- // 调 print() 并判断浏览器是否真的进入了打印流程。
445
- // 微信等内嵌浏览器没有打印栈,print() 会被静默吞掉,只能靠 beforeprint 判断
446
- printWindow (win) {
447
- if (!win || typeof win.print !== 'function') return false
448
- let engaged = false
449
- const mark = () => { engaged = true }
450
- const mql = win.matchMedia ? win.matchMedia('print') : null
451
- const listen = (target, on) => {
452
- if (!target) return
453
- try { target[on ? 'addEventListener' : 'removeEventListener']('beforeprint', mark) } catch (e) {}
454
- }
455
- listen(win, true)
456
- listen(window, true)
457
- if (mql && mql.addListener) mql.addListener(mark)
458
- try {
459
- win.focus()
460
- win.print()
461
- } catch (e) {
462
- engaged = false
463
- }
464
- listen(win, false)
465
- listen(window, false)
466
- if (mql && mql.removeListener) mql.removeListener(mark)
467
- return engaged
468
- }
469
-
470
- // 用 pdf.js 把 PDF 每页画成图片,绕开浏览器自带的 PDF 阅读器
471
- pdfDocToImages (pdfDoc, scale = 3) {
472
- if (!pdfDoc || typeof pdfDoc.getPage !== 'function') return Promise.resolve([])
473
- const images = []
474
- let chain = Promise.resolve()
475
- for (let num = 1; num <= pdfDoc.numPages; num++) {
476
- chain = chain
477
- .then(() => pdfDoc.getPage(num))
478
- .then(page => {
479
- const viewport = page.getViewport({ scale })
480
- const canvas = document.createElement('canvas')
481
- canvas.width = viewport.width
482
- canvas.height = viewport.height
483
- const task = page.render({ canvasContext: canvas.getContext('2d'), viewport })
484
- const done = task && task.promise ? task.promise : Promise.resolve()
485
- return done.then(() => images.push(canvas.toDataURL('image/jpeg', 0.92)))
486
- })
487
- }
488
- return chain.then(() => images)
489
- }
490
-
491
- getBrowserVersion () {
492
- const ua = window.navigator.userAgent
493
- if (window.opera) {
494
- return {
495
- browser: 'opera',
496
- version: window.opera.version()
497
- }
498
- } else if (/AppleWebKit\/(\S+)/.test(ua)) {
499
- if (/Chrome\/(\S+)/.test(ua)) {
500
- return {
501
- brower: 'chrome',
502
- version: RegExp.$1
503
- }
504
- } else if (/Version\/(\S+)/.test(ua)) {
505
- return {
506
- brower: 'safari',
507
- version: RegExp.$1
508
- }
509
- } else {
510
- return {
511
- brower: 'safari'
512
- }
513
- }
514
- } else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
515
- return {
516
- brower: 'konq',
517
- version: RegExp.$1
518
- }
519
- } else if (/rv:([^\\)]+)\) Gecko\/\d{8}/.test(ua)) {
520
- if (/Firefox\/(\S+)/.test(ua)) {
521
- // 火狐浏览器
522
- return {
523
- brower: 'firefox',
524
- version: RegExp.$1
525
- }
526
- } else {
527
- // 其他情况
528
- }
529
- } else if (/MSIE ([^;]+)/.test(ua)) {
530
- return {
531
- brower: 'ie',
532
- version: RegExp.$1
533
- }
534
- }
535
- return {}
536
- }
537
-
538
- runExp (exp, point) {
539
- if (!point) point = this
540
- exp = ' ' + exp.trim()
541
- // eslint-disable-next-line
542
- const quickRegex = /([\s\+\-\*\/%&\(\|\^!\*~]\s*?)([a-zA-Z_$][a-zA-Z_$0-9]*?)/g
543
- exp = exp.replace(quickRegex, (a, b, c) => {
544
- return b + 'point.' + c
545
- })
546
- // eslint-disable-next-line no-new-func
547
- const func = new Function('point', 'return ' + exp)
548
- return func(point)
549
- }
550
-
551
- // 根据高级查询获取js表达式
552
- runExpByScope (scopeList, data = {}) {
553
- let str = ''
554
- // Form data is null before its request resolves. Treat it as an empty record
555
- // while evaluating visibility rules so legacy product pages can render.
556
- const record = data || {}
557
- // 由调用方 .call(组件实例) 进来,#{key} 相对组件实例取值
558
- const that = this
559
- // 当前时间变量:高级筛选里写 $[currentDate.date]/$[currentDate.month] 等(或 #{...})解析为当前时间,供日期判断
560
- const getTimeVar = (key) => {
561
- switch (key) {
562
- case 'now':
563
- case 'currentDate.time': return formula.NOW('YYYY-MM-DD HH:mm:ss') // 当前时间(精确到秒)
564
- case 'today':
565
- case 'currentDate':
566
- case 'currentDate.date': return formula.NOW('YYYY-MM-DD') // 系统日期(精确到天)
567
- case 'currentDate.month': return formula.NOW('YYYY-MM') // 当前月份(精确到月)
568
- case 'currentDate.year': return formula.NOW('YYYY') // 当前年度(精确到年)
569
- default: return null
570
- }
571
- }
572
- try {
573
- const router = window.router
574
- // 高级查询里的 user.roleCode 取当前角色。菜单直达路由没有 role 段(末段是 menuKey),
575
- // 直读 params.role 会得到 undefined,生成的表达式变成 "undefined"=='xxx'、按角色过滤
576
- // 的数据范围整条失效。统一走 resolveRouteRole:URL 优先、其次 localStorage。
577
- const currentRoleCode = resolveRouteRole(router.currentRoute.value)
578
- scopeList.forEach((scope, index) => {
579
- let { mode, values = '', field } = scope
580
- const fieldInfo = this.$meta ? this.$meta.getAttribute(field) || {} : {}
581
- const { dataType, optionType, isMultiple } = fieldInfo
582
- if (/#\{(.+)\}/.test(values)) {
583
- try {
584
- const valueKey = values.replace(/^#\{(.+)\}$/, (match, key) => key)
585
- const timeVar = getTimeVar(valueKey)
586
- // 显式传作用域,不能靠直接 eval 捕获 that(见 eval-with.js)
587
- values = timeVar != null ? timeVar : (evalWith(`that.${valueKey}`, { that }) || evalWith(`that.$query.${valueKey}`, { that }) || evalWith(`that.$tab.${valueKey}`, { that }) || '')
588
- } catch (error) {
589
- values = ''
590
- }
591
- }
592
- if (/\$\[(.+)\]/.test(values)) {
593
- try {
594
- const valueKey = values.replace(/\$\[(.+)\]/, (match, key) => key)
595
- const timeVar = getTimeVar(valueKey)
596
- // 显式传作用域,不能靠直接 eval 捕获 that(见 eval-with.js)
597
- values = timeVar != null ? timeVar : (evalWith(`that.${valueKey}`, { that }) || evalWith(`that.$query.${valueKey}`, { that }) || evalWith(`that.$tab.${valueKey}`, { that }) || '')
598
- } catch (err) {
599
- console.error(err)
600
- values = ''
601
- }
602
- }
603
- let valueArr = []
604
- if (typeof values === 'string') {
605
- values = values.replace(/[\r\n"]/g, '').replace(/\\$/g, '/')
606
- if (!values) valueArr = []
607
- else valueArr = values.split(',')
608
- }
609
- str += `${scope.lb || ''}`
610
- const user = window.store.state.user.user || {}
611
- // user.xxx 这类字段相对当前用户取值:显式传作用域,不能靠直接 eval 捕获 user(见 eval-with.js)
612
- let fieldValue = field === 'user.roleCode' ? currentRoleCode : field.includes('user.') ? evalWith(field, { user }) : record[field] || ''
613
- // eslint-disable-next-line no-new-wrappers
614
- fieldValue = typeof fieldValue === 'string' ? fieldValue.replace(/[\r\n"]/g, '').replace(/\\$/g, '/') : fieldValue
615
- if (mode === 'is_null' || mode === 'not_null' || mode === 'equal' || mode === 'not_equal') {
616
- str += `"${fieldValue}"${mode === 'is_null' || mode === 'equal' ? '==' : '!='}${mode === 'equal' || mode === 'not_equal' ? `'${values}'` : '""'}`
617
- } else if (['greater', 'greater_equal', 'less', 'less_equal'].includes(mode)) {
618
- str += `${fieldValue}${mode === 'greater' ? '>' : mode === 'greater_equal' ? '>=' : mode === 'less' ? '<' : '<='}${values}`
619
- } else if (['in', 'not_in', 'in_scope', 'not_in_scope'].includes(mode)) {
620
- if (dataType) {
621
- if (dataType === 'list' || dataType === 'object' || dataType === 'user' || dataType === 'dept') {
622
- const multiple = isMultiple === '1' || optionType === 'multibox' || optionType === 'multiList'
623
- str += `${mode === 'not_in' || mode === 'not_in_scope' ? '!' : ''}${multiple && fieldValue ? `"${fieldValue.split(',')}".includes("${values}")` : `${JSON.stringify(valueArr)}.includes("${fieldValue}")`}`
624
- } else {
625
- str += `${mode === 'not_in' || mode === 'not_in_scope' ? '!' : ''}${`"${fieldValue}".includes("${values}")`}`
626
- }
627
- } else {
628
- str += `${mode === 'not_in' || mode === 'not_in_scope' ? '!' : ''}${valueArr.length > 1 ? `${JSON.stringify(valueArr)}.includes("${fieldValue}")` : `"${fieldValue}".includes("${values}")`}`
629
- }
630
- } else if (['begin', 'end'].includes(mode)) {
631
- str += `"${fieldValue}".${mode === 'begin' ? 'startsWith' : 'endsWith'}('${values}')`
632
- } else if (['date_greater', 'date_greater_equal', 'date_less', 'date_less_equal'].includes(mode)) {
633
- str += `"${fieldValue}"${mode === 'date_greater' ? '>' : mode === 'date_greater_equal' ? '>=' : mode === 'date_less' ? '<' : '<='}'${values}'`
634
- } else {
635
- str += `${mode === 'not_like' ? '!' : ''}"${fieldValue}".includes(${values})`
636
- }
637
- str += `${scope.rb || ''} ${index === scopeList.length - 1 ? '' : scope.link === 'or' ? '||' : '&&'} `
638
- })
639
- // eslint-disable-next-line no-eval
640
- const flag = eval(str)
641
- return flag
642
- } catch (error) {
643
- console.error(error + ',str=' + str)
644
- return false
645
- }
646
- }
647
-
648
- awaitWrap (promise) {
649
- return promise
650
- .then(data => ({ error: null, data }))
651
- .catch(error => ({ error, data: null }))
652
- }
653
-
654
- isLoadLibrary (systemCode) {
655
- return this.loadSystemLibrarys[systemCode]
656
- }
657
-
658
- loadJS (options) {
659
- var url = options.url
660
- var callback = typeof options.callback === 'function' ? options.callback : function () {}
661
-
662
- const script = document.createElement('script')
663
- script.type = 'text/javascript'
664
- // IE
665
- if (script.readyState) {
666
- script.onreadystatechange = function () {
667
- if (script.readyState === 'loaded' || script.readyState === 'complete') {
668
- script.onreadystatechange = null
669
- callback()
670
- }
671
- }
672
- } else {
673
- // 其他浏览器
674
- script.onload = function () {
675
- callback()
676
- }
677
- }
678
- // 失败也必须回调(同 loadCss 的 onerror 处理)。原先只绑 onload:URL 404 / 跨域被拦 /
679
- // 文件服务抖动时回调永不触发 → usePlugin() 的 promise 永不 resolve → 等它的地方**无限期挂住**,
680
- // 且 util.loadPlugins 把这个永远 pending 的 promise 缓存了,重试也一样挂、连报错都没有。
681
- // 现在回调照走(拿到的 globalThis.<lib> 是 undefined,调用方能自己判空),并留一条 error 定位。
682
- script.onerror = function () {
683
- console.error('[util.loadJS] 加载失败:' + url)
684
- callback()
685
- }
686
- script.src = url
687
- document.getElementsByTagName('head')[0].appendChild(script)
688
- }
689
-
690
- loadJs (file, cb) {
691
- const that = this
692
- if (!that.oldLibrary) that.oldLibrary = {}
693
- if (window.library) {
694
- for (const key in window.library) {
695
- if (!that.oldLibrary[key]) that.oldLibrary[key] = {}
696
- for (const compKey in (window.library[key] || {})) {
697
- try {
698
- that.oldLibrary[key][compKey] = cloneDeep(window.library[key][compKey])
699
- } catch (error) {
700
- console.error(compKey, error)
701
- }
702
- }
703
- }
704
- }
705
- const script = document.createElement('script')
706
- script.type = 'text/javascript'
707
- script.src = file
708
- script.onload = async function (js) {
709
- const componentLibrary = window.library
710
- // 远程库 install 去重(判据 name@version),2026-09-02 补。
711
- //
712
- // 为什么必须有:window.library 只累加、从不清空,而本 onload 会遍历它的**每一个** key。
713
- // · 一个系统配了 N 个 js(产品库 + 项目库 + …)是串行加载的 → 第 1 个库被 install N 次;
714
- // · 产品若已被**本地打进壳**(dev 壳的 app.use(hr)),它不进 window.library,
715
- // 平台看不见 ⇒ 远程那份照样下载并再 install 一次。
716
- // 双装时 app.component 有 `if (!component)` 守卫不会重,但 store.registerModule /
717
- // router.addRoute 没守卫 —— 实测就是 [vuex] duplicate getter key: selectedPersons。
718
- // 证据见 test/unit/load-js-install-dedup.test.js。
719
- //
720
- // ⚠️ 只在 app 已就绪时记账:useVuePlugin 在 _app 为空时是直接 return 的空操作,
721
- // 那种情况下记了账就等于永久跳过,库再也装不上。
722
- // 产品库自己的 install 也应查同一张表(见 docs/PROJECT-GUIDE.md),
723
- // 覆盖本地打包那条平台看不见的路——两边合起来才是完整去重。
724
- if (!window.__epxInstalledLibs__) window.__epxInstalledLibs__ = {}
725
- const appReady = !!getApp()
726
- for (const key in componentLibrary) {
727
- const lib = componentLibrary[key]
728
- const sig = key + '@' + ((lib && lib.version) || '')
729
- if (!window.__epxInstalledLibs__[sig]) {
730
- // ⚠️ 记账必须在 install **之后**。
731
- // 产品库自己的 install 开头也查这张表(PROJECT-GUIDE 约定的幂等写法:
732
- // `if (marks[sig]) return`)。先记账就会让它一进门就 return —— 表里有记录、
733
- // 组件一个都没注册,症状是 window.__epxInstalledLibs__ 有 hr@3.0.1 但
734
- // app 里 0 个 epx-hr-* 组件,且完全静默。
735
- // 2026-09-04 product/hr 实测(两侧去重分两次提交,撞在一起才暴露)。
736
- if (appReady) {
737
- if (!installingLibraries.has(sig)) {
738
- const install = Promise.resolve().then(() => useVuePlugin(lib, { store: window.store }))
739
- .then(() => { window.__epxInstalledLibs__[sig] = { source: 'remote', at: Date.now() } })
740
- .finally(() => installingLibraries.delete(sig))
741
- installingLibraries.set(sig, install)
742
- }
743
- try { await installingLibraries.get(sig) } catch (error) { console.error(error) }
744
- }
745
- }
746
- const components = componentLibrary[key] || {}
747
- for (const key in components) {
748
- if (!window.componentLibrary[key]) window.componentLibrary[key] = components[key]
749
- }
750
- if (!isEqual(componentLibrary[key], that.oldLibrary[key])) {
751
- // 修复:原写在 that(util 单例)上,全仓没有任何代码读 that.productLibrary,
752
- // 改到 window 上才是这行代码的原意。develop 同期把 loadLibrary.js 的产品库识别
753
- // 换成了引用快照对比(不再依赖这个全局),这里目前也没有实际读者——按 develop
754
- // 原样保留这处写入(无害),不额外删減。
755
- if (componentLibrary[key] && componentLibrary[key].version) window.productLibrary = componentLibrary[key]
756
- }
757
- }
758
-
759
- cb && cb()
760
- }
761
- document.body.appendChild(script)
762
- }
763
-
764
- loadCss (options) {
765
- var url = options.url
766
- var callback = typeof options.callback === 'function' ? options.callback : function () {}
767
- var id = options.id
768
- var node = document.createElement('link')
769
- var supportOnload = 'onload' in node
770
- var isOldWebKit = +navigator.userAgent.replace(/.*(?:AppleWebKit|AndroidWebKit)\/?(\d+).*/i, '$1') < 536 // webkit旧内核做特殊处理
771
- var protectNum = 300000 // 阈值10分钟,一秒钟执行pollCss 500次
772
-
773
- node.rel = 'stylesheet'
774
- node.type = 'text/css'
775
- node.href = url
776
- if (typeof id !== 'undefined') {
777
- node.id = id
778
- }
779
- document.getElementsByTagName('head')[0].appendChild(node)
780
-
781
- // for Old WebKit and Old Firefox
782
- if (isOldWebKit || !supportOnload) {
783
- // Begin after node insertion
784
- const timer = setTimeout(function () {
785
- pollCss(node, callback, 0)
786
- clearTimeout(timer)
787
- }, 1)
788
- return
789
- }
790
-
791
- if (supportOnload) {
792
- node.onload = onload
793
- node.onerror = function () {
794
- // 加载失败(404)
795
- onload()
796
- }
797
- } else {
798
- node.onreadystatechange = function () {
799
- if (/loaded|complete/.test(node.readyState)) {
800
- onload()
801
- }
802
- }
803
- }
804
-
805
- function onload () {
806
- // 确保只跑一次下载操作
807
- node.onload = node.onerror = node.onreadystatechange = null
808
-
809
- // 清空node引用,在低版本IE,不清除会造成内存泄露
810
- node = null
811
-
812
- callback()
813
- }
814
-
815
- // 循环判断css是否已加载成功
816
- /*
817
- * @param node -- link节点
818
- * @param callback -- 回调函数
819
- * @param step -- 计步器,避免无限循环
820
- */
821
- function pollCss (node, callback, step) {
822
- var sheet = node.sheet
823
- var isLoaded
824
-
825
- step += 1
826
-
827
- // 保护,大于10分钟,则不再轮询
828
- if (step > protectNum) {
829
- isLoaded = true
830
-
831
- // 清空node引用
832
- node = null
833
-
834
- callback()
835
- return
836
- }
837
-
838
- if (isOldWebKit) {
839
- // for WebKit < 536
840
- if (sheet) {
841
- isLoaded = true
842
- }
843
- } else if (sheet) {
844
- // for Firefox < 9.0
845
- try {
846
- if (sheet.cssRules) {
847
- isLoaded = true
848
- }
849
- } catch (ex) {
850
- // 火狐特殊版本,通过特定值获知是否下载成功
851
- // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
852
- // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
853
- // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
854
- if (ex.name === 'NS_ERROR_DOM_SECURITY_ERR') {
855
- isLoaded = true
856
- }
857
- }
858
- }
859
-
860
- const timer = setTimeout(function () {
861
- if (isLoaded) {
862
- // 延迟20ms是为了给下载的样式留够渲染的时间
863
- callback()
864
- } else {
865
- pollCss(node, callback, step)
866
- }
867
- clearTimeout(timer)
868
- }, 20)
869
- }
870
- }
871
-
872
- dataURLtoBlob (dataurl) {
873
- var arr = dataurl.split(',')
874
- var mime = arr[0].match(/:(.*?);/)[1]
875
- var bstr = atob(arr[1])
876
- var n = bstr.length
877
- var u8arr = new Uint8Array(n)
878
- while (n--) {
879
- u8arr[n] = bstr.charCodeAt(n)
880
- }
881
- return new Blob([u8arr], { type: mime })
882
- }
883
-
884
- dataURLtoFile (dataurl, filename) {
885
- var arr = dataurl.split(','); var mime = arr[0].match(/:(.*?);/)[1]
886
- var bstr = atob(arr[1]); var n = bstr.length; var u8arr = new Uint8Array(n)
887
- while (n--) {
888
- u8arr[n] = bstr.charCodeAt(n)
889
- }
890
- const blob = new Blob([u8arr], { type: mime })
891
-
892
- return new File([blob], filename, { type: mime })
893
- }
894
-
895
- usePlugin (plugin) {
896
- const loadFile = (files) => {
897
- let fileNum = 0
898
- return new Promise(resolve => {
899
- for (const file of files) {
900
- if (file.includes('api.map.baidu.com') || file.includes('api.tianditu.gov.cn')) {
901
- this.loadJS({
902
- url: file,
903
- callback () {
904
- fileNum++
905
- if (fileNum === files.length) {
906
- return resolve()
907
- }
908
- }
909
- })
910
- } else if (file.includes('.js')) {
911
- this.loadJS({
912
- url: file,
913
- callback () {
914
- fileNum++
915
- if (fileNum === files.length) {
916
- return resolve()
917
- }
918
- }
919
- })
920
- } else if (file.includes('.css')) {
921
- this.loadCss({
922
- url: file,
923
- callback () {
924
- fileNum++
925
- if (fileNum === files.length) {
926
- return resolve()
927
- }
928
- }
929
- })
930
- } else {
931
- fileNum++
932
- if (fileNum === files.length) {
933
- return resolve()
934
- }
935
- }
936
- }
937
- })
938
- }
939
- if (this.loadPlugins[plugin]) return this.loadPlugins[plugin]
940
- const publicPath = this.getPublicPath()
941
- const jsDir = this.getStaticJsDir()
942
- if (plugin === 'luckysheet') {
943
- const loadPromise = loadFile([publicPath + 'luckysheet/pluginsCss.css', publicPath + 'luckysheet/plugins.css', publicPath + 'luckysheet/luckysheet.css', publicPath + '/luckysheet/iconfont.css', publicPath + 'luckysheet/plugin.js', publicPath + 'luckysheet/luckysheet.umd.js'])
944
- .then(() => globalThis.luckysheet)
945
- this.loadPlugins[plugin] = loadPromise
946
- return loadPromise
947
- } else if (plugin === 'BMap') {
948
- const loadPromise = new Promise(resolve => {
949
- const ak = window.BMAP_AK || 'zySbI4xZUSRVrbb9Ev3H5xg4hA1md1gh'
950
- loadFile([`//api.map.baidu.com/api?v=3.0&ak=${ak}&callback=loadBMapCallback`])
951
- .then(() => window.BMap)
952
- window.loadBMapCallback = () => {
953
- resolve(window.BMap)
954
- }
955
- })
956
-
957
- this.loadPlugins[plugin] = loadPromise
958
- return loadPromise
959
- } else if (plugin === 'TMap') {
960
- const loadPromise = new Promise(resolve => {
961
- const ak = window.TMAP_AK || '83086471ebe2a3e5292841ecaa3f7055'
962
- loadFile([`//api.tianditu.gov.cn/api?v=4.0&tk=${ak}&callback=loadTMapCallback`])
963
- .then(() => window.T && resolve(window.T))
964
- window.loadTMapCallback = () => {
965
- resolve(window.T)
966
- }
967
- })
968
-
969
- this.loadPlugins[plugin] = loadPromise
970
- return loadPromise
971
- } else if (plugin === 'tinymce') {
972
- const loadPromise = loadFile([publicPath + jsDir + 'tinymce/tinymce.min.js'])
973
- .then(() => window.tinymce)
974
- this.loadPlugins[plugin] = loadPromise
975
- return loadPromise
976
- } else if (plugin === 'onlyoffice') {
977
- // ONLYOFFICE Document Server 地址在 config.js 中配置(window.ONLYOFFICE_URL),未配置时不加载
978
- const officeUrl = this.getOfficeServerUrl()
979
- if (!officeUrl) return Promise.reject(new Error('未配置 ONLYOFFICE_URL'))
980
- const loadPromise = loadFile([`${officeUrl}/web-apps/apps/api/documents/api.js`])
981
- .then(() => {
982
- if (!window.DocsAPI) throw new Error('文档服务加载失败')
983
- return window.DocsAPI
984
- })
985
- .catch(err => {
986
- // 失败的 promise 不能留在缓存里,否则后续重试直接拿到旧的失败结果
987
- delete this.loadPlugins[plugin]
988
- throw err
989
- })
990
- this.loadPlugins[plugin] = loadPromise
991
- return loadPromise
992
- } else if (plugin === 'echarts') {
993
- const loadPromise = loadFile([publicPath + jsDir + 'echarts.min.js?v=6.0.0-full', publicPath + jsDir + 'ecStat.min.js'])
994
- .then(() => globalThis.echarts)
995
- this.loadPlugins[plugin] = loadPromise
996
- return loadPromise
997
- } else if (plugin === 'pdf') {
998
- const browerVersion = this.getBrowserVersion()
999
- const isOldChrome = (browerVersion.brower === 'chrome' || browerVersion.brower === 'firefox') && browerVersion.version.split('.')[0] < 92
1000
- const isIos = this.isIos()
1001
- const loadPromise = loadFile([publicPath + jsDir + plugin + (isOldChrome || isIos ? '.2.4.456.js' : '.min.js')])
1002
- .then(() => {
1003
- const pdf = globalThis.pdfjsLib
1004
- pdf.disableWorker = true
1005
- pdf.GlobalWorkerOptions.workerSrc = publicPath + jsDir + (isOldChrome || isIos ? 'pdf.worker.js' : 'pdf.worker.min.js')
1006
- return pdf
1007
- })
1008
- this.loadPlugins[plugin] = loadPromise
1009
- return loadPromise
1010
- } else {
1011
- const loadPromise = loadFile([publicPath + jsDir + plugin + '.min.js'])
1012
- .then(() => global[plugin])
1013
- this.loadPlugins[plugin] = loadPromise
1014
- return loadPromise
1015
- }
1016
- }
1017
-
1018
- utf8to16 (str) {
1019
- var hexOut = false
1020
- var out, i, len, c
1021
- var char2, char3
1022
- var charCode
1023
-
1024
- out = hexOut ? [] : ''
1025
- len = str.length
1026
- i = 0
1027
- while (i < len) {
1028
- c = hexOut ? str[i++] : str.charCodeAt(i++)
1029
- switch (c >> 4) {
1030
- case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
1031
- // 0xxxxxxx
1032
- hexOut ? out.push(str[i - 1]) : out += str.charAt(i - 1)
1033
- break
1034
- case 12: case 13:
1035
- // 110x xxxx 10xx xxxx
1036
- char2 = hexOut ? str[i++] : str.charCodeAt(i++)
1037
- charCode = ((c & 0x1F) << 6) | (char2 & 0x3F); hexOut ? out.push(charCode) : out += String.fromCharCode(charCode)
1038
- break
1039
- case 14:
1040
- // 1110 xxxx 10xx xxxx 10xx xxxx
1041
- char2 = hexOut ? str[i++] : str.charCodeAt(i++)
1042
- char3 = hexOut ? str[i++] : str.charCodeAt(i++)
1043
- charCode = ((c & 0x0F) << 12) |
1044
- ((char2 & 0x3F) << 6) |
1045
- ((char3 & 0x3F) << 0)
1046
- hexOut ? out.push(charCode) : out += String.fromCharCode(charCode)
1047
- break
1048
- }
1049
- }
1050
-
1051
- return out
1052
- }
1053
-
1054
- genId (length = 3) {
1055
- return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
1056
- }
1057
-
1058
- uuid () {
1059
- if (crypto?.randomUUID) {
1060
- return crypto.randomUUID()
1061
- } else {
1062
- const S4 = () => {
1063
- return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
1064
- }
1065
- return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4())
1066
- }
1067
- }
1068
-
1069
- copyToClipboard (text, callback) {
1070
- if (navigator.clipboard) {
1071
- navigator.clipboard.writeText(text)
1072
- // eslint-disable-next-line standard/no-callback-literal
1073
- callback && callback(true)
1074
- return
1075
- }
1076
-
1077
- var copyInput = document.getElementById('COPY_INPUT')
1078
- if (!copyInput) {
1079
- copyInput = document.createElement('input')
1080
- copyInput.setAttribute('id', 'COPY_INPUT')
1081
- copyInput.style.position = 'fixed'
1082
- copyInput.style.left = '-100%'
1083
- copyInput.style.top = '0'
1084
- copyInput.style.zIndex = -100
1085
- copyInput.style.opacity = 0
1086
- document.body.appendChild(copyInput)
1087
- }
1088
- copyInput.value = text
1089
- copyInput.focus()
1090
- copyInput.select()
1091
-
1092
- // document.execCommand 可能会被废弃
1093
- if (document.execCommand('copy')) {
1094
- document.execCommand('copy')
1095
- }
1096
-
1097
- copyInput.blur()
1098
-
1099
- // eslint-disable-next-line standard/no-callback-literal
1100
- callback && callback(true)
1101
- }
1102
-
1103
- getQueryValue (path, obj) {
1104
- if (!path || !obj) return ''
1105
- const value = path.split('.').reduce((acc, key) => acc && acc[key], obj)
1106
- return isNil(value) ? '' : value
1107
- }
1108
-
1109
- getValueByPath (path, obj) {
1110
- const keys = []
1111
- let currentKey = ''
1112
- let inBrackets = false
1113
-
1114
- for (const char of path) {
1115
- if (char === '[') {
1116
- inBrackets = true
1117
- if (currentKey) {
1118
- keys.push(currentKey)
1119
- currentKey = ''
1120
- }
1121
- } else if (char === ']') {
1122
- inBrackets = false
1123
- if (currentKey) {
1124
- const newKey = currentKey.replace(/^['"](.+)['"]$/, '$1')
1125
- keys.push(newKey)
1126
- currentKey = ''
1127
- }
1128
- } else if (char === '.' && !inBrackets) {
1129
- if (currentKey) {
1130
- keys.push(currentKey)
1131
- currentKey = ''
1132
- }
1133
- } else {
1134
- currentKey += char
1135
- }
1136
- }
1137
-
1138
- if (currentKey) {
1139
- keys.push(currentKey)
1140
- }
1141
- return keys.reduce((acc, key) => acc && acc[key], obj)
1142
- }
1143
-
1144
- /**
1145
- * 解析操作按钮「后续事件」的参数配置 afterTriggerParams
1146
- * 支持的占位符:
1147
- * $[res] 取接口返回值本身
1148
- * $[res.a.b] 取接口返回值的深层字段
1149
- * $[key] 取组件实例上的值,取不到再依次回退 $tab / $query / $pageParams / $compData,支持 a.b.c 深层路径
1150
- * #[field] 取数据字段值
1151
- * 整串就是一个占位符时保留取到的原始类型(对象 / 数组 / 数字),混在文案里则替换为文本
1152
- * @param {Object} afterTriggerParams 页面配置的参数对象
1153
- * @param {Object} [options]
1154
- * @param {*} [options.res] 接口返回值,供 $[res] / $[res.x] 取值
1155
- * @param {Object|Array} [options.data] #[field] 的取值数据:传对象取该条的字段值,
1156
- * 传数组视为多条记录(批量操作),各条的值用逗号拼接
1157
- * @param {Object} ctx 组件实例,作为 $[key] 的取值上下文
1158
- * @returns {Object} 解析后的新参数对象,不修改入参
1159
- */
1160
- formatAfterTriggerParams (afterTriggerParams, { res, data } = {}, ctx = {}) {
1161
- const _params = { ...afterTriggerParams }
1162
- // data 传数组 = 多条记录(批量操作),传对象 = 单条数据
1163
- const isMultiple = Array.isArray(data)
1164
- const rows = [].concat(data === undefined ? [] : data).filter(item => !isNil(item))
1165
- const paramData = { ...ctx.$tab, ...ctx.$query, ...ctx.$pageParams, ...ctx.$compData }
1166
- const blank = value => (isNil(value) ? '' : value)
1167
- const getResValue = path => blank(this.getValueByPath(path, res))
1168
- const getCtxValue = path => {
1169
- const value = this.getValueByPath(path, ctx)
1170
- // 组件实例上没有(或为空)时才回退到页签 / 路由 / 页面 / 组件参数
1171
- return blank(isNil(value) || value === '' ? this.getValueByPath(path, paramData) : value)
1172
- }
1173
- const getDataValue = path => {
1174
- const values = rows.map(item => this.getValueByPath(path, item))
1175
- // 多条记录按逗号拼接(不过滤空值,保证多个 #[] 参数的列一一对应),与 $formatParams 的 #[] 规则一致
1176
- return isMultiple ? values.map(item => blank(item)).join(',') : blank(values[0])
1177
- }
1178
- // 没传 res 的调用点不占用 res 前缀,按普通取值处理
1179
- const hasRes = res !== undefined
1180
- const getValue = (flag, path) => {
1181
- if (flag === '#') return getDataValue(path)
1182
- if (!hasRes) return getCtxValue(path)
1183
- if (path === 'res') return blank(res)
1184
- if (path.indexOf('res.') === 0) return getResValue(path.slice(4))
1185
- return getCtxValue(path)
1186
- }
1187
- for (const key in _params) {
1188
- const config = _params[key]
1189
- if (typeof config !== 'string') continue
1190
- // 整串只有一个占位符时直接取值,避免 String.replace 把对象等类型转成文本
1191
- const isWhole = /^[$#]\[.+\]$/.test(config) && config.indexOf('$[', 2) === -1 && config.indexOf('#[', 2) === -1
1192
- let value = config
1193
- if (isWhole) {
1194
- value = getValue(config[0], config.slice(2, -1))
1195
- } else if (/[$#]\[.+\]/.test(config)) {
1196
- value = config.replace(/([$#])\[((?:[^[\]]|\[[^\]]*\])+)\]/g, (match, flag, path) => getValue(flag, path))
1197
- }
1198
- _params[key] = blank(value)
1199
- }
1200
- return _params
1201
- }
1202
-
1203
- evalExpression (el, data, errorValue = false) {
1204
- const formulaLibrary = {}
1205
- for (const key in formula) {
1206
- formulaLibrary[key] = formula[key]
1207
- }
1208
- const _data = {}
1209
- for (const key in data) {
1210
- if (key && key !== '[object Object]') {
1211
- if (!key.includes('#')) {
1212
- if (key.includes('|')) {
1213
- const newKey = key.replaceAll('|', '$')
1214
- el.replaceAll(key.replaceAll('|', '.'), newKey)
1215
- _data[newKey] = data[key]
1216
- } else {
1217
- _data[key] = data[key]
1218
- }
1219
- }
1220
- }
1221
- }
1222
- try {
1223
- // eslint-disable-next-line no-new-func
1224
- return new Function(...Object.keys(formulaLibrary), ...Object.keys(_data), `return ${el}`)(...Object.values(formulaLibrary), ...Object.values(_data))
1225
- } catch (e) {
1226
- // ⚠️ 这个 catch 是本函数几乎所有调用点共用的唯一失败通道(showIf / limitConditionEl /
1227
- // dataFormat / markerCondition / showCondition,以及 $formatContent 内部的 {{...}} 分支等),
1228
- // 不能整体降噪——公式/条件表达式真写错了(语法错、访问链错)时,控制台报错是唯一线索。
1229
- //
1230
- // 只对 ReferenceError("xxx is not defined")单独降级:这类错误专指表达式引用了一个
1231
- // 不在本次 data/公式库作用域内的标识符,最典型的是 $formatContent 默认作用域
1232
- // (...this.$tab, ...pageParams, ...this.$query, ...this.$compData)不包含 appConfig /
1233
- // clientConfig 这类只按实例上下文解析的绑定——每次页面加载只要 $configs 里有形如
1234
- // {{appConfig['CLIENT|CODE']}} 的占位符就会稳定触发。这是预期路径而非错误:component.js
1235
- // $formatContent 自己的注释写明「表达式执行失败时保留原内容,占位符可继续交由下面的
1236
- // $[...] 分支/$formatUrl 等按实例上下文解析」,失败后 el/占位符原样保留,不影响功能。
1237
- // 其余错误类型(SyntaxError 等语法写挂了、TypeError 等运行时访问出错)大概率是页面配置
1238
- // 里表达式真的写错了,继续用 error 级别,不跟着一起静音。
1239
- if (e instanceof ReferenceError) {
1240
- console.debug('[evalExpression] 标识符不在求值作用域内(占位符可能待其他解析器按实例上下文处理):', e.message, '| 表达式:', el)
1241
- } else {
1242
- console.error(e)
1243
- }
1244
- return errorValue
1245
- }
1246
- }
1247
-
1248
- openWindow (url) {
1249
- const a = document.createElement('a')
1250
- a.href = url
1251
- a.target = '_blank'
1252
- document.body.appendChild(a)
1253
- a.click()
1254
- document.body.removeChild(a)
1255
- }
1256
-
1257
- encrypt (content, key, { type } = {}) {
1258
- if (type === 'aes') {
1259
- return import('crypto-js').then(CryptoJS => {
1260
- const base64Key = atob(key)
1261
- const aesKey = CryptoJS.enc.Utf8.parse(base64Key.toString(CryptoJS.enc.Utf8))
1262
- const encrypted = CryptoJS.AES.encrypt(content, aesKey, {
1263
- mode: CryptoJS.mode.CBC,
1264
- padding: CryptoJS.pad.Pkcs7
1265
- })
1266
- return encrypted.toString()
1267
- })
1268
- } else {
1269
- return import('jsencrypt/bin/jsencrypt.min').then(module => {
1270
- const JSEncrypt = module.default
1271
- const encrypt = new JSEncrypt()
1272
- encrypt.setPublicKey(key)
1273
- return encrypt.encrypt(content)
1274
- })
1275
- }
1276
- }
1277
-
1278
- decrypt (content, { type, key }) {
1279
- if (type === 'aes') {
1280
- return import('crypto-js').then(CryptoJS => {
1281
- const base64Key = atob(key)
1282
- const aesKey = CryptoJS.enc.Utf8.parse(base64Key.toString(CryptoJS.enc.Utf8))
1283
- const decrypted = CryptoJS.AES.decrypt(content, aesKey, {
1284
- iv: key,
1285
- mode: CryptoJS.mode.CBC,
1286
- padding: CryptoJS.pad.Pkcs7
1287
- })
1288
- return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
1289
- })
1290
- } else {
1291
- return import('jsencrypt/bin/jsencrypt.min').then(module => {
1292
- const JSEncrypt = module.default
1293
- const encrypt = new JSEncrypt()
1294
- encrypt.setPrivateKey(key)
1295
- return encrypt.decrypt(content)
1296
- })
1297
- }
1298
- }
1299
-
1300
- appendCss (id, css) {
1301
- const style = document.createElement('style')
1302
- style.id = 'style_' + id
1303
- style.textContent = css.replace(/:current/g, '.css_' + id).replace(/url\s*\(([\w+-/.\s]+)\)/g, (str, $1) => {
1304
- return `url(${util.getFileUrl($1)})`
1305
- })
1306
-
1307
- style.setAttribute('type', 'text/css')
1308
- document.head.appendChild(style)
1309
- }
1310
-
1311
- removeCss (id) {
1312
- const element = document.getElementById('style_' + id)
1313
- if (element) document.head.removeChild(element)
1314
- }
1315
- }
1316
-
1317
- const util = new Util()
1318
-
1319
- // 环境工具绑定单例,按函数导入时保留 this 和加载缓存。
1320
- export const getSystemMeta = (...args) => util.getSystemMeta(...args)
1321
- export const getFileUrl = (...args) => util.getFileUrl(...args)
1322
- export const getSysId = (...args) => util.getSysId(...args)
1323
- export const toLogin = (...args) => util.toLogin(...args)
1324
- export const usePlugin = (...args) => util.usePlugin(...args)
1325
- // 产品会 .call(vm, ...) 传组件作用域,不能把它固定绑定到 util。
1326
- export function runExpByScope (...args) {
1327
- return util.runExpByScope.apply(this == null ? util : this, args)
1328
- }
1329
-
1330
- export default util
1
+ // import _router from '@epxing/core/src/router'
2
+ // 不 import store:本文件运行时全部经 window.store(store/index.js 挂载)访问。
3
+ // 原 import 是死导入,且构成 utils→store 反向边(4 个循环依赖的共同主干,P4 拆包 @epxing/utils 必须为叶子)。
4
+ import storage, { getXsrfToken } from '@epxing/utils/src/storage'
5
+ import { consumeUrlToken as consumeStoredUrlToken, getStoredToken, setStoredToken, removeStoredToken } from '@epxing/utils/src/token-storage'
6
+ import { isEqual, cloneDeep, isNil } from 'lodash-es'
7
+ import * as formula from '@epxing/utils/src/formula'
8
+ import { evalWith } from '@epxing/utils/src/eval-with'
9
+ import { useVuePlugin, getApp } from '@epxing/core/src/app'
10
+ // 纯函数层,零依赖(见 menu-route.js 头注释)——不会与 core 成环
11
+ import { resolveRouteRole } from '@epxing/core/src/menu-route'
12
+ import chartRuntimeAssets, { wordcloudUrl } from '../chart-runtime-assets'
13
+ // 安装旧库会等待兼容模块的动态 import;并发 script.onload 必须共享同一次安装。
14
+ const installingLibraries = new Map()
15
+ class Util {
16
+ constructor () {
17
+ this.sysId = ''
18
+ this.loadPlugins = {}
19
+ this.loadSystemLibrarys = {}
20
+ }
21
+
22
+ getSysId () {
23
+ const searchSysId = this.search('FM_SYS_ID', window.location.search)
24
+ const hashSysId = this.search('FM_SYS_ID')
25
+ return hashSysId || searchSysId
26
+ }
27
+
28
+ setSysId (sysId) {
29
+ const { CLIENT_ID = '' } = window.store.state.config
30
+ storage.set('EPX_FM_SYS_ID-' + CLIENT_ID, sysId)
31
+ this.sysId = sysId
32
+ }
33
+
34
+ getPublicPath () {
35
+ return window.ROUTER_MODE === 'history' ? window.BASE_URL || '/' : './'
36
+ }
37
+
38
+ // 运行时动态加载的三方库(echarts/pdf/tinymce/signature_pad 等)所在目录,相对 publicPath。
39
+ // 默认 'js/' —— 产品项目脚手架的 public/js 构建后就在 dist/js/,行为不变;
40
+ // 本平台把静态资源归入 dist/assets/js/,由其 config.js 注入 window.STATIC_JS_DIR 覆盖。
41
+ getStaticJsDir () {
42
+ return window.STATIC_JS_DIR || 'js/'
43
+ }
44
+
45
+ getRootPath () {
46
+ if (window.ROUTER_MODE === 'history') {
47
+ const reg = /\/((app|hall|login|system|page|s|task|feishuTask)\/.+)/g
48
+ return location.origin + (location.pathname === '/' ? '' : location.pathname.replace(reg, ''))
49
+ }
50
+ return location.origin + location.pathname + (location.pathname.endsWith('/') ? '#' : '/#')
51
+ }
52
+
53
+ getRoutePath () {
54
+ const router = window.router
55
+ const { fullPath } = router.currentRoute.value || {}
56
+ let path = fullPath
57
+ if (!path || path === '/') {
58
+ path = window.ROUTER_MODE === 'history' ? location.pathname : location.hash
59
+ const reg = /\/((app|hall|login|system|page|s|task|feishuTask)\/.+)/g
60
+ const flag = reg.exec(path)
61
+ if (flag) return RegExp.$1
62
+ }
63
+ return path
64
+ }
65
+
66
+ getClientCode () {
67
+ // 1. 当前路由里取 2. clientConfig 2.当前路径中取- 判断是history还是hash
68
+ const router = window.router
69
+ const { params = {} } = router.currentRoute.value || {}
70
+ const { config = {} } = window.store.state || {}
71
+ const { clientConfig = {} } = config
72
+ const { CODE } = clientConfig
73
+ const clientCode = params.client || CODE
74
+ if (!clientCode) {
75
+ const path = this.getRoutePath()
76
+ let reg = /app\/role\/select\/(\w+)/
77
+ let regResult = reg.exec(path)
78
+ if (!regResult) {
79
+ reg = /[login|app|hall|system|client]\/(\w+)/
80
+ regResult = reg.exec(path)
81
+ }
82
+ return RegExp.$1
83
+ }
84
+ return clientCode
85
+ }
86
+
87
+ setToken (v, routeClientCode) {
88
+ const clientCode = routeClientCode || this.getClientCode()
89
+ setStoredToken(`${clientCode}-token`, v)
90
+ }
91
+
92
+ getToken (routeClientCode) {
93
+ const clientCode = routeClientCode || this.getClientCode()
94
+ return getStoredToken(`${clientCode}-token`)
95
+ }
96
+
97
+ removeToken (routeClientCode) {
98
+ const clientCode = routeClientCode || this.getClientCode()
99
+ removeStoredToken(`${clientCode}-token`)
100
+ }
101
+
102
+ consumeUrlToken (routeClientCode) {
103
+ const clientCode = routeClientCode || this.getClientCode()
104
+ return consumeStoredUrlToken(clientCode ? `${clientCode}-token` : '')
105
+ }
106
+
107
+ search (name, path) {
108
+ if (!name) {
109
+ const searchStr = (path || window.location.hash || window.location.search).split('?')[1]
110
+ if (!searchStr) return
111
+ const searchArray = searchStr.split('&')
112
+ const searchObj = {}
113
+ searchArray.map(item => {
114
+ const key = item.split('=')[0]
115
+ const value = item.split('=')[1]
116
+ searchObj[key] = value
117
+ })
118
+ return searchObj
119
+ }
120
+ const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
121
+ const searchStr = '?' + (path || window.location.hash || window.location.search).split('?')[1]
122
+ const r = searchStr.substr(1).match(reg)
123
+ if (r != null) {
124
+ return unescape(r[2])
125
+ } else {
126
+ return null
127
+ }
128
+ }
129
+
130
+ getCurrentLanguage (routeClientCode) {
131
+ const { config = {}, user = {} } = window.store.state || {}
132
+ const { clientConfig = {} } = config
133
+ const { userInfo = {} } = user
134
+ const { locale = {} } = window.store.state || {}
135
+ // 全仓字段名是 LANGUAGE_DEFAULT(20+ 处一致,ClientFormItem.vue 里它是真实的数据库
136
+ // attriName),这里原来错写成 DEFAULT_LANGUAGE,兜底分支因此恒为 undefined,已改正
137
+ const { CODE: configuredClientCode, LANGUAGE_DEFAULT: defaultLanguage } = clientConfig
138
+ const { LANGUAGE: userLanguage } = userInfo
139
+ const clientCode = routeClientCode || configuredClientCode
140
+ // INITIALIZE_LOCALE(store/modules/locale.js)已落地当前语言时优先取它——它是唯一同时
141
+ // 兼顾"客户是否启用多语言""用户/浏览器偏好""受支持语言集合"三者的权威来源
142
+ if (locale.clientCode === clientCode && locale.currentLocale) return locale.currentLocale
143
+ const storedLanguage = storage.get((clientCode || '') + '-language')
144
+ // routeClientCode 指定了别的客户(如引导期还没切到当前客户)时,不能用本客户的
145
+ // user/默认语言兜底——那两个值语义上属于 configuredClientCode,答非所问
146
+ const canUseLoadedDefaults = !routeClientCode || routeClientCode === configuredClientCode
147
+ return storedLanguage || (canUseLoadedDefaults ? userLanguage || defaultLanguage : '') || 'zh-CN'
148
+ }
149
+
150
+ setCurrentLanguage (language) {
151
+ return window.store.dispatch('CHANGE_LOCALE', language)
152
+ }
153
+
154
+ isChineseLanguage (language = this.getCurrentLanguage()) {
155
+ return language === '010' || /^zh(?:-|$)/i.test(language || '')
156
+ }
157
+
158
+ toPx (v) {
159
+ if (v) {
160
+ const str = String(v)
161
+ return str.includes('px') ? str : str + 'px'
162
+ }
163
+ return ''
164
+ }
165
+
166
+ // 返回登陆页
167
+ toLogin (clearPath) {
168
+ const router = window.router
169
+ if (window.openedPopup && window.openedPopup.length > 0) {
170
+ window.openedPopup.forEach(item => item.close())
171
+ window.openedPopup = []
172
+ }
173
+ // 获取当前应用信息
174
+ const { config = {} } = window.store.state || {}
175
+ const { appConfig = {} } = config
176
+ const { fullPath, params = {} } = router.currentRoute.value || {}
177
+ const clientCode = this.getClientCode()
178
+ const system = params.system || appConfig['SYSTEM|CODE']
179
+ const app = appConfig.CODE || params.app
180
+
181
+ this.removeToken()
182
+
183
+ window.store.commit('DELETE_USER')
184
+ window.store.commit('DELETE_TABS')
185
+ const path = this.getRoutePath()
186
+ // 删除_token参数
187
+ const pathname = path.split('?')[0]
188
+ if (fullPath && !fullPath.includes('/login')) {
189
+ const query = {}
190
+ let _path = ''
191
+ if (!(fullPath === '/' && pathname === '/')) {
192
+ _path = fullPath === '/' ? path : fullPath
193
+ }
194
+ if (_path) {
195
+ query.path = _path.replace(/(\?|&)(_token|client_code|client)=([\w|.]+)(&?)/g, '$1')
196
+ }
197
+ if (clearPath) delete query.path
198
+ delete query._token
199
+ router.push({
200
+ name: (appConfig['SYSTEM|CODE'] === 'epxing-frame' && appConfig.CODE === 'FM_SERVICE_HALL') ? 'hallLogin' : window.IP ? 'login' : 'localLogin',
201
+ params: {
202
+ // ⚠️ 键名必须是 client —— 路由定义是 /login/:client/:system/:app?。
203
+ // 写成 clientCode 会被 Vue Router 丢弃([Vue Router warn] Discarded invalid param(s)),
204
+ // 平时看不出来是因为 Router 4 按 name 跳转时会从**当前路由继承** client;
205
+ // 一旦当前路由没有 client(如落在 /#/),就变成 Missing required param "client" → 白屏。
206
+ client: clientCode,
207
+ system,
208
+ app
209
+ },
210
+ query
211
+ })
212
+ }
213
+ }
214
+
215
+ urlToBase64 (url) {
216
+ return new Promise((resolve, reject) => {
217
+ const image = new Image()
218
+ image.onload = function () {
219
+ const canvas = document.createElement('canvas')
220
+ canvas.width = this.width
221
+ canvas.height = this.height
222
+ // 将图片插入画布并开始绘制
223
+ canvas.getContext('2d').drawImage(image, 0, 0)
224
+ // result
225
+ const result = canvas.toDataURL('image/png')
226
+ resolve({ data: result, width: this.width, height: this.height })
227
+ }
228
+ image.setAttribute('crossOrigin', 'Anonymous')
229
+ image.src = url
230
+ // 图片加载失败的错误处理
231
+ image.onerror = () => {
232
+ reject(new Error('转换失败'))
233
+ }
234
+ })
235
+ }
236
+
237
+ getFileUrl (id, client, flag = true, useSignOrigin = false) {
238
+ // 兼容:第二个参数可传对象 { client, flag, useSignOrigin },旧的位置参数写法 (id, client, flag, useSignOrigin) 仍然可用
239
+ if (client !== null && typeof client === 'object') {
240
+ const options = client
241
+ client = options.client
242
+ flag = options.flag !== undefined ? options.flag : true
243
+ useSignOrigin = options.useSignOrigin || false
244
+ }
245
+ const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID, MINIO_IP, MINIO_FILE_URL } = window
246
+ if (!id) return
247
+ if (id.includes('http') || id.startsWith('data:image') || id.includes('//')) return id
248
+
249
+ if (id.includes('/')) return `${MINIO_IP || IP}${MINIO_FILE_URL || 'file-server'}${id}`
250
+ const { CODE: clientCode = BASE_CLIENT_ID } = window.store.state.config.clientConfig
251
+ const { 'SYSTEM|DOMAIN|CONTEXT': domainContext, 'SYSTEM|CODE': sysCode = BASE_SYSTEM_ID, 'SYSTEM|SIGN_ORIGIN': signOrigin } = window.store.state.config.appConfig
252
+ const projectName = domainContext || BASE_PROJECT_NAME
253
+ const baseUrl = IP ? `${IP}${projectName}/` : ''
254
+ const inter = `api/v1/${client || clientCode}/${sysCode}/download?id=${id}`
255
+ let flieUrl = baseUrl + inter
256
+
257
+ // 两处运算符优先级修复(vue3 侧与本次 token 续期修复各自独立发现同一处 bug):
258
+ // ① `client || clientCode + '-token'` 里 `+` 比 `||` 紧,实际是 `client || (clientCode + '-token')`
259
+ // —— 传了 client 实参时会去读 `<client>` 这个键(少了 `-token` 后缀),永远拿不到 token。
260
+ // 目前全仓没有调用方传非空 client,所以这条是**潜伏**的;但一旦有人传就悄悄失效。
261
+ // ② 结尾的 `|| ''` 同理会被 `+` 抢先而恒不生效;getStoredToken 内部已自带空串兜底
262
+ // (取不到时返回 `''` 而非 undefined),改用它之后不用再套这层 `|| ''`。
263
+ if (flag) flieUrl += '&EPXTID=' + btoa(getStoredToken((client || clientCode) + '-token')) + '&xsrf_token=' + getXsrfToken()
264
+ // 当系统配置了签名地址 SIGN_ORIGIN 时,按需将文件地址的域名替换为该地址
265
+ if (useSignOrigin && signOrigin) flieUrl = flieUrl.replace(/^https?:\/\/[^/]+/, signOrigin.replace(/\/+$/, ''))
266
+ return flieUrl
267
+ }
268
+
269
+ getOfficeFileUrl (id, client, flag = true, useSignOrigin = false) {
270
+ // 兼容:第二个参数可传对象 { client, flag, useSignOrigin },旧的位置参数写法 (id, client, flag, useSignOrigin) 仍然可用
271
+ if (client !== null && typeof client === 'object') {
272
+ const options = client
273
+ client = options.client
274
+ flag = options.flag !== undefined ? options.flag : true
275
+ useSignOrigin = options.useSignOrigin || false
276
+ }
277
+ const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID } = window
278
+ const { CODE: clientCode = BASE_CLIENT_ID } = window.store.state.config.clientConfig
279
+ const { 'SYSTEM|DOMAIN|CONTEXT': domainContext, 'SYSTEM|CODE': sysCode = BASE_SYSTEM_ID, 'SYSTEM|SIGN_ORIGIN': signOrigin } = window.store.state.config.appConfig
280
+ const projectName = domainContext || BASE_PROJECT_NAME
281
+ const baseUrl = IP ? `${IP}${projectName}/` : ''
282
+ const inter = `api/v1/${client || clientCode}/${sysCode}/onlineview?fileId=${id}`
283
+ let flieUrl = baseUrl + inter
284
+ // 同 getFileUrl 的两处优先级问题,改法一致(详见那里的注释)。
285
+ if (flag) flieUrl += '&token=' + getStoredToken((client || clientCode) + '-token') + '&xsrf_token=' + getXsrfToken()
286
+ // 当系统配置了签名地址 SIGN_ORIGIN 时,按需将文件地址的域名替换为该地址
287
+ if (useSignOrigin && signOrigin) flieUrl = flieUrl.replace(/^https?:\/\/[^/]+/, signOrigin.replace(/\/+$/, ''))
288
+ return flieUrl
289
+ }
290
+
291
+ // ONLYOFFICE Document Server 地址,来自 config.js 的 window.ONLYOFFICE_URL(未配置返回空串,调用方据此降级)
292
+ // 建议配成与主站同源的反代路径(如 /onlyoffice),可同时避免 mixed content 与跨域问题
293
+ getOfficeServerUrl () {
294
+ const url = (window.ONLYOFFICE_URL || '').replace(/\/+$/, '')
295
+ if (!url) return ''
296
+ // 已是完整地址(含协议或 //)原样返回
297
+ if (!url.startsWith('/') || url.startsWith('//')) return url
298
+ // 相对路径:本地开发(localhost)返回相对,走 devServer 同源代理绕过跨域;
299
+ // 线上拼主站 window.IP 直连(前端与 onlyoffice 同域名则同源)
300
+ const host = window.location.hostname
301
+ if (host === 'localhost' || host === '127.0.0.1') return url
302
+ return (window.IP || '').replace(/\/+$/, '') + url
303
+ }
304
+
305
+ getAbsPath (path, params = {}) {
306
+ let paramStr = ''
307
+ for (const i in params) {
308
+ paramStr += '&' + i + '=' + params[i]
309
+ }
310
+ const { IP, BASE_PROJECT_NAME } = window
311
+ const { 'SYSTEM|DOMAIN|CONTEXT': domainContext } = window.store.state.config.appConfig
312
+ const projectName = domainContext || BASE_PROJECT_NAME
313
+
314
+ const { clientConfig = {} } = window.store.state.config
315
+ return (IP ? (IP + projectName) : '') + path + `?client=${clientConfig.CODE}` + '&xsrf_token=' + getXsrfToken() + paramStr
316
+ }
317
+
318
+ getPlatformRequestUrl (params = {}) {
319
+ const router = window.router
320
+
321
+ const { params: routeParams = {} } = router.currentRoute.value || {}
322
+ const { appConfig = {} } = window.store.state.config
323
+ const client = routeParams.client || appConfig['CLIENT|CODE']
324
+ const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID } = window
325
+ const baseUrl = IP ? `${IP}${BASE_PROJECT_NAME}/` : ''
326
+ const platformUrl = `api/v1/${BASE_CLIENT_ID}/${BASE_SYSTEM_ID}/${params.entityId}/${params.action}`
327
+ let paramStr = ''
328
+ for (const i in params) {
329
+ if (i !== 'action' && i !== 'entityId') {
330
+ paramStr += (paramStr.includes('?') ? '&' : '?') + i + '=' + params[i]
331
+ }
332
+ }
333
+ // 结尾 `|| ''` 同样会被 `+` 抢先而恒不生效(这里 client 已在上面解析好,没有键名那个问题);
334
+ // getStoredToken 内部已自带空串兜底,不用再套这层 `|| ''`。
335
+ return baseUrl + platformUrl + paramStr + '&token=' + getStoredToken(client + '-token') + '&xsrf_token=' + getXsrfToken()
336
+ }
337
+
338
+ getRequestUrl (params = {}, { requestKey = 'api' } = {}) {
339
+ const { IP, BASE_PROJECT_NAME, BASE_SYSTEM_ID, BASE_CLIENT_ID } = window
340
+ const { CODE: clientCode = BASE_CLIENT_ID } = window.store.state.config.clientConfig
341
+ const { 'SYSTEM|DOMAIN|CONTEXT': domainContext, 'SYSTEM|CODE': sysCode = BASE_SYSTEM_ID } = window.store.state.config.appConfig
342
+ const projectName = domainContext || BASE_PROJECT_NAME
343
+ const baseUrl = IP ? `${IP}${projectName}/` : ''
344
+ const platformUrl = `${requestKey}/v1/${clientCode}/${sysCode}/${params.entityId}/${params.action}`
345
+ let paramStr = ''
346
+ const xsrfToken = getXsrfToken()
347
+ if (xsrfToken) params.xsrf_token = xsrfToken
348
+ for (const i in params) {
349
+ if (i !== 'action' && i !== 'entityId') {
350
+ paramStr += (paramStr.includes('?') ? '&' : '?') + i + '=' + params[i]
351
+ }
352
+ }
353
+ return baseUrl + platformUrl + paramStr
354
+ }
355
+
356
+ getSystemMeta (metaCode) {
357
+ const router = window.router
358
+
359
+ const { name, params: routeParams = {} } = router.currentRoute.value || {}
360
+ const { appConfig = {} } = window.store.state.config
361
+ const client = routeParams.client || appConfig['CLIENT|CODE']
362
+ const system = routeParams.system || appConfig['SYSTEM|CODE']
363
+ return name === 'hall' ? `${metaCode}@system@${client}` : `${metaCode}@${system}@${client}`
364
+ }
365
+
366
+ isWeiXin () {
367
+ var ua = window.navigator.userAgent.toLowerCase()
368
+ // 修复:ua.match() 返回数组(或 null),永远不会 === 字符串 'micromessenger',
369
+ // 原写法恒为 false——微信内嵌浏览器判断从未生效过。
370
+ // 移动端、Windows/Mac 桌面版微信以及企业微信的内置浏览器 UA 里都带 micromessenger
371
+ return ua.indexOf('micromessenger') !== -1
372
+ }
373
+
374
+ isFeishu () {
375
+ var ua = window.navigator.userAgent.toLowerCase()
376
+ if (ua.match(/Lark/i)) {
377
+ return true
378
+ } else {
379
+ return false
380
+ }
381
+ }
382
+
383
+ isDingTalk () {
384
+ const ua = navigator.userAgent.toLowerCase()
385
+ return ua.match(/dingtalk/i) !== null
386
+ }
387
+
388
+ isIos () {
389
+ var ua = window.navigator.userAgent.toLowerCase()
390
+ // eslint-disable-next-line
391
+ return !!ua.match(/i[^;]+;( U;)? CPU.+Mac OS X/i)
392
+ }
393
+
394
+ // 浏览器能否在页面内直接渲染 PDF。不能的话,给 iframe / a 标签赋 PDF 地址
395
+ // 会被当成文件下载(微信内置浏览器会直接弹「下载 / 用其他应用打开」)
396
+ canPreviewPdfInline () {
397
+ // 微信内置浏览器(含桌面版、企业微信)既不能内联渲染 PDF,也不支持网页发起打印
398
+ if (this.isWeiXin()) return false
399
+ if (typeof navigator.pdfViewerEnabled === 'boolean') return navigator.pdfViewerEnabled
400
+ // 老浏览器没有 pdfViewerEnabled,退回看是否注册了 PDF 处理器
401
+ const mimeTypes = navigator.mimeTypes || {}
402
+ return !!(mimeTypes['application/pdf'] || mimeTypes['application/x-google-chrome-pdf'])
403
+ }
404
+
405
+ // 把一段 HTML 放进临时 iframe 打印,返回浏览器是否真的进入了打印流程。
406
+ // 打印的是 HTML 而不是 PDF,所以不会触发「下载 / 用其他应用打开」弹窗
407
+ printHtml (html) {
408
+ return new Promise(resolve => {
409
+ const frame = document.createElement('iframe')
410
+ frame.setAttribute('aria-hidden', 'true')
411
+ frame.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;opacity:0'
412
+ document.body.appendChild(frame)
413
+ let removed = false
414
+ const remove = () => {
415
+ if (removed) return
416
+ removed = true
417
+ if (frame.parentNode) frame.parentNode.removeChild(frame)
418
+ }
419
+ const doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document)
420
+ if (!doc) {
421
+ remove()
422
+ return resolve(false)
423
+ }
424
+ doc.open()
425
+ doc.write(html)
426
+ doc.close()
427
+ // 图片是 dataURL,也要等它们解码完再打印,否则会打出空白页
428
+ const images = Array.prototype.slice.call(doc.images || [])
429
+ const loaded = images.map(img => img.complete
430
+ ? Promise.resolve()
431
+ : new Promise(resolve => { img.onload = img.onerror = resolve }))
432
+ Promise.all(loaded).then(() => {
433
+ const win = frame.contentWindow
434
+ // print() 在有的浏览器里是异步的,过早移除 iframe 会打断打印
435
+ try { win.addEventListener('afterprint', remove) } catch (e) {}
436
+ const printed = this.printWindow(win)
437
+ // 没进打印流程就直接收掉,留着只是白占内存(图片是 dataURL,可能有好几 MB)
438
+ if (printed) setTimeout(remove, 60000)
439
+ else remove()
440
+ resolve(printed)
441
+ })
442
+ })
443
+ }
444
+
445
+ // 调 print() 并判断浏览器是否真的进入了打印流程。
446
+ // 微信等内嵌浏览器没有打印栈,print() 会被静默吞掉,只能靠 beforeprint 判断
447
+ printWindow (win) {
448
+ if (!win || typeof win.print !== 'function') return false
449
+ let engaged = false
450
+ const mark = () => { engaged = true }
451
+ const mql = win.matchMedia ? win.matchMedia('print') : null
452
+ const listen = (target, on) => {
453
+ if (!target) return
454
+ try { target[on ? 'addEventListener' : 'removeEventListener']('beforeprint', mark) } catch (e) {}
455
+ }
456
+ listen(win, true)
457
+ listen(window, true)
458
+ if (mql && mql.addListener) mql.addListener(mark)
459
+ try {
460
+ win.focus()
461
+ win.print()
462
+ } catch (e) {
463
+ engaged = false
464
+ }
465
+ listen(win, false)
466
+ listen(window, false)
467
+ if (mql && mql.removeListener) mql.removeListener(mark)
468
+ return engaged
469
+ }
470
+
471
+ // 用 pdf.js 把 PDF 每页画成图片,绕开浏览器自带的 PDF 阅读器
472
+ pdfDocToImages (pdfDoc, scale = 3) {
473
+ if (!pdfDoc || typeof pdfDoc.getPage !== 'function') return Promise.resolve([])
474
+ const images = []
475
+ let chain = Promise.resolve()
476
+ for (let num = 1; num <= pdfDoc.numPages; num++) {
477
+ chain = chain
478
+ .then(() => pdfDoc.getPage(num))
479
+ .then(page => {
480
+ const viewport = page.getViewport({ scale })
481
+ const canvas = document.createElement('canvas')
482
+ canvas.width = viewport.width
483
+ canvas.height = viewport.height
484
+ const task = page.render({ canvasContext: canvas.getContext('2d'), viewport })
485
+ const done = task && task.promise ? task.promise : Promise.resolve()
486
+ return done.then(() => images.push(canvas.toDataURL('image/jpeg', 0.92)))
487
+ })
488
+ }
489
+ return chain.then(() => images)
490
+ }
491
+
492
+ getBrowserVersion () {
493
+ const ua = window.navigator.userAgent
494
+ if (window.opera) {
495
+ return {
496
+ browser: 'opera',
497
+ version: window.opera.version()
498
+ }
499
+ } else if (/AppleWebKit\/(\S+)/.test(ua)) {
500
+ if (/Chrome\/(\S+)/.test(ua)) {
501
+ return {
502
+ brower: 'chrome',
503
+ version: RegExp.$1
504
+ }
505
+ } else if (/Version\/(\S+)/.test(ua)) {
506
+ return {
507
+ brower: 'safari',
508
+ version: RegExp.$1
509
+ }
510
+ } else {
511
+ return {
512
+ brower: 'safari'
513
+ }
514
+ }
515
+ } else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
516
+ return {
517
+ brower: 'konq',
518
+ version: RegExp.$1
519
+ }
520
+ } else if (/rv:([^\\)]+)\) Gecko\/\d{8}/.test(ua)) {
521
+ if (/Firefox\/(\S+)/.test(ua)) {
522
+ // 火狐浏览器
523
+ return {
524
+ brower: 'firefox',
525
+ version: RegExp.$1
526
+ }
527
+ } else {
528
+ // 其他情况
529
+ }
530
+ } else if (/MSIE ([^;]+)/.test(ua)) {
531
+ return {
532
+ brower: 'ie',
533
+ version: RegExp.$1
534
+ }
535
+ }
536
+ return {}
537
+ }
538
+
539
+ runExp (exp, point) {
540
+ if (!point) point = this
541
+ exp = ' ' + exp.trim()
542
+ // eslint-disable-next-line
543
+ const quickRegex = /([\s\+\-\*\/%&\(\|\^!\*~]\s*?)([a-zA-Z_$][a-zA-Z_$0-9]*?)/g
544
+ exp = exp.replace(quickRegex, (a, b, c) => {
545
+ return b + 'point.' + c
546
+ })
547
+ // eslint-disable-next-line no-new-func
548
+ const func = new Function('point', 'return ' + exp)
549
+ return func(point)
550
+ }
551
+
552
+ // 根据高级查询获取js表达式
553
+ runExpByScope (scopeList, data = {}) {
554
+ let str = ''
555
+ // Form data is null before its request resolves. Treat it as an empty record
556
+ // while evaluating visibility rules so legacy product pages can render.
557
+ const record = data || {}
558
+ // 由调用方 .call(组件实例) 进来,#{key} 相对组件实例取值
559
+ const that = this
560
+ // 当前时间变量:高级筛选里写 $[currentDate.date]/$[currentDate.month] 等(或 #{...})解析为当前时间,供日期判断
561
+ const getTimeVar = (key) => {
562
+ switch (key) {
563
+ case 'now':
564
+ case 'currentDate.time': return formula.NOW('YYYY-MM-DD HH:mm:ss') // 当前时间(精确到秒)
565
+ case 'today':
566
+ case 'currentDate':
567
+ case 'currentDate.date': return formula.NOW('YYYY-MM-DD') // 系统日期(精确到天)
568
+ case 'currentDate.month': return formula.NOW('YYYY-MM') // 当前月份(精确到月)
569
+ case 'currentDate.year': return formula.NOW('YYYY') // 当前年度(精确到年)
570
+ default: return null
571
+ }
572
+ }
573
+ try {
574
+ const router = window.router
575
+ // 高级查询里的 user.roleCode 取当前角色。菜单直达路由没有 role 段(末段是 menuKey),
576
+ // 直读 params.role 会得到 undefined,生成的表达式变成 "undefined"=='xxx'、按角色过滤
577
+ // 的数据范围整条失效。统一走 resolveRouteRole:URL 优先、其次 localStorage。
578
+ const currentRoleCode = resolveRouteRole(router.currentRoute.value)
579
+ scopeList.forEach((scope, index) => {
580
+ let { mode, values = '', field } = scope
581
+ const fieldInfo = this.$meta ? this.$meta.getAttribute(field) || {} : {}
582
+ const { dataType, optionType, isMultiple } = fieldInfo
583
+ if (/#\{(.+)\}/.test(values)) {
584
+ try {
585
+ const valueKey = values.replace(/^#\{(.+)\}$/, (match, key) => key)
586
+ const timeVar = getTimeVar(valueKey)
587
+ // 显式传作用域,不能靠直接 eval 捕获 that(见 eval-with.js)
588
+ values = timeVar != null ? timeVar : (evalWith(`that.${valueKey}`, { that }) || evalWith(`that.$query.${valueKey}`, { that }) || evalWith(`that.$tab.${valueKey}`, { that }) || '')
589
+ } catch (error) {
590
+ values = ''
591
+ }
592
+ }
593
+ if (/\$\[(.+)\]/.test(values)) {
594
+ try {
595
+ const valueKey = values.replace(/\$\[(.+)\]/, (match, key) => key)
596
+ const timeVar = getTimeVar(valueKey)
597
+ // 显式传作用域,不能靠直接 eval 捕获 that(见 eval-with.js)
598
+ values = timeVar != null ? timeVar : (evalWith(`that.${valueKey}`, { that }) || evalWith(`that.$query.${valueKey}`, { that }) || evalWith(`that.$tab.${valueKey}`, { that }) || '')
599
+ } catch (err) {
600
+ console.error(err)
601
+ values = ''
602
+ }
603
+ }
604
+ let valueArr = []
605
+ if (typeof values === 'string') {
606
+ values = values.replace(/[\r\n"]/g, '').replace(/\\$/g, '/')
607
+ if (!values) valueArr = []
608
+ else valueArr = values.split(',')
609
+ }
610
+ str += `${scope.lb || ''}`
611
+ const user = window.store.state.user.user || {}
612
+ // user.xxx 这类字段相对当前用户取值:显式传作用域,不能靠直接 eval 捕获 user(见 eval-with.js)
613
+ let fieldValue = field === 'user.roleCode' ? currentRoleCode : field.includes('user.') ? evalWith(field, { user }) : record[field] || ''
614
+ // eslint-disable-next-line no-new-wrappers
615
+ fieldValue = typeof fieldValue === 'string' ? fieldValue.replace(/[\r\n"]/g, '').replace(/\\$/g, '/') : fieldValue
616
+ if (mode === 'is_null' || mode === 'not_null' || mode === 'equal' || mode === 'not_equal') {
617
+ str += `"${fieldValue}"${mode === 'is_null' || mode === 'equal' ? '==' : '!='}${mode === 'equal' || mode === 'not_equal' ? `'${values}'` : '""'}`
618
+ } else if (['greater', 'greater_equal', 'less', 'less_equal'].includes(mode)) {
619
+ str += `${fieldValue}${mode === 'greater' ? '>' : mode === 'greater_equal' ? '>=' : mode === 'less' ? '<' : '<='}${values}`
620
+ } else if (['in', 'not_in', 'in_scope', 'not_in_scope'].includes(mode)) {
621
+ if (dataType) {
622
+ if (dataType === 'list' || dataType === 'object' || dataType === 'user' || dataType === 'dept') {
623
+ const multiple = isMultiple === '1' || optionType === 'multibox' || optionType === 'multiList'
624
+ str += `${mode === 'not_in' || mode === 'not_in_scope' ? '!' : ''}${multiple && fieldValue ? `"${fieldValue.split(',')}".includes("${values}")` : `${JSON.stringify(valueArr)}.includes("${fieldValue}")`}`
625
+ } else {
626
+ str += `${mode === 'not_in' || mode === 'not_in_scope' ? '!' : ''}${`"${fieldValue}".includes("${values}")`}`
627
+ }
628
+ } else {
629
+ str += `${mode === 'not_in' || mode === 'not_in_scope' ? '!' : ''}${valueArr.length > 1 ? `${JSON.stringify(valueArr)}.includes("${fieldValue}")` : `"${fieldValue}".includes("${values}")`}`
630
+ }
631
+ } else if (['begin', 'end'].includes(mode)) {
632
+ str += `"${fieldValue}".${mode === 'begin' ? 'startsWith' : 'endsWith'}('${values}')`
633
+ } else if (['date_greater', 'date_greater_equal', 'date_less', 'date_less_equal'].includes(mode)) {
634
+ str += `"${fieldValue}"${mode === 'date_greater' ? '>' : mode === 'date_greater_equal' ? '>=' : mode === 'date_less' ? '<' : '<='}'${values}'`
635
+ } else {
636
+ str += `${mode === 'not_like' ? '!' : ''}"${fieldValue}".includes(${values})`
637
+ }
638
+ str += `${scope.rb || ''} ${index === scopeList.length - 1 ? '' : scope.link === 'or' ? '||' : '&&'} `
639
+ })
640
+ // eslint-disable-next-line no-eval
641
+ const flag = eval(str)
642
+ return flag
643
+ } catch (error) {
644
+ console.error(error + ',str=' + str)
645
+ return false
646
+ }
647
+ }
648
+
649
+ awaitWrap (promise) {
650
+ return promise
651
+ .then(data => ({ error: null, data }))
652
+ .catch(error => ({ error, data: null }))
653
+ }
654
+
655
+ isLoadLibrary (systemCode) {
656
+ return this.loadSystemLibrarys[systemCode]
657
+ }
658
+
659
+ loadJS (options) {
660
+ var url = options.url
661
+ var callback = typeof options.callback === 'function' ? options.callback : function () {}
662
+
663
+ const script = document.createElement('script')
664
+ script.type = 'text/javascript'
665
+ // IE
666
+ if (script.readyState) {
667
+ script.onreadystatechange = function () {
668
+ if (script.readyState === 'loaded' || script.readyState === 'complete') {
669
+ script.onreadystatechange = null
670
+ callback()
671
+ }
672
+ }
673
+ } else {
674
+ // 其他浏览器
675
+ script.onload = function () {
676
+ callback()
677
+ }
678
+ }
679
+ // 失败也必须回调(同 loadCss 的 onerror 处理)。原先只绑 onload:URL 404 / 跨域被拦 /
680
+ // 文件服务抖动时回调永不触发 → usePlugin() 的 promise 永不 resolve → 等它的地方**无限期挂住**,
681
+ // 且 util.loadPlugins 把这个永远 pending 的 promise 缓存了,重试也一样挂、连报错都没有。
682
+ // 调用方提供 onerror 时明确返回失败;旧调用方继续收到回调,避免无限等待。
683
+ script.onerror = function () {
684
+ console.error('[util.loadJS] 加载失败:' + url)
685
+ if (options.onerror) options.onerror(new Error('脚本加载失败:' + url))
686
+ else callback()
687
+ }
688
+ script.src = url
689
+ document.getElementsByTagName('head')[0].appendChild(script)
690
+ }
691
+
692
+ loadJs (file, cb) {
693
+ const that = this
694
+ if (!that.oldLibrary) that.oldLibrary = {}
695
+ if (window.library) {
696
+ for (const key in window.library) {
697
+ if (!that.oldLibrary[key]) that.oldLibrary[key] = {}
698
+ for (const compKey in (window.library[key] || {})) {
699
+ try {
700
+ that.oldLibrary[key][compKey] = cloneDeep(window.library[key][compKey])
701
+ } catch (error) {
702
+ console.error(compKey, error)
703
+ }
704
+ }
705
+ }
706
+ }
707
+ const script = document.createElement('script')
708
+ script.type = 'text/javascript'
709
+ script.src = file
710
+ script.onload = async function (js) {
711
+ const componentLibrary = window.library
712
+ // 远程库 install 去重(判据 name@version),2026-09-02 补。
713
+ //
714
+ // 为什么必须有:window.library 只累加、从不清空,而本 onload 会遍历它的**每一个** key。
715
+ // · 一个系统配了 N 个 js(产品库 + 项目库 + …)是串行加载的 → 第 1 个库被 install N 次;
716
+ // · 产品若已被**本地打进壳**(dev 壳的 app.use(hr)),它不进 window.library,
717
+ // 平台看不见 ⇒ 远程那份照样下载并再 install 一次。
718
+ // 双装时 app.component 有 `if (!component)` 守卫不会重,但 store.registerModule /
719
+ // router.addRoute 没守卫 —— 实测就是 [vuex] duplicate getter key: selectedPersons。
720
+ // 证据见 test/unit/load-js-install-dedup.test.js。
721
+ //
722
+ // ⚠️ 只在 app 已就绪时记账:useVuePlugin 在 _app 为空时是直接 return 的空操作,
723
+ // 那种情况下记了账就等于永久跳过,库再也装不上。
724
+ // 产品库自己的 install 也应查同一张表(见 docs/PROJECT-GUIDE.md),
725
+ // 覆盖本地打包那条平台看不见的路——两边合起来才是完整去重。
726
+ if (!window.__epxInstalledLibs__) window.__epxInstalledLibs__ = {}
727
+ const appReady = !!getApp()
728
+ for (const key in componentLibrary) {
729
+ const lib = componentLibrary[key]
730
+ const sig = key + '@' + ((lib && lib.version) || '')
731
+ if (!window.__epxInstalledLibs__[sig]) {
732
+ // ⚠️ 记账必须在 install **之后**。
733
+ // 产品库自己的 install 开头也查这张表(PROJECT-GUIDE 约定的幂等写法:
734
+ // `if (marks[sig]) return`)。先记账就会让它一进门就 return —— 表里有记录、
735
+ // 组件一个都没注册,症状是 window.__epxInstalledLibs__ 有 hr@3.0.1 但
736
+ // app 里 0 个 epx-hr-* 组件,且完全静默。
737
+ // 2026-09-04 product/hr 实测(两侧去重分两次提交,撞在一起才暴露)。
738
+ if (appReady) {
739
+ if (!installingLibraries.has(sig)) {
740
+ const install = Promise.resolve().then(() => useVuePlugin(lib, { store: window.store }))
741
+ .then(() => { window.__epxInstalledLibs__[sig] = { source: 'remote', at: Date.now() } })
742
+ .finally(() => installingLibraries.delete(sig))
743
+ installingLibraries.set(sig, install)
744
+ }
745
+ try { await installingLibraries.get(sig) } catch (error) { console.error(error) }
746
+ }
747
+ }
748
+ const components = componentLibrary[key] || {}
749
+ for (const key in components) {
750
+ if (!window.componentLibrary[key]) window.componentLibrary[key] = components[key]
751
+ }
752
+ if (!isEqual(componentLibrary[key], that.oldLibrary[key])) {
753
+ // 修复:原写在 that(util 单例)上,全仓没有任何代码读 that.productLibrary,
754
+ // 改到 window 上才是这行代码的原意。develop 同期把 loadLibrary.js 的产品库识别
755
+ // 换成了引用快照对比(不再依赖这个全局),这里目前也没有实际读者——按 develop
756
+ // 原样保留这处写入(无害),不额外删減。
757
+ if (componentLibrary[key] && componentLibrary[key].version) window.productLibrary = componentLibrary[key]
758
+ }
759
+ }
760
+
761
+ cb && cb()
762
+ }
763
+ document.body.appendChild(script)
764
+ }
765
+
766
+ loadCss (options) {
767
+ var url = options.url
768
+ var callback = typeof options.callback === 'function' ? options.callback : function () {}
769
+ var id = options.id
770
+ var node = document.createElement('link')
771
+ var supportOnload = 'onload' in node
772
+ var isOldWebKit = +navigator.userAgent.replace(/.*(?:AppleWebKit|AndroidWebKit)\/?(\d+).*/i, '$1') < 536 // webkit旧内核做特殊处理
773
+ var protectNum = 300000 // 阈值10分钟,一秒钟执行pollCss 500次
774
+
775
+ node.rel = 'stylesheet'
776
+ node.type = 'text/css'
777
+ node.href = url
778
+ if (typeof id !== 'undefined') {
779
+ node.id = id
780
+ }
781
+ document.getElementsByTagName('head')[0].appendChild(node)
782
+
783
+ // for Old WebKit and Old Firefox
784
+ if (isOldWebKit || !supportOnload) {
785
+ // Begin after node insertion
786
+ const timer = setTimeout(function () {
787
+ pollCss(node, callback, 0)
788
+ clearTimeout(timer)
789
+ }, 1)
790
+ return
791
+ }
792
+
793
+ if (supportOnload) {
794
+ node.onload = onload
795
+ node.onerror = function () {
796
+ // 加载失败(404)
797
+ onload()
798
+ }
799
+ } else {
800
+ node.onreadystatechange = function () {
801
+ if (/loaded|complete/.test(node.readyState)) {
802
+ onload()
803
+ }
804
+ }
805
+ }
806
+
807
+ function onload () {
808
+ // 确保只跑一次下载操作
809
+ node.onload = node.onerror = node.onreadystatechange = null
810
+
811
+ // 清空node引用,在低版本IE,不清除会造成内存泄露
812
+ node = null
813
+
814
+ callback()
815
+ }
816
+
817
+ // 循环判断css是否已加载成功
818
+ /*
819
+ * @param node -- link节点
820
+ * @param callback -- 回调函数
821
+ * @param step -- 计步器,避免无限循环
822
+ */
823
+ function pollCss (node, callback, step) {
824
+ var sheet = node.sheet
825
+ var isLoaded
826
+
827
+ step += 1
828
+
829
+ // 保护,大于10分钟,则不再轮询
830
+ if (step > protectNum) {
831
+ isLoaded = true
832
+
833
+ // 清空node引用
834
+ node = null
835
+
836
+ callback()
837
+ return
838
+ }
839
+
840
+ if (isOldWebKit) {
841
+ // for WebKit < 536
842
+ if (sheet) {
843
+ isLoaded = true
844
+ }
845
+ } else if (sheet) {
846
+ // for Firefox < 9.0
847
+ try {
848
+ if (sheet.cssRules) {
849
+ isLoaded = true
850
+ }
851
+ } catch (ex) {
852
+ // 火狐特殊版本,通过特定值获知是否下载成功
853
+ // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
854
+ // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
855
+ // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
856
+ if (ex.name === 'NS_ERROR_DOM_SECURITY_ERR') {
857
+ isLoaded = true
858
+ }
859
+ }
860
+ }
861
+
862
+ const timer = setTimeout(function () {
863
+ if (isLoaded) {
864
+ // 延迟20ms是为了给下载的样式留够渲染的时间
865
+ callback()
866
+ } else {
867
+ pollCss(node, callback, step)
868
+ }
869
+ clearTimeout(timer)
870
+ }, 20)
871
+ }
872
+ }
873
+
874
+ dataURLtoBlob (dataurl) {
875
+ var arr = dataurl.split(',')
876
+ var mime = arr[0].match(/:(.*?);/)[1]
877
+ var bstr = atob(arr[1])
878
+ var n = bstr.length
879
+ var u8arr = new Uint8Array(n)
880
+ while (n--) {
881
+ u8arr[n] = bstr.charCodeAt(n)
882
+ }
883
+ return new Blob([u8arr], { type: mime })
884
+ }
885
+
886
+ dataURLtoFile (dataurl, filename) {
887
+ var arr = dataurl.split(','); var mime = arr[0].match(/:(.*?);/)[1]
888
+ var bstr = atob(arr[1]); var n = bstr.length; var u8arr = new Uint8Array(n)
889
+ while (n--) {
890
+ u8arr[n] = bstr.charCodeAt(n)
891
+ }
892
+ const blob = new Blob([u8arr], { type: mime })
893
+
894
+ return new File([blob], filename, { type: mime })
895
+ }
896
+
897
+ usePlugin (plugin) {
898
+ const loadFile = (files) => {
899
+ let fileNum = 0
900
+ return new Promise(resolve => {
901
+ for (const file of files) {
902
+ if (file.includes('api.map.baidu.com') || file.includes('api.tianditu.gov.cn')) {
903
+ this.loadJS({
904
+ url: file,
905
+ callback () {
906
+ fileNum++
907
+ if (fileNum === files.length) {
908
+ return resolve()
909
+ }
910
+ }
911
+ })
912
+ } else if (file.includes('.js')) {
913
+ this.loadJS({
914
+ url: file,
915
+ callback () {
916
+ fileNum++
917
+ if (fileNum === files.length) {
918
+ return resolve()
919
+ }
920
+ }
921
+ })
922
+ } else if (file.includes('.css')) {
923
+ this.loadCss({
924
+ url: file,
925
+ callback () {
926
+ fileNum++
927
+ if (fileNum === files.length) {
928
+ return resolve()
929
+ }
930
+ }
931
+ })
932
+ } else {
933
+ fileNum++
934
+ if (fileNum === files.length) {
935
+ return resolve()
936
+ }
937
+ }
938
+ }
939
+ })
940
+ }
941
+ if (this.loadPlugins[plugin]) return this.loadPlugins[plugin]
942
+ const publicPath = this.getPublicPath()
943
+ const jsDir = this.getStaticJsDir()
944
+ if (plugin === 'luckysheet') {
945
+ const loadPromise = loadFile([publicPath + 'luckysheet/pluginsCss.css', publicPath + 'luckysheet/plugins.css', publicPath + 'luckysheet/luckysheet.css', publicPath + 'luckysheet/iconfont.css', publicPath + 'luckysheet/plugin.js', publicPath + 'luckysheet/luckysheet.umd.js'])
946
+ .then(() => globalThis.luckysheet)
947
+ this.loadPlugins[plugin] = loadPromise
948
+ return loadPromise
949
+ } else if (plugin === 'BMap') {
950
+ const loadPromise = new Promise(resolve => {
951
+ const ak = window.BMAP_AK || 'zySbI4xZUSRVrbb9Ev3H5xg4hA1md1gh'
952
+ loadFile([`//api.map.baidu.com/api?v=3.0&ak=${ak}&callback=loadBMapCallback`])
953
+ .then(() => window.BMap)
954
+ window.loadBMapCallback = () => {
955
+ resolve(window.BMap)
956
+ }
957
+ })
958
+
959
+ this.loadPlugins[plugin] = loadPromise
960
+ return loadPromise
961
+ } else if (plugin === 'TMap') {
962
+ const loadPromise = new Promise(resolve => {
963
+ const ak = window.TMAP_AK || '83086471ebe2a3e5292841ecaa3f7055'
964
+ loadFile([`//api.tianditu.gov.cn/api?v=4.0&tk=${ak}&callback=loadTMapCallback`])
965
+ .then(() => window.T && resolve(window.T))
966
+ window.loadTMapCallback = () => {
967
+ resolve(window.T)
968
+ }
969
+ })
970
+
971
+ this.loadPlugins[plugin] = loadPromise
972
+ return loadPromise
973
+ } else if (plugin === 'tinymce') {
974
+ const loadPromise = loadFile([publicPath + jsDir + 'tinymce/tinymce.min.js'])
975
+ .then(() => window.tinymce)
976
+ this.loadPlugins[plugin] = loadPromise
977
+ return loadPromise
978
+ } else if (plugin === 'onlyoffice') {
979
+ // ONLYOFFICE Document Server 地址在 config.js 中配置(window.ONLYOFFICE_URL),未配置时不加载
980
+ const officeUrl = this.getOfficeServerUrl()
981
+ if (!officeUrl) return Promise.reject(new Error('未配置 ONLYOFFICE_URL'))
982
+ const loadPromise = loadFile([`${officeUrl}/web-apps/apps/api/documents/api.js`])
983
+ .then(() => {
984
+ if (!window.DocsAPI) throw new Error('文档服务加载失败')
985
+ return window.DocsAPI
986
+ })
987
+ .catch(err => {
988
+ // 失败的 promise 不能留在缓存里,否则后续重试直接拿到旧的失败结果
989
+ delete this.loadPlugins[plugin]
990
+ throw err
991
+ })
992
+ this.loadPlugins[plugin] = loadPromise
993
+ return loadPromise
994
+ } else if (plugin === 'echarts') {
995
+ // 显式配置静态目录的平台继续使用原地址;独立产品使用包内资源。
996
+ const files = window.STATIC_JS_DIR
997
+ ? [publicPath + jsDir + 'echarts.min.js?v=6.0.0-full', publicPath + jsDir + 'ecStat.min.js']
998
+ : chartRuntimeAssets
999
+ const loadPromise = Promise.all(files.map(url => new Promise((resolve, reject) => {
1000
+ this.loadJS({ url, callback: resolve, onerror: reject })
1001
+ })))
1002
+ .then(() => {
1003
+ if (!globalThis.echarts || !globalThis.ecStat) throw new Error('图表资源加载失败,请检查 ECharts 和 ecStat 脚本')
1004
+ return globalThis.echarts
1005
+ })
1006
+ .catch(error => {
1007
+ delete this.loadPlugins[plugin]
1008
+ throw error
1009
+ })
1010
+ this.loadPlugins[plugin] = loadPromise
1011
+ return loadPromise
1012
+ } else if (plugin === 'echarts-wordcloud') {
1013
+ const url = window.STATIC_JS_DIR ? publicPath + jsDir + 'echarts-wordcloud.min.js' : wordcloudUrl
1014
+ const loadPromise = new Promise((resolve, reject) => {
1015
+ this.loadJS({ url, callback: resolve, onerror: reject })
1016
+ }).catch(error => {
1017
+ delete this.loadPlugins[plugin]
1018
+ throw error
1019
+ })
1020
+ this.loadPlugins[plugin] = loadPromise
1021
+ return loadPromise
1022
+ } else if (plugin === 'pdf') {
1023
+ const browerVersion = this.getBrowserVersion()
1024
+ const isOldChrome = (browerVersion.brower === 'chrome' || browerVersion.brower === 'firefox') && browerVersion.version.split('.')[0] < 92
1025
+ const isIos = this.isIos()
1026
+ const loadPromise = loadFile([publicPath + jsDir + plugin + (isOldChrome || isIos ? '.2.4.456.js' : '.min.js')])
1027
+ .then(() => {
1028
+ const pdf = globalThis.pdfjsLib
1029
+ pdf.disableWorker = true
1030
+ pdf.GlobalWorkerOptions.workerSrc = publicPath + jsDir + (isOldChrome || isIos ? 'pdf.worker.js' : 'pdf.worker.min.js')
1031
+ return pdf
1032
+ })
1033
+ this.loadPlugins[plugin] = loadPromise
1034
+ return loadPromise
1035
+ } else {
1036
+ const loadPromise = loadFile([publicPath + jsDir + plugin + '.min.js'])
1037
+ .then(() => global[plugin])
1038
+ this.loadPlugins[plugin] = loadPromise
1039
+ return loadPromise
1040
+ }
1041
+ }
1042
+
1043
+ utf8to16 (str) {
1044
+ var hexOut = false
1045
+ var out, i, len, c
1046
+ var char2, char3
1047
+ var charCode
1048
+
1049
+ out = hexOut ? [] : ''
1050
+ len = str.length
1051
+ i = 0
1052
+ while (i < len) {
1053
+ c = hexOut ? str[i++] : str.charCodeAt(i++)
1054
+ switch (c >> 4) {
1055
+ case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
1056
+ // 0xxxxxxx
1057
+ hexOut ? out.push(str[i - 1]) : out += str.charAt(i - 1)
1058
+ break
1059
+ case 12: case 13:
1060
+ // 110x xxxx 10xx xxxx
1061
+ char2 = hexOut ? str[i++] : str.charCodeAt(i++)
1062
+ charCode = ((c & 0x1F) << 6) | (char2 & 0x3F); hexOut ? out.push(charCode) : out += String.fromCharCode(charCode)
1063
+ break
1064
+ case 14:
1065
+ // 1110 xxxx 10xx xxxx 10xx xxxx
1066
+ char2 = hexOut ? str[i++] : str.charCodeAt(i++)
1067
+ char3 = hexOut ? str[i++] : str.charCodeAt(i++)
1068
+ charCode = ((c & 0x0F) << 12) |
1069
+ ((char2 & 0x3F) << 6) |
1070
+ ((char3 & 0x3F) << 0)
1071
+ hexOut ? out.push(charCode) : out += String.fromCharCode(charCode)
1072
+ break
1073
+ }
1074
+ }
1075
+
1076
+ return out
1077
+ }
1078
+
1079
+ genId (length = 3) {
1080
+ return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
1081
+ }
1082
+
1083
+ uuid () {
1084
+ if (crypto?.randomUUID) {
1085
+ return crypto.randomUUID()
1086
+ } else {
1087
+ const S4 = () => {
1088
+ return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
1089
+ }
1090
+ return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4())
1091
+ }
1092
+ }
1093
+
1094
+ copyToClipboard (text, callback) {
1095
+ if (navigator.clipboard) {
1096
+ navigator.clipboard.writeText(text)
1097
+ // eslint-disable-next-line standard/no-callback-literal
1098
+ callback && callback(true)
1099
+ return
1100
+ }
1101
+
1102
+ var copyInput = document.getElementById('COPY_INPUT')
1103
+ if (!copyInput) {
1104
+ copyInput = document.createElement('input')
1105
+ copyInput.setAttribute('id', 'COPY_INPUT')
1106
+ copyInput.style.position = 'fixed'
1107
+ copyInput.style.left = '-100%'
1108
+ copyInput.style.top = '0'
1109
+ copyInput.style.zIndex = -100
1110
+ copyInput.style.opacity = 0
1111
+ document.body.appendChild(copyInput)
1112
+ }
1113
+ copyInput.value = text
1114
+ copyInput.focus()
1115
+ copyInput.select()
1116
+
1117
+ // document.execCommand 可能会被废弃
1118
+ if (document.execCommand('copy')) {
1119
+ document.execCommand('copy')
1120
+ }
1121
+
1122
+ copyInput.blur()
1123
+
1124
+ // eslint-disable-next-line standard/no-callback-literal
1125
+ callback && callback(true)
1126
+ }
1127
+
1128
+ getQueryValue (path, obj) {
1129
+ if (!path || !obj) return ''
1130
+ const value = path.split('.').reduce((acc, key) => acc && acc[key], obj)
1131
+ return isNil(value) ? '' : value
1132
+ }
1133
+
1134
+ getValueByPath (path, obj) {
1135
+ const keys = []
1136
+ let currentKey = ''
1137
+ let inBrackets = false
1138
+
1139
+ for (const char of path) {
1140
+ if (char === '[') {
1141
+ inBrackets = true
1142
+ if (currentKey) {
1143
+ keys.push(currentKey)
1144
+ currentKey = ''
1145
+ }
1146
+ } else if (char === ']') {
1147
+ inBrackets = false
1148
+ if (currentKey) {
1149
+ const newKey = currentKey.replace(/^['"](.+)['"]$/, '$1')
1150
+ keys.push(newKey)
1151
+ currentKey = ''
1152
+ }
1153
+ } else if (char === '.' && !inBrackets) {
1154
+ if (currentKey) {
1155
+ keys.push(currentKey)
1156
+ currentKey = ''
1157
+ }
1158
+ } else {
1159
+ currentKey += char
1160
+ }
1161
+ }
1162
+
1163
+ if (currentKey) {
1164
+ keys.push(currentKey)
1165
+ }
1166
+ return keys.reduce((acc, key) => acc && acc[key], obj)
1167
+ }
1168
+
1169
+ /**
1170
+ * 解析操作按钮「后续事件」的参数配置 afterTriggerParams
1171
+ * 支持的占位符:
1172
+ * $[res] 取接口返回值本身
1173
+ * $[res.a.b] 取接口返回值的深层字段
1174
+ * $[key] 取组件实例上的值,取不到再依次回退 $tab / $query / $pageParams / $compData,支持 a.b.c 深层路径
1175
+ * #[field] 取数据字段值
1176
+ * 整串就是一个占位符时保留取到的原始类型(对象 / 数组 / 数字),混在文案里则替换为文本
1177
+ * @param {Object} afterTriggerParams 页面配置的参数对象
1178
+ * @param {Object} [options]
1179
+ * @param {*} [options.res] 接口返回值,供 $[res] / $[res.x] 取值
1180
+ * @param {Object|Array} [options.data] #[field] 的取值数据:传对象取该条的字段值,
1181
+ * 传数组视为多条记录(批量操作),各条的值用逗号拼接
1182
+ * @param {Object} ctx 组件实例,作为 $[key] 的取值上下文
1183
+ * @returns {Object} 解析后的新参数对象,不修改入参
1184
+ */
1185
+ formatAfterTriggerParams (afterTriggerParams, { res, data } = {}, ctx = {}) {
1186
+ const _params = { ...afterTriggerParams }
1187
+ // data 传数组 = 多条记录(批量操作),传对象 = 单条数据
1188
+ const isMultiple = Array.isArray(data)
1189
+ const rows = [].concat(data === undefined ? [] : data).filter(item => !isNil(item))
1190
+ const paramData = { ...ctx.$tab, ...ctx.$query, ...ctx.$pageParams, ...ctx.$compData }
1191
+ const blank = value => (isNil(value) ? '' : value)
1192
+ const getResValue = path => blank(this.getValueByPath(path, res))
1193
+ const getCtxValue = path => {
1194
+ const value = this.getValueByPath(path, ctx)
1195
+ // 组件实例上没有(或为空)时才回退到页签 / 路由 / 页面 / 组件参数
1196
+ return blank(isNil(value) || value === '' ? this.getValueByPath(path, paramData) : value)
1197
+ }
1198
+ const getDataValue = path => {
1199
+ const values = rows.map(item => this.getValueByPath(path, item))
1200
+ // 多条记录按逗号拼接(不过滤空值,保证多个 #[] 参数的列一一对应),与 $formatParams 的 #[] 规则一致
1201
+ return isMultiple ? values.map(item => blank(item)).join(',') : blank(values[0])
1202
+ }
1203
+ // 没传 res 的调用点不占用 res 前缀,按普通取值处理
1204
+ const hasRes = res !== undefined
1205
+ const getValue = (flag, path) => {
1206
+ if (flag === '#') return getDataValue(path)
1207
+ if (!hasRes) return getCtxValue(path)
1208
+ if (path === 'res') return blank(res)
1209
+ if (path.indexOf('res.') === 0) return getResValue(path.slice(4))
1210
+ return getCtxValue(path)
1211
+ }
1212
+ for (const key in _params) {
1213
+ const config = _params[key]
1214
+ if (typeof config !== 'string') continue
1215
+ // 整串只有一个占位符时直接取值,避免 String.replace 把对象等类型转成文本
1216
+ const isWhole = /^[$#]\[.+\]$/.test(config) && config.indexOf('$[', 2) === -1 && config.indexOf('#[', 2) === -1
1217
+ let value = config
1218
+ if (isWhole) {
1219
+ value = getValue(config[0], config.slice(2, -1))
1220
+ } else if (/[$#]\[.+\]/.test(config)) {
1221
+ value = config.replace(/([$#])\[((?:[^[\]]|\[[^\]]*\])+)\]/g, (match, flag, path) => getValue(flag, path))
1222
+ }
1223
+ _params[key] = blank(value)
1224
+ }
1225
+ return _params
1226
+ }
1227
+
1228
+ evalExpression (el, data, errorValue = false) {
1229
+ const formulaLibrary = {}
1230
+ for (const key in formula) {
1231
+ formulaLibrary[key] = formula[key]
1232
+ }
1233
+ const _data = {}
1234
+ for (const key in data) {
1235
+ if (key && key !== '[object Object]') {
1236
+ if (!key.includes('#')) {
1237
+ if (key.includes('|')) {
1238
+ const newKey = key.replaceAll('|', '$')
1239
+ el.replaceAll(key.replaceAll('|', '.'), newKey)
1240
+ _data[newKey] = data[key]
1241
+ } else {
1242
+ _data[key] = data[key]
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ try {
1248
+ // eslint-disable-next-line no-new-func
1249
+ return new Function(...Object.keys(formulaLibrary), ...Object.keys(_data), `return ${el}`)(...Object.values(formulaLibrary), ...Object.values(_data))
1250
+ } catch (e) {
1251
+ // ⚠️ 这个 catch 是本函数几乎所有调用点共用的唯一失败通道(showIf / limitConditionEl /
1252
+ // dataFormat / markerCondition / showCondition,以及 $formatContent 内部的 {{...}} 分支等),
1253
+ // 不能整体降噪——公式/条件表达式真写错了(语法错、访问链错)时,控制台报错是唯一线索。
1254
+ //
1255
+ // 只对 ReferenceError("xxx is not defined")单独降级:这类错误专指表达式引用了一个
1256
+ // 不在本次 data/公式库作用域内的标识符,最典型的是 $formatContent 默认作用域
1257
+ // (...this.$tab, ...pageParams, ...this.$query, ...this.$compData)不包含 appConfig /
1258
+ // clientConfig 这类只按实例上下文解析的绑定——每次页面加载只要 $configs 里有形如
1259
+ // {{appConfig['CLIENT|CODE']}} 的占位符就会稳定触发。这是预期路径而非错误:component.js
1260
+ // $formatContent 自己的注释写明「表达式执行失败时保留原内容,占位符可继续交由下面的
1261
+ // $[...] 分支/$formatUrl 等按实例上下文解析」,失败后 el/占位符原样保留,不影响功能。
1262
+ // 其余错误类型(SyntaxError 等语法写挂了、TypeError 等运行时访问出错)大概率是页面配置
1263
+ // 里表达式真的写错了,继续用 error 级别,不跟着一起静音。
1264
+ if (e instanceof ReferenceError) {
1265
+ console.debug('[evalExpression] 标识符不在求值作用域内(占位符可能待其他解析器按实例上下文处理):', e.message, '| 表达式:', el)
1266
+ } else {
1267
+ console.error(e)
1268
+ }
1269
+ return errorValue
1270
+ }
1271
+ }
1272
+
1273
+ openWindow (url) {
1274
+ const a = document.createElement('a')
1275
+ a.href = url
1276
+ a.target = '_blank'
1277
+ document.body.appendChild(a)
1278
+ a.click()
1279
+ document.body.removeChild(a)
1280
+ }
1281
+
1282
+ encrypt (content, key, { type } = {}) {
1283
+ if (type === 'aes') {
1284
+ return import('crypto-js').then(cryptoModule => {
1285
+ const CryptoJS = 'default' in cryptoModule ? cryptoModule.default : cryptoModule
1286
+ const base64Key = atob(key)
1287
+ const aesKey = CryptoJS.enc.Utf8.parse(base64Key.toString(CryptoJS.enc.Utf8))
1288
+ const encrypted = CryptoJS.AES.encrypt(content, aesKey, {
1289
+ mode: CryptoJS.mode.CBC,
1290
+ padding: CryptoJS.pad.Pkcs7
1291
+ })
1292
+ return encrypted.toString()
1293
+ })
1294
+ } else {
1295
+ return import('jsencrypt/bin/jsencrypt.min').then(module => {
1296
+ const JSEncrypt = module.default
1297
+ const encrypt = new JSEncrypt()
1298
+ encrypt.setPublicKey(key)
1299
+ return encrypt.encrypt(content)
1300
+ })
1301
+ }
1302
+ }
1303
+
1304
+ decrypt (content, { type, key }) {
1305
+ if (type === 'aes') {
1306
+ return import('crypto-js').then(cryptoModule => {
1307
+ const CryptoJS = 'default' in cryptoModule ? cryptoModule.default : cryptoModule
1308
+ const base64Key = atob(key)
1309
+ const aesKey = CryptoJS.enc.Utf8.parse(base64Key.toString(CryptoJS.enc.Utf8))
1310
+ const decrypted = CryptoJS.AES.decrypt(content, aesKey, {
1311
+ iv: key,
1312
+ mode: CryptoJS.mode.CBC,
1313
+ padding: CryptoJS.pad.Pkcs7
1314
+ })
1315
+ return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
1316
+ })
1317
+ } else {
1318
+ return import('jsencrypt/bin/jsencrypt.min').then(module => {
1319
+ const JSEncrypt = module.default
1320
+ const encrypt = new JSEncrypt()
1321
+ encrypt.setPrivateKey(key)
1322
+ return encrypt.decrypt(content)
1323
+ })
1324
+ }
1325
+ }
1326
+
1327
+ appendCss (id, css) {
1328
+ const style = document.createElement('style')
1329
+ style.id = 'style_' + id
1330
+ style.textContent = css.replace(/:current/g, '.css_' + id).replace(/url\s*\(([\w+-/.\s]+)\)/g, (str, $1) => {
1331
+ return `url(${util.getFileUrl($1)})`
1332
+ })
1333
+
1334
+ style.setAttribute('type', 'text/css')
1335
+ document.head.appendChild(style)
1336
+ }
1337
+
1338
+ removeCss (id) {
1339
+ const element = document.getElementById('style_' + id)
1340
+ if (element) document.head.removeChild(element)
1341
+ }
1342
+ }
1343
+
1344
+ const util = new Util()
1345
+
1346
+ // 环境工具绑定单例,按函数导入时保留 this 和加载缓存。
1347
+ export const getSystemMeta = (...args) => util.getSystemMeta(...args)
1348
+ export const getFileUrl = (...args) => util.getFileUrl(...args)
1349
+ export const getSysId = (...args) => util.getSysId(...args)
1350
+ export const toLogin = (...args) => util.toLogin(...args)
1351
+ export const usePlugin = (...args) => util.usePlugin(...args)
1352
+ // 产品会 .call(vm, ...) 传组件作用域,不能把它固定绑定到 util。
1353
+ export function runExpByScope (...args) {
1354
+ return util.runExpByScope.apply(this == null ? util : this, args)
1355
+ }
1356
+
1357
+ export default util