cloud-web-corejs 1.0.283 → 1.0.285

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 (69) hide show
  1. package/package.json +1 -1
  2. package/src/components/VabUpload/mixins.js +126 -19
  3. package/src/components/VabUpload/view.vue +248 -209
  4. package/src/components/excelExport/conditionExportConfig.js +112 -0
  5. package/src/components/excelExport/exportType.js +8 -0
  6. package/src/components/excelExport/index.vue +7 -1
  7. package/src/components/excelExport/mixins.js +410 -259
  8. package/src/components/mobile/wf/addTaskUserdialog.vue +100 -80
  9. package/src/components/mobile/wf/content.vue +1705 -1556
  10. package/src/components/mobile/wf/deleteTaskUserDialog.vue +73 -54
  11. package/src/components/mobile/wf/mixins/wfModifyDialogMixin.js +8 -6
  12. package/src/components/mobile/wf/selectUserDialog.vue +118 -0
  13. package/src/components/mobile/wf/setCandidateDialog.vue +69 -48
  14. package/src/components/mobile/wf/urgingDialog.vue +75 -52
  15. package/src/components/mobile/wf/wfModifyDialog.vue +4 -0
  16. package/src/components/xform/docs/2026-08 xform H5 P0 /345/256/236/347/216/260/350/247/204/346/240/274.md" +528 -0
  17. package/src/components/xform/docs/2026-08 xform H5 P1 /345/256/236/347/216/260/350/247/204/346/240/274.md" +412 -0
  18. package/src/components/xform/docs/2026-08 xform H5 P2 /345/256/236/347/216/260/350/247/204/346/240/274.md" +287 -0
  19. package/src/components/xform/docs/2026-08 xform /347/247/273/345/212/250/347/253/257/345/270/203/345/261/200/345/257/271/351/275/220 bill_setting /346/226/271/346/241/210.md" +400 -0
  20. package/src/components/xform/form-designer/designer.js +5 -0
  21. package/src/components/xform/form-designer/form-widget/container-widget/detail-h5-widget.vue +243 -227
  22. package/src/components/xform/form-designer/form-widget/container-widget/h5-card-pane-widget.vue +228 -207
  23. package/src/components/xform/form-designer/form-widget/container-widget/h5-card-widget.vue +165 -136
  24. package/src/components/xform/form-designer/form-widget/dialog/fileReferenceDialog.vue +2 -3
  25. package/src/components/xform/form-designer/form-widget/dialog/formDialog.vue +3 -3
  26. package/src/components/xform/form-designer/form-widget/dialog/importDialog.vue +190 -185
  27. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +2641 -1833
  28. package/src/components/xform/form-designer/form-widget/dialog/searchFormDialog.vue +2 -3
  29. package/src/components/xform/form-designer/form-widget/field-widget/form-item-wrapper.vue +39 -1
  30. package/src/components/xform/form-designer/form-widget/field-widget/import-button-widget.vue +18 -5
  31. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +2 -1
  32. package/src/components/xform/form-designer/form-widget/field-widget/singleUpload-widget.vue +1 -1
  33. package/src/components/xform/form-designer/form-widget/field-widget/table-export-button-widget.vue +2 -1
  34. package/src/components/xform/form-designer/form-widget/field-widget/vabUpload-widget.vue +3 -2
  35. package/src/components/xform/form-designer/indexMixin.js +35 -1
  36. package/src/components/xform/form-designer/setting-panel/property-editor/container-list-h5/list-h5-editor.vue +19 -5
  37. package/src/components/xform/form-designer/setting-panel/property-editor/field-import-button/import-button-editor.vue +62 -1
  38. package/src/components/xform/form-designer/setting-panel/property-editor/field-import-button/import2-button-editor.vue +236 -96
  39. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/empty-number-input.vue +51 -0
  40. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/select-export-item-button-editor.vue +27 -2
  41. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/table-export-button-editor.vue +33 -3
  42. package/src/components/xform/form-designer/setting-panel/property-editor/h5Span-editor.vue +51 -0
  43. package/src/components/xform/form-designer/setting-panel/property-editor/itemStyle-editor.vue +39 -0
  44. package/src/components/xform/form-designer/setting-panel/propertyRegister.js +10 -0
  45. package/src/components/xform/form-designer/toolbar-panel/index.vue +6 -1
  46. package/src/components/xform/form-designer/toolbar-panel/indexMixin.js +13 -0
  47. package/src/components/xform/form-designer/widget-panel/indexMixin.js +22 -6
  48. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +88 -3
  49. package/src/components/xform/form-render/container-item/data-table-mixin.js +3 -1
  50. package/src/components/xform/form-render/container-item/detail-h5-item.vue +204 -34
  51. package/src/components/xform/form-render/container-item/h5-card-pane-item.vue +160 -140
  52. package/src/components/xform/form-render/container-item/list-h5-item.vue +1917 -1700
  53. package/src/components/xform/form-render/index.vue +7 -1
  54. package/src/components/xform/form-render/indexMixin.js +42 -4
  55. package/src/components/xform/lang/en-US.js +10 -0
  56. package/src/components/xform/lang/zh-CN.js +10 -0
  57. package/src/components/xform/styles/h5.scss +2305 -0
  58. package/src/directive/append-to-body/index.js +48 -0
  59. package/src/index.js +4 -0
  60. package/src/views/bd/setting/form_template/edit.vue +25 -3
  61. package/src/views/bd/setting/form_template/formDesignerDialog.vue +6 -0
  62. package/src/views/bd/setting/form_template/formTemplateType.js +43 -0
  63. package/src/views/bd/setting/form_template/list.vue +1 -0
  64. package/src/views/bd/setting/form_template/mixins/edit.js +64 -0
  65. package/src/views/bd/setting/form_template/mixins/list.js +27 -0
  66. package/src/views/user/form/vform/designer.vue +3 -1
  67. package/src/components/xform/docs/2026-09 /346/235/241/344/273/266/345/257/274/345/207/272/345/244/247/346/225/260/346/215/256/351/207/217/345/264/251/346/272/203/346/216/222/346/237/245.md" +0 -389
  68. package/src/components/xform/docs/2026-09 /347/274/226/350/276/221/350/241/250/350/241/214/345/206/205/344/277/235/345/255/230/346/240/241/351/252/214/350/214/203/345/233/264/344/270/216/345/210/227/345/277/205/345/241/253/350/247/243/346/236/220/344/277/256/345/244/215.md" +0 -233
  69. package/src/components/xform/form-render/container-item/list-h5-item2.vue +0 -1340
@@ -1,389 +0,0 @@
1
- # 条件导出大数据量浏览器崩溃 —— 排查与治理方案(2026-09)
2
-
3
- > 状态:**除 P1-1 外全部已编码 + 单测过,待联调**(P0-1 / P0-2 / P1-2 / P2-1 / P2-2 / P3 / S1 / S2 / S3 / S5 / S6,
4
- > 外加 2026-07 第三期的可中断取消与 blob 下载)。P1-1(导出前置总量确认)涉及阈值与文案的产品口径,留待决策。
5
- > 实施明细见 §八,共六轮(第六轮是代码审查查出的 4 个问题,含前面几轮自己引入的 2 个)。
6
- > 排查范围:`src/components/excelExport/**`、`src/components/xform/form-render/container-item/data-table-mixin.js`、`src/components/table/util/index.js`。
7
- > 现象:xform 列表点「列表导出 → 条件导出」,数据量大时页面无响应 / 标签页崩溃(Out of memory 或 Aw, Snap)。
8
- > 行号均为 2026-09-01 工作区快照,后续改动会漂移,以函数名为准。
9
-
10
- ---
11
-
12
- ## 一、链路全景
13
-
14
- ```
15
- xform 按钮 table-export-button-widget.toDo(null) // type = null,即 EXPORT_TYPE.CONDITION
16
- → data-table-mixin.exportData() [data-table-mixin.js:3383]
17
- → $excelExport({ targetRef, type, prefix, ... })
18
- → excelExport/mixins.exc() [mixins.js:104]
19
- · isItemExport → 校验明细导出列是否维护
20
- · requiresCheckedRows → 条件导出为 false,不校验勾选
21
- → exportFieldDialog(字段树选列)
22
- → confirmExportFieldDialog({ columns, leafColumns }) [mixins.js:1090]
23
- · isRowExport(type) === false → 走分页通道 startProcess()
24
- → startProcess() [mixins.js:174]
25
- · cloneDeep(originOption.param()) 存为 this.queryParam(**只克隆一次**)
26
- · createExcelFile() 后端建模板,拿 uuid / ippaaapp / nowDate
27
- → handleLoopToDo() [mixins.js:341]
28
- ├─ loopToDo(0, 1) 首页:searchCount=true,拿 total / pages
29
- ├─ setInterval(100ms) 调度 [mixins.js:578] 并发上限 = limitThreadNum(默认 5)
30
- │ 每次派发 loopToDo(1, num) 拉一页
31
- ├─ 每页 .then → handleData()
32
- │ ├─ getExcelData(rows) [mixins.js:861] 同步转二维数组
33
- │ └─ addExcelData() POST /excel/appendExcelData?pageNumber=n
34
- └─ excedNum >= totalPages → hadleMergeExcel() [mixins.js:255]
35
- 按 limitFileSize(默认 10 万行/文件)分卷合并 → 列表出现下载行
36
- ```
37
-
38
- 取数侧(xform):
39
-
40
- ```
41
- originOption.exportAjax(param, options) [data-table-mixin.js:2521]
42
- → proxyConfig.ajax.query({ page, param, customParam }) [data-table-mixin.js:2558]
43
- · customParam.export = true、customParam.exportParam = 导出 options
44
- · done(res) 分支:if (customParam.export) → initExportFieldSchemaData(rows)
45
- else → initValue(rows) (正常列表通道)
46
- ```
47
-
48
- **关键差异**:条件导出的行**从不进入 vxe 表格**,只是过一遍前端做取值转换。但取数回调里仍然为每一行建了一套完整的行级组件 schema —— 这是本次崩溃的核心。
49
-
50
- ---
51
-
52
- ## 二、崩溃主因
53
-
54
- ### 主因 1(决定性):每导出一行深克隆一整套行级组件 schema,且只增不减
55
-
56
- 调用链:
57
-
58
- | 位置 | 行为 |
59
- |------|------|
60
- | `data-table-mixin.js:2646` | 每页返回时 `this.initExportFieldSchemaData(rows)` |
61
- | `data-table-mixin.js:5828` `initExportFieldSchemaData` | 对**每一行**:补 `_X_ROW_KEY` → `rowIdData.push()` → `createRowFieldSchemaMap()` → 写入 `fieldSchemaMap[rowKey]` |
62
- | `data-table-mixin.js:5770` `createRowFieldSchemaMap` | 遍历**全部叶子列**,对每个 `params.widget` / `params.editWidget` 做 `cloneFieldSchema`(`deepClone` 整份 schema,含 options / rules / 脚本文本),`params.widgetList` 再 `cloneDeep` 一份 |
63
-
64
- 问题点:
65
-
66
- 1. **只增不减**。`fieldSchemaMap` 只有两条释放路径:删除行时 `delete this.fieldSchemaMap[row._X_ROW_KEY]`(`:3411`),以及正常查询走 `initFieldSchemaData` 整体重建赋值(`:5724`)。条件导出期间两者都不会发生,因此整个导出过程内存单调上涨,直到导出结束后用户再查询一次才释放。
67
- 2. **克隆的是全部叶子列,与用户在字段树里勾了几列无关**。少勾字段并不能减轻内存。
68
- 3. **量级**:单份 widget schema 通常 1~4 KB JSON,堆内存约为其 3~5 倍。10 个组件展示列 → 每行几十 KB。1 万行即 GB 级,10 万行必崩。
69
- 4. **附带开销**:`createRowFieldSchemaMap` 每行都调一次 `getTableFullColumns()` → vxe `getTableColumn()` 内部对 `collectColumn/fullColumn` 做 `slice(0)` 拷贝,等于每行重建一遍列数组;`rowIdData.push()` 是响应式数组,每行触发一次 dep.notify。
70
-
71
- **这解释了"为什么小数据量正常、大数据量必崩",也解释了"为什么有的表单崩有的不崩"**:纯展示列(`formatS` 类型、无 widget)克隆结果是空对象,代价接近 0;只有**配了组件展示列**(select / status / a-link / vabsearch 等)的列表才会踩雷,且列越多越快崩。
72
-
73
- ### 主因 2:逐单元格 `new Vue` + `$mount` 取值,并顺带执行用户表单脚本
74
-
75
- `getExcelData` → `getExportCellValue`(`mixins.js:838`)已有快路径:`exportVal` → `slots.filterVal` → 无渲染函数则 `formatter`/原始字段。但以下三类列会落到兜底渲染:
76
-
77
- - `column.renderCell`
78
- - 函数型 `slots.default`(`formatS === "render"` 的自定义渲染列)
79
- - 字符串插槽 `slots.default = "widget" / "widgetList" / "editTreeButtonGroup"`
80
-
81
- 其中 `"widget"` 列的判定见 `data-table-mixin.js:1839`:只有 widget 类型落在 `input / number / text / select / checkbox / radio / status / vabsearch / date / textarea / a-link / a-text` 这 12 种白名单内才挂 `slots.filterVal` 走纯函数;**白名单之外的组件类型(cascader、switch、time、rate、picture-upload、file-upload、rich-editor、area-select、census 等)全部走渲染兜底**。
82
-
83
- 兜底路径 `table/util/index.js:165 renderComponentVNode`:
84
-
85
- ```
86
- new TempSoltConstructor() → ensureVNodeKey → $mount() → domRender(爬 DOM,含 getComputedStyle) → $destroy()
87
- ```
88
-
89
- 挂载的是**完整的 xform 字段组件**,其生命周期会跑:
90
-
91
- ```
92
- created: handleCreatedEnterEvent / initFieldModel / registerToRefList / initEventHandler
93
- / buildFieldRules / handleOnCreated / initOptionItemsHandle
94
- mounted: handleOnMounted
95
- ```
96
-
97
- 即**用户配置的表单脚本会按「行数 × 列数」次执行**。脚本内若有 `scriptHttp` 请求,等于按单元格数量发请求。10 万行 × 30 列 = 300 万次组件挂载 + 300 万次脚本执行,主线程直接锁死。
98
-
99
- ### 主因 3:主线程从不让步
100
-
101
- `getExcelData` 是同步双层 `forEach`,一页(默认 1000 行)一次性跑完,紧接着 `JSON.stringify` 出几十 MB 的二维数组作为请求体。并发 5 页几乎同时到达时,主线程被连续占用数秒至数十秒,进度条与「取消」按钮都不响应——用户感知就是"卡死/崩溃",即使还没真正 OOM。
102
-
103
- ---
104
-
105
- ## 三、次要问题(放大器与正确性隐患)
106
-
107
- | # | 位置 | 问题 | 后果 |
108
- |---|------|------|------|
109
- | S1 | `mixins.js:578` 调度器 | `waitReqNum` 只统计**拉数**并发,`addExcelData` 上送**完全不受控**;且浏览器 HTTP/1.1 每域名 6 连接,上送会排在拉数之后 | 上送慢于拉数时 pending POST 无限堆积,每个攥着一份几 MB 的 JSON body,反压对调度器不可见 |
110
- | S2 | `mixins.js:525` | `Object.assign(cpParam, aParam)` 每页复用**同一个** `queryParam` 对象(`startProcess` 里只 cloneDeep 了一次) | 5 个并发请求共享同一份参数对象。目前靠"interval 间隔 100ms、微任务已 flush"侥幸不出错;请求拦截器一旦变为异步(token 续期等),会出现两页带同一个 `current` → **数据重复 + 缺页,合并后静默错误**,不报错 |
111
- | S3 | `mixins.js:238` `getExportPageSize` | `pageSize` 仅按"有无图片列"从 1000 降到 150,**不随列数/字段宽度自适应** | 宽表(100+ 列)单页体积是窄表的十几倍,同样的 1000 行/页压力天差地别 |
112
- | S4 | 全链路 | **没有任何总量闸门**。`limitFileSize`(默认 10 万)只是"每个 Excel 文件放多少行"的分卷参数,不是导出上限 | 用户对着几百万行的表点条件导出,前端会老老实实开始跑 |
113
- | S5 | `mixins.js:610` `.catch` | 异常路径没有 `waitReqNum--`(只有重试成功后 `.then` 里补减一次) | 并发窗口被永久占用,导出卡住不前 |
114
- | S6 | `mixins.js:900` `startProcess2` | 勾选/当前页导出把行**一次性单请求**上送;`getCheckboxRecords(true)` 是跨页全量勾选 | 勾选很多同样会崩 / 请求体超限。**已修,见 §八第三轮** |
115
-
116
- ---
117
-
118
- ## 四、取证方法(改之前先量)
119
-
120
- 1. **确认主因 1 的量级**:Chrome DevTools → Memory → 导出开始前打一个 Heap snapshot,跑到 ~5000 行时再打一个,对比 Retained Size,看 `fieldSchemaMap` 的保留大小与增长斜率。
121
- 2. **确认主因 2 是否命中**:在 `renderComponentVNode` 入口加一个计数器(临时),或在 Performance 面板录制一段,看 `Vue.prototype._init` / `$mount` 的自身时间占比。
122
- 3. **区分两个主因谁先致命**:把待导出列表换成一张**纯展示列(无 widget)**的表做同量级导出。如果不崩 → 主因 1 为主;如果仍卡但内存平稳 → 主因 2/3 为主。
123
-
124
- ---
125
-
126
- ## 五、治理方案(按投入产出比排序)
127
-
128
- ### P0-1 · 导出通道跳过行级 schema 构建 ★最高优先 —— **已实施**
129
-
130
- **依据**:条件导出的行不进表格、不会被 `#widget` 插槽渲染,行级 schema 对导出的唯一用处是 `filterVal` 里那一处判定:
131
-
132
- ```js
133
- // data-table-mixin.js:1860 附近
134
- if (fieldWidget && rowKey && this.fieldSchemaMap?.[rowKey]?.[fieldWidget.id]) {
135
- fieldWidget = this.getRowWidget(obj);
136
- }
137
- ```
138
-
139
- 该判定本身就带"取不到就回退列模板 `column.params.widget`"的分支,即**不建行级 schema 时行为已定义**。
140
-
141
- **动作**:在导出分支(`customParam.export === true`)不再调用 `initExportFieldSchemaData`,或让该函数在导出模式下只补 `_X_ROW_KEY` 而不建 schema。
142
-
143
- **风险核实结论(已完成,结论:可以走,无需退保守方案)**:
144
-
145
- 行级 schema 存在的原意是支持"同列组件在不同行互不干扰"。核实下来,**导出行的行级 schema 是列模板的等价深克隆,仅 `id` 不同**,没有任何路径会让它按行分化:
146
-
147
- | 可能造成按行差异的机制 | 实际作用对象 | 对导出行的影响 |
148
- |---|---|---|
149
- | 远程选项 / 公共属性字典异步同步(`collectColumnWidgetConfigRequests` → `handleColumnWidgetConfig`) | **列模板** `tableColumns[].widget` 本身,完成后再整体重建各行 schema | 无。回退列模板反而拿到更新的那份 |
150
- | 行内脚本改 options(`setDisabled` / `setOptionItems` 等) | 已挂载的组件**实例**(其 `field` prop 指向行级 schema) | ⚠️ **这一行当初判错了**:导出行虽不进表格,但取值兜底会**逐单元格临时挂载**组件,`created`/`mounted` 里的用户脚本照样执行、照样写 `field.options`。所以导出行**不能直接拿列模板**,必须给克隆 —— 详见 §八第六轮 #25 |
151
- | 动态字段引擎 `dynamicFieldEngine` / `dynamicFieldMixin` | 全文检索确认**完全不触碰 `fieldSchemaMap`** | 无 |
152
- | `createRowFieldSchemaMap` 自身 | 只读列(`getTableFullColumns`),**不读任何行数据** | 产出与行内容无关,各行必然相同 |
153
-
154
- **编码时发现的、文档初稿未覆盖的坑**:光在取数回调里跳过构建不够。`#widget` 插槽的渲染兜底会走到 `getTableColumnWidget`,它在 schema 缺失时**按行懒建一整套再 `$set`**(`data-table-mixin.js:314`)—— 等于把省下的开销原样加回来,而且走 `$set` 还会被 Vue 深度观测,比原来更贵。取值侧必须同步收口,见 §八。
155
-
156
- (备选保守方案:保留构建、每页上送后清掉该页 `rowKey`,内存降到 O(并发页 × 页大小) 但 CPU 侧的 deepClone 开销仍在。核实通过后**未采用**。)
157
-
158
- ### P0-2 · 上送纳入并发闸门 —— **已实施**
159
-
160
- 把 `waitReqNum` 从"拉数在途"改成"拉数 + 上送在途"总数,`addExcelData` 的 `.then/.catch` 里再释放窗口,形成真实反压。顺带修 S5(`.catch` 漏减)。
161
-
162
- ### P1-1 · 导出前置总量确认 —— **未做(待产品定阈值与文案)**
163
-
164
- 首页拿到 `total` 后,超过阈值(建议 5 万,可由导出按钮属性覆盖)弹窗提示行数与预估耗时,让用户确认或回去收窄条件。这是唯一能挡住"几百万行"极端场景的手段。
165
-
166
- ### P1-2 · `getExcelData` 分片让步 —— **已实施**
167
-
168
- 每 200 行 `await` 一次宏任务(`setTimeout 0` / `scheduler.yield`),保证进度条刷新与「取消」按钮可用。配合 P0-2 后,单页处理时长会显著下降。
169
-
170
- ### P2-1 · 修 `cpParam` 复用(S2)—— **已实施(随 P0-2 一起做掉)**
171
-
172
- 每页构造独立参数对象(`{ ...this.queryParam }` 浅拷 + 独立 conditions),消除并发共享。属正确性修复,与性能无关但顺手。
173
-
174
- ### P2-2 · `pageSize` 随列数自适应(S3)—— **已实施**
175
-
176
- 按 `leafColumns.length` 分档下调(如 >50 列降到 500,>100 列降到 200),控制单页请求体与单次同步转换时长。
177
-
178
- ### P3 · 扩大 `filterVal` 白名单 —— **已实施(核实后收敛为 script-input 一个类型)**
179
-
180
- 初稿以为"cascader / switch / time / rate / picture-upload …"这些类型大量落在兜底路径上。核实设计器实际能配出来的列类型(`table-column-dialog.vue` 的 formatS 下拉 → `columnFormatMap`)后,结论要收窄:
181
-
182
- | formatS 选项 | widget 类型 | 导出走向 |
183
- |---|---|---|
184
- | editInput / editNumber / editDate / textarea / editSelect / checkbox / radio / editSearch / editStatus / text / aText / aLink | input / number / date / textarea / select / checkbox / radio / vabsearch / status / text / a-text / a-link | **已在白名单**,纯函数 |
185
- | dropdown / editDelete / editButton / button(及各类树操作) | button / dropdown / a-link | 被 `isNonDataExportColumn` 整列剔除,根本不进导出 |
186
- | editAttachment | baseAttachment | 走 `exportType: Image/Image2` 的独立取值通道,不该塞进 filterVal |
187
- | widgetRender | 组合多个子组件 | **无法白名单化**,本质是多组件拼装 |
188
- | render | 用户自定义 vNode | **无法白名单化** |
189
- | **editScriptInput** | **script-input** | **唯一漏网的数据列类型 → 本次补上** |
190
-
191
- `script-input` 值得单独补,因为它不只是性能问题:它内部是**异步组件** `code-editor`,而 `renderComponentVNode` 是同步挂载后立刻爬 DOM —— 异步组件那一刻还没解析完,**这类列此前导出为空**。补进白名单后既走纯函数(省掉逐单元格挂一个代码编辑器,是所有类型里最贵的一种),又顺带修好了空值。
192
-
193
- 结论:**P3 到此收口**。剩余的兜底渲染原以为是 render / widgetRender / 附件列三类,但**附件列那条是错的** —— 见 §八第五轮,它的两笔挂载都可以摘掉。真正无法用白名单消除的只有 render 与 widgetRender 两类:它们是用户自己写的渲染逻辑,前端无从预知语义。
194
-
195
- 把 `data-table-mixin.js:1839` 的 12 种类型白名单按实际使用情况补齐(至少覆盖 cascader / switch / time),减少走 vNode 挂载兜底的列。这是治标,但对特定表单收益立竿见影。
196
-
197
- ---
198
-
199
- ## 六、回归清单(实施后)
200
-
201
- - 条件导出:小数据量(<100 行)导出内容与改动前**逐字段比对一致**(重点:状态列、下拉列、超链接按钮列、日期列、动态字段列);
202
- - 条件导出:1 万 / 10 万行量级,观察内存曲线是否平稳、进度条是否持续刷新、「取消」是否可点;
203
- - 分卷合并:跨 10 万行的多文件场景,文件数与行数正确、无缺页;
204
- - 明细条件导出(`exportItemQuery`)与明细选择导出(`exportItem`)走同一分页通道,需同样验证;
205
- - 选择导出 / 当前页导出(`startProcess2` 单包通道)行为不变;
206
- - 带图片列导出(`pageSize` 降到 150 的分支);
207
- - 导出中途断网 → 弹错并置红进度条;
208
- - 两个页签同时导出互不干扰(2026-07 E1 已修,回归防倒退);
209
- - 导出结束后回到列表做一次正常查询与行编辑,确认 `fieldSchemaMap` / `rowIdData` 状态正确(**若采用 P0-1 的保守方案,这一条是重点**)。
210
-
211
- ---
212
-
213
- ## 七、与既有文档的关系
214
-
215
- - `src/components/xform/docs/2026-07 table与excelExport修复及导出优化.md`:第一期稳定性、第二期性能(取值快路径、vNode 实例复用)已实施;其"第三期(分片上送、可中断取消、blob 下载)未实施"的备忘与本文 P0-2 / P1-2 / S6 重叠,可一并推进。
216
- - `docs/列表导出明细条件导出与自定义字段方案.md`:明细条件导出(`exportItemQuery`)的通道定义,本文的所有分页侧结论同样适用于它。
217
-
218
- ## 八、实施记录(2026-09-01,待联调)
219
-
220
- 验证:`npx eslint` 三个改动文件零新增错误(存量 `operator-linebreak` 欠账不变);`npx jest` 48 套 703 用例全绿,其中新增 `tests/unit/components/xform/exportRowSchema.spec.js` 10 条。
221
-
222
- ### 改动清单
223
-
224
- | # | 文件 | 改动 |
225
- |---|------|------|
226
- | 1 | `data-table-mixin.js` 新增 `initExportRows` | 导出取数只补 `_X_ROW_KEY` + 打 `_X_EXPORT_ROW` 标记,**不建行级 schema、不 push `rowIdData`** |
227
- | 2 | `data-table-mixin.js` proxyConfig.ajax.query 的 `done` | `customParam.export` 分支由 `initExportFieldSchemaData(rows)` 改调 `initExportRows(rows)`。**树表懒加载那处(`initExportFieldSchemaData` 的另一个调用点)保持不动**——那批行是要进表格的真实行 |
228
- | 3 | `data-table-mixin.js` `getTableColumnWidget` | 导出行直接返回列模板并短路,避免懒建 + `$set`(见 P0-1 的坑) |
229
- | 4 | `data-table-mixin.js` `getRowWidget` | 导出行回退列模板 / 传入的 widgetList 模板。不加这条会在 `fieldSchemaMap[undefined]` 上取值直接崩 |
230
- | 5 | `data-table-item.vue` `#widgetList` 的 `v-if` | 原条件依赖 `fieldSchemaMap[rowKey]` 存在,跳过构建后 widgetList 列会**整列导出为空**;改为行主键存在且(有行级 schema **或** 是导出行) |
231
- | 6 | `excelExport/mixins.js` `handleData` | 新增 `releaseWindow()`,并发窗口改为在**上送 `addExcelData` 落地后**释放(`.then` / `.catch` 各一次,`released` 幂等保护);`handleData` 增加 `flag` 形参,首页 `flag=0` 不占窗口故不释放 |
232
- | 7 | `excelExport/mixins.js` `loopToDo` 拉取 `.then` | 删掉原先"拉取完成即 `waitReqNum--`"的分支(S5 一并消失:窗口在整页闭环后才释放,重试期间窗口正确保持占用) |
233
- | 8 | `excelExport/mixins.js` `loopToDo` | `let params = that.queryParam` → 每页 `cloneDeep`,消除并发页共享同一参数对象(S2) |
234
-
235
- ### 单测覆盖(`exportRowSchema.spec.js`)
236
-
237
- - `initExportRows`:打标记 / 不建 schema / 不堆 `rowIdData` / 已有行主键不重生成 / 空入参不炸
238
- - `initExportFieldSchemaData`:**仍按行建 schema**(锁住树表懒加载行为不被误改)
239
- - `getTableColumnWidget`:导出行返模板且不 `$set`;**普通行仍懒建**(回归保护);缺行主键/缺列组件返 null
240
- - `getRowWidget`:导出行回退列模板、widgetList 回退传入模板;普通行仍取行级 schema
241
-
242
- ### 第二轮改动清单(P1-2 / P2-2 / P3)
243
-
244
- | # | 文件 | 改动 |
245
- |---|------|------|
246
- | 9 | `excelExport/mixins.js` `getExcelData` | 改 `async`;外层 `rows.forEach` 换成 for-of,每 `EXPORT_YIELD_ROWS`(200) 行 `await yieldToBrowser()` 让出一个**宏任务**(微任务不会让浏览器插渲染帧,进度条照样不动)。让出后检查 `exportError / _isDestroyed`,已中止就返回 `null` 不再算完这一页 |
247
- | 10 | `excelExport/mixins.js` `addExcelData` | 改 `async` 并 `await getExcelData`;拿到 `null` 直接返回 `{type:"abort"}` **不发上送请求**(半页数据写进去会污染文件)。两个调用方原本就按 `resultMsg2.type === "success"` 分支,中止走各自的 `abortExport()`(无 msg,不重复弹窗) |
248
- | 11 | `excelExport/mixins.js` `getExportPageSize` | 增加按列数降档:>100 列 → 200,>50 列 → 500;与图片列的 150 档、调用方配置取 `Math.min`,谁更紧用谁 |
249
- | 12 | `data-table-mixin.js` filterVal 白名单 | 补 `script-input`(两处:`types` 数组 + 直返 value 的分支)。理由见 §五 P3 |
250
-
251
- ### 第二轮单测(`tests/unit/components/excelExport/exportPaging.spec.js`,13 条)
252
-
253
- - `getExcelData`:二维数组取值与列序、剔除 `exportDisabled` 列、不足一档全同步跑完、超过一档仍算完全部行、让出期间 `exportError` / `_isDestroyed` 各返回 `null`
254
- - `addExcelData`:取值返回 null 时短路且 `$http` 未被调用;正常路径 URL 带对 `pageNumber` / `uuid`、body 是转换结果
255
- - `getExportPageSize`:窄表 1000 / >50 列 500 / >100 列 200 / 调用方配置更小时取更小 / 图片列 150 档更紧时取 150
256
-
257
- ### 第三轮改动(S6 勾选 / 当前页导出)
258
-
259
- `startProcess2` 是与条件导出并列的另一条通道(不走进度弹框,直接下载)。此前把全部行塞进**一个** `addExcelData` 请求,而 `getCheckboxRecords(true)` 是跨页全量勾选,选得多时请求体轻易超限被网关拒,整批取值转换与序列化也一次性压在主线程上。
260
-
261
- | # | 位置 | 改动 |
262
- |---|------|------|
263
- | 13 | `mixins.js` `startProcess2` | 按 `getExportPageSize()` 切片,**串行** append 页 1..N,最后 `mergeExcel(1, N)` 再下载。刻意不做并发——避免把条件导出那边"上送无闸门"的老问题复制过来 |
264
- | 14 | 同上 | 补全错误处理:建文件 / 任一页上送 / 合并失败都 `abortExport`(此前**全链路无 catch、失败完全静默**)。任一页失败即停,不发合并——缺页合并会静默丢行。取值阶段返回的 `abort` 静默停住,不重复弹窗 |
265
-
266
- **行为兼容**:勾选量不超过一页(默认 1000 行,绝大多数场景)时请求序列与旧实现**完全一致**——一次 append + `merge(1,1)` + 下载。多页时才切片。
267
-
268
- **保持不变的**:仍然只产出**一个文件**,不像条件导出那样按 `limitFileSize` 分卷。勾选量真的超过 10 万行时会得到一个超大文件——但这与旧实现一致(旧实现在那之前就已经因请求体超限而失败了),不在本次范围内。
269
-
270
- **未做**:`loading` 仍是每页请求各开各的遮罩,多页时会有短暂闪烁。要消除得自己托管一个 `$baseLoading`,而 element 的 loading 与 request.js 的遮罩管理有历史耦合,风险大于收益,未动。
271
-
272
- ### 第四轮改动(2026-07 第三期:可中断取消 + blob 下载)
273
-
274
- #### 取消语义收口 —— 已实施
275
-
276
- 此前「取消」只做了两件事:`clearExportTimer()` + 销毁组件。**已经发出去的那几页请求照样会回调下来**,继续取值、继续上送、继续合并,最后甚至弹出一个半截文件的下载框;组件销毁后 `$refs["exportTable"].insertAt` 还会直接抛错。
277
-
278
- | # | 位置 | 改动 |
279
- |---|------|------|
280
- | 15 | `mixins.js` data + `isExportStopped()` | 新增 `aborted` 标记与统一判据 `isExportStopped() = aborted \|\| exportError \|\| _isDestroyed`。`dialogClose2` 与 `beforeDestroy` 置 `aborted`(**最小化不算取消**,`isMinimize` 分支照旧提前返回) |
281
- | 16 | 全部异步续点 | 7 处入口加判据:`handleData`、`loopToDo`(含 `.catch` 递归重试)、`addExcelData`、`getExcelData`(让出点)、`hadleMergeExcel` 及其 `mergeExcel().then`、`startProcess`/`startProcess2` 的 `createExcelFile().then`、`startProcess2` 的 `mergeExcel().then`(**取消后不自动弹下载**) |
282
-
283
- 注意 `exportAll` **没有**加判据:它同时是进度弹框里那个手动下载按钮的处理函数,卡住它会连带堵掉"失败后手动下载已生成分卷"的正常用法。
284
-
285
- #### blob 下载 —— 已实施,**但需联调确认后端**
286
-
287
- `/excel/download` 此前是**动态建 form、POST、`target=_blank`**,并把 `access_token` 作为表单字段随请求体提交。改为 `$http({responseType:"blob", resultType:"other"})`:token 由 `request.js` 的请求拦截器注入 `X-Token` / `Authorization` 头,不必再明文带 token;失败时也能拿到响应体做判断(表单 POST 打开新窗口后失败,用户只会看到空白页或一坨错误 JSON)。
288
-
289
- 文件名从 `Content-Disposition` 解析(`filename*=UTF-8''` 优先,其次 `filename="..."`,都没有则用导出标题 + `.xlsx`),并在点击后 `revokeObjectURL` 释放。
290
-
291
- **风险与兜底**:后端是否放开了该接口的头鉴权,前端无法验证。因此保留原表单 POST 作为**自动回退** —— 响应不是文件流(**白名单判定**,见第七轮 #31)或请求直接失败时,`console.warn` 后走老路径。**下载不会因这次改造而失效**。同体系的 `/export_ins/download` 已经是 blob + 头鉴权的用法(见 `components/baseInputExport/mixins.js`),大概率是通的,但联调时请确认走的是哪条路径(看控制台有没有 `[excelExport] blob 下载不可用,回退表单 POST`)。
292
-
293
- #### 中止在途请求 —— 已实施(含 `request.js` 一行改动,已获批准)
294
-
295
- | # | 位置 | 改动 |
296
- |---|------|------|
297
- | 17 | `utils/request.js` 错误拦截器 | **最前面**加 `if (axios.isCancel(error)) return Promise.reject(error);`。理由见下方"为什么曾经卡住"。这是本轮唯一一处 `request.js` 改动,**已单独获批** |
298
- | 18 | `mixins.js` | 新增 `cancelSource` + `getCancelToken()` / `cancelPendingRequests()`。一次导出 = 一个组件实例(`index.js` 的 `initInstance` 会先销毁旧实例),故一个 source 覆盖本次导出全部请求,不会跨导出串味 |
299
- | 19 | `mixins.js` 四个请求点 | `createExcelFile` / `addExcelData` / `mergeExcel` / 分页取数的 `$commonHttp` 全部挂 `cancelToken`。blob 下载**不挂**,理由见第七轮 #30 |
300
- | 20 | `mixins.js` + `data-table-mixin.js` | 取数走 `originOption.exportAjax` 时请求由 xform 侧构造,前端拿不到那个 config——令牌顺着导出参数下发(`options.cancelToken`),`exportAjax` 塞进 `customParam.config`,最终由 `formHttp` 的 `...opts` 透传给 axios |
301
- | 21 | `mixins.js` 取消触发点 | `dialogClose2`(用户取消)、`beforeDestroy`、`abortExport` 三处调 `cancelPendingRequests()`。**`abortExport` 必须先置 `exportError` 再取消**,否则其余在途请求 reject 时它们的 `.catch` 会重复弹提示 |
302
- | 22 | `mixins.js` 六个 `.catch` | 统一先问 `isExportStopped()` 再决定是否重试/提示。`exportAll` 的 catch 额外判 `axios.isCancel`——取消导致的失败**不能**回退表单 POST,否则取消之后又弹出一次下载 |
303
-
304
- **为什么曾经卡住**(留给后来人):项目用 axios **0.18.1**,只有 `CancelToken`(`AbortController`/`signal` 要 0.22+);取消时 reject 的 `Cancel` 对象**不带 `.config`**,`request.js` 错误拦截器 `let config = error.config` 拿到 undefined 后走 `else` 分支**无条件**弹 `ErroreBox`,而它是**弹窗不是 toast**——取消 5 个在途请求就连弹 5 个错误框。`errorMsg: false` 这条逃生口在 `if (config)` 里面,Cancel 走不到。所以只能在拦截器最前面拦。
305
-
306
- **已知残留**:xform 取数通道被取消时,`formHttp` 内部那个 `new Promise` 只在 success/fail/error 回调里 settle,而 Cancel 短路后 `config.error` 不会被调用,因此该 Promise **永远 pending**。请求本身已在网络层中止(目的达到),下游全被 `isExportStopped()` 拦住,组件随即销毁,影响仅是最多 `limitThreadNum` 个挂起的 Promise 闭包。要彻底干净得改 `formHttp`,不在本轮范围。
307
-
308
- #### 第四轮单测(`exportPaging.spec.js` 增至 39 条)
309
-
310
- `isExportStopped` 三来源判据;`addExcelData` 已取消时连取值都不做;`startProcess2` 在"建文件返回后被取消"与"合并返回时被取消"两个时点各自停住不下载;令牌一次导出只建一次 / 取消后清空可重建 / 空 source 取消不炸 / `abortExport` 的置位与取消**顺序**;`resolveDownloadFileName` 四种响应头形态;`exportAll` 走 blob、URL 不含 `access_token`、JSON 错误体回退、请求失败回退、**取消不回退**(第七轮 #30 后又加了「下载不共用导出令牌」与「下载在途时关弹框仍下得下来」)。
311
-
312
- ### 第五轮改动(附件列的两笔无谓挂载)
313
-
314
- 起因是复盘时被问住:"附件图片为什么需要逐单元格 `new Vue` + `$mount`?" —— 核下来**不需要**,第四轮把它归进"无法消除"是错的。附件列有两条各自独立的浪费:
315
-
316
- | # | 位置 | 改动 |
317
- |---|------|------|
318
- | 23 | `mixins.js` `getExcelData` 的 Image 分支 | 原先**无条件**先 `getExportCellValue(params)` 再看 `row[column.field]`。而附件的正常形态是**数组**,那条分支用 `row[column.field]` 自己拼 `[EXPIMG]`,把取值结果**整个覆盖**;空值分支同样直接给 `null`。三条路径里两条完全丢弃。改为**延迟求值**,只在单值(字段里直接存 URL)分支才调。**零行为变化** |
319
- | 24 | `dynamicSchemaUtil.js` `isBlankExportColumn` + `data-table-mixin.js` `params.exportBlank` + `mixins.js` `getExcelData` | **没配「导出类型」的附件列**导出时**列保留、单元格统一留空**,且不触发取值。此前它落到最后的 `else` → 挂载附件组件爬 DOM 拿文件名文本 |
320
-
321
- **#24 的口径(需求方 2026-09-01 定)**:
322
-
323
- - **列保留、格留空**,不是整列剔除 —— 与 `isNonDataExportColumn`(整列剔除,字段树里都看不到)是两个不同粒度,注释里写清楚了别混
324
- - **配了导出类型(Image/Image2)的附件列不在此列**,即使本次导出没开 `showImageAtTable`、图片最终不写进文件,也**保持原有行为不变**
325
- - 列上显式配了 `exportVal` 时以它为准,判定放在 `exportVal` 之后
326
-
327
- **收益**:附件列是导出的重灾区 —— 有图片列时 `pageSize` 会被压到 150,等于同样行数要多发 6 倍请求。这两处摘掉后,附件列在绝大多数路径上不再挂载组件。
328
-
329
- 单测 `tests/unit/components/xform/blankExportColumn.spec.js`(6 条):判定的三类边界;留空且**不触发取值**(用 spy 调用次数与 column.field 断言);列仍在、长度不变;`exportVal` 优先。
330
-
331
- ### 第六轮:代码审查发现的 4 个问题(均已修)
332
-
333
- 前五轮跑完后做了一次针对性代码审查,查出 4 处,其中前两处是前面几轮**自己引入的**。
334
-
335
- | # | 位置 | 问题与修法 |
336
- |---|------|------|
337
- | 25 | `data-table-mixin.js` `getTableColumnWidget` / `getRowWidget` | **P0-1 的直接副作用**:短路后导出行拿到的是**共享的列模板对象**。而仍会逐单元格挂载的那几类列(`widgetRender` 自定义组件列、Image2 但本次没开 `showImageAtTable` 的附件列)挂载时会跑用户脚本,`setDisabled` / `setHidden` / `setOptionItems` 是**直接写 `field.options`** 的(`fieldMixin.js:1723` 等),于是脚本把状态写进了列模板;而 `createRowFieldSchemaMap` 正是从该模板克隆各行 schema,污染会一直留到重新加载表单配置。改动前导出行有自己的克隆,写入随克隆丢弃。<br>**修法**:新增 `getExportRowWidget(row, widget)` —— 按需惰性克隆,缓存以**不可枚举**属性挂在行对象上随行丢弃。必须**记忆化**:`#widget` 插槽一个单元格里会多次取值(`v-if` / `:field` / `:key` / 字段名),每次返回新对象会让这几处互相对不上。代价只落在真正会挂载的少数列上 |
338
- | 26 | `mixins.js` `exportAll` 的 `.catch` | 回退表单 POST 被 `isExportStopped()` 拦住,而它含 `exportError`;`exportAll` 同时是进度弹框里的**手动下载按钮**。场景:分卷合并到第 3 个文件失败 → `exportError=true`,前两个已合并的文件仍在下载列表里 → 用户点下载 → blob 失败 → 直接 return 不回退 → **点了毫无反应**,而回退兜底恰恰是为这种情况准备的。<br>**修法**:守卫收窄为只挡 `axios.isCancel(e)`;同时给该请求补上 `cancelToken`(此前没挂,那行 `isCancel` 是死分支,文档也据此误称下载可取消) |
339
- | 27 | `utils/request.js` 请求拦截器 | 第四轮加的 `isCancel` 短路在 `removeModel(config)` **之前**返回,被取消的请求不会清理请求拦截器创建的 `ajaxOverlay`(`:77`)与 `loadingInstance`(`:89`)—— 任何调用方在 `modal` 默认开启或 `isLoading:true` 时取消请求,页面就永久留一个关不掉的转圈遮罩。`Cancel` 对象不带 `config`,在错误拦截器那侧补不了。<br>**修法**:在请求拦截器里挂到令牌自己的 `promise` 上(`config.cancelToken.promise.then(() => removeModel(config))`),取消时精确清理**本次请求**创建的那份。**这是 `request.js` 的第二处改动**,属于修复第四轮引入的缺陷 |
340
- | 28 | `mixins.js` `getExcelData` 的 `exportBlank` 判定 | `exportBlank` 是建列时按 `t.exportType` 算死的,而 `params.exportType` 之后还会被 `tableColumnConfig` 脚本覆盖;只认 `exportBlank` 会把运行期才拿到导出类型的图片列**错误地导成空且无任何提示**。<br>**修法**:条件收紧为 `exportBlank && !exportType` |
341
-
342
- 单测同步:`exportRowSchema.spec.js` 增至 14 条(三条原本断言"返回列模板"的改为断言克隆,另加记忆化、跨行独立、改克隆不污染模板、缓存不可枚举);`exportPaging.spec.js` 加"导出失败后手动下载仍回退"与"下载请求带取消令牌";`blankExportColumn.spec.js` 加"运行期才拿到 exportType 的列不被留空吃掉"。
343
-
344
- ### 第七轮:第二次代码审查(3 处,均已修)
345
-
346
- 第六轮之后又过了一遍导出全链路(含 PDF 整页导出),查出 3 处,前两处仍是前几轮自己引入的。
347
-
348
- | # | 位置 | 问题与修法 |
349
- |---|------|------|
350
- | 29 | `utils/pdfUtil.js` `pickScale` | `MAX_CANVAS_SIDE` / `MAX_CANVAS_AREA` 刚算出回退倍率,末行 `Math.max(scale, 1)` 又把它抬回 1,**这段保护整个作废**:内容高度超过 16384 CSS px 的长页面(整页导出的常态)算出来的上限本就 <1,抬回 1 之后 canvas 照样越界、`toDataURL` 返回空白图 —— 正是这段代码想拦的那个失败。<br>**修法**:下限改成 `MIN_SCALE = 0.2`(再往下字糊到认不出,不如让它明确失败)。html2canvas 支持 `scale < 1`。<br>顺带记一笔:`MAX_CANVAS_AREA` 那条**恒不生效** —— 两边都 ≤ 16384 时面积最大恰为 2^28,面积上限永远 ≥ 单边上限。留着无害,别误以为它在起作用 |
351
- | 30 | `mixins.js` `exportAll` 的 `cancelToken` | 第六轮 #26 给下载补的令牌**是本次导出那一个**,于是下载被绑进了导出的生命周期。场景:分卷文件较大 → 用户点「下载」→ 一时没反应 → 点「取消」或「确定」关掉进度弹框(两个按钮都走 `dialogClose2` → `cancelPendingRequests`)→ 请求被掐断 → `.catch` 里 `isCancel` 命中、**刻意不回退表单 POST** → **文件静默地下不下来,且没有任何提示**。旧的表单 POST 脱离组件生命周期,本就不受关弹框影响,这是改造引入的倒退。<br>**修法**:下载**不挂**任何取消令牌 —— 它不是导出流水线的一环,而是用户单独发起的动作。`isCancel` 守卫防御性保留(组件销毁等外部因素仍可能取消)。这样 #26 那条「补上 cancelToken」被推翻,但它指出的 `isExportStopped()` 拦回退问题仍按 #26 修着 |
352
- | 31 | `mixins.js` `exportAll` 的响应体判据 | 判「不是文件流」用的是黑名单(`type` 含 `application/json`)。网关拦截、登录态失效这类失败常见的是 **200 + `text/html`**,黑名单放它过去,一张 HTML 错误页会被原样存成打不开的 `.xlsx` —— 用户拿到的是损坏文件而不是回退后的正常下载。<br>**修法**:改白名单 `isDownloadableBlob()`,只认 `application/vnd.openxmlformats` / `application/vnd.ms-excel` / `application/octet-stream` / `application/zip` 前缀;后端不给 `Content-Type` 时 `blob.type` 为空串,按二进制放行(旧表单 POST 本来也下得动,不能因改造反而下不动) |
353
-
354
- | 32 | `data-table-mixin.js` `done()` 里 `resolve(res)` 之后那整段成功副作用 | 导出取数复用的是普通查询那条 `proxyConfig.ajax.query`,此前**只在「建不建行级 schema」一处分了叉**(P0-1),成功副作用照跑。它们对导出零意义 —— 数据在 `resolve(res)` 时已交给 excelExport,回调还排在 `$nextTick` + `setTimeout(0)` 之后,改什么都影响不到导出内容 —— 但跑了有害:<br>① `formScriptCallback`(用户配的「数据加载完成」脚本)按导出页数被连着触发 N 次,`rows` 是屏幕上看不见的数据;脚本里发的请求还会跟导出取数抢连接(每域名 6 条);<br>② `tableOption.callback` 是 `formModel[fieldKeyName] = rows`,而 `getValue()` 正是读它 —— **一次条件导出之后 `getValue()` 返回的是导出最后一页,不是当前页**,页面上看不出任何异常,直到下次查询才被冲掉;<br>③ 树表分支对没进表格的导出行 `setTreeExpand`、去 `fullAllDataRowMap` 里查它们,中途还反复开关 `$grid.treeConfig.lazy`,并发几页时互相打架。<br>**修法**:`if (res.type === "success")` 收窄为 `shouldEmitQueryLoaded(res, customParam)`(`success && !customParam?.export`),三个回调抽成 `emitTableDataLoaded(rows, tableOption)` 便于单测。`customParam.export` 由 `exportAjax` 统一置位,覆盖条件导出与明细的选择/条件导出三条会发请求的通道;勾选导出/当前页导出不走这里。<br>**既有问题**,非本轮改出来的 |
355
- | 33 | `mixins.js` `lockMenu` / `unlockMenu` | 「导出中不让关页签」是**真锁**(`meta.affix` + `user_affix` → TagsView 关闭图标置灰、`closeSelectedTag` 直接 return,且 `delOthersViews` / `delAllViews` 都保留 affix,「关闭其他」「全部关闭」同样删不掉),而上锁与解锁**都按「调用当时的 `$route.path`」现查页签**。解锁发生在组件销毁,那时的当前路由未必还是上锁时那一个:<br>**A 页开始条件导出 → 切到 B 页(弹框可最小化,切走完全合法)→ 在 B 页再点一次导出** → `initInstance` 先销毁旧实例 → 旧实例 `unlockMenu` 查到的是 B(B 还没锁,什么也没做)→ **A 页签永久锁死**,只能刷新页面(`visitedViews` 无持久化)。<br>另一条:导出中途路由跳走且宿主视图被销毁 —— excelExport 是独立 root 实例,不随宿主销毁,`beforeDestroy` 压根不触发,锁不解;此时弹框 DOM 已被移走,用户也点不到取消。<br>**修法**:`lockMenu` 把实际锁上的那个页签记进 `lockedView`,`unlockMenu` 直接解它、不再二次查找。本就是固定页签(`affix`)的仍不接管,解锁时也不动它。第二条路径转为「下一次导出时被顶掉并正确解锁」,不再是死锁。<br>**既有问题**,非本轮改出来的 |
356
- 单测同步:`exportPaging.spec.js` 把「下载请求带取消令牌」改为「下载不共用导出的取消令牌」,另加「下载在途时关掉进度弹框,文件照样下得下来」「200 返回 HTML 错误页也要回退」「没给 Content-Type 时按二进制放行」;`pdfUtil.spec.js` 新增「截图倍率」4 条(普通尺寸不回退、超单边上限必须降到 1 以下、调用方指定倍率同样受约束、低于下限停在下限)。新增 `exportQuerySideEffects.spec.js`(7 条):闸门的三类判据、`emitTableDataLoaded` 三个回调都派发、以及组合场景「导出三页不触发脚本也不覆盖表单模型 / 普通查询照旧回写」。新增 `exportMenuLock.spec.js`(7 条):就地取消能解锁、切页签后解的仍是原页签、B 页再导出时 A 的锁跟着解、固定页签不接管也不误解、重复解锁幂等、页签已被删掉不炸。
357
-
358
- ### 仍未做
359
-
360
- - **P1-1 导出前置总量确认** —— 需要定阈值(初稿建议 5 万)与文案,且要决定放在导出按钮属性还是全局 settings,属产品口径,未擅自定。这是唯一能挡住"几百万行"极端场景的手段,建议尽快定。
361
- - **导出完成通知** —— 本轮明确不做。
362
-
363
- - **服务端临时文件清理** —— **后端没有可调的接口**。全仓检索无 `/excel/clear`、`/excel/cancel` 之类端点,2026-07 文档那句"后端如有 `/excel/clear` 类接口可接入 beforeDestroy"至今仍是假设。取消后服务端那份半成品文件仍会留下,需要后端先提供清理接口(建议入参与 `mergeExcel` 一致:`uuid` + `ippaaapp`),前端在 `beforeDestroy` 里接一行即可。
364
-
365
- #### 为什么"中止在途请求"卡住了
366
-
367
- 技术上完全可行 —— `request.js` 导出的就是 axios 实例本身(`export default service`),任何 axios 配置项都能透传,只要给导出的每个请求挂 `cancelToken`,取消时 `source.cancel()` 即可。
368
-
369
- 卡点在错误拦截器:
370
-
371
- 1. 项目用的是 **axios 0.18.1**,只有 `CancelToken`(`AbortController`/`signal` 要 0.22+);
372
- 2. axios 取消时 reject 的是 `Cancel` 对象,**不带 `.config`**;
373
- 3. `request.js` 的错误拦截器 `let config = error.config` 拿到 undefined,走 `else` 分支 **无条件** `ErroreBox.msg(...)`;
374
- 4. `ErroreBox.msg` 是**弹窗**不是 toast(`components/errorMsg/index.js`)。
375
-
376
- 结果:取消 5 个在途请求 = 连弹 5 个错误框。而 `errorMsg: false` 这条逃生口在 `if (config)` 分支里,Cancel 走不到。
377
-
378
- 根治只需在错误拦截器最前面加一行 `if (axios.isCancel(error)) return Promise.reject(error);` —— 但 **CLAUDE.md 明确要求"避免直接修改 `request.js`"**,故未擅自改动,留待决策。
379
-
380
- (评估过不改 request.js 的绕法:往 `service.interceptors.response.handlers` 数组 `unshift` 自己的处理器抢在内置之前、再给 Cancel 补一个 `errorMsg:false` 的假 config。能work,但那是伸手进 axios 内部结构、且对全应用生效,比在 request.js 加一行的风险更大,不推荐。)
381
-
382
- ### 联调时重点看
383
-
384
- 1. 导出内容与改动前**逐字段比对**(尤其 select / status / vabsearch 列的显示值、`widgetList` 列、render 列);
385
- 2. **脚本输入框列导出值会从空变成实际内容**——这是 P3 顺带修好的,属预期变化,别当成回归;
386
- 3. 内存曲线是否随行数走平(改动前是单调上涨);
387
- 4. 导出过程中进度条应持续刷新、「取消」可点(分片让出的直接目的);
388
- 5. 并发窗口改动后吞吐会下降(窗口现在覆盖"拉取 + 上送"整个往返),宽表还会额外降 pageSize。若实测偏慢,调导出按钮的 `limitThreadNum` / `exportPageSize`,而不是退回旧逻辑。
389
-