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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/API.md +715 -714
  2. package/README.md +3 -1
  3. package/dist/.cache/route-chart-release/frontend/frame/public/js/ecStat.min.js +4 -0
  4. package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts-wordcloud.min.js +6 -0
  5. package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts.min.js +49 -0
  6. package/dist/_virtual/_plugin-vue_export-helper.js +10 -0
  7. package/dist/_virtual/jsencrypt.min.js +1 -1
  8. package/dist/assets/role_select-DTqRlFNz.webp +0 -0
  9. package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/jsencrypt@3.5.4/_vendor/jsencrypt/bin/jsencrypt.min.js +1 -1
  10. package/dist/src/RootApp.js +54 -0
  11. package/dist/src/api/createHttp.js +6 -0
  12. package/dist/src/app-ui-settings.js +49 -0
  13. package/dist/src/axios.js +10 -15
  14. package/dist/src/chart-runtime-assets.js +19 -0
  15. package/dist/src/chart-runtime-sources.js +8 -0
  16. package/dist/src/create-app.js +3 -6
  17. package/dist/src/index.js +2 -0
  18. package/dist/src/loadComponent.js +2 -2
  19. package/dist/src/menu-route.js +11 -0
  20. package/dist/src/mixins/component.js +3 -0
  21. package/dist/src/navigation-context.js +47 -0
  22. package/dist/src/navigation.js +43 -12
  23. package/dist/src/package-install.js +3 -2
  24. package/dist/src/plugins/element-plus.js +8 -8
  25. package/dist/src/public-api.js +4 -2
  26. package/dist/src/route-components.js +9 -0
  27. package/dist/src/router/config.js +7 -8
  28. package/dist/src/runtime-services.js +1 -1
  29. package/dist/src/runtime-utils/meta.js +3 -0
  30. package/dist/src/runtime-utils/pdf.viewer.js +31 -15
  31. package/dist/src/runtime-utils/util.js +28 -5
  32. package/dist/src/shared-packages.js +1 -3
  33. package/dist/src/store/modules/list.js +25 -3
  34. package/dist/src/store/modules/meta.js +49 -7
  35. package/dist/src/style-presets.js +4 -4
  36. package/dist/src/ui-settings.js +90 -18
  37. package/dist/src/views/App.js +48 -0
  38. package/dist/src/views/AppRoleSelect.js +233 -0
  39. package/dist/src/views/Error.js +210 -0
  40. package/dist/src/views/Module.js +173 -0
  41. package/dist/style.css +251 -0
  42. package/package.json +4 -4
  43. package/src/RootApp.vue +75 -0
  44. package/src/api/createHttp.js +7 -0
  45. package/src/api/index.js +47 -47
  46. package/src/app-ui-settings.js +31 -0
  47. package/src/axios.js +311 -318
  48. package/src/chart-runtime-assets.js +13 -0
  49. package/src/chart-runtime-sources.js +5 -0
  50. package/src/create-app.js +3 -7
  51. package/src/index.js +28 -28
  52. package/src/install.js +157 -157
  53. package/src/loadComponent.js +187 -187
  54. package/src/loadLibrary.js +99 -99
  55. package/src/menu-route.js +12 -1
  56. package/src/mixins/component.js +1464 -1461
  57. package/src/navigation-context.js +49 -0
  58. package/src/navigation.js +208 -176
  59. package/src/package-install.js +2 -1
  60. package/src/plugins/element-plus.js +302 -302
  61. package/src/plugins/frame.js +178 -178
  62. package/src/public-api.js +1 -0
  63. package/src/route-components.js +7 -0
  64. package/src/router/config.js +861 -864
  65. package/src/router/public.js +1 -1
  66. package/src/runtime-services.js +1 -1
  67. package/src/runtime-utils/fieldFactory.js +179 -179
  68. package/src/runtime-utils/meta.js +399 -378
  69. package/src/runtime-utils/pdf.viewer.js +672 -644
  70. package/src/runtime-utils/util.js +1359 -1330
  71. package/src/shared-packages.js +1 -3
  72. package/src/store/modules/config.js +351 -351
  73. package/src/store/modules/list.js +30 -3
  74. package/src/store/modules/locale.js +236 -236
  75. package/src/store/modules/meta.js +314 -265
  76. package/src/style-presets.js +4 -4
  77. package/src/ui-settings.js +84 -23
  78. package/src/views/App.vue +53 -0
  79. package/src/views/AppRoleSelect.vue +334 -0
  80. package/src/views/Error.vue +63 -0
  81. package/src/views/Module.vue +210 -0
  82. package/src/xss.js +26 -26
  83. package/types/app.d.ts +10 -10
  84. package/types/axios.d.ts +1 -1
  85. package/types/component-mixin.d.ts +1 -1
  86. package/types/field-factory.d.ts +1 -1
  87. package/types/global-mixin.d.ts +1 -1
  88. package/types/http.d.ts +1 -1
  89. package/types/index.d.ts +3 -0
  90. package/types/meta.d.ts +1 -1
  91. package/types/navigation.d.ts +21 -21
  92. package/types/pdf-viewer.d.ts +1 -1
  93. package/types/runtime.d.ts +13 -13
  94. package/types/store.d.ts +1 -1
  95. package/types/util.d.ts +2 -2
  96. /package/dist/{build → .cache/route-chart-release/frontend/frame/build}/stubs/empty-components.js +0 -0
  97. /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-icons.names.js +0 -0
  98. /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-plus.lazy.js +0 -0
  99. /package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/element-plus@2.14.3_vue@3.5.39_typescript@5.9.3_/_vendor/element-plus/dist/locale/en.js +0 -0
  100. /package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/element-plus@2.14.3_vue@3.5.39_typescript@5.9.3_/_vendor/element-plus/dist/locale/zh-cn.js +0 -0
@@ -1,1461 +1,1464 @@
1
- /* eslint-disable no-eval */
2
- /* eslint-disable no-unused-vars */
3
- // 组件实例
4
- import { mapState, mapGetters } from 'vuex'
5
- import { evalWith } from '@epxing/utils/src/eval-with'
6
- import { CREATE_TAB } from '@epxing/core/src/store/actions'
7
- import util from '@epxing/core/src/runtime-utils/util'
8
- import { isSafeFn } from '@epxing/utils/src/security'
9
- import { isCustomOption } from './custom-option'
10
- // 纯函数层,零依赖(见 menu-route.js 头注释),不会与 router/store 成环
11
- import { resolveRouteRole } from '@epxing/core/src/menu-route'
12
- // 条件初始化:与 global.js / methods-to-prototype.js 共享同一个弹窗栈,
13
- // 无条件重置会让先求值模块捕获的引用变成孤儿数组
14
- if (!window.openedPopup) {
15
- window.openedPopup = []
16
- }
17
- const openedPopup = window.openedPopup
18
-
19
- /**
20
- * ⚙️ T17 修复轮 1(评审 C2)——**一行开关**:keep-alive 剪枝/页签销毁这条路径上要不要执行
21
- * 用户脚本 `onDestory`(设计器事件面板里的「组件销毁时」,见 designer/src/visual/config/Event.vue)。
22
- *
23
- * false(当前值)= 保持 Task 17 之前的可观察语义:这条路径**不**触发 onDestory。
24
- * true = 放开,让这些脚本随真卸载开始执行。
25
- *
26
- * 为什么默认 false:Task 17 的目标是内存修复,不是行为变更。真卸载唤醒了成千上万个此前从未跑过的
27
- * `unmounted`,其中就包括这个**面向低代码用户脚本的逃逸口**——线上页面配置里可能已经存在
28
- * 从迁到 Vue 3 之后一次都没执行过的 onDestory 脚本,它们可以 $store.commit / $bus.off / 写 window /
29
- * 发请求 / 跳转,内容不可静态判定、也没有任何人验证过。让它们在一次内存修复里**顺带开始跑**,
30
- * 属于带风险的范围蔓延。放开前必须先盘点存量配置(FM_PAGE_CONFIG 里 componentEvents.onDestory
31
- * 非空的有多少、分别写了什么),再灰度。
32
- *
33
- * 注意作用域:这个开关**只**管"整棵页面树被销毁"这条路径。页面还活着、只是某个组件被 v-if 摘掉
34
- * 之类的单点卸载,onDestory 照常执行(那条路径在 Task 17 之前本来就在跑,不能反向改掉)。
35
- */
36
- const EXEC_ON_DESTORY_ON_TEARDOWN = false
37
-
38
- /**
39
- * 沿 parent 链找 RouterView 打的 `_epxTeardown` 标记(在 RouterView 的 removeKeepAliveCache /
40
- * beforeUnmount 里打),用来区分"整棵页面树被销毁"与"页面还活着、单个组件被摘掉"。
41
- * 不能用"父实例 scope 是否已 stop"来判:unmountComponent 的顺序是
42
- * 「bum → scope.stop() → 递归卸载子树 → queuePostRenderEffect(um)」,而被剪枝页面的父级是
43
- * KeepAlive,它的 scope 在那一刻仍然 active,判据在最要紧的那一层恰好失效。
44
- */
45
- function isEpxTeardown (inst) {
46
- let p = inst
47
- let guard = 0
48
- while (p && guard++ < 500) {
49
- if (p._epxTeardown === true) return true
50
- p = p.parent
51
- }
52
- return false
53
- }
54
-
55
- // T31:改组件注册表(page._epxReg,替代 $children 树遍历,Vue3 无 $children)。
56
- // this = 页面实例(调用方 apply 到 $parentPage/页面)。
57
- function broadcast (id, eventName, ...params) {
58
- const reg = this && this._epxReg
59
- if (!reg) return
60
- const child = reg.byId.get(id)
61
- if (child && child[eventName]) child[eventName].apply(child, params)
62
- }
63
- function broadcastCom (componentName, eventName, params) {
64
- const reg = this && this._epxReg
65
- if (!reg) return
66
- const set = reg.byName.get('epx-' + componentName)
67
- if (set) for (const child of set) child.$emit.apply(child, [eventName].concat(params))
68
- }
69
- function getComponentById (id) {
70
- const reg = this && this._epxReg
71
- return reg ? reg.byId.get(id) : undefined
72
- }
73
-
74
- function findNavId (menus = [], menuId, parent) {
75
- for (let i = 0; i < menus.length; i++) {
76
- const menu = menus[i]
77
- if (menu.ID === menuId) {
78
- return { ...menu, parentMenu: parent }
79
- } else {
80
- if (menu.children) {
81
- const activeMenu = findNavId(menu.children, menuId, menu)
82
- if (activeMenu) return activeMenu
83
- }
84
- }
85
- }
86
- }
87
-
88
- function appendCss (id, css) {
89
- let style = document.createElement('style')
90
- style.id = 'style_' + id
91
- style.textContent = css.replace(/:current/g, '.css_' + id).replace(/url\s*\(([\w+-/.\s]+)\)/g, (str, $1) => {
92
- return `url(${util.getFileUrl($1)})`
93
- })
94
-
95
- style.setAttribute('type', 'text/css')
96
- document.head.appendChild(style)
97
- style = null
98
- }
99
- function removeCss (id) {
100
- let element = document.getElementById('style_' + id)
101
- if (element) {
102
- document.head.removeChild(element)
103
- element = null
104
- }
105
- }
106
- export default {
107
- $componentType: 'component',
108
- // T33(关 compat 同刻翻转):
109
- //
110
- // 为什么最终要 true:Vue2 里 `inheritAttrs: false` **不管 class/style** —— 它们走
111
- // `$vnode.data.staticClass/class`,无条件落到根元素。Vue3 把 class/style 并进了 attrs,
112
- // false 会把它们一起挡掉 → 父传的 class 全丢。recon-classloss.mjs 实测:**20 个组件**受影响。
113
- //
114
- // 为什么不能提前翻:compat 开着时 Vue2 语义已经让 class 落根了,此时翻成 true 只剩副作用——
115
- // 未声明的级联属性被序列化成 DOM attribute,**渲染直接断**(实测:应用开发平台左侧菜单
116
- // 92 项 → 0 项,且 errors×0 静默失败;commit 1e2ab4f1c 当时测到的 "元素 1642→702" 同因)。
117
- // ⇒ 与 `.native` 一样,语义在开关翻转前后互斥,只能同一刻改。
118
- //
119
- // 翻转时的配套(见 .claude/codemod/codemod-forward-attrs.mjs):
120
- // 1. 转发给子组件的 `$attrs` 已剔掉 class/style(`$forwardAttrs`),否则容器根上的 class
121
- // 会被复制到它的每个子组件上;✅ 已完成
122
- // 2. 级联下发的 currentRoute / pageInfo / queryParams 已上提为 props;✅ 已完成
123
- // 3. 透明代理组件(epx-component / widget-preview)保留自己的 `inheritAttrs: false` +
124
- // 原样转发 `$attrs` —— 它们没有自己的根,class 要送到所渲染的叶子组件上。✅ 已完成
125
- // 4. 翻转后仍会有未声明的级联属性落到根(如 currentQuery / inline)→ 那一刀时按实测逐个声明。
126
- inheritAttrs: true,
127
- navConfigs: {
128
- flag: false
129
- },
130
-
131
- props: {
132
- // 当前页面信息
133
- page: {
134
- type: Object
135
- },
136
- // 当前对象元数据信息
137
- meta: {
138
- type: Object,
139
- default () {
140
- return {}
141
- }
142
- },
143
- compId: {
144
- type: [String, Number]
145
- }, // 组件id
146
- contentType: String, // 模板名称
147
- _component: {
148
- type: Object,
149
- default () {
150
- return {}
151
- }
152
- },
153
- compStyle: {
154
- type: Object,
155
- default () {
156
- return {}
157
- }
158
- }, // 组件样式
159
- compConfigs: {
160
- type: Object,
161
- default () {
162
- return {}
163
- }
164
- },
165
- compFlex: {
166
- type: Boolean
167
- },
168
- compTitle: String,
169
- items: Array, //
170
- itemConfigs: Object,
171
- metaId: String,
172
- // 以下三个由 epx-component / epx-page 沿 $attrs 级联下发,此前无人声明 → 停在 $attrs 里。
173
- // Vue3 的 $attrs 含 class/style,将来把 inheritAttrs 打开(让父传的 class 落到 root)时,
174
- // 未声明的属性会被序列化成 DOM attribute(对象值变 "[object Object]")。声明成 props 即被消费掉。
175
- // 详见 T30b-compat-warning-census.md「INSTANCE_ATTRS 否定结果被推翻」一节。
176
- currentRoute: Object,
177
- pageInfo: Object,
178
- // Component.vue 给**每个** leaf 都绑 :query-params,但只有 EntityList 自己声明了它
179
- // (同形状 Object/default {},合并无冲突)。核对过:无 mixin 使用者把 queryParams 写在
180
- // data() 或 computed 里,故可安全上提。formData 则不行——有 6 个使用者写在 data()。
181
- queryParams: {
182
- type: Object,
183
- default () {
184
- return {}
185
- }
186
- }
187
- },
188
- data () {
189
- return {
190
- pageConfig: {},
191
- $defaultConfigs: {},
192
- httpLoading: {},
193
- component: {},
194
- configs: {}
195
- }
196
- },
197
- inject: {
198
- pageId: {
199
- type: String,
200
- default: null
201
- },
202
- pageMetaId: {
203
- type: String,
204
- default: null
205
- },
206
- // T31:最近的 epx 父容器(provide 下发),mounted 注册进其 _epxChildren 支撑子树遍历
207
- _epxParentComp: {
208
- default: null
209
- },
210
- upPage: {
211
- type: Object,
212
- default: () => null
213
- },
214
- // 获取所属标签页
215
- $getTab: {
216
- type: Function,
217
- default: null
218
- },
219
- template: {
220
- type: Object,
221
- default: null
222
- },
223
- createRoute: {
224
- default: null
225
- },
226
- destoryRoute: {
227
- default: null
228
- },
229
- reloadRoute: {
230
- default: null
231
- }
232
- },
233
- /**
234
- * 向下传递页面实例和模板
235
- *
236
- * 设计说明:
237
- * 1. 页面组件(isPage=true)在 BasePage.vue 中提供 upPage,这里不需要重复提供
238
- * 2. 非页面组件需要继续向下传递 upPage,确保所有子组件都能访问到页面实例
239
- *
240
- * 为什么需要 inject 后再 provide?
241
- * - Vue 的 provide/inject 是单向的,只向下传递,不会自动向上查找
242
- * - 如果中间某个组件不 provide,其子组件将无法获取 upPage(即使祖先组件提供了)
243
- * - 通过在 mixin 中统一处理,确保所有组件都能自动传递,避免遗漏
244
- *
245
- * 这种模式被称为"透传"(pass-through),是 Vue provide/inject 的常见用法
246
- * 优点:
247
- * - 统一在 mixin 中处理,所有组件自动继承,无需手动处理
248
- * - 确保页面实例能够传递到任意深度的子组件
249
- * - 避免因中间组件忘记 provide 而导致子组件无法获取的问题
250
- */
251
- provide () {
252
- // T31:每 epx 组件把自己作为「最近 epx 父容器」下发,子组件 mounted 时注册进 _epxChildren,
253
- // 支撑子树级遍历(Card.refesh/AddForm 容器子收集,替代 $children 递归)。
254
- const base = { _epxParentComp: this }
255
- // 页面组件已经在 BasePage.vue 中提供了 upPage,这里不需要重复提供
256
- // 非页面组件需要继续向下传递,确保子组件能够访问到页面实例
257
- if (!this.isPage) {
258
- base.upPage = this.upPage // 透传页面实例
259
- base.template = this.template // 透传模板
260
- }
261
- // T28:Vue3 的 provide 合并不容忍 undefined 返回值
262
- return base
263
- },
264
- computed: {
265
- ...mapState({
266
- appMenus: state => state.config.appMenus,
267
- $screen: state => state.config.screen || {}, // 屏幕信息
268
- $tabs: state => state.tabs.tabs, // 标签页
269
- metas: state => state.meta.metas, // 元数据信息集合
270
- user: state => state.user.user, // 用户信息
271
- componentGlobalConfig: state => state.config.componentGlobalConfig, // 组件全局配置
272
- systemComponentGlobalConfig: state => state.config.systemComponentGlobalConfig, // 组件全局配置
273
- currentMenu: state => state.config.currentMenu // 当前导航菜单
274
- }),
275
- ...mapGetters([
276
- 'getMeta',
277
- 'getPageById'
278
- ]),
279
- CLIENT () {
280
- const { CLIENT: queryClient } = this.$query
281
- const { CLIENT: tabClient } = this.$tab
282
- const { client: routeClient } = this.$route.query
283
- const { client: userClient } = this.user
284
- const { CLIENT: appClient, 'CLIENT|ID': clientId } = this.appConfig
285
- return queryClient || tabClient || routeClient || userClient || appClient || clientId
286
- },
287
- SYSTEM () {
288
- const { SYSTEM: querySystem } = this.$query
289
- const { SYSTEM: tabSystem } = this.$tab
290
- const { system: routeSystem } = this.$route.query
291
- const { configSystem = {} } = this.$store.state.config
292
- const { SYSTEM: appSystem, 'SYSTEM|ID': systemId } = this.appConfig
293
- return querySystem || tabSystem || routeSystem || configSystem.ID || appSystem || systemId
294
- },
295
- APP () {
296
- const { APP: queryApp } = this.$query
297
- const { APP: tabApp } = this.$tab
298
- return queryApp || tabApp
299
- },
300
- $tab () {
301
- if (this.$getTab) return this.$getTab()
302
- return this.$activeTab || {}
303
- },
304
- /**
305
- * 选中标签页
306
- */
307
- $activeTab () {
308
- return this.$tabs.find(item => item.active) || {}
309
- },
310
- $tabQuery () {
311
- return this.$tabs.find(item => item.active) || {}
312
- },
313
- isPage () {
314
- const { $componentType } = this.$options
315
- return $componentType === 'page'
316
- },
317
- /**
318
- * 当前页面参数
319
- */
320
- $query () {
321
- // 获取该组件是否是页面
322
- // 获取路由参数
323
- // 当前路由不是登录页
324
- const routeQuery = {}
325
- const { name, query = {}, params = {} } = this.$route || {}
326
- if (name !== 'hallLogin' && name !== 'login') {
327
- for (const key in query) {
328
- if (query[key]) routeQuery[key] = query[key]
329
- }
330
- for (const key in params) {
331
- if (params[key]) routeQuery[key] = params[key]
332
- }
333
- }
334
- if (!this.$_.isEmpty(this.currentQuery)) {
335
- const currentQuery = this.currentQuery || {}
336
- return { ...routeQuery, ...currentQuery }
337
- } else {
338
- // 获取上级页面
339
- if (this.upPage) {
340
- const currentQuery = this.upPage.currentQuery || {}
341
- return { ...routeQuery, ...currentQuery, ...(this.upPage.pageParams || {}) }
342
- }
343
- return routeQuery
344
- }
345
- },
346
- $configVersion () {
347
- const queryPageConfigs = this.$query.pageConfigs || {}
348
- const { config = queryPageConfigs } = this.$pageInfo
349
- return config.version
350
- },
351
- getComponentId () {
352
- return component => {
353
- const queryPageConfigs = this.$query.pageConfigs || {}
354
- const { config = queryPageConfigs } = this.$pageInfo
355
- if (!this.isPage && !config.version) {
356
- const { id } = this.$component
357
- return id + '-' + component.id
358
- }
359
- return component.id
360
- }
361
- },
362
- $component: {
363
- get () {
364
- return this.component
365
- },
366
- set (v) {
367
- this.component = v
368
- }
369
- },
370
- $subcomponents () {
371
- const { items } = this.$component
372
- // 判断是否迭代
373
- return this.$formatComponents(items, this._component) || []
374
- },
375
- $compData () {
376
- const iterator = this._component.iterator || {}
377
- return { ...iterator, $templateVariables: this.$templateVariables }
378
- },
379
- $configs: {
380
- get () {
381
- // 格式化内容
382
- const _configs = {}
383
- // 动态参数的取值来源建**一次**就够。$formatContent 的默认参数是
384
- // `{ ...$tab, ...pageParams, ...$query, ...$compData }`,而默认参数是**每次调用都求值**的——
385
- // 原先这里逐个字符串键调用,一个组件有多少个字符串配置就重建多少遍同一个对象。
386
- // 实测(.claude 微基准,28 个字符串键):合并对象 324 µs、两遍正则只要 28 µs,
387
- // 也就是说这条路径 92% 的开销花在重复构造上,提出来能砍掉约九成。
388
- //
389
- // ⚠️ 必须**惰性**构造,不能提到循环外无条件建:那样 getter 会无条件依赖
390
- // $tab/$query/$compData,连一个字符串配置都没有的组件也会在路由参数变化时失效重算,
391
- // 平白多出依赖边。惰性构造与原先「首次调用 $formatContent 时才求值」的依赖集完全一致。
392
- let data
393
- for (const key in this.configs) {
394
- const item = this.configs[key]
395
- if (typeof item === 'string') {
396
- if (data === undefined) {
397
- data = { ...this.$tab, ...(this.upPage?.pageParams || {}), ...this.$query, ...this.$compData }
398
- }
399
- _configs[key] = this.$formatContent(item, data)
400
- } else if (/operates$/i.test(key)) {
401
- // 按钮文案统一走界面文案翻译,覆盖组件内置的默认按钮(含运行时注入的默认操作)
402
- _configs[key] = this.$translateOperates(item)
403
- } else {
404
- _configs[key] = this.configs[key]
405
- }
406
- }
407
- return _configs
408
- },
409
- set (v) {
410
- this.configs = v
411
- }
412
- },
413
- // 当前页面id
414
- $pageMetaId () {
415
- // 获取当前传递的metaId
416
- let metaId = null
417
- if (this.meta && this.meta.id) {
418
- metaId = this.meta.id
419
- }
420
- return metaId || this.pageMetaId
421
- },
422
- /**
423
- * 当前组件对象元数据id 不设置 默认为当前页面对象
424
- */
425
- $metaId () {
426
- const { metaId } = this.$configs
427
- return this.metaId || metaId || this.$pageMetaId || this.$query.metaId
428
- },
429
- // 当前组件对象
430
- $meta () {
431
- if (this.$metaId) {
432
- const meta = this.getMeta(this.$metaId)
433
- return meta
434
- } else {
435
- return null
436
- }
437
- },
438
- $metaKey () {
439
- if (this.$meta) {
440
- return this.$meta.getKey() || 'ID'
441
- } else {
442
- return 'ID'
443
- }
444
- },
445
- $parentPage () {
446
- // 父级页面
447
- const { parentPage } = this.component
448
- return parentPage
449
- },
450
- $parentPageModel () {
451
- const { parentPage = {} } = this.component
452
- return parentPage.model
453
- },
454
- $pageId () {
455
- const { pageId } = this.page || {}
456
- return this.isPage ? pageId : this.pageId || pageId
457
- },
458
- $pageInfo () {
459
- if (this.$pageId) return this.getPageById(this.$pageId) || {}
460
-
461
- return this.page || (!this.isPage && this.upPage && this.upPage.page) || {}
462
- },
463
- $pageParams () {
464
- if (this.isPage) return this.pageParams || {}
465
- else if (this.upPage) return this.upPage.pageParams || {}
466
- return {}
467
- },
468
- $templateVariables () {
469
- return this.template?.variables || {}
470
- },
471
- /**
472
- * 事件处理总线
473
- */
474
- $event () {
475
- const vm = this
476
- let comp = vm
477
- while (comp.$parent && comp.$options.$componentType !== 'page') comp = comp.$parent
478
- return {
479
- emit (event, ...params) {
480
- // T25:程序化通道走 $epxEmit(Vue3 无实例事件);$emit 保留给模板 @listener
481
- comp.$epxEmit(event, [...params])
482
- comp.$emit(event, [...params])
483
- },
484
- on (event, callback) {
485
- const handler = params => callback.apply(comp, params)
486
- comp.$epxOn(event, handler)
487
- // 记录在组件实例上,beforeDestroy 时解除绑定
488
- if (!vm._pageEventHandlers) vm._pageEventHandlers = []
489
- vm._pageEventHandlers.push({ comp, event, handler })
490
- }
491
- }
492
- },
493
- $currentRole () {
494
- // 菜单直达路由(appMenu/hallMenu)没有 role 段,URL 末段是 menuKey——直接读
495
- // $route.params.role 会得到 undefined,$currentRole 恒为 {}。页面配置的 showIf
496
- // 表达式里也拿得到 $currentRole(见下方 elData),空值会让按角色显示的节点全部消失。
497
- const role = resolveRouteRole(this.$route)
498
- const { serviceRoles = {} } = this.user
499
- const roleList = serviceRoles[this.appConfig.ID] || []
500
- let roleCode = role
501
- if (role && role.endsWith('-client')) {
502
- roleCode = role.replace(/-client$/, '')
503
- // 从通用角色获取
504
- return roleList.find(item => item.code === roleCode && item.type === 'client') || {}
505
- } else {
506
- return roleList.find(item => item.code === roleCode && item.type !== 'client') || {}
507
- }
508
- },
509
- $currentRoleId () {
510
- return this.$currentRole.id
511
- }
512
- },
513
- watch: {
514
- compConfigs (v) {
515
- if (!this.$_.isEqual(this.oldCompConfigs, v)) {
516
- this._createComponent()
517
- }
518
- this.oldCompConfigs = this.$_.cloneDeep(v)
519
- }
520
- },
521
- methods: {
522
- // ========== T31 组件注册表(替代 $children 树遍历)==========
523
- // 页面级作用域:注册表挂在页面组件(isPage)上,非页面组件经 upPage 就近找页面注册。
524
- _epxPage () {
525
- if (this.isPage) return this
526
- const inst = this.upPage && this.upPage.instance
527
- return inst || null
528
- },
529
- _epxRegistry (page) {
530
- const p = page || this._epxPage()
531
- if (!p) return null
532
- if (!p._epxReg) p._epxReg = { byId: new Map(), byName: new Map() }
533
- return p._epxReg
534
- },
535
- _epxRegister () {
536
- // 页面级注册表(broadcast/getComponentById 用)
537
- const reg = this._epxRegistry()
538
- if (reg) {
539
- if (this.compId != null) reg.byId.set(this.compId, this)
540
- const name = this.$options.name
541
- if (name) {
542
- let set = reg.byName.get(name)
543
- if (!set) { set = new Set(); reg.byName.set(name, set) }
544
- set.add(this)
545
- }
546
- }
547
- // 父子注册(子树遍历用,Card.refesh/AddForm 容器):注册进最近 epx 父容器
548
- const parent = this._epxParentComp
549
- if (parent && parent !== this) {
550
- if (!parent._epxChildren) parent._epxChildren = new Set()
551
- parent._epxChildren.add(this)
552
- }
553
- },
554
- _epxDeregister () {
555
- const page = this._epxPage()
556
- if (page && page._epxReg) {
557
- const reg = page._epxReg
558
- if (this.compId != null && reg.byId.get(this.compId) === this) reg.byId.delete(this.compId)
559
- const name = this.$options.name
560
- const set = name && reg.byName.get(name)
561
- if (set) set.delete(this)
562
- }
563
- const parent = this._epxParentComp
564
- if (parent && parent._epxChildren) parent._epxChildren.delete(this)
565
- },
566
- // T31:遍历子树内 epx 组件(含深层后代,递归 _epxChildren),cb(comp)。替代 $children 递归。
567
- _epxWalkChildren (cb) {
568
- const stack = this._epxChildren ? [...this._epxChildren] : []
569
- while (stack.length) {
570
- const c = stack.pop()
571
- if (!c) continue
572
- cb(c)
573
- if (c._epxChildren) for (const gc of c._epxChildren) stack.push(gc)
574
- }
575
- },
576
- $formatComponents (arr, component = this._component) {
577
- const { iterator: compIterator } = component
578
- const queryPageConfigs = this.pageConfig || this.$query.pageConfigs || {}
579
- const { config: pageConfigs = queryPageConfigs } = this.$pageInfo
580
- const itemConfigs = pageConfigs.itemConfigs || {}
581
- const page = this.upPage || this
582
- const components = []
583
- if (!arr) return []
584
- for (const item of arr) {
585
- const { configs = {}, id } = item
586
- const compConfigs = itemConfigs[id] || {}
587
- const allConfigs = { ...compConfigs, ...configs }
588
- const { iterator, showIf, showTerminal } = allConfigs
589
- const terminal = this.$isMobile ? 'mobile' : 'pc'
590
- if (!showTerminal || showTerminal.includes(terminal)) {
591
- if (iterator?.data) {
592
- const { data, name = 'item', key = 'ID', index = 'index' } = iterator
593
- let iteratorData = []
594
- if (Array.isArray(data)) {
595
- iteratorData = data
596
- } else {
597
- const matches = /^\$\[([\w.|$-]+)\]$/.exec(data)
598
- if (matches) {
599
- const data = { ...this.$tab, ...(page.pageParams || {}), ...this.$query, ...this.$compData, ...(compIterator || {}), template: this.template, $templateVariables: this.$templateVariables }
600
- iteratorData = this.$util.getValueByPath(matches[1], data)
601
- }
602
- }
603
- if (iteratorData && Array.isArray(iteratorData)) {
604
- for (let i = 0; i < iteratorData.length; i++) {
605
- const data = iteratorData[i]
606
- const iteratorKey = data[key] || i
607
- if (showIf) {
608
- const elData = { user: this.user, $currentRole: this.$currentRole, $tab: this.$tab, ...this.$query, ...this.$pageParams, ...(compIterator || {}), [name]: data, [index]: i, key: iteratorKey, template: this.template, $templateVariables: this.$templateVariables }
609
- if (typeof showIf === 'object') {
610
- if (showIf.el && !util.evalExpression(showIf.el, elData)) {
611
- continue
612
- }
613
- } else {
614
- if (!util.evalExpression(showIf, elData)) {
615
- continue
616
- }
617
- }
618
- }
619
- const component = { ...item, key: item.id + '-' + iteratorKey, iterator: { ...(compIterator || {}), [name]: data, [index]: i, key: iteratorKey } }
620
- if (item.type === 'template') {
621
- components.push(component)
622
- } else {
623
- components.push(component)
624
- }
625
- }
626
- }
627
- } else {
628
- const elData = { user: this.user, $currentRole: this.$currentRole, $tab: this.$tab, ...this.$query, ...this.$pageParams, ...(compIterator || {}), template: this.template, $templateVariables: this.$templateVariables }
629
- if (showIf) {
630
- if (typeof showIf === 'string' && !util.evalExpression(showIf, elData)) {
631
- continue
632
- } else if (showIf.el && !util.evalExpression(showIf.el, elData)) {
633
- continue
634
- }
635
- }
636
- const component = compIterator ? { ...item, iterator: compIterator } : { ...item }
637
- component.key = component.id
638
- if (item.type === 'template') {
639
- components.push(component)
640
- } else {
641
- components.push(component)
642
- }
643
- }
644
- }
645
- }
646
- return components
647
- },
648
- $formatContent (content, data = { ...this.$tab, ...(this.upPage?.pageParams || {}), ...this.$query, ...this.$compData }) {
649
- if (!content) return content
650
- // 当内容整体以 {{ 开头、}} 结尾时,直接返回表达式执行结果,避免 replace 将对象类型强制转换为字符串
651
- const fullMatch = content.match(/^\s*\{\{([\w-.,:?+\s|$#[/\]()"']+)\}\}\s*$/)
652
- if (fullMatch) {
653
- try {
654
- // 表达式执行失败时保留原内容,占位符可继续交由下面的 $[...] 分支按实例上下文解析
655
- return this.$util.evalExpression(fullMatch[1], data, content)
656
- } catch (error) {
657
- return content
658
- }
659
- }
660
- return content.replace(/\{\{([\w-.,:?+\s|$#[/\]()"']+)\}\}/g, (macth, key) => {
661
- try {
662
- // 执行表达式,失败时保留原占位符
663
- return this.$util.evalExpression(key, data, macth)
664
- } catch (error) {
665
- return `{{${key}}}`
666
- }
667
- }).replace(/\$\[([\w-.|$#]+)\]/g, (macth, key) => {
668
- try {
669
- const value = this.$util.getValueByPath(key, this) || this.$util.getValueByPath(key, data)
670
- return this.$_.isNil(value) ? '' : value
671
- } catch (error) {
672
- return `$[${key}]`
673
- }
674
- })
675
- },
676
- /**
677
- * 翻译按钮文案。组件代码内置的默认按钮(如「保存」「返回」)不在页面配置里,
678
- * 服务端配置投影覆盖不到,只能走界面文案通道:以中文原文为 key 调 $t。
679
- * 页面里配置出来的按钮文案已由服务端投影翻好,$t 未命中会原样返回 key,
680
- * 因此对它们无副作用,重复调用也是幂等的。
681
- * 按原数组缓存结果,保证同一份配置每次拿到的引用不变。
682
- */
683
- $translateOperates (operates) {
684
- // 组件库脱离 i18n 插件单独使用时不做翻译,避免影响渲染主链路
685
- if (!Array.isArray(operates) || !this.$i18n || typeof this.$t !== 'function') return operates
686
- const cache = this._operatesI18nCache || (this._operatesI18nCache = new WeakMap())
687
- if (cache.has(operates)) return cache.get(operates)
688
- let translated = false
689
- const result = operates.map(operate => {
690
- if (!operate || typeof operate !== 'object') return operate
691
- const { text } = operate
692
- if (typeof text !== 'string' || !text) return operate
693
- const value = this.$t(text)
694
- if (value === text) return operate
695
- translated = true
696
- return { ...operate, text: value }
697
- })
698
- // 无一条命中译文时返回原数组,默认语言下等同于未改动
699
- const value = translated ? result : operates
700
- cache.set(operates, value)
701
- return value
702
- },
703
- /**
704
- * 解析 url 中的变量占位符,支持 {{CLIENT}} 和 $[CLIENT] 两种写法
705
- * 变量可为实例路径表达式,如 {{$route.params.client}}、{{appConfig['CLIENT']}}
706
- * 取值顺序与 $formatParams 一致:组件实例(CLIENT/SYSTEM/APP 等计算属性)→ 标签页/页面参数
707
- * 无法解析的占位符保留原样
708
- */
709
- $formatUrl (url) {
710
- if (!url) return url
711
- return url.replace(/\{\{([^}]+)\}\}|\$\[((?:[^[\]]|\[[^\]]*\])+)\]/g, (match, curlyKey, bracketKey) => {
712
- const key = (curlyKey || bracketKey).trim()
713
- try {
714
- const value = this.$util.getValueByPath(key, this)
715
- if (!this.$_.isNil(value) && value !== '') return value
716
- const fallback = this.$util.getValueByPath(key, { ...this.$tab, ...this.$query, ...this.$pageParams, ...this.$compData })
717
- return this.$_.isNil(fallback) ? match : fallback
718
- } catch (error) {
719
- return match
720
- }
721
- })
722
- },
723
- $formatScope (scopeParams, data = this.data) {
724
- const _scopeParams = this.$_.cloneDeep(scopeParams)
725
- const that = this
726
- const paramData = { ...this.$tab, ...this.$query, ...this.$pageParams, ...that.$compData }
727
- for (const scopeKey in _scopeParams) {
728
- const scope = _scopeParams[scopeKey]
729
- if (!scope) continue
730
- let values = scope.values
731
- if (/#\{([\w-.|$#]+)\}/.test(values)) {
732
- values = values.replace(/#\{([\w-.|$#]+)\}/, (match, key) => {
733
- try {
734
- // #{key} 里的 key 是相对组件实例的路径:显式传作用域,不能靠直接 eval 捕获 that(见 eval-with.js)
735
- return evalWith(`that.${key}`, { that }) || evalWith(`that.$query.${key}`, { that }) || evalWith(`that.$tab.${key}`, { that }) || ''
736
- } catch (error) {
737
- return scope.values
738
- }
739
- })
740
- }
741
- if (/\$\[([\w-_.|$#]+)\]/.test(values)) {
742
- values = values.replace(/\$\[([\w-.|$#]+)\]/, (match, key) => {
743
- try {
744
- return that.$util.getValueByPath(key, this) || that.$util.getValueByPath(key, paramData) || ''
745
- } catch (error) {
746
- return scope.values
747
- }
748
- })
749
- }
750
- if (/#\[([\w-.|$#]+)\]/.test(values)) {
751
- values = values.replace(/#\[([\w-.|$#]+)\]/, (match, key) => {
752
- try {
753
- return evalWith(`data.${key}`, { data }) || ''
754
- } catch (error) {
755
- return scope.values
756
- }
757
- })
758
- }
759
- if (/\{\{([\w-_.,|$#[\]()"'\s]+)\}\}/g.test(values)) {
760
- values = values.replace(/\{\{([\w-.,|$#[\]()"'\s]+)\}\}/g, (macth, key) => {
761
- try {
762
- // 执行表达式
763
- return this.$util.evalExpression(key, paramData)
764
- } catch (error) {
765
- return `{{${key}}}`
766
- }
767
- })
768
- }
769
- scope.values = values
770
- if (scope.scopeParams) {
771
- scope.scopeParams = this.$formatScope(scope.scopeParams, data)
772
- }
773
- }
774
- return _scopeParams
775
- },
776
- goWebLink (config) {
777
- const { webLinkUrl } = config
778
- if (!webLinkUrl) return
779
- window.open(this.$formatUrl(webLinkUrl))
780
- },
781
- async goApp (config, item) {
782
- let { serviceId, params = {} } = config
783
- // 格式化params
784
- const formatParams = this.$formatParams(params, item)
785
- if (!serviceId && formatParams.serviceId) serviceId = formatParams.serviceId
786
- if (!serviceId) return
787
- let app = {}
788
- await this.$http.get({
789
- entityId: 'FM_SERVICE',
790
- id: serviceId
791
- })
792
- .then(data => {
793
- if (data && data.data) {
794
- app = data.data
795
- }
796
- })
797
- if (!app.SYSTEM) return
798
- this.$http.get({
799
- link: 'client',
800
- entityId: 'FM_SYSTEM',
801
- id: app.SYSTEM
802
- })
803
- .then(data => {
804
- if (data && data.data) {
805
- const rootPath = this.$util.getRootPath()
806
- let url = `${rootPath}/app/${data.data['CLIENT|CODE']}/${data.data.CODE}/${app.CODE}`
807
- // 拼接 query 参数(排除 serviceId 和空值)
808
- const queryPairs = []
809
- Object.keys(formatParams).forEach(key => {
810
- if (key === 'serviceId') return
811
- const value = formatParams[key]
812
- if (value === undefined || value === null || value === '') return
813
- queryPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
814
- })
815
- if (queryPairs.length) url += `?${queryPairs.join('&')}`
816
- window.open(url)
817
- }
818
- })
819
- },
820
- /**
821
- * 通用打印
822
- */
823
- print ({ num = 0, printWay = 'custom', printCode = '', cptName = '', params = {}, ...props }, row) {
824
- // 格式化params
825
- let selections = []
826
- // 检测是否选中元素
827
- if (row) {
828
- selections = [{ ...row }]
829
- } else {
830
- if (this.selection) selections = [...this.selection]
831
- }
832
- if (num !== 0 && num !== '0') {
833
- if (selections.length === 0) {
834
- this.$message.error(this.$t('please select a record'))
835
- return
836
- } else if (selections.length > 1) {
837
- if (num === 1 || num === '1') {
838
- this.$message.error(this.$t('Only one record can be selected'))
839
- return
840
- }
841
- }
842
- }
843
- // 添加主键
844
- if (!params.id) params.id = `#[${this.$metaKey}]`
845
- const formatParams = this.$formatParams(params, selections)
846
- if (printWay === 'fine') {
847
- const ids = selections.map(item => `'${item[this.$metaKey]}'`)
848
- formatParams.id = ids.join(',')
849
- this.fineReportPrint(cptName, formatParams)
850
- } else {
851
- this.customReportPrint(printCode, formatParams, props)
852
- }
853
- },
854
- /**
855
- * 自定义报表打印方法
856
- * @param {String} code 报表编号
857
- * @param {Object} params 参数
858
- */
859
- customReportPrint (code, params, props) {
860
- if (params.code) code = params.code
861
- this.$http.find({
862
- entityId: 'FM_FORM',
863
- 'scope.CODE': code
864
- })
865
- .then(data => {
866
- const { NAME } = data.data
867
- this.$createPage({
868
- component: 'excel-preview-page',
869
- name: NAME,
870
- code,
871
- params,
872
- openType: props.openMode,
873
- ...props
874
- })
875
- })
876
- },
877
- fineReportPrint (cptName, params) {
878
- const { 'CLIENT|CODE': client } = this.appConfig
879
- const rootPath = window.IP + 'report/ReportServer'
880
- let query = `?client=${client}&reportlet=com.epxing.frame.core.report.ReportReader&__bypagesize__=false&cptname=${cptName}`
881
- for (const key in params) {
882
- query += '&' + key + '=' + params[key]
883
- }
884
-
885
- const token = this.$util.getToken(client) || ''
886
- window.open(rootPath + query + '&token=' + token)
887
- },
888
-
889
- /**
890
- * 执行自定义JS
891
- */
892
- executeCustomJs (functionName, ...args) {
893
- let pageEvents = {}
894
- if (this.isPage) {
895
- pageEvents = this.pageEvents
896
- } else if (this.template) {
897
- pageEvents = this.template.pageEvents
898
- } else {
899
- pageEvents = this.upPage.pageEvents
900
- }
901
- if (functionName && pageEvents[functionName]) {
902
- const fn = pageEvents[functionName]
903
- if (!isSafeFn(fn)) return false
904
- return fn.call(this, ...args)
905
- } else {
906
- return true
907
- }
908
- },
909
- customJs ({ function: functionName, ...otherConfigs }, row) {
910
- this.executeCustomJs(functionName, row || otherConfigs, otherConfigs)
911
- },
912
-
913
- commonRuleAction (config, ...props) {
914
- this.commonAction({ ...config, requestType: 'rule' }, ...props)
915
- },
916
- /**
917
- * 通用请求
918
- * @param action 请求命令
919
- * @param rule 请求业务规则
920
- * @param metaId 请求对象,默认为当前页面对象
921
- * @param num 选中记录数(1,必须选中且只能选中一条记录;n:必须选中至少一条记录;0不需要选中记录)
922
- * @param comfirmMsg 操作提交之前的确认信息(如果为空,就不需要确认)
923
- * @param params #[列表对象的某字段名]",(如果是选中的多条记录,将字段值拼成一个字符串,用逗号隔开) "$[当前页面的某变量名]"
924
- * @param successMsg 操作成功的提示
925
- * @param failMsg 操作失败的提示
926
- * @param row 操作的列
927
- */
928
- commonAction ({ id, text, requestType, action, rule, metaId = (this.$metaId), num = 0, confirm, confirmMsg = null, confirmButtonText, cancelButtonText, mask = false, params = {}, successMsg = '处理成功', failMsg = '处理失败', afterTrigger, afterTriggerPage, afterTriggerPageOpenMode, afterTriggerParams }, row) {
929
- let selections = []
930
- // 检测是否选中元素
931
- if (row) {
932
- selections = [{ ...row }]
933
- } else {
934
- if (this.selection) selections = [...this.selection]
935
- }
936
- if (selections.length === 0) {
937
- if (num !== 0 && num !== '0') {
938
- this.$message.error(this.$t('please select a record'))
939
- return
940
- }
941
- } else if (selections.length > 1) {
942
- if (num === 1 || num === '1') {
943
- this.$message.error(this.$t('Only one record can be selected'))
944
- return
945
- }
946
- }
947
-
948
- if (!params.id) {
949
- params.id = `#[${this.$metaKey}]`
950
- }
951
- const formatParams = this.$formatParams(params, selections)
952
- const loadingKey = id || text || `common-${action}`
953
- // 提交方法
954
- const submitFn = () => {
955
- if (this.httpLoading[loadingKey]) {
956
- return this.$message.error('正在提交,请稍后...')
957
- }
958
- this.httpLoading[loadingKey] = true
959
- let loading = null
960
- if (mask) {
961
- loading = this.$loading({
962
- target: this.upPage ? this.upPage.$parent.$el : this.$el,
963
- background: 'rgba(255, 255, 255, 0.5)',
964
- text: '正在操作,请稍后...',
965
- lock: true
966
- })
967
- }
968
- const timer = setTimeout(() => {
969
- this.httpLoading[loadingKey] = false
970
- clearTimeout(timer)
971
- }, 2000)
972
- const params = {
973
- action,
974
- entityId: metaId,
975
- ...formatParams
976
- }
977
- if (requestType === 'rule') params.rule = rule
978
-
979
- this.$http.$post(params)
980
- .then((res) => {
981
- // 如果存在下一个事件,执行下一个事件
982
- const _afterTriggerParams = this.$formatAfterTriggerParams(afterTriggerParams, { res, data: row || this.data })
983
- this.$message.success(successMsg)
984
- if (afterTrigger) {
985
- this[afterTrigger]({ pageId: afterTriggerPage, openMode: afterTriggerPageOpenMode, params: _afterTriggerParams })
986
- } else {
987
- if (this.reloadData) this.reloadData()
988
- }
989
- // 广播「这一页的数据被改动了」。reloadData 只刷自己那张列表,够不到同一页上的
990
- // 统计卡:处理完一条待办,列表少一行而顶上的「待办 N」还是原来的数字,
991
- // 看着像统计不准。谁关心谁监听(见 CardNum 的 data-changed)
992
- if (this.$event) this.$event.emit('data-changed', { action, metaId })
993
- })
994
- .catch(err => {
995
- this.$message.error(err.message || failMsg)
996
- })
997
- .finally(() => {
998
- this.httpLoading[loadingKey] = false
999
- if (loading) loading.close()
1000
- })
1001
- }
1002
- // 判断是否需要确认弹窗
1003
- if (confirm && confirmMsg) {
1004
- this.$confirm(confirmMsg, this.$t('hint'), {
1005
- confirmButtonText: confirmButtonText || this.$t('confirm'),
1006
- cancelButtonText: cancelButtonText || this.$t('cancel'),
1007
- type: 'warning'
1008
- }).then(() => {
1009
- submitFn()
1010
- })
1011
- } else {
1012
- submitFn()
1013
- }
1014
- },
1015
- /**
1016
- * 下载文件
1017
- * @param action 请求命令
1018
- * @param rule 请求业务规则
1019
- * @param metaId 请求对象,默认为当前页面对象
1020
- * @param num 选中记录数(1,必须选中且只能选中一条记录;n:必须选中至少一条记录;0不需要选中记录)
1021
- * @param comfirmMsg 操作提交之前的确认信息(如果为空,就不需要确认)
1022
- * @param params #[列表对象的某字段名]",(如果是选中的多条记录,将字段值拼成一个字符串,用逗号隔开) "$[当前页面的某变量名]"
1023
- * @param successMsg 操作成功的提示
1024
- * @param failMsg 操作失败的提示
1025
- * @param row 操作的列
1026
- */
1027
- downloadFile ({ requestType, action, rule, metaId = (this.$metaId), num = 0, confirm, confirmMsg = null, confirmButtonText, cancelButtonText, mask = false, params = {}, successMsg = '处理成功', failMsg = '处理失败', afterTrigger, afterTriggerPage, afterTriggerParams }, row) {
1028
- let selections = []
1029
- // 检测是否选中元素
1030
- if (row) {
1031
- selections = [{ ...row }]
1032
- } else {
1033
- if (this.selection) selections = [...this.selection]
1034
- }
1035
- if (selections.length === 0) {
1036
- if (num !== 0 && num !== '0') {
1037
- this.$message.error(this.$t('please select a record'))
1038
- return
1039
- }
1040
- } else if (selections.length > 1) {
1041
- if (num === 1 || num === '1') {
1042
- this.$message.error(this.$t('Only one record can be selected'))
1043
- return
1044
- }
1045
- }
1046
-
1047
- if (!params.id) {
1048
- params.id = `#[${this.$metaKey}]`
1049
- }
1050
- // 格式化params
1051
- const formatParams = this.$formatParams(params, selections)
1052
- // 提交方法
1053
- const submitFn = () => {
1054
- if (this.httpLoading[`common-${action}`]) {
1055
- return this.$message.error('正在提交,请稍后...')
1056
- }
1057
- this.httpLoading[`common-${action}`] = true
1058
- let loading = null
1059
- if (mask) {
1060
- loading = this.$loading({
1061
- target: this.upPage ? this.upPage.$parent.$el : this.$el,
1062
- background: 'rgba(255, 255, 255, 0.5)',
1063
- text: '正在操作,请稍后...',
1064
- lock: true
1065
- })
1066
- }
1067
- const timer = setTimeout(() => {
1068
- this.httpLoading[`common-${action}`] = false
1069
- clearTimeout(timer)
1070
- }, 2000)
1071
- const params = {
1072
- action,
1073
- entityId: metaId,
1074
- ...formatParams
1075
- }
1076
- if (requestType === 'rule') params.rule = rule
1077
-
1078
- this.$http.$post(params, {
1079
- responseType: 'blob'
1080
- })
1081
- .then(({ blob, filename }) => {
1082
- var a = document.createElement('a')
1083
- document.body.appendChild(a)
1084
- const objectUrl = URL.createObjectURL(blob)
1085
- a.href = objectUrl
1086
- a.download = filename
1087
- a.click()
1088
- window.URL.revokeObjectURL(objectUrl)
1089
- document.body.removeChild(a)
1090
- // 如果存在下一个事件,执行下一个事件
1091
- const _afterTriggerParams = this.$formatAfterTriggerParams(afterTriggerParams, { data: row || this.data })
1092
- this.$message.success(successMsg)
1093
- if (afterTrigger) {
1094
- this[afterTrigger]({ pageId: afterTriggerPage, params: _afterTriggerParams })
1095
- } else {
1096
- if (this.reloadData) this.reloadData()
1097
- }
1098
- })
1099
- .catch(err => {
1100
- this.$message.error(err.message || failMsg)
1101
- })
1102
- .finally(() => {
1103
- this.httpLoading[`common-${action}`] = false
1104
- if (loading) loading.close()
1105
- })
1106
- }
1107
- // 判断是否需要确认弹窗
1108
- if (confirm && confirmMsg) {
1109
- this.$confirm(confirmMsg, this.$t('hint'), {
1110
- confirmButtonText: confirmButtonText || this.$t('confirm'),
1111
- cancelButtonText: cancelButtonText || this.$t('cancel'),
1112
- type: 'warning'
1113
- }).then(() => {
1114
- submitFn()
1115
- })
1116
- } else {
1117
- submitFn()
1118
- }
1119
- },
1120
- $refesh () {
1121
- // this.$children.forEach(child => {
1122
- // if (child.$options.componentType === 'component') {
1123
- // if (child.$refesh) child.$refesh()
1124
- // }
1125
- // })
1126
- },
1127
- $refeshPage () {
1128
- if (this.upPage) this.upPage.$refesh()
1129
- },
1130
- // 获取上级页面
1131
- $getParentPage () {
1132
- var parent = this.$parent || this.$root
1133
- var type = parent.$options.$componentType
1134
-
1135
- while (parent && (!type || type !== 'page')) {
1136
- parent = parent.$parent
1137
-
1138
- if (parent) {
1139
- type = parent.$options.componentName
1140
- }
1141
- }
1142
- if (parent) {
1143
- return parent
1144
- }
1145
- },
1146
- // 修改上级页面数据
1147
- $setParentPageModel (model) {
1148
- if (this.$parentPage) {
1149
- for (const key in model) {
1150
- this.$parentPage.model[key] = model[key]
1151
- }
1152
- }
1153
- },
1154
- // 调用上级页面内组件事件
1155
- $dispatchComponent (id, eventName, ...params) {
1156
- if (this.$parentPage) { // 组件id
1157
- broadcast.apply(this.$parentPage, [id, eventName, ...params])
1158
- }
1159
- },
1160
- // 获取页面内组件(T31:注册表 byId,替代 $children 树遍历)
1161
- $getPageComponent (id, current = this, prefix) {
1162
- const newId = prefix ? prefix + '-' + id : id
1163
- const page = current && current._epxPage ? current._epxPage() : this._epxPage()
1164
- const reg = page && page._epxReg
1165
- return reg ? reg.byId.get(newId) : undefined
1166
- },
1167
- $excuteEvent (key, ...args) {
1168
- const { componentEvents = [] } = this.$configs
1169
- const { [key]: keyEvents = [] } = componentEvents
1170
- keyEvents.map(event => this.executeCustomJs(event, ...args))
1171
- },
1172
- // 这些方法依赖实例属性(如 httpLoading),保留在混入中
1173
- $setPageConfig (page) {
1174
- const currentPage = page || this.page || {}
1175
- const { config = this.compConfigs } = currentPage
1176
- for (const key in this.$options) {
1177
- if (isCustomOption(key, this.$options[key])) {
1178
- this.pageConfig[key] = this.$options[key]
1179
- }
1180
- }
1181
- for (const key in config) {
1182
- try {
1183
- if (this.$_.isObject(config[key])) {
1184
- if (Array.isArray(config[key])) {
1185
- this.pageConfig[key] = [...config[key]]
1186
- } else {
1187
- // ⚠️ 别往 $options 上写。它是 Vue 按**组件类型**缓存的
1188
- // (appContext.optionsCache),同一个 app 里该类型所有实例共用同一个对象。
1189
- // 原先写的是 `if (!this.$options[key]) this.$options[key] = {}`,于是任何一个
1190
- // 页面配置里出现过的对象型键都被永久种进了共享 $options,此后所有同类型实例
1191
- // 都会读到 `{}` 而不是 `undefined`——假值变真值,`if (x)` 分支整个翻转,
1192
- // 而且**取决于页面访问顺序**,刷新即恢复,极难归因。
1193
- // `if (!x) x = {}` 之后再展开,与直接展开 `x || {}` 完全等价(原逻辑也只在
1194
- // 假值时赋 {}),所以去掉写入不改变合并结果。回归测试见
1195
- // test/unit/config-options-isolation.test.js。
1196
- this.pageConfig[key] = { ...(this.$options[key] || {}), ...config[key] }
1197
- }
1198
- } else {
1199
- this.pageConfig[key] = config[key]
1200
- }
1201
- } catch (error) {
1202
- this.pageConfig[key] = config[key]
1203
- }
1204
- }
1205
- },
1206
- $setCompConfigs () {
1207
- for (const key in this.$options) {
1208
- if (isCustomOption(key, this.$options[key])) {
1209
- this.configs[key] = this.$options[key]
1210
- }
1211
- }
1212
- for (const key in this.compConfigs) {
1213
- try {
1214
- if (this.$_.isObject(this.compConfigs[key])) {
1215
- if (Array.isArray(this.compConfigs[key])) {
1216
- this.configs[key] = [...this.compConfigs[key]]
1217
- } else {
1218
- // 同上:不写共享 $options(见 $setPageConfig 里的说明)
1219
- this.configs[key] = { ...(this.$options[key] || {}), ...this.compConfigs[key] }
1220
- }
1221
- } else {
1222
- this.configs[key] = this.compConfigs[key]
1223
- }
1224
- } catch (error) {
1225
- this.configs[key] = this.compConfigs[key]
1226
- }
1227
- }
1228
- },
1229
- _createComponent () {
1230
- let height = ''
1231
- // 页面配置
1232
- const _configs = {}
1233
- const globalConfigs = (this.componentGlobalConfig && this.componentGlobalConfig[this.contentType]) || {}
1234
- const systemGlobalConfigs = (this.systemComponentGlobalConfig && this.systemComponentGlobalConfig[this.contentType]) || {}
1235
- const queryPageConfigs = this.pageConfig || this.$query.pageConfigs || {}
1236
- const { config: pageConfigs = queryPageConfigs } = this.$pageInfo
1237
- const { css, itemConfigs = {}, ...otherConfigs } = pageConfigs
1238
- const _itemConfigs = this.itemConfigs || itemConfigs
1239
- const itemConfig = _itemConfigs[this.compId]
1240
- let allConfigs = { ...globalConfigs, ...systemGlobalConfigs, ...this.compConfigs, ...itemConfig }
1241
- if (this.isPage) {
1242
- allConfigs = { ...allConfigs, ...otherConfigs }
1243
- // 获取函数库
1244
- const exports = {}
1245
- // eslint-disable-next-line no-eval
1246
- eval('(function (exports) {' + pageConfigs.compileJS + ' }(exports));')
1247
- this.pageEvents = { ...exports }
1248
- if (css) {
1249
- appendCss(this.$pageId, css)
1250
- }
1251
- } else {
1252
- const { css } = this._component
1253
- if (css) {
1254
- appendCss(this.compId, css)
1255
- }
1256
- }
1257
- // 从组件 $options 收割「配置点默认值」。判定统一走 isCustomOption——
1258
- // 这里原先另有一份内联的保留名单,和 custom-option.js 的那份各写各的,已经漂了:
1259
- // 内联那份少 18 个键,其中最要命的是 **extends**。Vue 3 的 mergeOptions 对 extends
1260
- // 没有合并策略、直接把基类定义对象赋到解析后的 $options 上(runtime-core 4031-4039),
1261
- // 于是每个 `extends: XxxList` 的组件都会把**整个基类定义对象**当成配置点塞进 configs,
1262
- // 被 $configs 每次重算时整体拷贝、被 get-component 广播出去。
1263
- // 本仓库 61 个组件用了 extends。另有 activated/deactivated/updated 等钩子数组同样泄漏。
1264
- //
1265
- // 注意这个过滤只作用在「收割默认值」这一步;页面配置走下面的 allConfigs 合并,
1266
- // 不受影响——所以线上已有的页面配置数据语义不变。
1267
- for (const key in this.$options) {
1268
- if (isCustomOption(key, this.$options[key])) {
1269
- _configs[key] = this.$options[key]
1270
- }
1271
- }
1272
- for (const key in allConfigs) {
1273
- try {
1274
- if (this.$_.isObject(allConfigs[key])) {
1275
- if (Array.isArray(allConfigs[key])) {
1276
- _configs[key] = [...allConfigs[key]]
1277
- } else {
1278
- // 同上:不写共享 $options(见 $setPageConfig 里的说明)
1279
- _configs[key] = { ...(this.$options[key] || {}), ...allConfigs[key] }
1280
- }
1281
- } else {
1282
- _configs[key] = allConfigs[key]
1283
- }
1284
- } catch (error) {
1285
- _configs[key] = allConfigs[key]
1286
- }
1287
- }
1288
- const { items = [], mItems, style = {} } = _configs
1289
- delete _configs.items
1290
- const _items = this.items || items
1291
- const configs = { ...this.configs, ..._configs }
1292
- this.configs = configs
1293
-
1294
- if (configs.height) {
1295
- height = typeof configs.height === 'string' && configs.height.includes('%') ? configs.height : configs.height + 'px'
1296
- }
1297
- // 获取页面配置
1298
- const _style = { ...style, ...this.compStyle }
1299
- if (_style.backgroundImage) {
1300
- _style.backgroundImage = _style.backgroundImage.replace(/url\s*\(([\w+-/.\s]+)\)/g, (str, $1) => {
1301
- return `url(${util.getFileUrl($1)})`
1302
- })
1303
- }
1304
- if (height) _style.height = height
1305
- this.component = {
1306
- id: this.compId || this.$pageId,
1307
- title: this.$formatContent(this.compTitle || this.$attrs.title),
1308
- contentType: this.contentType,
1309
- style: _style,
1310
- configs: this.$configs,
1311
- flex: this.compFlex === undefined ? !!this.$configs.flex : !!this.compFlex,
1312
- items: _items, // 子节点
1313
- mItems,
1314
- metaId: this.$metaId,
1315
- meta: this.$meta,
1316
- parentPage: this.$getParentPage()
1317
- }
1318
- this.$emit('get-component', this.component)
1319
- this.$event.emit('get-component', this.component)
1320
- if (this.isPage) {
1321
- this.component.itemConfigs = itemConfigs
1322
- }
1323
- this.$emit('load-component', this.component)
1324
- this.$event.emit('load-component', this.component)
1325
- },
1326
-
1327
- // 创建组件前
1328
- beforeCreateComponent () {
1329
- },
1330
- // 初始化配置 可在此更改默认配置
1331
- beforeInitComponent () {
1332
-
1333
- },
1334
- /**
1335
- * 初始化组件
1336
- */
1337
- initComponent (component) {
1338
-
1339
- }
1340
- },
1341
- async created () {
1342
- // T31 修正:注册必须早于下面 _createComponent() 发出的 `load-component`——BasePage 收到该
1343
- // 事件后要用 getComponentById 拿本组件实例、给页面变量建 $watch,注册晚了就恒为 undefined、
1344
- // 在 BasePage.watchCompValue 里静默早退(页面变量恒空 ⇒ 由它驱动的 scopeParams 条件被
1345
- // createHttp 整条丢弃)。Vue2 靠 $children(在 _init 里就填好、早于 created)天然满足这个
1346
- // 不变式;Vue3 没有 $children,改用 _epxReg 之后这个先后顺序就得自己保证。
1347
- // mounted 里那句保留不删:Map.set / Set.add 幂等,重复注册零代价。
1348
- this._epxRegister()
1349
- // ⚠️ `compoennt` 是笔误(少个 n),**别顺手改对**——它是承重的。
1350
- // 原意见 global.js 里对称的那一对:「非 page → setCompConfigs / 非 component → setPageConfig」,
1351
- // 即 $setPageConfig() 本该只跑在页面上。笔误让条件恒真 ⇒ 多年来**每个**组件都在跑它,
1352
- // 于是 _createComponent() / $formatComponents() 的 pageConfig 回退路径,以及
1353
- // Collapse / ApprovalIndex / TaskList / TodoList / PermissManage / Template 等组件里的
1354
- // `this.pageConfig.xxx` 读取,全都建立在这个恒真之上。
1355
- // 拼写改对 = 所有非页面组件的 pageConfig 一次性清零 ⇒ 按 compId 下发的逐组件配置静默丢失
1356
- // (按钮不渲染、checkPageId / detailPageFlag 变 undefined 走错分支),而且现有单测抓不到
1357
- // (component-register-timing.test.js 把 $setPageConfig stub 掉了,只断言注册顺序)。
1358
- // 真要收敛回原意,得当独立重构立项:先给每个消费点补断言,再改条件,最后跑全量 e2e。
1359
- if (this.$options.$componentType !== 'compoennt') {
1360
- this.$setPageConfig()
1361
- }
1362
- this.$componentStatus = 'beforeCreate'
1363
- this.beforeCreateComponent()
1364
- // 创建组件
1365
- this._createComponent()
1366
- this.$componentStatus = 'created'
1367
- try {
1368
- this.oldCompConfigs = this.$_.cloneDeep(this.compConfigs)
1369
- } catch (error) {}
1370
- // 初始化组件配置
1371
- this.$componentStatus = 'beforeInit'
1372
- const component = this.beforeInitComponent()
1373
- if (component instanceof Promise) {
1374
- await component
1375
- }
1376
- // 如果this.$metaId 并且不存在this.$meta 获取meta对象
1377
- if (this.$metaId && !this.$meta) {
1378
- await this.$store.dispatch({
1379
- type: 'ADD_META',
1380
- metaId: this.$metaId
1381
- })
1382
- }
1383
- // 初始化组件
1384
- this.$componentStatus = 'init'
1385
- this.initComponent(this.$component)
1386
- this.$nextTick(() => {
1387
- if (this.$el) {
1388
- const compId = this.isPage ? this.$pageId : this.compId
1389
- if (this.$el.classList) {
1390
- this.$el.classList.add('css_' + compId)
1391
- } else {
1392
- this.$el.className = 'css_' + compId
1393
- }
1394
- }
1395
- })
1396
- },
1397
- mounted () {
1398
- // T31:组件注册表(替代 $children 树遍历,Vue3 无 $children)。每组件注册进其页面的
1399
- // _epxRegistry,broadcast/getComponentById 改 O(1) 查表。页面级作用域(upPage 就近)。
1400
- this._epxRegister()
1401
- // 系统返回(popstate)已由 @epxing/utils/src/nav-history 的全局唯一监听器统一处理,
1402
- // 不再每个页面各挂一个(keep-alive 缓存页/隐藏 tab 页的监听器会导致一次返回连弹多页)
1403
- this.$excuteEvent('onMounted')
1404
- },
1405
- unmounted () {
1406
- this._epxDeregister() // T31:从页面注册表注销
1407
- const compId = this.isPage ? this.$pageId : this.compId
1408
- removeCss(compId)
1409
- // 1. 清理事件监听器
1410
- if (this._pageEventHandlers && this._pageEventHandlers.length) {
1411
- for (const { comp, event, handler } of this._pageEventHandlers) {
1412
- // 解绑到真正绑定的 page component 上
1413
- if (comp && comp.$epxOff) comp.$epxOff(event, handler)
1414
- }
1415
- }
1416
- this._pageEventHandlers = []
1417
-
1418
- // 2. 清理 openedPopup 中对当前组件的引用(关键!防止连锁泄漏)
1419
- // 如果当前组件被保存在 openedPopup 数组中(通过 opener 引用),
1420
- // 即使组件销毁了,openedPopup 仍然持有引用,导致整个引用链无法回收
1421
- if (window.openedPopup && Array.isArray(window.openedPopup)) {
1422
- for (let i = window.openedPopup.length - 1; i >= 0; i--) {
1423
- const popup = window.openedPopup[i]
1424
- if (popup && popup.opener === this) {
1425
- // 清理 opener 引用,防止阻止垃圾回收
1426
- popup.opener = null
1427
- // 如果弹窗已经关闭,从数组中移除
1428
- if (popup.closed || !popup.visible) {
1429
- window.openedPopup.splice(i, 1)
1430
- }
1431
- }
1432
- }
1433
- }
1434
-
1435
- // 3. 清理 component 对象中的循环引用
1436
- if (this.component) {
1437
- // 清理 parentPage 引用(防止父页面持有子组件引用导致无法回收)
1438
- if (this.component.parentPage) {
1439
- this.component.parentPage = null
1440
- }
1441
- // 清理其他可能的引用
1442
- this.component = null
1443
- }
1444
-
1445
- // 4. 执行组件自定义的销毁事件
1446
- // ⚙️ 见文件顶部 EXEC_ON_DESTORY_ON_TEARDOWN(评审 C2):整棵页面树被销毁这条**本次新唤醒**的
1447
- // 路径上保持 Task 17 之前的语义(不跑用户脚本);单个组件被摘掉的路径照常跑。
1448
- if (EXEC_ON_DESTORY_ON_TEARDOWN || !isEpxTeardown(this.$)) {
1449
- this.$excuteEvent('onDestory')
1450
- }
1451
- },
1452
- beforeUnmount () {
1453
- // ⚙️ 与上面 onDestory 同一道闸门(最终评审 I-1):M0 的真卸载把 bum 和 um **两个**钩子一起
1454
- // 唤醒了,而 EXEC_ON_DESTORY_ON_TEARDOWN 原来只挡住了 um 那半边 —— `onBeforeDestory` 是
1455
- // 同一类低代码用户脚本的逃逸口(线上页面配置里可能存在从未执行过的脚本),漏掉它等于闸门只关了一半。
1456
- // `_epxTeardown` 由 RouterView 在剪枝前打上,bum 触发时已就位。
1457
- if (EXEC_ON_DESTORY_ON_TEARDOWN || !isEpxTeardown(this.$)) {
1458
- this.$excuteEvent('onBeforeDestory')
1459
- }
1460
- }
1461
- }
1
+ /* eslint-disable no-eval */
2
+ /* eslint-disable no-unused-vars */
3
+ // 组件实例
4
+ import { mapState, mapGetters } from 'vuex'
5
+ import { evalWith } from '@epxing/utils/src/eval-with'
6
+ import { CREATE_TAB } from '@epxing/core/src/store/actions'
7
+ import util from '@epxing/core/src/runtime-utils/util'
8
+ import { isSafeFn } from '@epxing/utils/src/security'
9
+ import { isCustomOption } from './custom-option'
10
+ // 纯函数层,零依赖(见 menu-route.js 头注释),不会与 router/store 成环
11
+ import { resolveRouteRole } from '@epxing/core/src/menu-route'
12
+ // 条件初始化:与 global.js / methods-to-prototype.js 共享同一个弹窗栈,
13
+ // 无条件重置会让先求值模块捕获的引用变成孤儿数组
14
+ if (!window.openedPopup) {
15
+ window.openedPopup = []
16
+ }
17
+ const openedPopup = window.openedPopup
18
+
19
+ /**
20
+ * ⚙️ T17 修复轮 1(评审 C2)——**一行开关**:keep-alive 剪枝/页签销毁这条路径上要不要执行
21
+ * 用户脚本 `onDestory`(设计器事件面板里的「组件销毁时」,见 designer/src/visual/config/Event.vue)。
22
+ *
23
+ * false(当前值)= 保持 Task 17 之前的可观察语义:这条路径**不**触发 onDestory。
24
+ * true = 放开,让这些脚本随真卸载开始执行。
25
+ *
26
+ * 为什么默认 false:Task 17 的目标是内存修复,不是行为变更。真卸载唤醒了成千上万个此前从未跑过的
27
+ * `unmounted`,其中就包括这个**面向低代码用户脚本的逃逸口**——线上页面配置里可能已经存在
28
+ * 从迁到 Vue 3 之后一次都没执行过的 onDestory 脚本,它们可以 $store.commit / $bus.off / 写 window /
29
+ * 发请求 / 跳转,内容不可静态判定、也没有任何人验证过。让它们在一次内存修复里**顺带开始跑**,
30
+ * 属于带风险的范围蔓延。放开前必须先盘点存量配置(FM_PAGE_CONFIG 里 componentEvents.onDestory
31
+ * 非空的有多少、分别写了什么),再灰度。
32
+ *
33
+ * 注意作用域:这个开关**只**管"整棵页面树被销毁"这条路径。页面还活着、只是某个组件被 v-if 摘掉
34
+ * 之类的单点卸载,onDestory 照常执行(那条路径在 Task 17 之前本来就在跑,不能反向改掉)。
35
+ */
36
+ const EXEC_ON_DESTORY_ON_TEARDOWN = false
37
+
38
+ /**
39
+ * 沿 parent 链找 RouterView 打的 `_epxTeardown` 标记(在 RouterView 的 removeKeepAliveCache /
40
+ * beforeUnmount 里打),用来区分"整棵页面树被销毁"与"页面还活着、单个组件被摘掉"。
41
+ * 不能用"父实例 scope 是否已 stop"来判:unmountComponent 的顺序是
42
+ * 「bum → scope.stop() → 递归卸载子树 → queuePostRenderEffect(um)」,而被剪枝页面的父级是
43
+ * KeepAlive,它的 scope 在那一刻仍然 active,判据在最要紧的那一层恰好失效。
44
+ */
45
+ function isEpxTeardown (inst) {
46
+ let p = inst
47
+ let guard = 0
48
+ while (p && guard++ < 500) {
49
+ if (p._epxTeardown === true) return true
50
+ p = p.parent
51
+ }
52
+ return false
53
+ }
54
+
55
+ // T31:改组件注册表(page._epxReg,替代 $children 树遍历,Vue3 无 $children)。
56
+ // this = 页面实例(调用方 apply 到 $parentPage/页面)。
57
+ function broadcast (id, eventName, ...params) {
58
+ const reg = this && this._epxReg
59
+ if (!reg) return
60
+ const child = reg.byId.get(id)
61
+ if (child && child[eventName]) child[eventName].apply(child, params)
62
+ }
63
+ function broadcastCom (componentName, eventName, params) {
64
+ const reg = this && this._epxReg
65
+ if (!reg) return
66
+ const set = reg.byName.get('epx-' + componentName)
67
+ if (set) for (const child of set) child.$emit.apply(child, [eventName].concat(params))
68
+ }
69
+ function getComponentById (id) {
70
+ const reg = this && this._epxReg
71
+ return reg ? reg.byId.get(id) : undefined
72
+ }
73
+
74
+ function findNavId (menus = [], menuId, parent) {
75
+ for (let i = 0; i < menus.length; i++) {
76
+ const menu = menus[i]
77
+ if (menu.ID === menuId) {
78
+ return { ...menu, parentMenu: parent }
79
+ } else {
80
+ if (menu.children) {
81
+ const activeMenu = findNavId(menu.children, menuId, menu)
82
+ if (activeMenu) return activeMenu
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ function appendCss (id, css) {
89
+ let style = document.createElement('style')
90
+ style.id = 'style_' + id
91
+ style.textContent = css.replace(/:current/g, '.css_' + id).replace(/url\s*\(([\w+-/.\s]+)\)/g, (str, $1) => {
92
+ return `url(${util.getFileUrl($1)})`
93
+ })
94
+
95
+ style.setAttribute('type', 'text/css')
96
+ document.head.appendChild(style)
97
+ style = null
98
+ }
99
+ function removeCss (id) {
100
+ let element = document.getElementById('style_' + id)
101
+ if (element) {
102
+ document.head.removeChild(element)
103
+ element = null
104
+ }
105
+ }
106
+ export default {
107
+ $componentType: 'component',
108
+ // T33(关 compat 同刻翻转):
109
+ //
110
+ // 为什么最终要 true:Vue2 里 `inheritAttrs: false` **不管 class/style** —— 它们走
111
+ // `$vnode.data.staticClass/class`,无条件落到根元素。Vue3 把 class/style 并进了 attrs,
112
+ // false 会把它们一起挡掉 → 父传的 class 全丢。recon-classloss.mjs 实测:**20 个组件**受影响。
113
+ //
114
+ // 为什么不能提前翻:compat 开着时 Vue2 语义已经让 class 落根了,此时翻成 true 只剩副作用——
115
+ // 未声明的级联属性被序列化成 DOM attribute,**渲染直接断**(实测:应用开发平台左侧菜单
116
+ // 92 项 → 0 项,且 errors×0 静默失败;commit 1e2ab4f1c 当时测到的 "元素 1642→702" 同因)。
117
+ // ⇒ 与 `.native` 一样,语义在开关翻转前后互斥,只能同一刻改。
118
+ //
119
+ // 翻转时的配套(见 .claude/codemod/codemod-forward-attrs.mjs):
120
+ // 1. 转发给子组件的 `$attrs` 已剔掉 class/style(`$forwardAttrs`),否则容器根上的 class
121
+ // 会被复制到它的每个子组件上;✅ 已完成
122
+ // 2. 级联下发的 currentRoute / pageInfo / queryParams 已上提为 props;✅ 已完成
123
+ // 3. 透明代理组件(epx-component / widget-preview)保留自己的 `inheritAttrs: false` +
124
+ // 原样转发 `$attrs` —— 它们没有自己的根,class 要送到所渲染的叶子组件上。✅ 已完成
125
+ // 4. 翻转后仍会有未声明的级联属性落到根(如 currentQuery / inline)→ 那一刀时按实测逐个声明。
126
+ inheritAttrs: true,
127
+ navConfigs: {
128
+ flag: false
129
+ },
130
+
131
+ props: {
132
+ // 当前页面信息
133
+ page: {
134
+ type: Object
135
+ },
136
+ // 当前对象元数据信息
137
+ meta: {
138
+ type: Object,
139
+ default () {
140
+ return {}
141
+ }
142
+ },
143
+ compId: {
144
+ type: [String, Number]
145
+ }, // 组件id
146
+ contentType: String, // 模板名称
147
+ _component: {
148
+ type: Object,
149
+ default () {
150
+ return {}
151
+ }
152
+ },
153
+ compStyle: {
154
+ type: Object,
155
+ default () {
156
+ return {}
157
+ }
158
+ }, // 组件样式
159
+ compConfigs: {
160
+ type: Object,
161
+ default () {
162
+ return {}
163
+ }
164
+ },
165
+ compFlex: {
166
+ type: Boolean
167
+ },
168
+ compTitle: String,
169
+ items: Array, //
170
+ itemConfigs: Object,
171
+ metaId: String,
172
+ // 以下三个由 epx-component / epx-page 沿 $attrs 级联下发,此前无人声明 → 停在 $attrs 里。
173
+ // Vue3 的 $attrs 含 class/style,将来把 inheritAttrs 打开(让父传的 class 落到 root)时,
174
+ // 未声明的属性会被序列化成 DOM attribute(对象值变 "[object Object]")。声明成 props 即被消费掉。
175
+ // 详见 T30b-compat-warning-census.md「INSTANCE_ATTRS 否定结果被推翻」一节。
176
+ currentRoute: Object,
177
+ pageInfo: Object,
178
+ // Component.vue 给**每个** leaf 都绑 :query-params,但只有 EntityList 自己声明了它
179
+ // (同形状 Object/default {},合并无冲突)。核对过:无 mixin 使用者把 queryParams 写在
180
+ // data() 或 computed 里,故可安全上提。formData 则不行——有 6 个使用者写在 data()。
181
+ queryParams: {
182
+ type: Object,
183
+ default () {
184
+ return {}
185
+ }
186
+ }
187
+ },
188
+ data () {
189
+ return {
190
+ pageConfig: {},
191
+ $defaultConfigs: {},
192
+ httpLoading: {},
193
+ component: {},
194
+ configs: {}
195
+ }
196
+ },
197
+ inject: {
198
+ pageId: {
199
+ type: String,
200
+ default: null
201
+ },
202
+ pageMetaId: {
203
+ type: String,
204
+ default: null
205
+ },
206
+ // T31:最近的 epx 父容器(provide 下发),mounted 注册进其 _epxChildren 支撑子树遍历
207
+ _epxParentComp: {
208
+ default: null
209
+ },
210
+ upPage: {
211
+ type: Object,
212
+ default: () => null
213
+ },
214
+ // 获取所属标签页
215
+ $getTab: {
216
+ type: Function,
217
+ default: null
218
+ },
219
+ template: {
220
+ type: Object,
221
+ default: null
222
+ },
223
+ createRoute: {
224
+ default: null
225
+ },
226
+ destoryRoute: {
227
+ default: null
228
+ },
229
+ reloadRoute: {
230
+ default: null
231
+ }
232
+ },
233
+ /**
234
+ * 向下传递页面实例和模板
235
+ *
236
+ * 设计说明:
237
+ * 1. 页面组件(isPage=true)在 BasePage.vue 中提供 upPage,这里不需要重复提供
238
+ * 2. 非页面组件需要继续向下传递 upPage,确保所有子组件都能访问到页面实例
239
+ *
240
+ * 为什么需要 inject 后再 provide?
241
+ * - Vue 的 provide/inject 是单向的,只向下传递,不会自动向上查找
242
+ * - 如果中间某个组件不 provide,其子组件将无法获取 upPage(即使祖先组件提供了)
243
+ * - 通过在 mixin 中统一处理,确保所有组件都能自动传递,避免遗漏
244
+ *
245
+ * 这种模式被称为"透传"(pass-through),是 Vue provide/inject 的常见用法
246
+ * 优点:
247
+ * - 统一在 mixin 中处理,所有组件自动继承,无需手动处理
248
+ * - 确保页面实例能够传递到任意深度的子组件
249
+ * - 避免因中间组件忘记 provide 而导致子组件无法获取的问题
250
+ */
251
+ provide () {
252
+ // T31:每 epx 组件把自己作为「最近 epx 父容器」下发,子组件 mounted 时注册进 _epxChildren,
253
+ // 支撑子树级遍历(Card.refesh/AddForm 容器子收集,替代 $children 递归)。
254
+ const base = { _epxParentComp: this }
255
+ // 页面组件已经在 BasePage.vue 中提供了 upPage,这里不需要重复提供
256
+ // 非页面组件需要继续向下传递,确保子组件能够访问到页面实例
257
+ if (!this.isPage) {
258
+ base.upPage = this.upPage // 透传页面实例
259
+ base.template = this.template // 透传模板
260
+ }
261
+ // T28:Vue3 的 provide 合并不容忍 undefined 返回值
262
+ return base
263
+ },
264
+ computed: {
265
+ ...mapState({
266
+ appMenus: state => state.config.appMenus,
267
+ $screen: state => state.config.screen || {}, // 屏幕信息
268
+ $tabs: state => state.tabs.tabs, // 标签页
269
+ metas: state => state.meta.metas, // 元数据信息集合
270
+ user: state => state.user.user, // 用户信息
271
+ componentGlobalConfig: state => state.config.componentGlobalConfig, // 组件全局配置
272
+ systemComponentGlobalConfig: state => state.config.systemComponentGlobalConfig, // 组件全局配置
273
+ currentMenu: state => state.config.currentMenu // 当前导航菜单
274
+ }),
275
+ ...mapGetters([
276
+ 'getMeta',
277
+ 'getPageById'
278
+ ]),
279
+ CLIENT () {
280
+ const { CLIENT: queryClient } = this.$query
281
+ const { CLIENT: tabClient } = this.$tab
282
+ const { client: routeClient } = this.$route.query
283
+ const { client: userClient } = this.user
284
+ const { CLIENT: appClient, 'CLIENT|ID': clientId } = this.appConfig
285
+ return queryClient || tabClient || routeClient || userClient || appClient || clientId
286
+ },
287
+ SYSTEM () {
288
+ const { SYSTEM: querySystem } = this.$query
289
+ const { SYSTEM: tabSystem } = this.$tab
290
+ const { system: routeSystem } = this.$route.query
291
+ const { configSystem = {} } = this.$store.state.config
292
+ const { SYSTEM: appSystem, 'SYSTEM|ID': systemId } = this.appConfig
293
+ return querySystem || tabSystem || routeSystem || configSystem.ID || appSystem || systemId
294
+ },
295
+ APP () {
296
+ const { APP: queryApp } = this.$query
297
+ const { APP: tabApp } = this.$tab
298
+ return queryApp || tabApp
299
+ },
300
+ $tab () {
301
+ if (this.$getTab) return this.$getTab()
302
+ return this.$activeTab || {}
303
+ },
304
+ /**
305
+ * 选中标签页
306
+ */
307
+ $activeTab () {
308
+ return this.$tabs.find(item => item.active) || {}
309
+ },
310
+ $tabQuery () {
311
+ return this.$tabs.find(item => item.active) || {}
312
+ },
313
+ isPage () {
314
+ const { $componentType } = this.$options
315
+ return $componentType === 'page'
316
+ },
317
+ /**
318
+ * 当前页面参数
319
+ */
320
+ $query () {
321
+ // 获取该组件是否是页面
322
+ // 获取路由参数
323
+ // 当前路由不是登录页
324
+ const routeQuery = {}
325
+ const { name, query = {}, params = {} } = this.$route || {}
326
+ if (name !== 'hallLogin' && name !== 'login') {
327
+ for (const key in query) {
328
+ if (query[key]) routeQuery[key] = query[key]
329
+ }
330
+ for (const key in params) {
331
+ if (params[key]) routeQuery[key] = params[key]
332
+ }
333
+ }
334
+ if (!this.$_.isEmpty(this.currentQuery)) {
335
+ const currentQuery = this.currentQuery || {}
336
+ return { ...routeQuery, ...currentQuery }
337
+ } else {
338
+ // 获取上级页面
339
+ if (this.upPage) {
340
+ const currentQuery = this.upPage.currentQuery || {}
341
+ return { ...routeQuery, ...currentQuery, ...(this.upPage.pageParams || {}) }
342
+ }
343
+ return routeQuery
344
+ }
345
+ },
346
+ $configVersion () {
347
+ const queryPageConfigs = this.$query.pageConfigs || {}
348
+ const { config = queryPageConfigs } = this.$pageInfo
349
+ return config.version
350
+ },
351
+ getComponentId () {
352
+ return component => {
353
+ const queryPageConfigs = this.$query.pageConfigs || {}
354
+ const { config = queryPageConfigs } = this.$pageInfo
355
+ if (!this.isPage && !config.version) {
356
+ const { id } = this.$component
357
+ return id + '-' + component.id
358
+ }
359
+ return component.id
360
+ }
361
+ },
362
+ $component: {
363
+ get () {
364
+ return this.component
365
+ },
366
+ set (v) {
367
+ this.component = v
368
+ }
369
+ },
370
+ $subcomponents () {
371
+ const { items } = this.$component
372
+ // 判断是否迭代
373
+ return this.$formatComponents(items, this._component) || []
374
+ },
375
+ $compData () {
376
+ const iterator = this._component.iterator || {}
377
+ return { ...iterator, $templateVariables: this.$templateVariables }
378
+ },
379
+ $configs: {
380
+ get () {
381
+ // 格式化内容
382
+ const _configs = {}
383
+ // 动态参数的取值来源建**一次**就够。$formatContent 的默认参数是
384
+ // `{ ...$tab, ...pageParams, ...$query, ...$compData }`,而默认参数是**每次调用都求值**的——
385
+ // 原先这里逐个字符串键调用,一个组件有多少个字符串配置就重建多少遍同一个对象。
386
+ // 实测(.claude 微基准,28 个字符串键):合并对象 324 µs、两遍正则只要 28 µs,
387
+ // 也就是说这条路径 92% 的开销花在重复构造上,提出来能砍掉约九成。
388
+ //
389
+ // ⚠️ 必须**惰性**构造,不能提到循环外无条件建:那样 getter 会无条件依赖
390
+ // $tab/$query/$compData,连一个字符串配置都没有的组件也会在路由参数变化时失效重算,
391
+ // 平白多出依赖边。惰性构造与原先「首次调用 $formatContent 时才求值」的依赖集完全一致。
392
+ let data
393
+ for (const key in this.configs) {
394
+ const item = this.configs[key]
395
+ if (typeof item === 'string') {
396
+ if (data === undefined) {
397
+ data = { ...this.$tab, ...(this.upPage?.pageParams || {}), ...this.$query, ...this.$compData }
398
+ }
399
+ _configs[key] = this.$formatContent(item, data)
400
+ } else if (/operates$/i.test(key)) {
401
+ // 按钮文案统一走界面文案翻译,覆盖组件内置的默认按钮(含运行时注入的默认操作)
402
+ _configs[key] = this.$translateOperates(item)
403
+ } else {
404
+ _configs[key] = this.configs[key]
405
+ }
406
+ }
407
+ return _configs
408
+ },
409
+ set (v) {
410
+ this.configs = v
411
+ }
412
+ },
413
+ // 当前页面id
414
+ $pageMetaId () {
415
+ // 获取当前传递的metaId
416
+ let metaId = null
417
+ if (this.meta && this.meta.id) {
418
+ metaId = this.meta.id
419
+ }
420
+ return metaId || this.pageMetaId
421
+ },
422
+ /**
423
+ * 当前组件对象元数据id 不设置 默认为当前页面对象
424
+ */
425
+ $metaId () {
426
+ const { metaId } = this.$configs
427
+ return this.metaId || metaId || this.$pageMetaId || this.$query.metaId
428
+ },
429
+ // 当前组件对象
430
+ $meta () {
431
+ if (this.$metaId) {
432
+ const meta = this.getMeta(this.$metaId)
433
+ return meta
434
+ } else {
435
+ return null
436
+ }
437
+ },
438
+ $metaKey () {
439
+ if (this.$meta) {
440
+ return this.$meta.getKey() || 'ID'
441
+ } else {
442
+ return 'ID'
443
+ }
444
+ },
445
+ $parentPage () {
446
+ // 父级页面
447
+ const { parentPage } = this.component
448
+ return parentPage
449
+ },
450
+ $parentPageModel () {
451
+ const { parentPage = {} } = this.component
452
+ return parentPage.model
453
+ },
454
+ $pageId () {
455
+ const { pageId } = this.page || {}
456
+ return this.isPage ? pageId : this.pageId || pageId
457
+ },
458
+ $pageInfo () {
459
+ if (this.$pageId) return this.getPageById(this.$pageId) || {}
460
+
461
+ return this.page || (!this.isPage && this.upPage && this.upPage.page) || {}
462
+ },
463
+ $pageParams () {
464
+ if (this.isPage) return this.pageParams || {}
465
+ else if (this.upPage) return this.upPage.pageParams || {}
466
+ return {}
467
+ },
468
+ $templateVariables () {
469
+ return this.template?.variables || {}
470
+ },
471
+ /**
472
+ * 事件处理总线
473
+ */
474
+ $event () {
475
+ const vm = this
476
+ let comp = vm
477
+ while (comp.$parent && comp.$options.$componentType !== 'page') comp = comp.$parent
478
+ return {
479
+ emit (event, ...params) {
480
+ // T25:程序化通道走 $epxEmit(Vue3 无实例事件);$emit 保留给模板 @listener
481
+ comp.$epxEmit(event, [...params])
482
+ comp.$emit(event, [...params])
483
+ },
484
+ on (event, callback) {
485
+ const handler = params => callback.apply(comp, params)
486
+ comp.$epxOn(event, handler)
487
+ // 记录在组件实例上,beforeDestroy 时解除绑定
488
+ if (!vm._pageEventHandlers) vm._pageEventHandlers = []
489
+ vm._pageEventHandlers.push({ comp, event, handler })
490
+ }
491
+ }
492
+ },
493
+ $currentRole () {
494
+ // 菜单直达路由(appMenu/hallMenu)没有 role 段,URL 末段是 menuKey——直接读
495
+ // $route.params.role 会得到 undefined,$currentRole 恒为 {}。页面配置的 showIf
496
+ // 表达式里也拿得到 $currentRole(见下方 elData),空值会让按角色显示的节点全部消失。
497
+ const role = resolveRouteRole(this.$route)
498
+ const { serviceRoles = {} } = this.user
499
+ const roleList = serviceRoles[this.appConfig.ID] || []
500
+ let roleCode = role
501
+ if (role && role.endsWith('-client')) {
502
+ roleCode = role.replace(/-client$/, '')
503
+ // 从通用角色获取
504
+ return roleList.find(item => item.code === roleCode && item.type === 'client') || {}
505
+ } else {
506
+ return roleList.find(item => item.code === roleCode && item.type !== 'client') || {}
507
+ }
508
+ },
509
+ $currentRoleId () {
510
+ return this.$currentRole.id
511
+ }
512
+ },
513
+ watch: {
514
+ compConfigs (v) {
515
+ if (!this.$_.isEqual(this.oldCompConfigs, v)) {
516
+ this._createComponent()
517
+ }
518
+ this.oldCompConfigs = this.$_.cloneDeep(v)
519
+ }
520
+ },
521
+ methods: {
522
+ // ========== T31 组件注册表(替代 $children 树遍历)==========
523
+ // 页面级作用域:注册表挂在页面组件(isPage)上,非页面组件经 upPage 就近找页面注册。
524
+ _epxPage () {
525
+ if (this.isPage) return this
526
+ const inst = this.upPage && this.upPage.instance
527
+ return inst || null
528
+ },
529
+ _epxRegistry (page) {
530
+ const p = page || this._epxPage()
531
+ if (!p) return null
532
+ if (!p._epxReg) p._epxReg = { byId: new Map(), byName: new Map() }
533
+ return p._epxReg
534
+ },
535
+ _epxRegister () {
536
+ // 页面级注册表(broadcast/getComponentById 用)
537
+ const reg = this._epxRegistry()
538
+ if (reg) {
539
+ if (this.compId != null) reg.byId.set(this.compId, this)
540
+ const name = this.$options.name
541
+ if (name) {
542
+ let set = reg.byName.get(name)
543
+ if (!set) { set = new Set(); reg.byName.set(name, set) }
544
+ set.add(this)
545
+ }
546
+ }
547
+ // 父子注册(子树遍历用,Card.refesh/AddForm 容器):注册进最近 epx 父容器
548
+ const parent = this._epxParentComp
549
+ if (parent && parent !== this) {
550
+ if (!parent._epxChildren) parent._epxChildren = new Set()
551
+ parent._epxChildren.add(this)
552
+ }
553
+ },
554
+ _epxDeregister () {
555
+ const page = this._epxPage()
556
+ if (page && page._epxReg) {
557
+ const reg = page._epxReg
558
+ if (this.compId != null && reg.byId.get(this.compId) === this) reg.byId.delete(this.compId)
559
+ const name = this.$options.name
560
+ const set = name && reg.byName.get(name)
561
+ if (set) set.delete(this)
562
+ }
563
+ const parent = this._epxParentComp
564
+ if (parent && parent._epxChildren) parent._epxChildren.delete(this)
565
+ },
566
+ // T31:遍历子树内 epx 组件(含深层后代,递归 _epxChildren),cb(comp)。替代 $children 递归。
567
+ _epxWalkChildren (cb) {
568
+ const stack = this._epxChildren ? [...this._epxChildren] : []
569
+ while (stack.length) {
570
+ const c = stack.pop()
571
+ if (!c) continue
572
+ cb(c)
573
+ if (c._epxChildren) for (const gc of c._epxChildren) stack.push(gc)
574
+ }
575
+ },
576
+ $formatComponents (arr, component = this._component) {
577
+ const { iterator: compIterator } = component
578
+ const queryPageConfigs = this.pageConfig || this.$query.pageConfigs || {}
579
+ const { config: pageConfigs = queryPageConfigs } = this.$pageInfo
580
+ const itemConfigs = pageConfigs.itemConfigs || {}
581
+ const page = this.upPage || this
582
+ const components = []
583
+ if (!arr) return []
584
+ for (const item of arr) {
585
+ const { configs = {}, id } = item
586
+ const compConfigs = itemConfigs[id] || {}
587
+ const allConfigs = { ...compConfigs, ...configs }
588
+ const { iterator, showIf, showTerminal } = allConfigs
589
+ const terminal = this.$isMobile ? 'mobile' : 'pc'
590
+ if (!showTerminal || showTerminal.includes(terminal)) {
591
+ if (iterator?.data) {
592
+ const { data, name = 'item', key = 'ID', index = 'index' } = iterator
593
+ let iteratorData = []
594
+ if (Array.isArray(data)) {
595
+ iteratorData = data
596
+ } else {
597
+ const matches = /^\$\[([\w.|$-]+)\]$/.exec(data)
598
+ if (matches) {
599
+ const data = { ...this.$tab, ...(page.pageParams || {}), ...this.$query, ...this.$compData, ...(compIterator || {}), template: this.template, $templateVariables: this.$templateVariables }
600
+ iteratorData = this.$util.getValueByPath(matches[1], data)
601
+ }
602
+ }
603
+ if (iteratorData && Array.isArray(iteratorData)) {
604
+ for (let i = 0; i < iteratorData.length; i++) {
605
+ const data = iteratorData[i]
606
+ const iteratorKey = data[key] || i
607
+ if (showIf) {
608
+ const elData = { user: this.user, $currentRole: this.$currentRole, $tab: this.$tab, ...this.$query, ...this.$pageParams, ...(compIterator || {}), [name]: data, [index]: i, key: iteratorKey, template: this.template, $templateVariables: this.$templateVariables }
609
+ if (typeof showIf === 'object') {
610
+ if (showIf.el && !util.evalExpression(showIf.el, elData)) {
611
+ continue
612
+ }
613
+ } else {
614
+ if (!util.evalExpression(showIf, elData)) {
615
+ continue
616
+ }
617
+ }
618
+ }
619
+ const component = { ...item, key: item.id + '-' + iteratorKey, iterator: { ...(compIterator || {}), [name]: data, [index]: i, key: iteratorKey } }
620
+ if (item.type === 'template') {
621
+ components.push(component)
622
+ } else {
623
+ components.push(component)
624
+ }
625
+ }
626
+ }
627
+ } else {
628
+ const elData = { user: this.user, $currentRole: this.$currentRole, $tab: this.$tab, ...this.$query, ...this.$pageParams, ...(compIterator || {}), template: this.template, $templateVariables: this.$templateVariables }
629
+ if (showIf) {
630
+ if (typeof showIf === 'string' && !util.evalExpression(showIf, elData)) {
631
+ continue
632
+ } else if (showIf.el && !util.evalExpression(showIf.el, elData)) {
633
+ continue
634
+ }
635
+ }
636
+ const component = compIterator ? { ...item, iterator: compIterator } : { ...item }
637
+ component.key = component.id
638
+ if (item.type === 'template') {
639
+ components.push(component)
640
+ } else {
641
+ components.push(component)
642
+ }
643
+ }
644
+ }
645
+ }
646
+ return components
647
+ },
648
+ $formatContent (content, data = { ...this.$tab, ...(this.upPage?.pageParams || {}), ...this.$query, ...this.$compData }) {
649
+ if (!content) return content
650
+ // 当内容整体以 {{ 开头、}} 结尾时,直接返回表达式执行结果,避免 replace 将对象类型强制转换为字符串
651
+ const fullMatch = content.match(/^\s*\{\{([\w-.,:?+\s|$#[/\]()"']+)\}\}\s*$/)
652
+ if (fullMatch) {
653
+ try {
654
+ // 表达式执行失败时保留原内容,占位符可继续交由下面的 $[...] 分支按实例上下文解析
655
+ return this.$util.evalExpression(fullMatch[1], data, content)
656
+ } catch (error) {
657
+ return content
658
+ }
659
+ }
660
+ return content.replace(/\{\{([\w-.,:?+\s|$#[/\]()"']+)\}\}/g, (macth, key) => {
661
+ try {
662
+ // 执行表达式,失败时保留原占位符
663
+ return this.$util.evalExpression(key, data, macth)
664
+ } catch (error) {
665
+ return `{{${key}}}`
666
+ }
667
+ }).replace(/\$\[([\w-.|$#]+)\]/g, (macth, key) => {
668
+ try {
669
+ const value = this.$util.getValueByPath(key, this) || this.$util.getValueByPath(key, data)
670
+ return this.$_.isNil(value) ? '' : value
671
+ } catch (error) {
672
+ return `$[${key}]`
673
+ }
674
+ })
675
+ },
676
+ /**
677
+ * 翻译按钮文案。组件代码内置的默认按钮(如「保存」「返回」)不在页面配置里,
678
+ * 服务端配置投影覆盖不到,只能走界面文案通道:以中文原文为 key 调 $t。
679
+ * 页面里配置出来的按钮文案已由服务端投影翻好,$t 未命中会原样返回 key,
680
+ * 因此对它们无副作用,重复调用也是幂等的。
681
+ * 按原数组缓存结果,保证同一份配置每次拿到的引用不变。
682
+ */
683
+ $translateOperates (operates) {
684
+ // 组件库脱离 i18n 插件单独使用时不做翻译,避免影响渲染主链路
685
+ if (!Array.isArray(operates) || !this.$i18n || typeof this.$t !== 'function') return operates
686
+ const cache = this._operatesI18nCache || (this._operatesI18nCache = new WeakMap())
687
+ if (cache.has(operates)) return cache.get(operates)
688
+ let translated = false
689
+ const result = operates.map(operate => {
690
+ if (!operate || typeof operate !== 'object') return operate
691
+ const { text } = operate
692
+ if (typeof text !== 'string' || !text) return operate
693
+ const value = this.$t(text)
694
+ if (value === text) return operate
695
+ translated = true
696
+ return { ...operate, text: value }
697
+ })
698
+ // 无一条命中译文时返回原数组,默认语言下等同于未改动
699
+ const value = translated ? result : operates
700
+ cache.set(operates, value)
701
+ return value
702
+ },
703
+ /**
704
+ * 解析 url 中的变量占位符,支持 {{CLIENT}} 和 $[CLIENT] 两种写法
705
+ * 变量可为实例路径表达式,如 {{$route.params.client}}、{{appConfig['CLIENT']}}
706
+ * 取值顺序与 $formatParams 一致:组件实例(CLIENT/SYSTEM/APP 等计算属性)→ 标签页/页面参数
707
+ * 无法解析的占位符保留原样
708
+ */
709
+ $formatUrl (url) {
710
+ if (!url) return url
711
+ return url.replace(/\{\{([^}]+)\}\}|\$\[((?:[^[\]]|\[[^\]]*\])+)\]/g, (match, curlyKey, bracketKey) => {
712
+ const key = (curlyKey || bracketKey).trim()
713
+ try {
714
+ const value = this.$util.getValueByPath(key, this)
715
+ if (!this.$_.isNil(value) && value !== '') return value
716
+ const fallback = this.$util.getValueByPath(key, { ...this.$tab, ...this.$query, ...this.$pageParams, ...this.$compData })
717
+ return this.$_.isNil(fallback) ? match : fallback
718
+ } catch (error) {
719
+ return match
720
+ }
721
+ })
722
+ },
723
+ $formatScope (scopeParams, data = this.data) {
724
+ const _scopeParams = this.$_.cloneDeep(scopeParams)
725
+ const that = this
726
+ const paramData = { ...this.$tab, ...this.$query, ...this.$pageParams, ...that.$compData }
727
+ for (const scopeKey in _scopeParams) {
728
+ const scope = _scopeParams[scopeKey]
729
+ if (!scope) continue
730
+ let values = scope.values
731
+ if (/#\{([\w-.|$#]+)\}/.test(values)) {
732
+ values = values.replace(/#\{([\w-.|$#]+)\}/, (match, key) => {
733
+ try {
734
+ // #{key} 里的 key 是相对组件实例的路径:显式传作用域,不能靠直接 eval 捕获 that(见 eval-with.js)
735
+ return evalWith(`that.${key}`, { that }) || evalWith(`that.$query.${key}`, { that }) || evalWith(`that.$tab.${key}`, { that }) || ''
736
+ } catch (error) {
737
+ return scope.values
738
+ }
739
+ })
740
+ }
741
+ if (/\$\[([\w-_.|$#]+)\]/.test(values)) {
742
+ values = values.replace(/\$\[([\w-.|$#]+)\]/, (match, key) => {
743
+ try {
744
+ return that.$util.getValueByPath(key, this) || that.$util.getValueByPath(key, paramData) || ''
745
+ } catch (error) {
746
+ return scope.values
747
+ }
748
+ })
749
+ }
750
+ if (/#\[([\w-.|$#]+)\]/.test(values)) {
751
+ values = values.replace(/#\[([\w-.|$#]+)\]/, (match, key) => {
752
+ try {
753
+ return evalWith(`data.${key}`, { data }) || ''
754
+ } catch (error) {
755
+ return scope.values
756
+ }
757
+ })
758
+ }
759
+ if (/\{\{([\w-_.,|$#[\]()"'\s]+)\}\}/g.test(values)) {
760
+ values = values.replace(/\{\{([\w-.,|$#[\]()"'\s]+)\}\}/g, (macth, key) => {
761
+ try {
762
+ // 执行表达式
763
+ return this.$util.evalExpression(key, paramData)
764
+ } catch (error) {
765
+ return `{{${key}}}`
766
+ }
767
+ })
768
+ }
769
+ scope.values = values
770
+ if (scope.scopeParams) {
771
+ scope.scopeParams = this.$formatScope(scope.scopeParams, data)
772
+ }
773
+ }
774
+ return _scopeParams
775
+ },
776
+ goWebLink (config) {
777
+ const { webLinkUrl } = config
778
+ if (!webLinkUrl) return
779
+ window.open(this.$formatUrl(webLinkUrl))
780
+ },
781
+ async goApp (config, item) {
782
+ let { serviceId, params = {} } = config
783
+ // 格式化params
784
+ const formatParams = this.$formatParams(params, item)
785
+ if (!serviceId && formatParams.serviceId) serviceId = formatParams.serviceId
786
+ if (!serviceId) return
787
+ let app = {}
788
+ await this.$http.get({
789
+ entityId: 'FM_SERVICE',
790
+ id: serviceId
791
+ })
792
+ .then(data => {
793
+ if (data && data.data) {
794
+ app = data.data
795
+ }
796
+ })
797
+ if (!app.SYSTEM) return
798
+ this.$http.get({
799
+ link: 'client',
800
+ entityId: 'FM_SYSTEM',
801
+ id: app.SYSTEM
802
+ })
803
+ .then(data => {
804
+ if (data && data.data) {
805
+ const rootPath = this.$util.getRootPath()
806
+ let url = `${rootPath}/app/${data.data['CLIENT|CODE']}/${data.data.CODE}/${app.CODE}`
807
+ // 拼接 query 参数(排除 serviceId 和空值)
808
+ const queryPairs = []
809
+ Object.keys(formatParams).forEach(key => {
810
+ if (key === 'serviceId') return
811
+ const value = formatParams[key]
812
+ if (value === undefined || value === null || value === '') return
813
+ queryPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
814
+ })
815
+ if (queryPairs.length) url += `?${queryPairs.join('&')}`
816
+ window.open(url)
817
+ }
818
+ })
819
+ },
820
+ /**
821
+ * 通用打印
822
+ */
823
+ print ({ num = 0, printWay = 'custom', printCode = '', cptName = '', params = {}, ...props }, row) {
824
+ // 格式化params
825
+ let selections = []
826
+ // 检测是否选中元素
827
+ if (row) {
828
+ selections = [{ ...row }]
829
+ } else {
830
+ if (this.selection) selections = [...this.selection]
831
+ }
832
+ if (num !== 0 && num !== '0') {
833
+ if (selections.length === 0) {
834
+ this.$message.error(this.$t('please select a record'))
835
+ return
836
+ } else if (selections.length > 1) {
837
+ if (num === 1 || num === '1') {
838
+ this.$message.error(this.$t('Only one record can be selected'))
839
+ return
840
+ }
841
+ }
842
+ }
843
+ // 添加主键
844
+ if (!params.id) params.id = `#[${this.$metaKey}]`
845
+ const formatParams = this.$formatParams(params, selections)
846
+ if (printWay === 'fine') {
847
+ const ids = selections.map(item => `'${item[this.$metaKey]}'`)
848
+ formatParams.id = ids.join(',')
849
+ this.fineReportPrint(cptName, formatParams)
850
+ } else {
851
+ this.customReportPrint(printCode, formatParams, props)
852
+ }
853
+ },
854
+ /**
855
+ * 自定义报表打印方法
856
+ * @param {String} code 报表编号
857
+ * @param {Object} params 参数
858
+ */
859
+ customReportPrint (code, params, props) {
860
+ if (params.code) code = params.code
861
+ this.$http.find({
862
+ entityId: 'FM_FORM',
863
+ 'scope.CODE': code
864
+ })
865
+ .then(data => {
866
+ const { NAME } = data.data
867
+ this.$createPage({
868
+ component: 'excel-preview-page',
869
+ name: NAME,
870
+ code,
871
+ params,
872
+ openType: props.openMode,
873
+ ...props
874
+ })
875
+ })
876
+ },
877
+ fineReportPrint (cptName, params) {
878
+ const { 'CLIENT|CODE': client } = this.appConfig
879
+ const rootPath = window.IP + 'report/ReportServer'
880
+ let query = `?client=${client}&reportlet=com.epxing.frame.core.report.ReportReader&__bypagesize__=false&cptname=${cptName}`
881
+ for (const key in params) {
882
+ query += '&' + key + '=' + params[key]
883
+ }
884
+
885
+ const token = this.$util.getToken(client) || ''
886
+ window.open(rootPath + query + '&token=' + token)
887
+ },
888
+
889
+ /**
890
+ * 执行自定义JS
891
+ */
892
+ executeCustomJs (functionName, ...args) {
893
+ let pageEvents = {}
894
+ if (this.isPage) {
895
+ pageEvents = this.pageEvents
896
+ } else if (this.template) {
897
+ pageEvents = this.template.pageEvents
898
+ } else {
899
+ pageEvents = this.upPage.pageEvents
900
+ }
901
+ if (functionName && pageEvents[functionName]) {
902
+ const fn = pageEvents[functionName]
903
+ if (!isSafeFn(fn)) return false
904
+ return fn.call(this, ...args)
905
+ } else {
906
+ return true
907
+ }
908
+ },
909
+ customJs ({ function: functionName, ...otherConfigs }, row) {
910
+ this.executeCustomJs(functionName, row || otherConfigs, otherConfigs)
911
+ },
912
+
913
+ commonRuleAction (config, ...props) {
914
+ this.commonAction({ ...config, requestType: 'rule' }, ...props)
915
+ },
916
+ /**
917
+ * 通用请求
918
+ * @param action 请求命令
919
+ * @param rule 请求业务规则
920
+ * @param metaId 请求对象,默认为当前页面对象
921
+ * @param num 选中记录数(1,必须选中且只能选中一条记录;n:必须选中至少一条记录;0不需要选中记录)
922
+ * @param comfirmMsg 操作提交之前的确认信息(如果为空,就不需要确认)
923
+ * @param params #[列表对象的某字段名]",(如果是选中的多条记录,将字段值拼成一个字符串,用逗号隔开) "$[当前页面的某变量名]"
924
+ * @param successMsg 操作成功的提示
925
+ * @param failMsg 操作失败的提示
926
+ * @param row 操作的列
927
+ */
928
+ commonAction ({ id, text, requestType, action, rule, metaId = (this.$metaId), num = 0, confirm, confirmMsg = null, confirmButtonText, cancelButtonText, mask = false, params = {}, successMsg = '处理成功', failMsg = '处理失败', afterTrigger, afterTriggerPage, afterTriggerPageOpenMode, afterTriggerParams }, row) {
929
+ let selections = []
930
+ // 检测是否选中元素
931
+ if (row) {
932
+ selections = [{ ...row }]
933
+ } else {
934
+ if (this.selection) selections = [...this.selection]
935
+ }
936
+ if (selections.length === 0) {
937
+ if (num !== 0 && num !== '0') {
938
+ this.$message.error(this.$t('please select a record'))
939
+ return
940
+ }
941
+ } else if (selections.length > 1) {
942
+ if (num === 1 || num === '1') {
943
+ this.$message.error(this.$t('Only one record can be selected'))
944
+ return
945
+ }
946
+ }
947
+
948
+ if (!params.id) {
949
+ params.id = `#[${this.$metaKey}]`
950
+ }
951
+ const formatParams = this.$formatParams(params, selections)
952
+ const loadingKey = id || text || `common-${action}`
953
+ // 提交方法
954
+ const submitFn = () => {
955
+ if (this.httpLoading[loadingKey]) {
956
+ return this.$message.error('正在提交,请稍后...')
957
+ }
958
+ this.httpLoading[loadingKey] = true
959
+ let loading = null
960
+ if (mask) {
961
+ loading = this.$loading({
962
+ target: this.upPage ? this.upPage.$parent.$el : this.$el,
963
+ background: 'rgba(255, 255, 255, 0.5)',
964
+ text: '正在操作,请稍后...',
965
+ lock: true
966
+ })
967
+ }
968
+ const timer = setTimeout(() => {
969
+ this.httpLoading[loadingKey] = false
970
+ clearTimeout(timer)
971
+ }, 2000)
972
+ const params = {
973
+ action,
974
+ entityId: metaId,
975
+ ...formatParams
976
+ }
977
+ if (requestType === 'rule') params.rule = rule
978
+
979
+ this.$http.$post(params)
980
+ .then((res) => {
981
+ // 如果存在下一个事件,执行下一个事件
982
+ const _afterTriggerParams = this.$formatAfterTriggerParams(afterTriggerParams, { res, data: row || this.data })
983
+ this.$message.success(successMsg)
984
+ if (afterTrigger) {
985
+ this[afterTrigger]({ pageId: afterTriggerPage, openMode: afterTriggerPageOpenMode, params: _afterTriggerParams })
986
+ } else {
987
+ if (this.reloadData) this.reloadData()
988
+ }
989
+ // 广播「这一页的数据被改动了」。reloadData 只刷自己那张列表,够不到同一页上的
990
+ // 统计卡:处理完一条待办,列表少一行而顶上的「待办 N」还是原来的数字,
991
+ // 看着像统计不准。谁关心谁监听(见 CardNum 的 data-changed)
992
+ if (this.$event) this.$event.emit('data-changed', { action, metaId })
993
+ })
994
+ .catch(err => {
995
+ this.$message.error(err.message || failMsg)
996
+ })
997
+ .finally(() => {
998
+ this.httpLoading[loadingKey] = false
999
+ if (loading) loading.close()
1000
+ })
1001
+ }
1002
+ // 判断是否需要确认弹窗
1003
+ if (confirm && confirmMsg) {
1004
+ this.$confirm(confirmMsg, this.$t('hint'), {
1005
+ confirmButtonText: confirmButtonText || this.$t('confirm'),
1006
+ cancelButtonText: cancelButtonText || this.$t('cancel'),
1007
+ type: 'warning'
1008
+ }).then(() => {
1009
+ submitFn()
1010
+ })
1011
+ } else {
1012
+ submitFn()
1013
+ }
1014
+ },
1015
+ /**
1016
+ * 下载文件
1017
+ * @param action 请求命令
1018
+ * @param rule 请求业务规则
1019
+ * @param metaId 请求对象,默认为当前页面对象
1020
+ * @param num 选中记录数(1,必须选中且只能选中一条记录;n:必须选中至少一条记录;0不需要选中记录)
1021
+ * @param comfirmMsg 操作提交之前的确认信息(如果为空,就不需要确认)
1022
+ * @param params #[列表对象的某字段名]",(如果是选中的多条记录,将字段值拼成一个字符串,用逗号隔开) "$[当前页面的某变量名]"
1023
+ * @param successMsg 操作成功的提示
1024
+ * @param failMsg 操作失败的提示
1025
+ * @param row 操作的列
1026
+ */
1027
+ downloadFile ({ requestType, action, rule, metaId = (this.$metaId), num = 0, confirm, confirmMsg = null, confirmButtonText, cancelButtonText, mask = false, params = {}, successMsg = '处理成功', failMsg = '处理失败', afterTrigger, afterTriggerPage, afterTriggerParams }, row) {
1028
+ let selections = []
1029
+ // 检测是否选中元素
1030
+ if (row) {
1031
+ selections = [{ ...row }]
1032
+ } else {
1033
+ if (this.selection) selections = [...this.selection]
1034
+ }
1035
+ if (selections.length === 0) {
1036
+ if (num !== 0 && num !== '0') {
1037
+ this.$message.error(this.$t('please select a record'))
1038
+ return
1039
+ }
1040
+ } else if (selections.length > 1) {
1041
+ if (num === 1 || num === '1') {
1042
+ this.$message.error(this.$t('Only one record can be selected'))
1043
+ return
1044
+ }
1045
+ }
1046
+
1047
+ if (!params.id) {
1048
+ params.id = `#[${this.$metaKey}]`
1049
+ }
1050
+ // 格式化params
1051
+ const formatParams = this.$formatParams(params, selections)
1052
+ // 提交方法
1053
+ const submitFn = () => {
1054
+ if (this.httpLoading[`common-${action}`]) {
1055
+ return this.$message.error('正在提交,请稍后...')
1056
+ }
1057
+ this.httpLoading[`common-${action}`] = true
1058
+ let loading = null
1059
+ if (mask) {
1060
+ loading = this.$loading({
1061
+ target: this.upPage ? this.upPage.$parent.$el : this.$el,
1062
+ background: 'rgba(255, 255, 255, 0.5)',
1063
+ text: '正在操作,请稍后...',
1064
+ lock: true
1065
+ })
1066
+ }
1067
+ const timer = setTimeout(() => {
1068
+ this.httpLoading[`common-${action}`] = false
1069
+ clearTimeout(timer)
1070
+ }, 2000)
1071
+ const params = {
1072
+ action,
1073
+ entityId: metaId,
1074
+ ...formatParams
1075
+ }
1076
+ if (requestType === 'rule') params.rule = rule
1077
+
1078
+ this.$http.$post(params, {
1079
+ responseType: 'blob'
1080
+ })
1081
+ .then(({ blob, filename }) => {
1082
+ var a = document.createElement('a')
1083
+ document.body.appendChild(a)
1084
+ const objectUrl = URL.createObjectURL(blob)
1085
+ a.href = objectUrl
1086
+ a.download = filename
1087
+ a.click()
1088
+ window.URL.revokeObjectURL(objectUrl)
1089
+ document.body.removeChild(a)
1090
+ // 如果存在下一个事件,执行下一个事件
1091
+ const _afterTriggerParams = this.$formatAfterTriggerParams(afterTriggerParams, { data: row || this.data })
1092
+ this.$message.success(successMsg)
1093
+ if (afterTrigger) {
1094
+ this[afterTrigger]({ pageId: afterTriggerPage, params: _afterTriggerParams })
1095
+ } else {
1096
+ if (this.reloadData) this.reloadData()
1097
+ }
1098
+ })
1099
+ .catch(err => {
1100
+ this.$message.error(err.message || failMsg)
1101
+ })
1102
+ .finally(() => {
1103
+ this.httpLoading[`common-${action}`] = false
1104
+ if (loading) loading.close()
1105
+ })
1106
+ }
1107
+ // 判断是否需要确认弹窗
1108
+ if (confirm && confirmMsg) {
1109
+ this.$confirm(confirmMsg, this.$t('hint'), {
1110
+ confirmButtonText: confirmButtonText || this.$t('confirm'),
1111
+ cancelButtonText: cancelButtonText || this.$t('cancel'),
1112
+ type: 'warning'
1113
+ }).then(() => {
1114
+ submitFn()
1115
+ })
1116
+ } else {
1117
+ submitFn()
1118
+ }
1119
+ },
1120
+ $refesh () {
1121
+ // this.$children.forEach(child => {
1122
+ // if (child.$options.componentType === 'component') {
1123
+ // if (child.$refesh) child.$refesh()
1124
+ // }
1125
+ // })
1126
+ },
1127
+ $refeshPage () {
1128
+ if (this.upPage) this.upPage.$refesh()
1129
+ },
1130
+ // 获取上级页面
1131
+ $getParentPage () {
1132
+ var parent = this.$parent || this.$root
1133
+ var type = parent.$options.$componentType
1134
+
1135
+ while (parent && (!type || type !== 'page')) {
1136
+ parent = parent.$parent
1137
+
1138
+ if (parent) {
1139
+ type = parent.$options.componentName
1140
+ }
1141
+ }
1142
+ if (parent) {
1143
+ return parent
1144
+ }
1145
+ },
1146
+ // 修改上级页面数据
1147
+ $setParentPageModel (model) {
1148
+ if (this.$parentPage) {
1149
+ for (const key in model) {
1150
+ this.$parentPage.model[key] = model[key]
1151
+ }
1152
+ }
1153
+ },
1154
+ // 调用上级页面内组件事件
1155
+ $dispatchComponent (id, eventName, ...params) {
1156
+ if (this.$parentPage) { // 组件id
1157
+ broadcast.apply(this.$parentPage, [id, eventName, ...params])
1158
+ }
1159
+ },
1160
+ // 获取页面内组件(T31:注册表 byId,替代 $children 树遍历)
1161
+ $getPageComponent (id, current = this, prefix) {
1162
+ const newId = prefix ? prefix + '-' + id : id
1163
+ const page = current && current._epxPage ? current._epxPage() : this._epxPage()
1164
+ const reg = page && page._epxReg
1165
+ return reg ? reg.byId.get(newId) : undefined
1166
+ },
1167
+ $excuteEvent (key, ...args) {
1168
+ const { componentEvents = [] } = this.$configs
1169
+ const { [key]: keyEvents = [] } = componentEvents
1170
+ keyEvents.map(event => this.executeCustomJs(event, ...args))
1171
+ },
1172
+ // 这些方法依赖实例属性(如 httpLoading),保留在混入中
1173
+ $setPageConfig (page) {
1174
+ const currentPage = page || this.page || {}
1175
+ const { config = this.compConfigs } = currentPage
1176
+ for (const key in this.$options) {
1177
+ if (isCustomOption(key, this.$options[key])) {
1178
+ this.pageConfig[key] = this.$options[key]
1179
+ }
1180
+ }
1181
+ for (const key in config) {
1182
+ try {
1183
+ if (this.$_.isObject(config[key])) {
1184
+ if (Array.isArray(config[key])) {
1185
+ this.pageConfig[key] = [...config[key]]
1186
+ } else {
1187
+ // ⚠️ 别往 $options 上写。它是 Vue 按**组件类型**缓存的
1188
+ // (appContext.optionsCache),同一个 app 里该类型所有实例共用同一个对象。
1189
+ // 原先写的是 `if (!this.$options[key]) this.$options[key] = {}`,于是任何一个
1190
+ // 页面配置里出现过的对象型键都被永久种进了共享 $options,此后所有同类型实例
1191
+ // 都会读到 `{}` 而不是 `undefined`——假值变真值,`if (x)` 分支整个翻转,
1192
+ // 而且**取决于页面访问顺序**,刷新即恢复,极难归因。
1193
+ // `if (!x) x = {}` 之后再展开,与直接展开 `x || {}` 完全等价(原逻辑也只在
1194
+ // 假值时赋 {}),所以去掉写入不改变合并结果。回归测试见
1195
+ // test/unit/config-options-isolation.test.js。
1196
+ this.pageConfig[key] = { ...(this.$options[key] || {}), ...config[key] }
1197
+ }
1198
+ } else {
1199
+ this.pageConfig[key] = config[key]
1200
+ }
1201
+ } catch (error) {
1202
+ this.pageConfig[key] = config[key]
1203
+ }
1204
+ }
1205
+ },
1206
+ $setCompConfigs () {
1207
+ for (const key in this.$options) {
1208
+ if (isCustomOption(key, this.$options[key])) {
1209
+ this.configs[key] = this.$options[key]
1210
+ }
1211
+ }
1212
+ for (const key in this.compConfigs) {
1213
+ try {
1214
+ if (this.$_.isObject(this.compConfigs[key])) {
1215
+ if (Array.isArray(this.compConfigs[key])) {
1216
+ this.configs[key] = [...this.compConfigs[key]]
1217
+ } else {
1218
+ // 同上:不写共享 $options(见 $setPageConfig 里的说明)
1219
+ this.configs[key] = { ...(this.$options[key] || {}), ...this.compConfigs[key] }
1220
+ }
1221
+ } else {
1222
+ this.configs[key] = this.compConfigs[key]
1223
+ }
1224
+ } catch (error) {
1225
+ this.configs[key] = this.compConfigs[key]
1226
+ }
1227
+ }
1228
+ },
1229
+ _createComponent () {
1230
+ let height = ''
1231
+ // 页面配置
1232
+ const _configs = {}
1233
+ const globalConfigs = (this.componentGlobalConfig && this.componentGlobalConfig[this.contentType]) || {}
1234
+ const systemGlobalConfigs = (this.systemComponentGlobalConfig && this.systemComponentGlobalConfig[this.contentType]) || {}
1235
+ const queryPageConfigs = this.pageConfig || this.$query.pageConfigs || {}
1236
+ const { config: pageConfigs = queryPageConfigs } = this.$pageInfo
1237
+ const { css, itemConfigs = {}, ...otherConfigs } = pageConfigs
1238
+ const _itemConfigs = this.itemConfigs || itemConfigs
1239
+ const itemConfig = _itemConfigs[this.compId]
1240
+ let allConfigs = { ...globalConfigs, ...systemGlobalConfigs, ...this.compConfigs, ...itemConfig }
1241
+ if (this.isPage) {
1242
+ allConfigs = { ...allConfigs, ...otherConfigs }
1243
+ // 获取函数库
1244
+ const exports = {}
1245
+ // eslint-disable-next-line no-eval
1246
+ eval('(function (exports) {' + pageConfigs.compileJS + ' }(exports));')
1247
+ this.pageEvents = { ...exports }
1248
+ if (css) {
1249
+ appendCss(this.$pageId, css)
1250
+ }
1251
+ } else {
1252
+ const { css } = this._component
1253
+ if (css) {
1254
+ appendCss(this.compId, css)
1255
+ }
1256
+ }
1257
+ // 从组件 $options 收割「配置点默认值」。判定统一走 isCustomOption——
1258
+ // 这里原先另有一份内联的保留名单,和 custom-option.js 的那份各写各的,已经漂了:
1259
+ // 内联那份少 18 个键,其中最要命的是 **extends**。Vue 3 的 mergeOptions 对 extends
1260
+ // 没有合并策略、直接把基类定义对象赋到解析后的 $options 上(runtime-core 4031-4039),
1261
+ // 于是每个 `extends: XxxList` 的组件都会把**整个基类定义对象**当成配置点塞进 configs,
1262
+ // 被 $configs 每次重算时整体拷贝、被 get-component 广播出去。
1263
+ // 本仓库 61 个组件用了 extends。另有 activated/deactivated/updated 等钩子数组同样泄漏。
1264
+ //
1265
+ // 注意这个过滤只作用在「收割默认值」这一步;页面配置走下面的 allConfigs 合并,
1266
+ // 不受影响——所以线上已有的页面配置数据语义不变。
1267
+ for (const key in this.$options) {
1268
+ if (isCustomOption(key, this.$options[key])) {
1269
+ _configs[key] = this.$options[key]
1270
+ }
1271
+ }
1272
+ for (const key in allConfigs) {
1273
+ try {
1274
+ if (this.$_.isObject(allConfigs[key])) {
1275
+ if (Array.isArray(allConfigs[key])) {
1276
+ _configs[key] = [...allConfigs[key]]
1277
+ } else {
1278
+ // 同上:不写共享 $options(见 $setPageConfig 里的说明)
1279
+ _configs[key] = { ...(this.$options[key] || {}), ...allConfigs[key] }
1280
+ }
1281
+ } else {
1282
+ _configs[key] = allConfigs[key]
1283
+ }
1284
+ } catch (error) {
1285
+ _configs[key] = allConfigs[key]
1286
+ }
1287
+ }
1288
+ const { items = [], mItems, style = {} } = _configs
1289
+ delete _configs.items
1290
+ const _items = this.items || items
1291
+ const configs = { ...this.configs, ..._configs }
1292
+ this.configs = configs
1293
+
1294
+ if (configs.height) {
1295
+ height = typeof configs.height === 'string' && configs.height.includes('%') ? configs.height : configs.height + 'px'
1296
+ }
1297
+ // 获取页面配置
1298
+ const _style = { ...style, ...this.compStyle }
1299
+ if (_style.backgroundImage) {
1300
+ _style.backgroundImage = _style.backgroundImage.replace(/url\s*\(([\w+-/.\s]+)\)/g, (str, $1) => {
1301
+ return `url(${util.getFileUrl($1)})`
1302
+ })
1303
+ }
1304
+ if (height) _style.height = height
1305
+ this.component = {
1306
+ id: this.compId || this.$pageId,
1307
+ title: this.$formatContent(this.compTitle || this.$attrs.title),
1308
+ contentType: this.contentType,
1309
+ style: _style,
1310
+ configs: this.$configs,
1311
+ flex: this.compFlex === undefined ? !!this.$configs.flex : !!this.compFlex,
1312
+ items: _items, // 子节点
1313
+ mItems,
1314
+ metaId: this.$metaId,
1315
+ meta: this.$meta,
1316
+ parentPage: this.$getParentPage()
1317
+ }
1318
+ this.$emit('get-component', this.component)
1319
+ this.$event.emit('get-component', this.component)
1320
+ if (this.isPage) {
1321
+ this.component.itemConfigs = itemConfigs
1322
+ }
1323
+ this.$emit('load-component', this.component)
1324
+ this.$event.emit('load-component', this.component)
1325
+ },
1326
+
1327
+ // 创建组件前
1328
+ beforeCreateComponent () {
1329
+ },
1330
+ // 初始化配置 可在此更改默认配置
1331
+ beforeInitComponent () {
1332
+
1333
+ },
1334
+ /**
1335
+ * 初始化组件
1336
+ */
1337
+ initComponent (component) {
1338
+
1339
+ }
1340
+ },
1341
+ async created () {
1342
+ // T31 修正:注册必须早于下面 _createComponent() 发出的 `load-component`——BasePage 收到该
1343
+ // 事件后要用 getComponentById 拿本组件实例、给页面变量建 $watch,注册晚了就恒为 undefined、
1344
+ // 在 BasePage.watchCompValue 里静默早退(页面变量恒空 ⇒ 由它驱动的 scopeParams 条件被
1345
+ // createHttp 整条丢弃)。Vue2 靠 $children(在 _init 里就填好、早于 created)天然满足这个
1346
+ // 不变式;Vue3 没有 $children,改用 _epxReg 之后这个先后顺序就得自己保证。
1347
+ // mounted 里那句保留不删:Map.set / Set.add 幂等,重复注册零代价。
1348
+ this._epxRegister()
1349
+ // ⚠️ `compoennt` 是笔误(少个 n),**别顺手改对**——它是承重的。
1350
+ // 原意见 global.js 里对称的那一对:「非 page → setCompConfigs / 非 component → setPageConfig」,
1351
+ // 即 $setPageConfig() 本该只跑在页面上。笔误让条件恒真 ⇒ 多年来**每个**组件都在跑它,
1352
+ // 于是 _createComponent() / $formatComponents() 的 pageConfig 回退路径,以及
1353
+ // Collapse / ApprovalIndex / TaskList / TodoList / PermissManage / Template 等组件里的
1354
+ // `this.pageConfig.xxx` 读取,全都建立在这个恒真之上。
1355
+ // 拼写改对 = 所有非页面组件的 pageConfig 一次性清零 ⇒ 按 compId 下发的逐组件配置静默丢失
1356
+ // (按钮不渲染、checkPageId / detailPageFlag 变 undefined 走错分支),而且现有单测抓不到
1357
+ // (component-register-timing.test.js 把 $setPageConfig stub 掉了,只断言注册顺序)。
1358
+ // 真要收敛回原意,得当独立重构立项:先给每个消费点补断言,再改条件,最后跑全量 e2e。
1359
+ if (this.$options.$componentType !== 'compoennt') {
1360
+ this.$setPageConfig()
1361
+ }
1362
+ this.$componentStatus = 'beforeCreate'
1363
+ this.beforeCreateComponent()
1364
+ // 创建组件
1365
+ this._createComponent()
1366
+ this.$componentStatus = 'created'
1367
+ try {
1368
+ this.oldCompConfigs = this.$_.cloneDeep(this.compConfigs)
1369
+ } catch (error) {}
1370
+ // 初始化组件配置
1371
+ this.$componentStatus = 'beforeInit'
1372
+ const component = this.beforeInitComponent()
1373
+ if (component instanceof Promise) {
1374
+ await component
1375
+ }
1376
+ if (this._isBeingDestroyed || this._isDestroyed) return
1377
+ // 如果this.$metaId 并且不存在this.$meta 获取meta对象
1378
+ if (this.$metaId && !this.$meta) {
1379
+ await this.$store.dispatch({
1380
+ type: 'ADD_META',
1381
+ metaId: this.$metaId
1382
+ })
1383
+ }
1384
+ if (this._isBeingDestroyed || this._isDestroyed) return
1385
+ // 初始化组件
1386
+ this.$componentStatus = 'init'
1387
+ this.initComponent(this.$component)
1388
+ this.$nextTick(() => {
1389
+ if (this._isBeingDestroyed || this._isDestroyed) return
1390
+ if (this.$el) {
1391
+ const compId = this.isPage ? this.$pageId : this.compId
1392
+ if (this.$el.classList) {
1393
+ this.$el.classList.add('css_' + compId)
1394
+ } else {
1395
+ this.$el.className = 'css_' + compId
1396
+ }
1397
+ }
1398
+ })
1399
+ },
1400
+ mounted () {
1401
+ // T31:组件注册表(替代 $children 树遍历,Vue3 无 $children)。每组件注册进其页面的
1402
+ // _epxRegistry,broadcast/getComponentById 改 O(1) 查表。页面级作用域(upPage 就近)。
1403
+ this._epxRegister()
1404
+ // 系统返回(popstate)已由 @epxing/utils/src/nav-history 的全局唯一监听器统一处理,
1405
+ // 不再每个页面各挂一个(keep-alive 缓存页/隐藏 tab 页的监听器会导致一次返回连弹多页)
1406
+ this.$excuteEvent('onMounted')
1407
+ },
1408
+ unmounted () {
1409
+ this._epxDeregister() // T31:从页面注册表注销
1410
+ const compId = this.isPage ? this.$pageId : this.compId
1411
+ removeCss(compId)
1412
+ // 1. 清理事件监听器
1413
+ if (this._pageEventHandlers && this._pageEventHandlers.length) {
1414
+ for (const { comp, event, handler } of this._pageEventHandlers) {
1415
+ // 解绑到真正绑定的 page component 上
1416
+ if (comp && comp.$epxOff) comp.$epxOff(event, handler)
1417
+ }
1418
+ }
1419
+ this._pageEventHandlers = []
1420
+
1421
+ // 2. 清理 openedPopup 中对当前组件的引用(关键!防止连锁泄漏)
1422
+ // 如果当前组件被保存在 openedPopup 数组中(通过 opener 引用),
1423
+ // 即使组件销毁了,openedPopup 仍然持有引用,导致整个引用链无法回收
1424
+ if (window.openedPopup && Array.isArray(window.openedPopup)) {
1425
+ for (let i = window.openedPopup.length - 1; i >= 0; i--) {
1426
+ const popup = window.openedPopup[i]
1427
+ if (popup && popup.opener === this) {
1428
+ // 清理 opener 引用,防止阻止垃圾回收
1429
+ popup.opener = null
1430
+ // 如果弹窗已经关闭,从数组中移除
1431
+ if (popup.closed || !popup.visible) {
1432
+ window.openedPopup.splice(i, 1)
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+
1438
+ // 3. 清理 component 对象中的循环引用
1439
+ if (this.component) {
1440
+ // 清理 parentPage 引用(防止父页面持有子组件引用导致无法回收)
1441
+ if (this.component.parentPage) {
1442
+ this.component.parentPage = null
1443
+ }
1444
+ // 清理其他可能的引用
1445
+ this.component = null
1446
+ }
1447
+
1448
+ // 4. 执行组件自定义的销毁事件
1449
+ // ⚙️ 见文件顶部 EXEC_ON_DESTORY_ON_TEARDOWN(评审 C2):整棵页面树被销毁这条**本次新唤醒**的
1450
+ // 路径上保持 Task 17 之前的语义(不跑用户脚本);单个组件被摘掉的路径照常跑。
1451
+ if (EXEC_ON_DESTORY_ON_TEARDOWN || !isEpxTeardown(this.$)) {
1452
+ this.$excuteEvent('onDestory')
1453
+ }
1454
+ },
1455
+ beforeUnmount () {
1456
+ // ⚙️ 与上面 onDestory 同一道闸门(最终评审 I-1):M0 的真卸载把 bum 和 um **两个**钩子一起
1457
+ // 唤醒了,而 EXEC_ON_DESTORY_ON_TEARDOWN 原来只挡住了 um 那半边 —— `onBeforeDestory` 是
1458
+ // 同一类低代码用户脚本的逃逸口(线上页面配置里可能存在从未执行过的脚本),漏掉它等于闸门只关了一半。
1459
+ // `_epxTeardown` 由 RouterView 在剪枝前打上,bum 触发时已就位。
1460
+ if (EXEC_ON_DESTORY_ON_TEARDOWN || !isEpxTeardown(this.$)) {
1461
+ this.$excuteEvent('onBeforeDestory')
1462
+ }
1463
+ }
1464
+ }