cloud-web-corejs 1.1.0-dev.10 → 1.1.0-dev.13

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/components/VabUpload/view.vue +25 -25
  3. package/src/components/excelExport/exportColumnMemory.js +118 -0
  4. package/src/components/excelExport/exportFieldDialog.vue +50 -38
  5. package/src/components/excelExport/index.js +30 -13
  6. package/src/components/excelExport/mixins.js +390 -64
  7. package/src/components/excelImport/mixins.js +898 -850
  8. package/src/components/mobile/wf/addTaskUserdialog.vue +100 -100
  9. package/src/components/mobile/wf/content.vue +5 -5
  10. package/src/components/mobile/wf/deleteTaskUserDialog.vue +73 -73
  11. package/src/components/mobile/wf/selectUserDialog.vue +8 -12
  12. package/src/components/mobile/wf/setCandidateDialog.vue +69 -69
  13. package/src/components/mobile/wf/urgingDialog.vue +75 -75
  14. package/src/components/table/index.js +14 -1
  15. package/src/components/table/vxeFilter/mixin.js +28 -18
  16. package/src/components/xform/docs/2026-07 table/344/270/216excelExport/344/277/256/345/244/215/345/217/212/345/257/274/345/207/272/344/274/230/345/214/226.md" +64 -57
  17. 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" +389 -0
  18. package/src/components/xform/form-designer/form-widget/container-widget/detail-h5-widget.vue +243 -243
  19. package/src/components/xform/form-designer/form-widget/container-widget/h5-card-pane-widget.vue +228 -228
  20. package/src/components/xform/form-designer/form-widget/container-widget/h5-card-widget.vue +165 -165
  21. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +61 -2
  22. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +3 -0
  23. package/src/components/xform/form-designer/form-widget/field-widget/singleUpload-widget.vue +145 -145
  24. package/src/components/xform/form-render/container-item/data-table-item.vue +350 -347
  25. package/src/components/xform/form-render/container-item/data-table-mixin.js +240 -37
  26. package/src/components/xform/form-render/container-item/detail-h5-item.vue +1 -1
  27. package/src/components/xform/form-render/container-item/list-h5-item.vue +1 -1
  28. package/src/components/xform/styles/h5.scss +483 -459
  29. package/src/components/xform/utils/dynamicSchemaUtil.js +385 -361
  30. package/src/layout/components/TagsView/index.vue +325 -323
  31. package/src/store/modules/tagsView.js +241 -217
  32. package/src/utils/importLimit.js +72 -0
  33. package/src/utils/pdfUtil.js +6 -1
  34. package/src/utils/request.js +17 -0
@@ -1,217 +1,241 @@
1
- import store from "../index.js";
2
- import _ from "lodash";
3
- import { isRepeatable, viewKey } from "@base/utils/repeatOpen";
4
-
5
- let state, mutations, actions;
6
- state = {
7
- visitedViews: [],
8
- cachedViews: [],
9
- };
10
-
11
- mutations = {
12
- ADD_VISITED_VIEW: (state, view) => {
13
- let title = view.meta.title;
14
- let add_routes = store.getters.add_routes;
15
- if (add_routes && add_routes.children && add_routes.children.length > 0) {
16
- let syncRoutes = add_routes.children[0].children;
17
- let tRoutes = syncRoutes.some((route) => {
18
- let flag = view.fullPath == route.path;
19
- if (flag) {
20
- title = route.meta.title;
21
- return flag;
22
- }
23
- });
24
- }
25
- const key = viewKey(view);
26
- if (state.visitedViews.some((v) => viewKey(v) === key)) return;
27
- // 可重复打开时,记录同一菜单的序号,展示时用于区分标签
28
- let repeatIndex = 0;
29
- if (isRepeatable(view)) {
30
- const sameCount = state.visitedViews.filter((v) => v.path === view.path);
31
- repeatIndex = sameCount.length + 1;
32
- }
33
- state.visitedViews.push(
34
- Object.assign({}, view, {
35
- title: title || "no-name",
36
- repeatIndex,
37
- })
38
- );
39
- },
40
- ADD_CACHED_VIEW: (state, view) => {
41
- // 缓存键与 keepAlive.js 的 getComponentName 保持一致(viewKey):
42
- // 可重复打开的视图按 fullPath 各自独立缓存,其余按 path。
43
- const key = viewKey(view);
44
- if (state.cachedViews.includes(key)) return;
45
- if (!view.meta.noCache) {
46
- state.cachedViews.push(key);
47
- }
48
- },
49
-
50
- DEL_VISITED_VIEW: (state, view) => {
51
- const key = viewKey(view);
52
- for (const [i, v] of state.visitedViews.entries()) {
53
- if (viewKey(v) === key) {
54
- state.visitedViews.splice(i, 1);
55
- break;
56
- }
57
- }
58
- },
59
- DEL_CACHED_VIEW: (state, view) => {
60
- // 兼容刷新场景(路径带 /redirect 前缀)与仅按 name 传入的情况,
61
- // 归一到与缓存键一致的 viewKey 再移除。
62
- let target = view;
63
- let str = "/redirect";
64
- if (view.path && view.path.startsWith(str)) {
65
- let realPath = view.path.substr(str.length);
66
- let realFullPath = (view.fullPath || view.path).substr(str.length);
67
- target = { ...view, path: realPath, fullPath: realFullPath };
68
- } else if (!view.path && view.name) {
69
- let cView = state.visitedViews.find((item) => item.name == view.name);
70
- target = cView || view;
71
- }
72
- const key = viewKey(target);
73
- if (key) {
74
- const index = state.cachedViews.indexOf(key);
75
- index > -1 && state.cachedViews.splice(index, 1);
76
- }
77
- },
78
-
79
- DEL_OTHERS_VISITED_VIEWS: (state, view) => {
80
- const key = viewKey(view);
81
- state.visitedViews = state.visitedViews.filter((v) => {
82
- return v.meta.affix || viewKey(v) === key;
83
- });
84
- },
85
- DEL_OTHERS_CACHED_VIEWS: (state, view) => {
86
- // const index = state.cachedViews.indexOf(view.name)
87
- const index = state.cachedViews.indexOf(viewKey(view));
88
- if (index > -1) {
89
- state.cachedViews = state.cachedViews.slice(index, index + 1);
90
- } else {
91
- // if index = -1, there is no cached tags
92
- state.cachedViews = [];
93
- }
94
- },
95
-
96
- DEL_ALL_VISITED_VIEWS: (state) => {
97
- // keep affix tags
98
- const affixTags = state.visitedViews.filter((tag) => tag.meta.affix);
99
- state.visitedViews = affixTags;
100
- },
101
- DEL_ALL_CACHED_VIEWS: (state) => {
102
- state.cachedViews = [];
103
- },
104
-
105
- UPDATE_VISITED_VIEW: (state, view) => {
106
- const key = viewKey(view);
107
- for (let v of state.visitedViews) {
108
- if (viewKey(v) === key) {
109
- v = Object.assign(v, view);
110
- break;
111
- }
112
- }
113
- },
114
- };
115
-
116
- actions = {
117
- addView({ dispatch }, view) {
118
- dispatch("addVisitedView", view);
119
- dispatch("addCachedView", view);
120
- },
121
- addVisitedView({ commit }, view) {
122
- commit("ADD_VISITED_VIEW", view);
123
- },
124
- addCachedView({ commit }, view) {
125
- commit("ADD_CACHED_VIEW", view);
126
- },
127
-
128
- delView({ dispatch, state }, view) {
129
- return new Promise((resolve) => {
130
- dispatch("delVisitedView", view);
131
- dispatch("delCachedView", view);
132
- resolve({
133
- visitedViews: [...state.visitedViews],
134
- cachedViews: [...state.cachedViews],
135
- });
136
- });
137
- },
138
- delVisitedView({ commit, state }, view) {
139
- return new Promise((resolve) => {
140
- commit("DEL_VISITED_VIEW", view);
141
- resolve([...state.visitedViews]);
142
- });
143
- },
144
- delCachedView({ commit, state }, view) {
145
- return new Promise((resolve) => {
146
- commit("DEL_CACHED_VIEW", view);
147
- resolve([...state.cachedViews]);
148
- });
149
- },
150
-
151
- delOthersViews({ dispatch, state }, view) {
152
- return new Promise((resolve) => {
153
- dispatch("delOthersVisitedViews", view);
154
- dispatch("delOthersCachedViews", view);
155
- resolve({
156
- visitedViews: [...state.visitedViews],
157
- cachedViews: [...state.cachedViews],
158
- });
159
- });
160
- },
161
- delOthersVisitedViews({ commit, state }, view) {
162
- return new Promise((resolve) => {
163
- commit("DEL_OTHERS_VISITED_VIEWS", view);
164
- resolve([...state.visitedViews]);
165
- });
166
- },
167
- delOthersCachedViews({ commit, state }, view) {
168
- return new Promise((resolve) => {
169
- commit("DEL_OTHERS_CACHED_VIEWS", view);
170
- resolve([...state.cachedViews]);
171
- });
172
- },
173
-
174
- delAllViews({ dispatch, state }, view) {
175
- return new Promise((resolve) => {
176
- dispatch("delAllVisitedViews", view);
177
- dispatch("delAllCachedViews", view);
178
- resolve({
179
- visitedViews: [...state.visitedViews],
180
- cachedViews: [...state.cachedViews],
181
- });
182
- });
183
- },
184
- delAllVisitedViews({ commit, state }) {
185
- return new Promise((resolve) => {
186
- commit("DEL_ALL_VISITED_VIEWS");
187
- resolve([...state.visitedViews]);
188
- });
189
- },
190
- delAllCachedViews({ commit, state }) {
191
- return new Promise((resolve) => {
192
- commit("DEL_ALL_CACHED_VIEWS");
193
- resolve([...state.cachedViews]);
194
- });
195
- },
196
-
197
- updateVisitedView({ commit }, view) {
198
- commit("UPDATE_VISITED_VIEW", view);
199
- },
200
- updateVisitedView2({ commit }, { view, title }) {
201
- let src = view || window.$vueRoot.$route;
202
- // 浅拷贝后再拷贝 meta,避免直接改写路由上共享的 meta.title
203
- let newView0 = { ...src };
204
- delete newView0.matched;
205
- newView0.meta = { ...(src.meta || {}), title };
206
- let newView = _.cloneDeep(newView0);
207
-
208
- commit("UPDATE_VISITED_VIEW", newView);
209
- },
210
- };
211
-
212
- export default {
213
- namespaced: true,
214
- state,
215
- mutations,
216
- actions,
217
- };
1
+ import store from "../index.js";
2
+ import _ from "lodash";
3
+ import { isRepeatable, viewKey } from "@base/utils/repeatOpen";
4
+
5
+ /**
6
+ * 页签是否「不可被批量关闭」:
7
+ * - meta.affix:菜单本身维护的固定页签;
8
+ * - meta.user_affix:运行期临时锁(如导出期间锁住当前页签)。
9
+ * 临时锁不能再写 meta.affix —— affix 会让 isRepeatable 翻转、viewKey 从 fullPath
10
+ * 塌成 path,页签缓存直接失配。所以这里两个标记都要判。
11
+ * @param {Object} view 页签对象
12
+ * @returns {Boolean}
13
+ */
14
+ function isPinned(view) {
15
+ const meta = (view && view.meta) || {};
16
+ return !!(meta.affix || meta.user_affix);
17
+ }
18
+
19
+ let state, mutations, actions;
20
+ state = {
21
+ visitedViews: [],
22
+ cachedViews: [],
23
+ };
24
+
25
+ mutations = {
26
+ ADD_VISITED_VIEW: (state, view) => {
27
+ let title = view.meta.title;
28
+ let add_routes = store.getters.add_routes;
29
+ if (add_routes && add_routes.children && add_routes.children.length > 0) {
30
+ let syncRoutes = add_routes.children[0].children;
31
+ let tRoutes = syncRoutes.some((route) => {
32
+ let flag = view.fullPath == route.path;
33
+ if (flag) {
34
+ title = route.meta.title;
35
+ return flag;
36
+ }
37
+ });
38
+ }
39
+ const key = viewKey(view);
40
+ if (state.visitedViews.some((v) => viewKey(v) === key)) return;
41
+ // 可重复打开时,记录同一菜单的序号,展示时用于区分标签
42
+ let repeatIndex = 0;
43
+ if (isRepeatable(view)) {
44
+ const sameCount = state.visitedViews.filter((v) => v.path === view.path);
45
+ repeatIndex = sameCount.length + 1;
46
+ }
47
+ state.visitedViews.push(
48
+ Object.assign({}, view, {
49
+ title: title || "no-name",
50
+ repeatIndex,
51
+ // meta 必须另拷一份:直接浅拷贝的话页签的 meta 与路由记录的 meta 是同一个
52
+ // 对象,任何往页签 meta 上写状态的代码(如导出锁写 user_affix)都会连带
53
+ // 改到 $route.meta —— 曾因此把 affix 写进路由,导致 isRepeatable 翻转、
54
+ // viewKey 从 fullPath 塌成 path,页签缓存失配、切回来整页重建。
55
+ meta: { ...(view.meta || {}) },
56
+ })
57
+ );
58
+ },
59
+ ADD_CACHED_VIEW: (state, view) => {
60
+ // 缓存键与 keepAlive.js getComponentName 保持一致(viewKey):
61
+ // 可重复打开的视图按 fullPath 各自独立缓存,其余按 path。
62
+ const key = viewKey(view);
63
+ if (state.cachedViews.includes(key)) return;
64
+ if (!view.meta.noCache) {
65
+ state.cachedViews.push(key);
66
+ }
67
+ },
68
+
69
+ DEL_VISITED_VIEW: (state, view) => {
70
+ const key = viewKey(view);
71
+ for (const [i, v] of state.visitedViews.entries()) {
72
+ if (viewKey(v) === key) {
73
+ state.visitedViews.splice(i, 1);
74
+ break;
75
+ }
76
+ }
77
+ },
78
+ DEL_CACHED_VIEW: (state, view) => {
79
+ // 兼容刷新场景(路径带 /redirect 前缀)与仅按 name 传入的情况,
80
+ // 归一到与缓存键一致的 viewKey 再移除。
81
+ let target = view;
82
+ let str = "/redirect";
83
+ if (view.path && view.path.startsWith(str)) {
84
+ let realPath = view.path.substr(str.length);
85
+ let realFullPath = (view.fullPath || view.path).substr(str.length);
86
+ target = { ...view, path: realPath, fullPath: realFullPath };
87
+ } else if (!view.path && view.name) {
88
+ let cView = state.visitedViews.find((item) => item.name == view.name);
89
+ target = cView || view;
90
+ }
91
+ const key = viewKey(target);
92
+ if (key) {
93
+ const index = state.cachedViews.indexOf(key);
94
+ index > -1 && state.cachedViews.splice(index, 1);
95
+ }
96
+ },
97
+
98
+ DEL_OTHERS_VISITED_VIEWS: (state, view) => {
99
+ const key = viewKey(view);
100
+ state.visitedViews = state.visitedViews.filter((v) => {
101
+ return isPinned(v) || viewKey(v) === key;
102
+ });
103
+ },
104
+ DEL_OTHERS_CACHED_VIEWS: (state, view) => {
105
+ // const index = state.cachedViews.indexOf(view.name)
106
+ const index = state.cachedViews.indexOf(viewKey(view));
107
+ if (index > -1) {
108
+ state.cachedViews = state.cachedViews.slice(index, index + 1);
109
+ } else {
110
+ // if index = -1, there is no cached tags
111
+ state.cachedViews = [];
112
+ }
113
+ },
114
+
115
+ DEL_ALL_VISITED_VIEWS: (state) => {
116
+ // keep affix tags
117
+ const affixTags = state.visitedViews.filter((tag) => isPinned(tag));
118
+ state.visitedViews = affixTags;
119
+ },
120
+ DEL_ALL_CACHED_VIEWS: (state) => {
121
+ state.cachedViews = [];
122
+ },
123
+
124
+ UPDATE_VISITED_VIEW: (state, view) => {
125
+ const key = viewKey(view);
126
+ for (let v of state.visitedViews) {
127
+ if (viewKey(v) === key) {
128
+ // ADD_VISITED_VIEW:meta 走合并而不是整体替换,
129
+ // 否则一次 update 就把页签 meta 换回路由那个共享对象,
130
+ // 并且会抹掉只存在于页签上的标记(user_affix 等)。
131
+ v = Object.assign(v, view, {
132
+ meta: { ...(v.meta || {}), ...(view.meta || {}) },
133
+ });
134
+ break;
135
+ }
136
+ }
137
+ },
138
+ };
139
+
140
+ actions = {
141
+ addView({ dispatch }, view) {
142
+ dispatch("addVisitedView", view);
143
+ dispatch("addCachedView", view);
144
+ },
145
+ addVisitedView({ commit }, view) {
146
+ commit("ADD_VISITED_VIEW", view);
147
+ },
148
+ addCachedView({ commit }, view) {
149
+ commit("ADD_CACHED_VIEW", view);
150
+ },
151
+
152
+ delView({ dispatch, state }, view) {
153
+ return new Promise((resolve) => {
154
+ dispatch("delVisitedView", view);
155
+ dispatch("delCachedView", view);
156
+ resolve({
157
+ visitedViews: [...state.visitedViews],
158
+ cachedViews: [...state.cachedViews],
159
+ });
160
+ });
161
+ },
162
+ delVisitedView({ commit, state }, view) {
163
+ return new Promise((resolve) => {
164
+ commit("DEL_VISITED_VIEW", view);
165
+ resolve([...state.visitedViews]);
166
+ });
167
+ },
168
+ delCachedView({ commit, state }, view) {
169
+ return new Promise((resolve) => {
170
+ commit("DEL_CACHED_VIEW", view);
171
+ resolve([...state.cachedViews]);
172
+ });
173
+ },
174
+
175
+ delOthersViews({ dispatch, state }, view) {
176
+ return new Promise((resolve) => {
177
+ dispatch("delOthersVisitedViews", view);
178
+ dispatch("delOthersCachedViews", view);
179
+ resolve({
180
+ visitedViews: [...state.visitedViews],
181
+ cachedViews: [...state.cachedViews],
182
+ });
183
+ });
184
+ },
185
+ delOthersVisitedViews({ commit, state }, view) {
186
+ return new Promise((resolve) => {
187
+ commit("DEL_OTHERS_VISITED_VIEWS", view);
188
+ resolve([...state.visitedViews]);
189
+ });
190
+ },
191
+ delOthersCachedViews({ commit, state }, view) {
192
+ return new Promise((resolve) => {
193
+ commit("DEL_OTHERS_CACHED_VIEWS", view);
194
+ resolve([...state.cachedViews]);
195
+ });
196
+ },
197
+
198
+ delAllViews({ dispatch, state }, view) {
199
+ return new Promise((resolve) => {
200
+ dispatch("delAllVisitedViews", view);
201
+ dispatch("delAllCachedViews", view);
202
+ resolve({
203
+ visitedViews: [...state.visitedViews],
204
+ cachedViews: [...state.cachedViews],
205
+ });
206
+ });
207
+ },
208
+ delAllVisitedViews({ commit, state }) {
209
+ return new Promise((resolve) => {
210
+ commit("DEL_ALL_VISITED_VIEWS");
211
+ resolve([...state.visitedViews]);
212
+ });
213
+ },
214
+ delAllCachedViews({ commit, state }) {
215
+ return new Promise((resolve) => {
216
+ commit("DEL_ALL_CACHED_VIEWS");
217
+ resolve([...state.cachedViews]);
218
+ });
219
+ },
220
+
221
+ updateVisitedView({ commit }, view) {
222
+ commit("UPDATE_VISITED_VIEW", view);
223
+ },
224
+ updateVisitedView2({ commit }, { view, title }) {
225
+ let src = view || window.$vueRoot.$route;
226
+ // 浅拷贝后再拷贝 meta,避免直接改写路由上共享的 meta.title
227
+ let newView0 = { ...src };
228
+ delete newView0.matched;
229
+ newView0.meta = { ...(src.meta || {}), title };
230
+ let newView = _.cloneDeep(newView0);
231
+
232
+ commit("UPDATE_VISITED_VIEW", newView);
233
+ },
234
+ };
235
+
236
+ export default {
237
+ namespaced: true,
238
+ state,
239
+ mutations,
240
+ actions,
241
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 导入文件闸门。
3
+ *
4
+ * 导入侧是整文件 readAsBinaryString 读进内存 → XLSX.read(占用通常是文件体积的
5
+ * 5~20 倍)→ 再逐格建行数组,全程同步且没有任何让步。此前没有大小与行数校验,
6
+ * 几十 MB 的 xlsx 会直接把标签页撑爆,且崩在 FileReader / XLSX 内部,用户看到的
7
+ * 同样是"页面崩溃"而不是一句提示。
8
+ *
9
+ * 阈值都可以由导入配置覆盖(maxFileSize / maxRowNum,也支持写在 importOption 上),
10
+ * 传 0 表示不限制。
11
+ */
12
+
13
+ /** 导入文件大小上限(字节)。 */
14
+ export const DEFAULT_IMPORT_FILE_SIZE = 20 * 1024 * 1024;
15
+
16
+ /** 导入数据行数上限(不含模板前 3 行表头)。 */
17
+ export const DEFAULT_IMPORT_ROW_NUM = 20000;
18
+
19
+ const readOption = (option, key) => {
20
+ if (option && option[key] !== undefined) return option[key];
21
+ if (option && option.importOption && option.importOption[key] !== undefined) {
22
+ return option.importOption[key];
23
+ }
24
+ return undefined;
25
+ };
26
+
27
+ const resolveLimit = (value, defaultValue) => {
28
+ let num = Number(value);
29
+ if (!isFinite(num) || num < 0) return defaultValue;
30
+ return num;
31
+ };
32
+
33
+ /** @param {Object} option 导入配置。@returns {Number} 文件大小上限,0 表示不限制。 */
34
+ export function getImportFileSizeLimit(option) {
35
+ return resolveLimit(readOption(option, "maxFileSize"), DEFAULT_IMPORT_FILE_SIZE);
36
+ }
37
+
38
+ /** @param {Object} option 导入配置。@returns {Number} 行数上限,0 表示不限制。 */
39
+ export function getImportRowLimit(option) {
40
+ return resolveLimit(readOption(option, "maxRowNum"), DEFAULT_IMPORT_ROW_NUM);
41
+ }
42
+
43
+ /** @param {File} file 待导入文件。@param {Object} option 导入配置。@returns {Boolean} */
44
+ export function isImportFileOversize(file, option) {
45
+ let limit = getImportFileSizeLimit(option);
46
+ return limit > 0 && !!file && file.size > limit;
47
+ }
48
+
49
+ /** @param {Number} rowNum 数据行数。@param {Object} option 导入配置。@returns {Boolean} */
50
+ export function isImportRowsOverLimit(rowNum, option) {
51
+ let limit = getImportRowLimit(option);
52
+ return limit > 0 && rowNum > limit;
53
+ }
54
+
55
+ /** @param {Number} bytes 字节数。@returns {Number} 保留一位小数的 MB 数。 */
56
+ export function toFileSizeMb(bytes) {
57
+ return Math.round(((bytes || 0) / 1024 / 1024) * 10) / 10;
58
+ }
59
+
60
+ /**
61
+ * 取单元格文本:w(格式化文本)缺失时回退 v(原始值)。
62
+ * 模板里手工粘贴或公式生成的数值格常常只有 v,直接 w.trim() 会 TypeError,
63
+ * 而调用点外层的 catch 会把它吞成"点了导入没反应"。
64
+ * @param {Object} cell xlsx 单元格。
65
+ * @returns {String|undefined}
66
+ */
67
+ export function getImportCellText(cell) {
68
+ if (cell === undefined || cell === null) return undefined;
69
+ let text = cell.w !== undefined && cell.w !== null ? cell.w : cell.v;
70
+ if (text === undefined || text === null) return undefined;
71
+ return String(text).trim();
72
+ }
@@ -11,6 +11,8 @@ const PAGE_MARGIN = 10;
11
11
  //超过之后 toDataURL 直接返回空白图——长详情页很容易撞上,这里按内容尺寸回退 scale。
12
12
  const MAX_CANVAS_SIDE = 16384;
13
13
  const MAX_CANVAS_AREA = 268435456;
14
+ //回退倍率的下限。再往下字就糊到认不出了,与其导出一份看不清的 PDF,不如让它明确失败
15
+ const MIN_SCALE = 0.2;
14
16
 
15
17
  const JPEG_QUALITY = 0.92;
16
18
 
@@ -103,7 +105,10 @@ function pickScale(width, height, expected) {
103
105
  MAX_CANVAS_SIDE / height,
104
106
  Math.sqrt(MAX_CANVAS_AREA / (width * height))
105
107
  );
106
- return Math.max(scale, 1);
108
+ //下限不能取 1:内容高度超过 16384 CSS px 的长页面(整页导出的常态)算出来的上限本就 <1
109
+ //抬回 1 等于把上面这段保护整个作废——canvas 照样越界,toDataURL 返回空白图,
110
+ //正是 MAX_CANVAS_SIDE 想拦的那个失败。html2canvas 支持 scale < 1,糊一点也要出得来。
111
+ return Math.max(scale, MIN_SCALE);
107
112
  }
108
113
 
109
114
  //Safari(WebKit) 的 getComputedStyle 会把颜色序列化成 color(srgb …) / color(display-p3 …),
@@ -92,6 +92,15 @@ service.interceptors.request.use(
92
92
  background: "unset",
93
93
  });
94
94
  }
95
+
96
+ // 被取消的请求走不到响应/错误拦截器里的 removeModel —— Cancel 对象不带 config,
97
+ // 那边无从知道该清理哪一个请求。不收口的话上面这两样会永久留在页面上(一个关不掉
98
+ // 的转圈遮罩)。挂到令牌自己的 promise 上,取消时精确清理本次请求创建的那份。
99
+ if (config.cancelToken && (config.ajaxOverlay || config.loadingInstance)) {
100
+ config.cancelToken.promise.then(function () {
101
+ removeModel(config);
102
+ });
103
+ }
95
104
  return config;
96
105
  },
97
106
  (error) => {
@@ -383,6 +392,14 @@ service.interceptors.response.use(
383
392
  }
384
393
  },
385
394
  (error) => {
395
+ // 主动取消(axios.CancelToken)不是错误,必须最先短路返回。
396
+ // axios 0.18 取消时 reject 的是 Cancel 对象,它**不带 config**,会一路落到下面
397
+ // 那个 else 分支无条件弹 ErroreBox —— 而 ErroreBox 是弹窗不是 toast,取消 N 个
398
+ // 在途请求就会连弹 N 个错误框。config.errorMsg 那条逃生口在 if (config) 里面,
399
+ // Cancel 走不到,所以只能在这里拦。调用方按需自行 catch。
400
+ if (axios.isCancel(error)) {
401
+ return Promise.reject(error);
402
+ }
386
403
  let config = error.config;
387
404
  if (
388
405
  error.response &&