cloud-web-corejs 1.0.281 → 1.0.283

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/components/excelExport/exportColumnMemory.js +118 -0
  3. package/src/components/excelExport/exportFieldDialog.vue +63 -40
  4. package/src/components/excelExport/exportItemConfigUtil.js +30 -0
  5. package/src/components/excelExport/index.js +30 -13
  6. package/src/components/excelExport/mixins.js +406 -68
  7. package/src/components/excelImport/index.vue +1 -1
  8. package/src/components/excelImport/mixins.js +56 -8
  9. package/src/components/table/index.js +14 -1
  10. package/src/components/table/vxeFilter/mixin.js +28 -18
  11. 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" +11 -4
  12. 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
  13. 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" +233 -0
  14. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +61 -2
  15. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +9 -0
  16. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/exportItemColumns-dialog.vue +7 -1
  17. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/select-export-item-button-editor.vue +51 -0
  18. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +4 -0
  19. package/src/components/xform/form-render/container-item/data-table-item.vue +7 -4
  20. package/src/components/xform/form-render/container-item/data-table-mixin.js +465 -128
  21. package/src/components/xform/utils/dynamicSchemaUtil.js +24 -0
  22. package/src/components/xform/utils/tableColumnHelper.js +46 -4
  23. package/src/components/xform/utils/textMaskUtil.js +6 -3
  24. package/src/layout/components/TagsView/index.vue +4 -2
  25. package/src/store/modules/tagsView.js +27 -3
  26. package/src/utils/assetUrl.js +19 -0
  27. package/src/utils/importLimit.js +72 -0
  28. package/src/utils/pdfUtil.js +6 -1
  29. package/src/utils/request.js +17 -0
  30. package/src/utils/vab.js +5 -1
@@ -161,6 +161,30 @@ export function isNonDataExportColumn(formatS, type, options) {
161
161
  && options?.isFormLabel !== true;
162
162
  }
163
163
 
164
+ /**
165
+ * 该列导出时是否只输出空值(列本身仍保留,表头与列位置不变)。
166
+ *
167
+ * 目前只有一种:**没有配「导出类型」的附件列**。附件的内容是文件,不配导出类型就意味着
168
+ * 这次导出既不写图片、也没有任何有意义的文本可写;而附件列没有 filterVal,走取值兜底会
169
+ * 落到 getCellValue 的 vNode 挂载,为了爬一段文件名文本给每个单元格挂一次附件组件 ——
170
+ * 既没价值又是导出耗时的大头。
171
+ *
172
+ * 与 isNonDataExportColumn 的区别:那个是**整列剔除**(字段树里都看不到,用于操作列/
173
+ * 脱敏列);这个是**列在、格空**,不改变导出文件的列结构。
174
+ *
175
+ * 注意配了导出类型(Image/Image2)的附件列不在此列 —— 即使本次导出没开 showImageAtTable、
176
+ * 图片最终不会写进文件,也保持原有行为不变。
177
+ *
178
+ * @param {String} formatS 列的展示格式。
179
+ * @param {String} type 展示组件类型。
180
+ * @param {String} exportType 列上配置的导出类型。
181
+ * @returns {Boolean}
182
+ */
183
+ export function isBlankExportColumn(formatS, type, exportType) {
184
+ if (exportType) return false;
185
+ return formatS === "editAttachment" || type === "baseAttachment";
186
+ }
187
+
164
188
  function isActionOrGroupColumn(item, formatS) {
165
189
  if (Array.isArray(item.children) && item.children.length) return true;
166
190
  return isActionColumn(item, formatS);
@@ -1,3 +1,47 @@
1
+ import { columnFormatMap } from "./util";
2
+
3
+ /**
4
+ * 列的某一侧(展示 / 编辑)是否真的配了组件。
5
+ *
6
+ * 判据与表单加载时的实例化口径一致(见 form-render/indexMixin 的 columnLoopDo):
7
+ * 只有 formatS / editFormatS 能映射出组件类型时该侧才会建 widget。纯格式化列
8
+ * (formatS 是 d1 这类格式码)与被改回空的列都不算。
9
+ * @param {Object} columnConfig 列配置。@param {Boolean} isEdit 是否编辑侧。
10
+ * @returns {Boolean}
11
+ */
12
+ function hasColumnWidgetSide(columnConfig, isEdit) {
13
+ if (isEdit) {
14
+ return !!(
15
+ columnConfig.editWidget || columnFormatMap[columnConfig.editFormatS]
16
+ );
17
+ }
18
+ return !!(columnConfig.widget || columnFormatMap[columnConfig.formatS]);
19
+ }
20
+
21
+ /**
22
+ * 取该侧生效的列级组件配置。
23
+ *
24
+ * 动态列走 widgetOptions / editWidgetOptions(本来就没有 formatS),直接生效;
25
+ * 设计器静态列的 columnOption / editColumnOption 必须门控:展示组件被改回空
26
+ * 之后那份 columnOption 仍会留在模板 JSON 里,无条件读会让这列凭空变必填
27
+ * (表头星号 + 拦保存),而加载时的 applyColumnMetaToFieldWidget 只读对应侧,
28
+ * 两边口径不一致正是该现象的根因。
29
+ * @param {Object} columnConfig 列配置。@param {Boolean} isEdit 是否编辑侧。
30
+ * @returns {Object|null}
31
+ */
32
+ function getEffectiveColumnOptions(columnConfig, isEdit) {
33
+ const dynamicOptions = isEdit
34
+ ? columnConfig.editWidgetOptions
35
+ : columnConfig.widgetOptions;
36
+ if (dynamicOptions) {
37
+ return dynamicOptions;
38
+ }
39
+ if (!hasColumnWidgetSide(columnConfig, isEdit)) {
40
+ return null;
41
+ }
42
+ return isEdit ? columnConfig.editColumnOption : columnConfig.columnOption;
43
+ }
44
+
1
45
  function buildColumnConfigMap(tableColumns, map = {}) {
2
46
  if (!tableColumns || !tableColumns.length) {
3
47
  return map;
@@ -56,10 +100,8 @@ export function resolveColumnRequiredFlag(columnConfig) {
56
100
  if (columnConfig.widget?.options?.required) {
57
101
  return true;
58
102
  }
59
- const widgetOptions =
60
- columnConfig.widgetOptions ?? columnConfig.columnOption;
61
- const editWidgetOptions =
62
- columnConfig.editWidgetOptions ?? columnConfig.editColumnOption;
103
+ const widgetOptions = getEffectiveColumnOptions(columnConfig, false);
104
+ const editWidgetOptions = getEffectiveColumnOptions(columnConfig, true);
63
105
  return !!(widgetOptions?.required || editWidgetOptions?.required);
64
106
  }
65
107
 
@@ -124,8 +124,10 @@ export function resolveTextMaskFlag(widgetOptions, ctx) {
124
124
  *
125
125
  * @param {Object} $grid vxe-grid 实例。
126
126
  * @param {Object} ctx 判定上下文,由 buildTextMaskContext 生成。
127
+ * @param {Array} [exportItemColumns] 本次明细导出实际使用的列(导出按钮上单独维护时由调用方
128
+ * 传入);不传则回落到表格属性上配的那套。
127
129
  */
128
- export function markMaskedExportColumns($grid, ctx) {
130
+ export function markMaskedExportColumns($grid, ctx, exportItemColumns) {
129
131
  if (!$grid || typeof $grid.getTableColumn !== "function") return;
130
132
 
131
133
  /** 打标记。列本就被其它规则(isNonDataExportColumn)剔除时不接管,避免撤销时误删。 */
@@ -174,6 +176,7 @@ export function markMaskedExportColumns($grid, ctx) {
174
176
  }
175
177
  };
176
178
  let originOption = $grid.params?.originOption;
177
- let exportItemColumns = originOption?.exportItemConfig?.columns || [];
178
- exportItemColumns.forEach(markItemColumn);
179
+ let tableItemColumns = originOption?.exportItemConfig?.columns;
180
+ let itemColumns = exportItemColumns || tableItemColumns || [];
181
+ itemColumns.forEach(markItemColumn);
179
182
  }
@@ -271,9 +271,11 @@ export default {
271
271
  this.closeMenu()
272
272
  },
273
273
  getIsLock() {
274
- let path = this.$route.path;
274
+ // 按 viewKey 而不是 path 定位:可重复打开时同一 path 有多个页签,
275
+ // 按 path 找到的可能是另一个实例(锁的是 A,却把 B 的刷新按钮禁掉)
276
+ let key = viewKey(this.$route)
275
277
  let visitedViews = this.$store.state.tagsView.visitedViews
276
- let currentItem = visitedViews.find(item => item.path == path)
278
+ let currentItem = visitedViews.find(item => viewKey(item) === key)
277
279
  return !!currentItem?.meta?.user_affix;
278
280
  },
279
281
  }
@@ -2,6 +2,20 @@ import store from "../index.js";
2
2
  import _ from "lodash";
3
3
  import { isRepeatable, viewKey } from "@base/utils/repeatOpen";
4
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
+
5
19
  let state, mutations, actions;
6
20
  state = {
7
21
  visitedViews: [],
@@ -34,6 +48,11 @@ mutations = {
34
48
  Object.assign({}, view, {
35
49
  title: title || "no-name",
36
50
  repeatIndex,
51
+ // meta 必须另拷一份:直接浅拷贝的话页签的 meta 与路由记录的 meta 是同一个
52
+ // 对象,任何往页签 meta 上写状态的代码(如导出锁写 user_affix)都会连带
53
+ // 改到 $route.meta —— 曾因此把 affix 写进路由,导致 isRepeatable 翻转、
54
+ // viewKey 从 fullPath 塌成 path,页签缓存失配、切回来整页重建。
55
+ meta: { ...(view.meta || {}) },
37
56
  })
38
57
  );
39
58
  },
@@ -79,7 +98,7 @@ mutations = {
79
98
  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
80
99
  const key = viewKey(view);
81
100
  state.visitedViews = state.visitedViews.filter((v) => {
82
- return v.meta.affix || viewKey(v) === key;
101
+ return isPinned(v) || viewKey(v) === key;
83
102
  });
84
103
  },
85
104
  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
@@ -95,7 +114,7 @@ mutations = {
95
114
 
96
115
  DEL_ALL_VISITED_VIEWS: (state) => {
97
116
  // keep affix tags
98
- const affixTags = state.visitedViews.filter((tag) => tag.meta.affix);
117
+ const affixTags = state.visitedViews.filter((tag) => isPinned(tag));
99
118
  state.visitedViews = affixTags;
100
119
  },
101
120
  DEL_ALL_CACHED_VIEWS: (state) => {
@@ -106,7 +125,12 @@ mutations = {
106
125
  const key = viewKey(view);
107
126
  for (let v of state.visitedViews) {
108
127
  if (viewKey(v) === key) {
109
- v = Object.assign(v, view);
128
+ // ADD_VISITED_VIEW:meta 走合并而不是整体替换,
129
+ // 否则一次 update 就把页签 meta 换回路由那个共享对象,
130
+ // 并且会抹掉只存在于页签上的标记(user_affix 等)。
131
+ v = Object.assign(v, view, {
132
+ meta: { ...(v.meta || {}), ...(view.meta || {}) },
133
+ });
110
134
  break;
111
135
  }
112
136
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 取出 require(图片) 的真实 URL。
3
+ *
4
+ * webpack4(url-loader,vue-cli 设了 esModule:false) 下 require 直接返回 URL 字符串;
5
+ * webpack5(vue-cli5 的 asset modules) 下返回的是 ES Module 命名空间对象,且该对象
6
+ * 由 Object.create(null) 生成——没有原型链就没有 toString,直接绑到 :src 上时
7
+ * setAttribute 会抛 "Cannot convert object to primitive value" 整棵子树渲染失败
8
+ * (webpack5 的 asset modules 没有 esModule 开关,只能在取值处兜底)。
9
+ *
10
+ * 消费应用的构建配置不受本库控制,凡是把 require 结果往 DOM 属性上放的地方都要过这里。
11
+ */
12
+ export function assetUrl(mod) {
13
+ if (mod && typeof mod === "object") {
14
+ return mod.default || "";
15
+ }
16
+ return mod || "";
17
+ }
18
+
19
+ export default assetUrl;
@@ -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 &&
package/src/utils/vab.js CHANGED
@@ -13,6 +13,7 @@ import XEUtils from "xe-utils";
13
13
  import prefixConfig from "@/prefixConfig.js";
14
14
  import moment from "moment";
15
15
  import { openView } from "./repeatOpen";
16
+ import { assetUrl } from "./assetUrl";
16
17
 
17
18
  let imFun = {
18
19
  request,
@@ -28,7 +29,8 @@ let imFun = {
28
29
  };
29
30
 
30
31
  function reqImg(a) {
31
- return require("@/resources/images/" + a);
32
+ /* webpack5 下 require 返回的是无原型的 Module 对象,直接当 src 用会崩,统一取真实 URL */
33
+ return assetUrl(require("@/resources/images/" + a));
32
34
  }
33
35
 
34
36
  const FILE_ICON_NAME_MAP = {
@@ -398,6 +400,8 @@ install = (Vue, opts = {}) => {
398
400
  Vue.prototype.$baseLodash = imFun.lodash;
399
401
  /* 全局事件总线 */
400
402
  Vue.prototype.$baseEventBus = new Vue();
403
+ /* 模板里 :src="$assetUrl(require(...))" 用,见 utils/assetUrl.js */
404
+ Vue.prototype.$assetUrl = assetUrl;
401
405
 
402
406
  Vue.prototype.$commonFileUtil = {
403
407
  // 文件类型获取与判断接口