@epxing/core 5.3.0-beta.1 → 5.3.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +30 -27
- package/README.md +5 -4
- package/dist/.cache/route-chart-release/frontend/frame/public/js/ecStat.min.js +4 -0
- package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts-wordcloud.min.js +4 -0
- package/dist/.cache/route-chart-release/frontend/frame/public/js/echarts.min.js +4 -0
- package/dist/_virtual/_plugin-vue_export-helper.js +10 -0
- package/dist/_virtual/jsencrypt.min.js +1 -1
- package/dist/assets/ecStat.min-BrTYPOXX.js +1 -0
- package/dist/assets/echarts-wordcloud.min-nWR4Hzny.js +3 -0
- package/dist/assets/echarts.min-DROS9E5X.js +45 -0
- package/dist/assets/role_select-DTqRlFNz.webp +0 -0
- package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/jsencrypt@3.5.4/_vendor/jsencrypt/bin/jsencrypt.min.js +1 -1
- package/dist/src/Container.js +1 -0
- package/dist/src/RootApp.js +54 -0
- package/dist/src/api/config.js +1 -0
- package/dist/src/api/createHttp.js +7 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/app-layout.js +1 -0
- package/dist/src/app-ui-settings.js +49 -0
- package/dist/src/app.js +1 -0
- package/dist/src/axios.js +11 -15
- package/dist/src/capture-ref.js +1 -0
- package/dist/src/chart-runtime-assets.js +9 -0
- package/dist/src/components/draggable/index.js +1 -0
- package/dist/src/create-app.js +4 -6
- package/dist/src/directives/clickoutside.js +1 -0
- package/dist/src/directives/global.js +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/lazy-library.js +1 -0
- package/dist/src/loadComponent.js +3 -2
- package/dist/src/loadLibrary.js +1 -0
- package/dist/src/menu-route.js +12 -0
- package/dist/src/mixins/component.js +4 -0
- package/dist/src/mixins/custom-option.js +1 -0
- package/dist/src/mixins/el-locale.js +1 -0
- package/dist/src/mixins/global.js +1 -0
- package/dist/src/mixins/index.js +1 -0
- package/dist/src/mixins/methods-to-prototype.js +1 -0
- package/dist/src/mixins/optimized-global.js +1 -0
- package/dist/src/mixins/popper.js +1 -0
- package/dist/src/navigation-context.js +47 -0
- package/dist/src/navigation.js +44 -12
- package/dist/src/package-install.js +4 -2
- package/dist/src/plugins/element-plus.js +9 -8
- package/dist/src/plugins/i18n.js +1 -0
- package/dist/src/plugins/library-locale.js +1 -0
- package/dist/src/progress-bar.js +1 -0
- package/dist/src/public-api.js +5 -2
- package/dist/src/register-components.js +1 -0
- package/dist/src/remote-deps.js +1 -0
- package/dist/src/route-components.js +9 -0
- package/dist/src/router/config.js +8 -8
- package/dist/src/router/index.js +1 -0
- package/dist/src/router/public.js +1 -0
- package/dist/src/runtime-services.js +2 -1
- package/dist/src/runtime-utils/fieldFactory.js +1 -0
- package/dist/src/runtime-utils/meta.js +4 -0
- package/dist/src/runtime-utils/pdf.viewer.js +32 -15
- package/dist/src/runtime-utils/util.js +30 -5
- package/dist/src/shallow-same.js +1 -0
- package/dist/src/shared-packages.js +2 -3
- package/dist/src/store/actions.js +1 -0
- package/dist/src/store/create-store.js +1 -0
- package/dist/src/store/index.js +1 -0
- package/dist/src/store/lru.js +1 -0
- package/dist/src/store/modules/config.js +1 -0
- package/dist/src/store/modules/file.js +1 -0
- package/dist/src/store/modules/index.js +1 -0
- package/dist/src/store/modules/list.js +26 -3
- package/dist/src/store/modules/locale.js +1 -0
- package/dist/src/store/modules/meta.js +50 -7
- package/dist/src/store/modules/page.js +1 -0
- package/dist/src/store/modules/router.js +1 -0
- package/dist/src/store/modules/tabs.js +1 -0
- package/dist/src/store/modules/user.js +1 -0
- package/dist/src/style-presets.js +5 -4
- package/dist/src/ui-settings.js +91 -18
- package/dist/src/viewport.js +1 -0
- package/dist/src/views/App.js +48 -0
- package/dist/src/views/AppRoleSelect.js +233 -0
- package/dist/src/views/Error.js +210 -0
- package/dist/src/views/Module.js +173 -0
- package/dist/src/vue.bus.js +1 -0
- package/dist/src/vue2-cancel-token-compat.js +1 -0
- package/dist/src/vue2-remote-shim.js +1 -0
- package/dist/src/vue2-render-compat.js +1 -0
- package/dist/src/vue2-select-style-compat.js +1 -0
- package/dist/src/watermark.js +1 -0
- package/dist/src/xss.js +1 -0
- package/dist/src/z-index.js +1 -0
- package/dist/style.css +251 -0
- package/package.json +6 -5
- package/src/RootApp.vue +75 -0
- package/src/api/createHttp.js +7 -0
- package/src/api/index.js +47 -47
- package/src/app-ui-settings.js +31 -0
- package/src/axios.js +311 -318
- package/src/chart-runtime-assets.js +7 -0
- package/src/create-app.js +3 -7
- package/src/index.js +28 -28
- package/src/install.js +157 -157
- package/src/loadComponent.js +187 -187
- package/src/loadLibrary.js +99 -99
- package/src/menu-route.js +12 -1
- package/src/mixins/component.js +1464 -1461
- package/src/navigation-context.js +49 -0
- package/src/navigation.js +208 -176
- package/src/package-install.js +2 -1
- package/src/plugins/element-plus.js +302 -302
- package/src/plugins/frame.js +178 -178
- package/src/public-api.js +1 -0
- package/src/route-components.js +7 -0
- package/src/router/config.js +861 -864
- package/src/router/public.js +1 -1
- package/src/runtime-services.js +1 -1
- package/src/runtime-utils/fieldFactory.js +179 -179
- package/src/runtime-utils/meta.js +399 -378
- package/src/runtime-utils/pdf.viewer.js +672 -644
- package/src/runtime-utils/util.js +1357 -1330
- package/src/shared-packages.js +1 -3
- package/src/store/modules/config.js +351 -351
- package/src/store/modules/list.js +30 -3
- package/src/store/modules/locale.js +236 -236
- package/src/store/modules/meta.js +314 -265
- package/src/style-presets.js +4 -4
- package/src/ui-settings.js +84 -23
- package/src/views/App.vue +53 -0
- package/src/views/AppRoleSelect.vue +334 -0
- package/src/views/Error.vue +63 -0
- package/src/views/Module.vue +210 -0
- package/src/xss.js +26 -26
- package/types/app.d.ts +10 -10
- package/types/axios.d.ts +1 -1
- package/types/component-mixin.d.ts +1 -1
- package/types/field-factory.d.ts +1 -1
- package/types/global-mixin.d.ts +1 -1
- package/types/http.d.ts +1 -1
- package/types/index.d.ts +3 -0
- package/types/meta.d.ts +1 -1
- package/types/navigation.d.ts +21 -21
- package/types/pdf-viewer.d.ts +1 -1
- package/types/runtime.d.ts +13 -13
- package/types/store.d.ts +1 -1
- package/types/util.d.ts +2 -2
- /package/dist/{build → .cache/route-chart-release/frontend/frame/build}/stubs/empty-components.js +0 -0
- /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-icons.names.js +0 -0
- /package/dist/{src → .cache/route-chart-release/frontend/frame/src}/element-plus.lazy.js +0 -0
- /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
- /package/dist/{_vendor → frontend/frame/_vendor}/.pnpm/element-plus@2.14.3_vue@3.5.39_typescript@5.9.3_/_vendor/element-plus/dist/locale/zh-cn.js +0 -0
package/src/mixins/component.js
CHANGED
|
@@ -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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
this
|
|
1385
|
-
|
|
1386
|
-
this.$
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
//
|
|
1402
|
-
//
|
|
1403
|
-
this
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
this
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
//
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
//
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
+
}
|