cloud-web-corejs 1.0.54-dev.787 → 1.0.54-dev.789

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/components/baseInputExport/mixins.js +13 -2
  3. package/src/components/code-editor/async.js +29 -0
  4. package/src/components/code-editor/index.vue +65 -6
  5. package/src/components/table/index.js +4 -0
  6. package/src/components/xform/docs/2026-08 /345/210/227/350/241/250/345/257/274/345/207/272/350/204/261/346/225/217/345/210/227/345/211/224/351/231/244/350/220/275/345/234/260/346/226/207/346/241/243.md" +173 -0
  7. package/src/components/xform/form-designer/form-widget/field-widget/form-item-wrapper.vue +18 -73
  8. package/src/components/xform/form-designer/form-widget/field-widget/script-input-widget.vue +3 -0
  9. package/src/components/xform/form-render/container-item/data-table-mixin.js +20 -0
  10. package/src/components/xform/form-render/container-item/list-h5-item.vue +12 -0
  11. package/src/components/xform/utils/textMaskUtil.js +179 -0
  12. package/src/index.js +2 -3
  13. package/src/store/config/index.js +224 -0
  14. package/src/store/modules/tagsView.js +4 -2
  15. package/src/views/bd/project/branch/bd_branch/edit.vue +145 -0
  16. package/src/views/bd/project/branch/bd_branch/list.vue +72 -0
  17. package/src/views/bd/project/branch/bd_branch/menuKindDialog.vue +106 -0
  18. package/src/views/bd/project/branch/bd_branch/mixins/edit.js +189 -0
  19. package/src/views/bd/project/branch/bd_branch/mixins/list.js +124 -0
  20. package/src/views/bd/project/branch/bd_branch/mixins/menuKindDialog.js +85 -0
  21. package/src/views/bd/project/branch/form_script/list.vue +22 -0
  22. package/src/views/bd/project/branch/form_template/list.vue +21 -0
  23. package/src/views/bd/project/branch/menu_kind/list.vue +21 -0
  24. package/src/views/bd/project/common/form_script/list.vue +16 -0
  25. package/src/views/bd/project/common/form_template/list.vue +15 -0
  26. package/src/views/bd/project/common/menu_kind/list.vue +15 -0
  27. package/src/views/bd/project/core/branchContext.js +100 -0
  28. package/src/views/bd/project/core/branchPage.js +78 -0
  29. package/src/views/bd/project/core/branchPageWrap.vue +216 -0
  30. package/src/views/bd/project/core/branchScope.js +44 -0
  31. package/src/views/bd/project/core/branchSelect.vue +106 -0
  32. package/src/views/bd/project/core/httpBranchPatch.js +58 -0
  33. package/src/views/bd/project/datatable/menu_kind/list.vue +15 -0
  34. package/src/views/bd/project/datatable/table_model/list.vue +15 -0
  35. package/src/views/bd/setting/form_template/formDesignerDialog.vue +1 -0
  36. package/src/views/bd/setting/form_template/list.vue +2 -2
  37. package/src/views/bd/setting/form_template/mixins/edit.js +3 -3
  38. package/src/views/bd/setting/form_template/mixins/list.js +3 -3
  39. package/src/views/bd/setting/form_template/mixins/list2.js +3 -3
  40. package/src/views/bd/setting/form_template/mixins/wf_list.js +3 -3
  41. package/src/views/bd/setting/form_template/wf_list.vue +2 -2
  42. package/src/views/user/form/vform/designer.vue +51 -22
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cloud-web-corejs",
3
3
  "private": false,
4
- "version": "1.0.54-dev.787",
4
+ "version": "1.0.54-dev.789",
5
5
  "scripts": {
6
6
  "dev": "vue-cli-service serve",
7
7
  "dev:micro": "vue-cli-service serve --port 17527",
@@ -376,8 +376,19 @@ modules = {
376
376
  }
377
377
 
378
378
  function downLoadFile(row, that) {
379
- let filename = row.filename || fileName;
380
- let realFileName = filename ? filename + "-" + row.uuid : row.uuid;
379
+ let realFileName;
380
+ if (row.filename) {
381
+ //后台返回了文件名,用文件名+文件格式(格式取自uuid的后缀),不再拼接uuid
382
+ let uuid = row.uuid || "";
383
+ let dotIndex = uuid.lastIndexOf(".");
384
+ let suffix = dotIndex > -1 ? uuid.slice(dotIndex) : "";
385
+ realFileName =
386
+ suffix && row.filename.slice(-suffix.length) !== suffix
387
+ ? row.filename + suffix
388
+ : row.filename;
389
+ } else {
390
+ realFileName = fileName ? fileName + "-" + row.uuid : row.uuid;
391
+ }
381
392
  let fileUrl =
382
393
  configUtil.baseUrl +
383
394
  that.option.prefix +
@@ -0,0 +1,29 @@
1
+ /**
2
+ * code-editor 的异步组件定义(ace 体积较大,保持独立 chunk)。
3
+ *
4
+ * 普通 `() => import(...)` 在 chunk 加载失败时会静默渲染成空白,与“编辑器渲染了但内容
5
+ * 为空”无法区分,排查成本很高,故统一走带 error 占位的高级异步组件形式。
6
+ */
7
+ export default function createCodeEditorAsyncComponent() {
8
+ return {
9
+ component: import("./index.vue"),
10
+ error: {
11
+ name: "CodeEditorLoadError",
12
+ render(h) {
13
+ return h(
14
+ "div",
15
+ {
16
+ staticClass: "ace-editor",
17
+ style: {
18
+ padding: "12px",
19
+ color: "#f56c6c",
20
+ fontSize: "12px",
21
+ },
22
+ },
23
+ "代码编辑器加载失败,请刷新页面重试"
24
+ );
25
+ },
26
+ },
27
+ timeout: 30000,
28
+ };
29
+ }
@@ -56,7 +56,7 @@ export default {
56
56
  },
57
57
  height: {
58
58
  type: String,
59
- default: "450px;",
59
+ default: "450px",
60
60
  },
61
61
  resize: {
62
62
  //是否开启语法检查,默认开启
@@ -66,7 +66,10 @@ export default {
66
66
  },
67
67
  computed: {
68
68
  editorStyle: function () {
69
- return "height:" + this.height;
69
+ // 对象形式绑定,避免调用方传入的 height 拼出非法 CSS。
70
+ // 历史调用方存在 height="414px;" 这种带分号的写法,对象形式下会被浏览器丢弃,需先清理。
71
+ let height = (this.height || "").trim().replace(/;+$/, "").trim();
72
+ return { height: height || "450px" };
70
73
  },
71
74
  modePath: function () {
72
75
  return "ace/mode/" + this.mode;
@@ -157,6 +160,20 @@ export default {
157
160
  //this.$emit('update:value', this.aceEditor.getValue()) // 触发更新事件, 实现.sync双向绑定!!
158
161
  this.$emit("input", this.aceEditor.getValue());
159
162
  });
163
+
164
+ this.observeSizeChange();
165
+ },
166
+ activated() {
167
+ // keep-alive 页签切回时 DOM 重新插入,需重新测量,否则内容不渲染
168
+ this.refresh();
169
+ },
170
+ beforeDestroy() {
171
+ this.disconnectObserver();
172
+ if (this.aceEditor) {
173
+ // ace 仅在 destroy 时才解绑它自己注册的 window resize 监听
174
+ this.aceEditor.destroy();
175
+ this.aceEditor = null;
176
+ }
160
177
  },
161
178
  data() {
162
179
  return {
@@ -164,11 +181,18 @@ export default {
164
181
  themePath: "ace/theme/sqlserver", // 不导入 webpack-resolver,该模块路径会报错
165
182
  // modePath: 'ace/mode/javascript', // 同上
166
183
  codeValue: this.value || "",
184
+ resizeObserver: null,
185
+ resizeRaf: null,
186
+ refreshPending: false,
167
187
  };
168
188
  },
169
189
  watch: {
170
190
  value(val) {
171
- if (!this.aceEditor) return;
191
+ if (!this.aceEditor) {
192
+ // 实例尚未创建(异步组件解析中)时不能丢弃,否则这次赋值永久丢失
193
+ this.codeValue = val || "";
194
+ return;
195
+ }
172
196
  const newVal = val || "";
173
197
  if (this.aceEditor.getValue() !== newVal) {
174
198
  this.aceEditor.setValue(newVal, -1);
@@ -188,13 +212,48 @@ export default {
188
212
  },
189
213
  height() {
190
214
  this.$nextTick(() => {
191
- if (this.aceEditor) {
192
- this.aceEditor.resize();
193
- }
215
+ this.refresh();
194
216
  });
195
217
  },
196
218
  },
197
219
  methods: {
220
+ /**
221
+ * ace 在 mounted 时同步初始化,若此刻祖先为 display:none(隐藏字段、非激活 tab、
222
+ * 未展开的弹窗等),字符与容器尺寸都会被测成 0,之后没有任何时机重新测量
223
+ * —— ace 自身只监听 window 的 resize。这里观测编辑器元素本身,尺寸一旦可用就重测。
224
+ */
225
+ observeSizeChange() {
226
+ if (typeof ResizeObserver !== "function" || !this.$refs.ace) return;
227
+ this.resizeObserver = new ResizeObserver(() => {
228
+ this.refresh();
229
+ });
230
+ this.resizeObserver.observe(this.$refs.ace);
231
+ },
232
+ disconnectObserver() {
233
+ if (this.resizeRaf !== null) {
234
+ cancelAnimationFrame(this.resizeRaf);
235
+ this.resizeRaf = null;
236
+ }
237
+ this.refreshPending = false;
238
+ if (this.resizeObserver) {
239
+ this.resizeObserver.disconnect();
240
+ this.resizeObserver = null;
241
+ }
242
+ },
243
+ /** 重新测量并重绘。尺寸不可用时跳过,等下一次可见再触发。 */
244
+ refresh() {
245
+ if (this.refreshPending) return;
246
+ this.refreshPending = true;
247
+ this.resizeRaf = requestAnimationFrame(() => {
248
+ this.refreshPending = false;
249
+ this.resizeRaf = null;
250
+ const el = this.$refs.ace;
251
+ if (!this.aceEditor || !el) return;
252
+ // 仍不可见时跳过,等真正显示后 ResizeObserver 会再次触发
253
+ if (!el.offsetWidth || !el.offsetHeight) return;
254
+ this.aceEditor.resize(true);
255
+ });
256
+ },
198
257
  addAutoCompletion(ace) {
199
258
  let acData = this.acData;
200
259
  let langTools = ace.require("ace/ext/language_tools");
@@ -576,6 +576,10 @@ function initOption(opts) {
576
576
  types: ["xlsx", "csv", "html", "xml", "txt"],
577
577
  // 仅保留当前页:mode=all 依赖的 queryAll 无鉴权实现,从未可用;全量导出请走 $excelExport
578
578
  modes: ["current"],
579
+ // 与 $excelExport 一致地剔除非数据列与脱敏列,避免绕开 $excelExport 导出明文
580
+ columnFilterMethod({ column }) {
581
+ return !column.params?.exportDisabled;
582
+ },
579
583
  },
580
584
  radioConfig: {
581
585
  labelField: "id",
@@ -0,0 +1,173 @@
1
+ # 列表导出脱敏列剔除(2026-08)
2
+
3
+ > 起因:配置了「加密」(密文文本)的字段,页面显示 `138****1234`,导出 Excel 却是完整明文。
4
+ > 排查范围:`src/components/xform/**`、`src/components/excelExport/**`、`src/components/table/**`。
5
+
6
+ ## 一、问题定性
7
+
8
+ 「加密」是**纯前端展示遮罩**,不是数据加密:
9
+
10
+ - 配置入口:属性面板 `textFlag-editor.vue` → `options.userTextRuleEnabled` + `options.userTextRuleConfig[]`
11
+ - 单条规则 `{ serveName, companyCodes, roleCodes, type, encryptFormula }`,`type=1` 角色不匹配时脱敏、`type=2` 角色匹配时脱敏,`encryptFormula` 形如 `3*3`
12
+ - 改造前实现只有一处:`form-item-wrapper.vue` —— `created` → `initShowType()` 算出 `widgetTextFlag=2`,模板改渲染 `<span>{{ getShowValue() }}</span>`,`getShowValue()` → `getProcessText()` 做星号替换
13
+
14
+ **原始值始终完整存在于 `row[field]` / `fieldModel` 中**,遮罩只发生在渲染层。
15
+
16
+ ## 二、根因:导出取值绕开渲染层
17
+
18
+ 导出取值优先级(`excelExport/mixins.js:828 getExportCellValue`):
19
+
20
+ 1. `column.params.exportVal`
21
+ 2. **`column.slots.filterVal(params)`** ← 命中点
22
+ 3. 无自定义渲染 → `row[field]`(有 `formatter` 则格式化)
23
+ 4. 兜底 `getCellValue()`:挂载 vNode 渲染再爬 DOM
24
+
25
+ 而 `data-table-mixin.js:1888` 对 `input / number / text / select / checkbox / radio / status / vabsearch / date / textarea / a-link / a-text` 这 12 类组件列统一注册了 `col.slots.filterVal`,函数体直接返回 `row[field]`(select/radio/checkbox 返回 optionLabel,vabsearch 返回 label 字段,date 返回格式化值)——**整条路径不碰 `form-item-wrapper`,不读 `widgetTextFlag`**。
26
+
27
+ 这 12 类恰好是最常配脱敏的字段类型。副作用是不一致:不在这 12 类里的组件列(`slots.default = "widget"`,无 `filterVal`)走第 4 条兜底,会真实渲染 wrapper 并爬 DOM,**导出的反而是星号**。同一张表有的列明文、有的列打码,且非有意设计。
28
+
29
+ 其它同源泄漏点:
30
+
31
+ | 路径 | 改造前 |
32
+ |---|---|
33
+ | 明细导出(`exportItemColumns`,`data-table-mixin.js:2406`) | 必明文:列只有 `field/title`,行数据后端原样返回 |
34
+ | 条件导出 / 全量导出 | 明文:`loopToDo` 分页拉后端原始行 → 同一个 `getExcelData` |
35
+ | H5 列表容器 `list-h5-item.vue` | 明文:独立 `exportData`,列带 `params.widget` 但无 `filterVal` |
36
+ | vxe 工具栏自带导出(`table/index.js` `exportConfig`) | 明文:vxe 用行原始值 |
37
+ | 列筛选下拉候选值(`vxeFilter/mixin.js`) | 明文(不属导出,本轮未处理) |
38
+ | 接口响应 / JS 内存 | 明文(需后端脱敏,前端不可解) |
39
+
40
+ ## 三、方案选型
41
+
42
+ | 档位 | 做法 | 结论 |
43
+ |---|---|---|
44
+ | 1 | 前端一致性:给 `filterVal` 结果也套一层遮罩 | 未选。要同步改 4 处取值路径,且"导出打码值"对下游数据消费无意义 |
45
+ | **2** | **命中脱敏规则的列直接整列不导出** | **本次采用** |
46
+ | 3 | 后端按角色脱敏返回 / 提供脱敏导出接口 | 真正解法,本轮范围外。方案 1、2 都只是"看不见",不是"拿不到" |
47
+
48
+ 方案 2 之所以成立:**所有导出(列表 / 条件 / 选择 / 明细)都必须先过 `exportFieldDialog`**(`mixins.js:133` 是唯一入口,`:1072` 回传 columns/leafColumns),而 `params.exportDisabled` 已被三处消费:
49
+
50
+ | 位置 | 效果 |
51
+ |---|---|
52
+ | `exportFieldDialog.vue:95 filterNonDataExportColumns`(`:160` 调用) | 字段选择树里不出现该列,子列全没了则父分组也去掉 |
53
+ | `exportFieldDialog.vue:319 dialogConfirm→loopDo` | 生成表头 JSON 时跳过 |
54
+ | `excelExport/mixins.js:860 getExcelData` | 取数时过滤 `leafColumns` |
55
+
56
+ 所以只要把标记打到列上即可一次收口,无需改任何取值逻辑。
57
+
58
+ > 曾评估过变体「保留表头、值置空」:需给通用 `excelExport` 新增 `exportMasked` 语义,且整列空白易被下游误读成"该记录无值",已否决。
59
+
60
+ ## 四、改动清单
61
+
62
+ ### 1. 新增 `src/components/xform/utils/textMaskUtil.js`
63
+
64
+ 脱敏判定的**唯一实现**,渲染侧与导出侧共用(否则界面打码、导出放行必然漂移)。
65
+
66
+ | 导出 | 职责 |
67
+ |---|---|
68
+ | `resolveTextMaskFlag(widgetOptions, ctx)` | 判定是否脱敏 + 带出命中的 `encryptFormula` |
69
+ | `compareToList(list1, list2)` | 编码列表匹配,空列表表示不限 |
70
+ | `hasTextMaskRule(widgetOptions)` | 是否配了有效规则(不代表当前用户命中) |
71
+ | `buildTextMaskContext(formRef, companyCode)` | 组装 `{ bdService, companyCode, roleCodeList }` |
72
+ | `markMaskedExportColumns($grid, ctx)` | 给命中列打 `params.exportDisabled`,并同步到明细导出列 |
73
+
74
+ 判定逻辑逐行照搬原 `initShowType`,包括两个易被"优化"掉的细节:
75
+
76
+ - 规则列表**全遍历而非命中即止**,后置规则会覆盖前置结论;
77
+ - `encryptFormula` 只要 serveName/companyCodes 命中就赋值,与 masked 与否无关。
78
+
79
+ ### 2. `form-item-wrapper.vue:676 initShowType`
80
+
81
+ 保留两个早退外壳(`!!this.designer`、`isLoadEntity && !dataId`)与 `field.options.widgetTextFlag` 写回,中间判定改调 util。行为等价,净减 73 行。
82
+
83
+ `compareToList` 保留为薄封装委托 util —— 本包对外发布,不删以免破坏下游调用。
84
+
85
+ ### 3. `data-table-mixin.js`
86
+
87
+ - `:3231 markMaskedColumnsForExport()` —— 3 行委托 util
88
+ - `:3245 exportData()` 入口调用
89
+ - `:253 mounted` 建列后 `$nextTick` 再调一次(见下节决策 3)
90
+
91
+ ### 4. `list-h5-item.vue:1664 / :1671`
92
+
93
+ H5 列表容器有独立 `exportData`,同样处理。这是编码阶段才发现的第二条泄漏路径。
94
+
95
+ ### 5. `components/table/index.js:580`
96
+
97
+ `exportConfig` 增加 `columnFilterMethod`,堵 vxe 工具栏自带导出旁路。
98
+
99
+ **行为变化**:原有的操作列、图片列等 `exportDisabled` 列也会从原生导出中消失。属修正,但需业务知晓。
100
+
101
+ ### 6. `tests/unit/components/xform/textMaskUtil.spec.js`
102
+
103
+ 17 个用例,覆盖规则匹配的各分支、fail-closed、自愈还原、分组表头递归、明细列对齐。
104
+
105
+ ## 五、关键设计决策
106
+
107
+ **决策 1 —— 判定放在导出触发时,不放在建列时。**
108
+ `bdService` / `userRoleDTOs` 由异步接口填充(`form-render/indexMixin.js:458、479、498`),建列时可能尚未就绪。
109
+
110
+ **决策 2 —— 不做"上下文未就绪"的收紧判断(踩过坑,见第十节)。**
111
+ 判定结论与渲染层严格一致即可:同一个 `resolveTextMaskFlag`、同一份用户上下文,且导出必然发生在单元格渲染之后(用户已经看到明文或密文)。**不要再引入任何"上下文是否就绪"的启发式**。
112
+
113
+ **决策 3 —— `mounted` 也标记一次。**
114
+ vxe 原生导出不走 `exportData()`,若只在导出入口标记,`columnFilterMethod` 会拿到一张没标记的列表,等于没堵。故建列后先按当时的上下文标记一次;此刻结论未必准,但只会"少防护"不会误伤,之后每次 `exportData` 会按最新上下文自愈修正。
115
+
116
+ **决策 4 —— 标记幂等且可自愈,用私有键区分归属。**
117
+ `exportDisabled` 也可能是 `isNonDataExportColumn`(`data-table-mixin.js:1710`)打的。若无差别地 `delete`,自愈会误放行操作列。故引入 `MASK_MARK_KEY = "exportDisabledByTextMask"`(`textMaskUtil.js:19`):
118
+
119
+ - 列本就带 `exportDisabled` 且无私有键 → 不接管,不打私有键
120
+ - 由本函数从无到有打上 → 同时写私有键
121
+ - 撤销时只在私有键存在时才删
122
+
123
+ ## 六、覆盖范围
124
+
125
+ | 泄漏点 | 现状 |
126
+ |---|---|
127
+ | 列表导出 / 条件导出 / 选择导出 | ✅ 堵住 |
128
+ | H5 列表导出 | ✅ 堵住 |
129
+ | 明细导出 | ⚠️ 按 field 名对齐,见局限 L1 |
130
+ | vxe 工具栏原生导出 | ✅ 决策 3 + 改动 5 |
131
+ | 列筛选下拉候选值明文 | ❌ 未纳入(不属导出) |
132
+ | 接口响应 / JS 内存明文 | ❌ 需后端脱敏 |
133
+ | 打印 / PDF | 走 DOM,应是打码的,**未验证** |
134
+ | 单元格区域复制 | **未验证**,疑似明文 |
135
+
136
+ ## 七、已知局限
137
+
138
+ - **L1**:明细导出列只有 `field/title`,只能按字段名匹配主表脱敏字段集合。明细独有字段、或明细导出脚本返回的字段名与主表不一致时,仍会明文导出。
139
+ - **L2**:动态列 / 行级 schema(`fieldSchemaMap`)下同列不同行配置可能不同,判定按列模板走;模板未配、个别行配了的情况会漏。
140
+ - **L3**:表单低代码脚本里直接读 `row.xxx`、或列上配了自定义 `params.exportVal` 的,不受影响。
141
+ - **L4**:脱敏是**列级**判定;将来若出现行级脱敏,本方案不适用。
142
+ - **L5**:`exportFieldDialog.addTableExportColumns()`(`:252`)会把勾选状态持久化到 `$grid.exportColumns`。脱敏列不在树里 → 记为 `export:false`。同一用户换成不脱敏角色后,该列默认不勾选,需手动勾一次。
143
+
144
+ ## 八、验证情况
145
+
146
+ 已验证:
147
+
148
+ - `npx jest`:13 套 157 个用例全过(新增 18 个)
149
+ - ESLint:新增文件 0 error;改动文件只剩改动前就存在的既有错误(全库 132 个,`npm run lint` 本就不通过)
150
+
151
+ **未做真机联调**,以下回归项待验:
152
+
153
+ 1. 配了脱敏的字段列,四种导出(列表 / 条件 / 选择 / 明细)文件中均无该列;
154
+ 2. 两种规则方向:`type=2 + 角色匹配`、`type=1 + 角色不匹配`;
155
+ 3. 未配脱敏的列完全不受影响;
156
+ 4. **分组表头 2 层 / 3 层下剔除叶子列后表头不错位**(`loopDo` 会重算 `colSpan/rowSpan`,本轮风险最高的一处);
157
+ 5. 页面遮罩显示效果与改造前一致(验证 `initShowType` 重构无回归);
158
+ 6. vxe 工具栏导出(若宿主开启了该按钮)。
159
+
160
+ ## 九、联调发现的问题(2026-08-04,已修)
161
+
162
+ **现象**:配了规则后,部分用户看明文、部分看密文(符合预期),但**所有用户导出时该列都消失**。
163
+
164
+ **根因**:首版实现有一个 `isTextMaskContextReady(ctx)`,用 `ctx.bdService` 非空 + 角色列表非空当作"用户上下文已就绪"的判据,未就绪时 fail-closed 剔除整列。但 `bdService` 来自 `/form_develop/getBdService`(`indexMixin.js:489`),`res.objx` 在大量环境本就是空值 —— 于是 `contextReady` 恒为 false,凡配了规则的列对所有用户一律被剔除。
165
+
166
+ **修复**:删除 `isTextMaskContextReady`,`markMaskedExportColumns` 直接采用 `resolveTextMaskFlag` 的结论。
167
+
168
+ **教训**:fail-closed 本身没错,错在"就绪"判据是拿业务值反推运行状态的启发式。这里根本不需要它 —— 渲染层用同一函数、同一上下文,且导出必然晚于渲染,结论天然一致;页面显示什么,导出就该按什么处理。回归用例见 spec 中「bdService 为空时结论仍只由规则决定」。
169
+
170
+ ## 十、同步提示
171
+
172
+ - `textMaskUtil.js` 属包内新增文件,海尔分支(stdxx)整体覆盖时需一并带上,否则 `form-item-wrapper.vue` 与两个容器的 import 会解析失败;
173
+ - 改动 5 落在 `components/table/index.js`(通用表格默认配置),影响面超出 xform,同步时注意与分支上的本地修改合并。
@@ -253,6 +253,10 @@ import {
253
253
  isVabsearchMultiWidget,
254
254
  } from "../../../../../components/xform/utils/util";
255
255
  import compareView from "../../../../../components/xform/form-render/compareView.vue";
256
+ import {
257
+ compareToList,
258
+ resolveTextMaskFlag,
259
+ } from "../../../../../components/xform/utils/textMaskUtil";
256
260
 
257
261
  export default {
258
262
  name: "form-item-wrapper",
@@ -680,81 +684,22 @@ export default {
680
684
  let formConfig = fJson.formConfig;
681
685
  if (formConfig.isLoadEntity && !formRef.dataId) return;
682
686
 
683
- let bdService = formRef.bdService;
684
- let companyCode = this.$store.getters.companyCode;
685
- let loginAccount = this.$store.getters.loginAccount;
686
-
687
- let userSaleOrgDTOs = formRef.userSaleOrgDTOs;
688
- let userRoleDTOs = formRef.userRoleDTOs;
689
-
690
- let saleOrgCodeList = userSaleOrgDTOs.map((item) => item.sn);
691
- let roleCodeList = userRoleDTOs.map((item) => item.roleCode);
692
-
693
- let optionModel = this.field.options;
694
-
695
- let flag = 0; //0 原本组件,1明文文本,2密文文本
696
- /*if (optionModel.showTextEnabled) {
697
- flag = 1;
698
- } else if (optionModel.showEncryptTextEnabled) {
699
- flag = 2;
700
- } else if (optionModel.showLinkTextEnabled) {
701
- flag = 3;
702
- }*/
703
- if (optionModel.userTextRuleEnabled) {
704
- let userTextRuleConfig = optionModel.userTextRuleConfig || [];
705
- userTextRuleConfig.forEach((item) => {
706
- let type = item.type ?? null;
707
- if (type !== null && type !== "") {
708
- let companyCodeStr = item.companyCodes;
709
- let companyCodes = companyCodeStr
710
- ? companyCodeStr.split(",").filter((item) => !!item)
711
- : [];
712
- /*
713
-
714
- let loginAccountStr = item.loginAccounts;
715
- let loginAccounts = loginAccountStr ? loginAccountStr.split(",").filter(item => !!item) : [];
716
-
717
- let saleOrgCodeStr = item.saleOrgCodes;
718
- let saleOrgCodes = saleOrgCodeStr ? saleOrgCodeStr.split(",").filter(item => !!item) : [];
719
- */
720
-
721
- let roleCodeStr = item.roleCodes;
722
- let roleCodes = roleCodeStr
723
- ? roleCodeStr.split(",").filter((item) => !!item)
724
- : [];
725
-
726
- let flag1 = !item.serveName || item.serveName == bdService;
727
- let flag2 =
728
- !companyCodes.length || companyCodes.includes(companyCode);
729
- // let flag3 = !loginAccounts.length || loginAccounts.includes(loginAccount)
730
- // let flag4 = this.compareToList(saleOrgCodes, saleOrgCodeList)
731
- if (flag1 && flag2) {
732
- this.encryptFormula = item.encryptFormula;
733
- let flag5 = this.compareToList(roleCodes, roleCodeList);
734
- if (flag5) {
735
- //匹配角色
736
- if (type == 2) flag = 2; //type=2时加密
737
- } else {
738
- //角色不匹配
739
- if (type == 1) flag = 2;
740
- }
741
- }
742
- }
743
- });
744
- }
745
- this.field.options.widgetTextFlag = flag;
746
- // this.showType = flag;
687
+ let userRoleDTOs = formRef.userRoleDTOs || [];
688
+ // 判定逻辑与导出侧(数据表格按列剔除)共用 textMaskUtil,
689
+ // 否则会出现界面打码、导出放行两套结论
690
+ let { masked, encryptFormula } = resolveTextMaskFlag(this.field.options, {
691
+ bdService: formRef.bdService,
692
+ companyCode: this.$store.getters.companyCode,
693
+ roleCodeList: userRoleDTOs.map((item) => item.roleCode),
694
+ });
695
+
696
+ this.encryptFormula = encryptFormula;
697
+ //0 原本组件,1明文文本,2密文文本
698
+ this.field.options.widgetTextFlag = masked ? 2 : 0;
747
699
  },
700
+ /* 实现已移入 textMaskUtil,此处保留方法名供下游调用(同名的是导入的模块函数) */
748
701
  compareToList(list1, list2) {
749
- if (!list1.length) {
750
- return true;
751
- }
752
- if (!list2.length) {
753
- return false;
754
- }
755
- return list1.some((item) => {
756
- return list2.some((item2) => item == item2);
757
- });
702
+ return compareToList(list1, list2);
758
703
  },
759
704
  isShowWidget() {
760
705
  let widgetTextFlag = this.field.options.widgetTextFlag;
@@ -38,6 +38,7 @@ import FormItemWrapper from "./form-item-wrapper";
38
38
  import emitter from "../../../utils/emitter";
39
39
  import i18n from "../../../utils/i18n";
40
40
  import fieldMixin from "./fieldMixin";
41
+ import createCodeEditorAsyncComponent from "../../../../code-editor/async";
41
42
 
42
43
  export default {
43
44
  name: "script-input-widget",
@@ -68,6 +69,8 @@ export default {
68
69
  },
69
70
  components: {
70
71
  FormItemWrapper,
72
+ // 不依赖 src/index.js 的全局注册,避免宿主应用未注册时整块消失
73
+ CodeEditor: createCodeEditorAsyncComponent,
71
74
  },
72
75
  inject: ["refList", "globalOptionData", "globalModel"],
73
76
  data() {
@@ -41,6 +41,10 @@ import {
41
41
  normalizeDynamicColumns as normalizeDynamicColumnList,
42
42
  resolveDynamicSchemaResponse,
43
43
  } from "../../utils/dynamicSchemaUtil";
44
+ import {
45
+ buildTextMaskContext,
46
+ markMaskedExportColumns,
47
+ } from "../../utils/textMaskUtil";
44
48
  import {
45
49
  resolveTreeFields,
46
50
  resolveTreeIdentityConfig,
@@ -243,6 +247,11 @@ modules = {
243
247
  this.bootstrapDynamicColumnsBeforeInit().then(() => {
244
248
  this.initColumnWidgetConfig(() => {
245
249
  this.initTableList();
250
+ // 让不走 $excelExport 的旁路(vxe 工具栏自带导出)也受控。此刻用户上下文
251
+ // 可能尚未就绪,结论未必准,之后每次 exportData 会按最新上下文自愈修正
252
+ this.$nextTick(() => {
253
+ this.markMaskedColumnsForExport();
254
+ });
246
255
  });
247
256
  });
248
257
  },
@@ -3218,11 +3227,22 @@ modules = {
3218
3227
  let tableRef = this.widget.id;
3219
3228
  return tableRef;
3220
3229
  },
3230
+ /** 导出前按脱敏规则剔除列,实现见 textMaskUtil.markMaskedExportColumns。 */
3231
+ markMaskedColumnsForExport() {
3232
+ markMaskedExportColumns(
3233
+ this.getGridTable(),
3234
+ buildTextMaskContext(
3235
+ this.getFormRef(),
3236
+ this.$store.getters.companyCode
3237
+ )
3238
+ );
3239
+ },
3221
3240
  /**
3222
3241
  * 使用统一 Excel 导出器导出当前表格。
3223
3242
  * @param {Object} option 导出参数;prefix 为空时自动使用当前服务前缀。
3224
3243
  */
3225
3244
  exportData(option) {
3245
+ this.markMaskedColumnsForExport();
3226
3246
  let tableRef = this.getTableRef();
3227
3247
  let serviceName = this.getFormRef().reportTemplate.serviceName;
3228
3248
  option.prefix = option.prefix || "/" + serviceName;
@@ -749,6 +749,10 @@ import {
749
749
  isVabsearchMultiWidget,
750
750
  } from "../../../../components/xform/utils/util";
751
751
  import * as formatUtil from "../../../../components/xform/utils/format";
752
+ import {
753
+ buildTextMaskContext,
754
+ markMaskedExportColumns,
755
+ } from "../../../../components/xform/utils/textMaskUtil";
752
756
  import { extendDeeply } from "../../../../utils/index.js";
753
757
  import render from "../../../../views/user/bill_setting/render.vue";
754
758
 
@@ -1656,7 +1660,15 @@ export default {
1656
1660
  current: currentPage + "",
1657
1661
  });
1658
1662
  },
1663
+ /** 导出前按脱敏规则剔除列,实现见 textMaskUtil.markMaskedExportColumns。 */
1664
+ markMaskedColumnsForExport() {
1665
+ markMaskedExportColumns(
1666
+ this.getGridTable(),
1667
+ buildTextMaskContext(this.getFormRef(), this.$store.getters.companyCode)
1668
+ );
1669
+ },
1659
1670
  exportData(option) {
1671
+ this.markMaskedColumnsForExport();
1660
1672
  let tableRef = this.getTableRef();
1661
1673
  let serverName = this.getFormRef().reportTemplate.serverName;
1662
1674
  option.prefix = option.prefix || serverName;