cloud-web-corejs 1.1.0-dev.22 → 1.1.0-dev.24

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/bizTab/BizTabListPage.vue +66 -33
  3. package/src/components/bizTab/README.md +3 -1
  4. package/src/components/mobile/wf/content.vue +27 -3
  5. package/src/components/table/index.js +5 -2
  6. package/src/components/table/plugins/cell-area/index.js +10 -3
  7. package/src/components/wf/wfStartDialog.vue +1 -0
  8. package/src/components/wf/wfUtil.js +47 -29
  9. package/src/components/wf/wfVisibility.js +6 -0
  10. package/src/components/xform/form-designer/form-widget/container-widget/data-table-mixin.js +7 -0
  11. package/src/components/xform/form-designer/form-widget/container-widget/data-table-widget.vue +3 -1
  12. package/src/components/xform/form-designer/form-widget/field-widget/fieldMixin.js +25 -1
  13. package/src/components/xform/form-designer/setting-panel/form-setting.vue +14 -2
  14. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/data-table-editor.vue +318 -12
  15. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/table-column-dialog.vue +23 -7
  16. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +48 -0
  17. package/src/components/xform/form-render/container-item/data-table-item.vue +8 -6
  18. package/src/components/xform/form-render/container-item/data-table-mixin.js +584 -24
  19. package/src/components/xform/form-render/container-item/detail-h5-item.vue +26 -16
  20. package/src/components/xform/form-render/container-item/detail-item.vue +350 -446
  21. package/src/components/xform/form-render/fieldControlMixin.js +2 -0
  22. package/src/components/xform/form-render/indexMixin.js +25 -15
  23. package/src/components/xform/mixins/businessLock.js +59 -0
  24. package/src/components/xform/mixins/wfStart.js +103 -0
  25. package/src/components/xform/mixins/wfVisibility.js +33 -0
  26. package/src/components/xform/styles/h5.scss +6 -1
  27. package/src/components/xform/utils/util.js +2 -0
  28. package/src/views/user/home/index.vue +1 -1
  29. package/src/views/user/home/net/distributor.vue +1015 -0
  30. package/src/views/user/home/net/index.vue +61 -0
@@ -29,7 +29,11 @@ import {
29
29
  } from "../../../../components/xform/utils/tableColumnHelper";
30
30
  import { mergeQueryParam } from "../../../../components/xform/utils/queryParamUtil";
31
31
  import { isFixedQueryWidget } from "../../../../components/xform/utils/fixedQueryUtil";
32
- import { isUrlValueColumn } from "../../../../components/xform/utils/attachmentValueUtil";
32
+ import {
33
+ isUrlValueColumn,
34
+ normalizeAttachmentValue,
35
+ toModelValue,
36
+ } from "../../../../components/xform/utils/attachmentValueUtil";
33
37
  import {
34
38
  EXPORT_TYPE,
35
39
  isItemExport,
@@ -204,6 +208,8 @@ modules = {
204
208
  treeKnownFormalKeyRegistry: new Set(),
205
209
  treeSingleSavePending: false,
206
210
  treeSingleSaveReconcileTask: null,
211
+ editAutoSaveTimers: {},
212
+ editAutoSaveQueue: null,
207
213
  // 列/网格未就绪时 setValue 只缓存数据,置此标志延后行 schema 构建
208
214
  pendingSchemaInit: false,
209
215
  };
@@ -234,6 +240,13 @@ modules = {
234
240
  customClass: function () {
235
241
  return this.widget.options.customClass || "";
236
242
  },
243
+ dataTableStyle: function () {
244
+ let width = this.widget.options.tableWidth;
245
+ if (width === null || width === undefined || width === "") return {};
246
+ width = String(width).trim();
247
+ if (/^\d+(?:\.\d+)?$/.test(width)) width += "px";
248
+ return { width };
249
+ },
237
250
  singleRowSelectFlag: function () {
238
251
  return !this.widget.options.showCheckBox;
239
252
  },
@@ -284,6 +297,10 @@ modules = {
284
297
  });
285
298
  },
286
299
  beforeDestroy: function () {
300
+ Object.values(this.editAutoSaveTimers || {}).forEach((timer) => {
301
+ clearTimeout(timer);
302
+ });
303
+ this.editAutoSaveTimers = {};
287
304
  this.unregisterFromRefList();
288
305
  },
289
306
  methods: {
@@ -942,6 +959,430 @@ modules = {
942
959
  let o = widget.options.name;
943
960
  return (widget.options.keyNameEnabled && widget.options.keyName) || o;
944
961
  },
962
+ /** @param {Object} obj VXE 单元格参数。@returns {Object|null} 当前单元格实际使用的编辑组件 schema。 */
963
+ getClipboardCellWidget(obj) {
964
+ const params = obj?.column?.params || {};
965
+ const template = params.editWidget || params.widget;
966
+ if (!template) return null;
967
+ const rowSchema = this.getRowFieldSchema(obj.row);
968
+ return rowSchema?.[template.id] || template;
969
+ },
970
+ /** @param {Object} obj VXE 单元格参数。@returns {Boolean} 粘贴是否可以修改该单元格。 */
971
+ canPasteTableCell(obj) {
972
+ const column = obj?.column;
973
+ if (!column?.editRender || column.editRender.enabled === false) {
974
+ return false;
975
+ }
976
+ const widget = this.getClipboardCellWidget(obj);
977
+ if (widget?.options?.disabled || widget?.options?.readonly) {
978
+ return false;
979
+ }
980
+ if (!this.isTableCellEditable(obj)) {
981
+ return false;
982
+ }
983
+ const editOpts = obj?.$table?.editOpts || {};
984
+ const beforeEditMethod = editOpts.beforeEditMethod || editOpts.activeMethod;
985
+ return !beforeEditMethod || beforeEditMethod(obj) !== false;
986
+ },
987
+ /**
988
+ * 把剪贴板里的展示文本转换成字段真实值。选项列按 label 反查 value,避免从表格
989
+ * 复制出的中文标签直接写进 code 字段。
990
+ * @param {Object} widget 字段组件 schema。
991
+ * @param {*} cellValue 剪贴板文本。
992
+ * @returns {{accepted: Boolean, value: *}}
993
+ */
994
+ resolveTablePasteValue(widget, cellValue) {
995
+ const type = widget?.type;
996
+ const options = widget?.options || {};
997
+ const text = cellValue == null ? "" : String(cellValue).trim();
998
+ if (type === "number") {
999
+ if (!text) return { accepted: true, value: null };
1000
+ return isNaN(text)
1001
+ ? { accepted: false, value: null }
1002
+ : { accepted: true, value: Number(text) };
1003
+ }
1004
+
1005
+ const isMultiple = type === "checkbox"
1006
+ || (type === "select" && options.multiple);
1007
+ const isOptionWidget = ["select", "radio", "checkbox", "status"]
1008
+ .includes(type);
1009
+ if (!isOptionWidget) {
1010
+ return { accepted: true, value: cellValue };
1011
+ }
1012
+
1013
+ const items = type === "status"
1014
+ ? options.statusParam || []
1015
+ : options.optionItems || [];
1016
+ const labelKey = this.getOptionItemLabelKey(widget);
1017
+ const valueKey = this.getOptionItemValueKey(widget);
1018
+ if (!text) {
1019
+ return { accepted: true, value: isMultiple ? [] : null };
1020
+ }
1021
+ const parts = isMultiple ? text.split(/[,,;;\n\r]+/) : [text];
1022
+ const values = [];
1023
+ for (let index = 0; index < parts.length; index++) {
1024
+ const part = parts[index].trim();
1025
+ const matched = items.find(
1026
+ (item) =>
1027
+ String(this.$t1(item[labelKey])) === part
1028
+ || String(item[valueKey]) === part
1029
+ );
1030
+ if (!matched) return { accepted: false, value: null };
1031
+ values.push(matched[valueKey]);
1032
+ }
1033
+ return {
1034
+ accepted: true,
1035
+ value: isMultiple ? values : values[0],
1036
+ };
1037
+ },
1038
+ /** @param {Object} obj VXE 单元格参数。@returns {Boolean} 是否为附件编辑列。 */
1039
+ isClipboardAttachmentCell(obj) {
1040
+ const widget = this.getClipboardCellWidget(obj);
1041
+ return !!widget && isAttachmentWidgetType(widget.type);
1042
+ },
1043
+ /**
1044
+ * @param {Object} obj VXE 单元格参数。
1045
+ * @param {File[]} files 剪贴板文件。
1046
+ * @returns {Promise<Boolean>} 至少一个附件上传成功时为 true。
1047
+ */
1048
+ pasteTableAttachments(obj, files) {
1049
+ if (!files.length || !this.$baseUpload?.upload) {
1050
+ return Promise.resolve(false);
1051
+ }
1052
+ const widget = this.getClipboardCellWidget(obj);
1053
+ const options = widget?.options || {};
1054
+ const field = obj.column.field;
1055
+ const currentRows = normalizeAttachmentValue(obj.row[field]);
1056
+ const limit = Number(options.limit) || 0;
1057
+ const remaining = limit
1058
+ ? Math.max(limit - currentRows.length, 0)
1059
+ : files.length;
1060
+ const acceptedFiles = files.slice(0, remaining);
1061
+ if (!acceptedFiles.length) {
1062
+ this.$message.warning(this.$t1("附件数量已达到上限"));
1063
+ return Promise.resolve(false);
1064
+ }
1065
+
1066
+ const tasks = acceptedFiles.map((file) => new Promise((resolve) => {
1067
+ let settled = false;
1068
+ const finish = (result) => {
1069
+ if (settled) return;
1070
+ settled = true;
1071
+ resolve(result);
1072
+ };
1073
+ try {
1074
+ const requestTask = this.$baseUpload.upload({
1075
+ file,
1076
+ isLoading: true,
1077
+ callback: (res) => {
1078
+ if (!res || res.type !== "success") {
1079
+ this.$message.error(
1080
+ res?.content
1081
+ || this.$t1("[{name}]上传失败", { name: file.name })
1082
+ );
1083
+ finish(false);
1084
+ return;
1085
+ }
1086
+ const attachment = res.objx || {};
1087
+ attachment.name = attachment.name || file.name;
1088
+ const rows = normalizeAttachmentValue(obj.row[field]).slice();
1089
+ if (limit && rows.length >= limit) {
1090
+ finish(false);
1091
+ return;
1092
+ }
1093
+ rows.push(attachment);
1094
+ this.$set(obj.row, field, toModelValue(rows, options));
1095
+ this.$nextTick(() => obj.$table?.updateFooter());
1096
+ finish(true);
1097
+ },
1098
+ });
1099
+ if (requestTask && typeof requestTask.catch === "function") {
1100
+ requestTask.catch((error) => {
1101
+ this.$message.error(
1102
+ error?.message
1103
+ || this.$t1("[{name}]上传失败", { name: file.name })
1104
+ );
1105
+ finish(false);
1106
+ });
1107
+ }
1108
+ } catch (error) {
1109
+ this.$message.error(
1110
+ error?.message || this.$t1("[{name}]上传失败", { name: file.name })
1111
+ );
1112
+ finish(false);
1113
+ }
1114
+ }));
1115
+ return Promise.all(tasks).then((results) => results.some(Boolean));
1116
+ },
1117
+ /** @param {Object} obj VXE 单元格参数。@returns {Boolean|Promise<Boolean>} 是否修改了单元格。 */
1118
+ pasteTableCell(obj) {
1119
+ if (!this.canPasteTableCell(obj)) return false;
1120
+ if (this.isClipboardAttachmentCell(obj)) {
1121
+ if (!this.widget.options.clipboardAttachmentEnabled) return false;
1122
+ // 文件必须在原生 paste 事件的同步调用栈中读取,异步后浏览器会清掉该数据。
1123
+ const event = window.event;
1124
+ const clipboardData = obj.clipData?.clipboardData
1125
+ || event?.clipboardData
1126
+ || window.clipboardData;
1127
+ const files = clipboardData
1128
+ ? Array.from(clipboardData.files || [])
1129
+ : [];
1130
+ return this.pasteTableAttachments(obj, files);
1131
+ }
1132
+ const widget = this.getClipboardCellWidget(obj);
1133
+ const resolved = this.resolveTablePasteValue(widget, obj.cellValue);
1134
+ if (resolved.accepted) {
1135
+ this.$set(obj.row, obj.column.field, resolved.value);
1136
+ return true;
1137
+ }
1138
+ if (this.widget.options.clipboardInvalidValueStrategy === "warn") {
1139
+ this.$message.warning(
1140
+ `字段 ${obj.column.title || obj.column.field} 的粘贴值无效,已跳过。`
1141
+ );
1142
+ }
1143
+ return false;
1144
+ },
1145
+ /**
1146
+ * @param {Object} $table VXE Table 实例。
1147
+ * @param {Array<Object>} rows 本次粘贴实际修改的行。
1148
+ * @description 按顺序复用单行保存,避免并发触发行保存锁。
1149
+ */
1150
+ async autoSaveClipboardRows($table, rows) {
1151
+ const savedKeys = new Set();
1152
+ for (const sourceRow of rows || []) {
1153
+ const rowKey = sourceRow?._X_ROW_KEY;
1154
+ if (rowKey && savedKeys.has(rowKey)) continue;
1155
+ if (rowKey) savedKeys.add(rowKey);
1156
+ const formRows = this.formModel[this.fieldKeyName] || [];
1157
+ const row = rowKey
1158
+ ? formRows.find((item) => item?._X_ROW_KEY === rowKey) || sourceRow
1159
+ : sourceRow;
1160
+ await this.saveEditRow(
1161
+ { row, $table },
1162
+ { skipConfirm: true }
1163
+ );
1164
+ }
1165
+ },
1166
+ /** @returns {Boolean} 是否开启编辑表格自动保存,兼容早期粘贴专用配置名。 */
1167
+ isTableAutoSaveEnabled() {
1168
+ const options = this.widget.options;
1169
+ return !!(
1170
+ options.autoSaveEnabled
1171
+ || options.clipboardAutoSaveEnabled
1172
+ );
1173
+ },
1174
+ /** @param {String} trigger 触发来源。@returns {Boolean} 当前来源是否启用自动保存。 */
1175
+ isAutoSaveTriggerEnabled(trigger) {
1176
+ const triggers = this.widget.options.autoSaveTrigger;
1177
+ if (!Array.isArray(triggers)) {
1178
+ return true;
1179
+ }
1180
+ return triggers.includes(trigger);
1181
+ },
1182
+ /**
1183
+ * @param {Object} obj 行内字段携带的 VXE 单元格参数。
1184
+ * @returns {Boolean} 是否已安排自动保存。
1185
+ * @description 失焦后短延迟保存;同一行重复失焦会合并为一次请求。
1186
+ */
1187
+ scheduleEditRowAutoSave(obj, trigger = "blur") {
1188
+ if (
1189
+ !this.widget.options.isEditTable
1190
+ || !this.isTableAutoSaveEnabled()
1191
+ || !this.isAutoSaveTriggerEnabled(trigger)
1192
+ || !obj?.row
1193
+ || !obj?.$table
1194
+ || !this.isTableCellEditable(obj, true)
1195
+ ) {
1196
+ return false;
1197
+ }
1198
+ const editSnapshot = obj.$table.editCloneRow;
1199
+ if (
1200
+ editSnapshot
1201
+ && this.$baseLodash.isEqual(editSnapshot, obj.row)
1202
+ ) {
1203
+ return false;
1204
+ }
1205
+ const rowKey = obj.row._X_ROW_KEY || "current";
1206
+ const configuredDelay = Number(this.widget.options.autoSaveDelay);
1207
+ const autoSaveDelay = Number.isFinite(configuredDelay)
1208
+ ? Math.max(configuredDelay, 0)
1209
+ : 200;
1210
+ clearTimeout(this.editAutoSaveTimers[rowKey]);
1211
+ this.editAutoSaveTimers[rowKey] = setTimeout(() => {
1212
+ delete this.editAutoSaveTimers[rowKey];
1213
+ const runSave = async () => {
1214
+ const rows = this.formModel[this.fieldKeyName] || [];
1215
+ const currentRow = obj.row._X_ROW_KEY
1216
+ ? rows.find((row) => row?._X_ROW_KEY === obj.row._X_ROW_KEY)
1217
+ || obj.row
1218
+ : obj.row;
1219
+ return this.saveEditRow(
1220
+ { ...obj, row: currentRow },
1221
+ { skipConfirm: true }
1222
+ );
1223
+ };
1224
+ this.editAutoSaveQueue = Promise.resolve(this.editAutoSaveQueue)
1225
+ .catch(() => false)
1226
+ .then(runSave)
1227
+ .catch((error) => {
1228
+ this.$message.error(error?.message || "失焦自动保存失败。");
1229
+ return false;
1230
+ });
1231
+ }, autoSaveDelay);
1232
+ return true;
1233
+ },
1234
+ /**
1235
+ * @param {Object} $table VXE 表实例。
1236
+ * @param {Array<String>} insertedRowKeys 本次粘贴自动补出的稳定行键。
1237
+ * @description 把自动补出的行同步回 xform 表单模型。树表的 fullData 只有根节点,
1238
+ * 因此只能合并本次新增节点,不能用它覆盖规范扁平数据。
1239
+ */
1240
+ syncClipboardTableRows($table, insertedRowKeys = []) {
1241
+ if (this.widget.options.isTreeTable) {
1242
+ if (!insertedRowKeys.length) return;
1243
+ const visibleRows = $table?.afterFullData || [];
1244
+ const keySet = new Set(insertedRowKeys);
1245
+ const insertedRows = visibleRows.filter(
1246
+ (row) => row?._X_ROW_KEY && keySet.has(row._X_ROW_KEY)
1247
+ );
1248
+ const formRows = Array.isArray(this.formModel[this.fieldKeyName])
1249
+ ? this.formModel[this.fieldKeyName]
1250
+ : [];
1251
+ const existingKeys = new Set(formRows.map((row) => row?._X_ROW_KEY));
1252
+ const nextRows = formRows.concat(
1253
+ insertedRows.filter((row) => !existingKeys.has(row._X_ROW_KEY))
1254
+ );
1255
+ this.$set(this.formModel, this.fieldKeyName, nextRows);
1256
+ this.initRowIdData();
1257
+ this.initFieldSchemaData();
1258
+ return;
1259
+ }
1260
+ const rows = $table?.getTableData()?.fullData || [];
1261
+ let formRows = this.formModel[this.fieldKeyName];
1262
+ if (!Array.isArray(formRows)) {
1263
+ this.$set(this.formModel, this.fieldKeyName, rows);
1264
+ this.fieldModel = rows;
1265
+ return;
1266
+ }
1267
+ if (formRows !== rows) {
1268
+ formRows.splice(0, formRows.length);
1269
+ formRows.push(...rows);
1270
+ }
1271
+ this.fieldModel = formRows;
1272
+ this.initRowIdData();
1273
+ this.initFieldSchemaData();
1274
+ },
1275
+ /** @returns {Object} xform 编辑表的区域复制粘贴配置。 */
1276
+ buildTableClipboardConfig() {
1277
+ const options = this.widget.options;
1278
+ if (!options.isEditTable || !options.clipboardEnabled) {
1279
+ return {};
1280
+ }
1281
+ let insertedRowKeys = [];
1282
+ let pasteBatch = null;
1283
+ const excludeFields = Array.from(new Set([
1284
+ "operate",
1285
+ ...(Array.isArray(options.clipboardExcludeFields)
1286
+ ? options.clipboardExcludeFields
1287
+ : []),
1288
+ ]));
1289
+ return {
1290
+ mouseConfig: { area: true },
1291
+ areaConfig: {
1292
+ multiple: false,
1293
+ excludeFields,
1294
+ selectCellByHeader: false,
1295
+ },
1296
+ keyboardConfig: {
1297
+ isClip: true,
1298
+ isEdit: true,
1299
+ isDel: false,
1300
+ isEsc: true,
1301
+ },
1302
+ clipConfig: {
1303
+ isCopy: true,
1304
+ isPaste: true,
1305
+ isCut: false,
1306
+ excludeFields,
1307
+ isRowIncrement: !!options.clipboardRowIncrementEnabled,
1308
+ beforePasteMethod: () => {
1309
+ pasteBatch = {
1310
+ rows: new Map(),
1311
+ tasks: [],
1312
+ };
1313
+ },
1314
+ createRowsMethod: ({ insertRows, targetRow }) => {
1315
+ insertedRowKeys = [];
1316
+ const treeContext = options.isTreeTable
1317
+ ? this.getTreeContext()
1318
+ : null;
1319
+ const rows = insertRows.map(() => {
1320
+ const row = {
1321
+ ...this.createNewTableData(true),
1322
+ _X_ROW_KEY: "row_" + generateId(),
1323
+ };
1324
+ if (treeContext) {
1325
+ row[treeContext.identityConfig.physicalIdField] = null;
1326
+ this.ensureNewTreeRowKey(row, treeContext);
1327
+ const insertMode = options.treePasteInsertMode || "root";
1328
+ const siblingParent = targetRow
1329
+ ? targetRow[treeContext.fields.parentField]
1330
+ : treeContext.identityConfig.rootParentValue;
1331
+ row[treeContext.fields.parentField]
1332
+ = insertMode === "sibling"
1333
+ ? siblingParent
1334
+ : treeContext.identityConfig.rootParentValue;
1335
+ }
1336
+ insertedRowKeys.push(row._X_ROW_KEY);
1337
+ return row;
1338
+ });
1339
+ if (treeContext) {
1340
+ this.validateNewTreeRows(rows, treeContext);
1341
+ }
1342
+ return rows;
1343
+ },
1344
+ pasteMethod: (obj) => {
1345
+ const batch = pasteBatch;
1346
+ const rememberRow = () => {
1347
+ if (!batch) return;
1348
+ const key = obj.row?._X_ROW_KEY || obj.row;
1349
+ batch.rows.set(key, obj.row);
1350
+ };
1351
+ const result = this.pasteTableCell(obj);
1352
+ if (result && typeof result.then === "function") {
1353
+ batch?.tasks.push(
1354
+ Promise.resolve(result).then((changed) => {
1355
+ if (changed) rememberRow();
1356
+ })
1357
+ );
1358
+ } else if (result) {
1359
+ rememberRow();
1360
+ }
1361
+ },
1362
+ afterPasteMethod: ({ $table }) => {
1363
+ $table.clearActived();
1364
+ this.syncClipboardTableRows($table, insertedRowKeys);
1365
+ insertedRowKeys = [];
1366
+ const batch = pasteBatch;
1367
+ pasteBatch = null;
1368
+ if (
1369
+ this.isTableAutoSaveEnabled()
1370
+ && this.isAutoSaveTriggerEnabled("paste")
1371
+ && batch
1372
+ ) {
1373
+ Promise.all(batch.tasks)
1374
+ .then(() => this.autoSaveClipboardRows(
1375
+ $table,
1376
+ Array.from(batch.rows.values())
1377
+ ))
1378
+ .catch((error) => {
1379
+ this.$message.error(error?.message || "粘贴后自动保存失败。");
1380
+ });
1381
+ }
1382
+ },
1383
+ },
1384
+ };
1385
+ },
945
1386
  /** @returns {String} 数据集必填校验提示。 */
946
1387
  getDatasetRequiredHint() {
947
1388
  if (this.widget.options.requiredHint) {
@@ -2418,13 +2859,13 @@ modules = {
2418
2859
  let pagerConfig = {};
2419
2860
  let editDefaultRow, treeNodeParam, treeConfig;
2420
2861
  let isTreeTable = this.widget.options.isTreeTable;
2862
+ if (this.widget.options.editDefaultRow) {
2863
+ editDefaultRow = this.handleCustomEvent(
2864
+ this.widget.options.editDefaultRow
2865
+ );
2866
+ }
2421
2867
  if (isTreeTable) {
2422
- pagerConfig.pagerClass = "is--hidden";
2423
- if (this.widget.options.editDefaultRow) {
2424
- editDefaultRow = this.handleCustomEvent(
2425
- this.widget.options.editDefaultRow
2426
- );
2427
- }
2868
+ const configuredTree = this.widget.options.treeConfig || {};
2428
2869
  treeConfig = {
2429
2870
  lazy: false,
2430
2871
  // 树表主键列显式声明为唯一事实源:vxe transform 建树(toArrayTree)
@@ -2441,6 +2882,8 @@ modules = {
2441
2882
  // 所有行,vxe 按引用匹配的展开记录会全部失效。reserve 按 _X_ROW_KEY
2442
2883
  // 恢复展开状态(克隆保留该键),查询重载会重新生成键,互不干扰。
2443
2884
  reserve: true,
2885
+ ...configuredTree,
2886
+ transform: true,
2444
2887
  loadMethod: ({ $table, row }) => {
2445
2888
  // 模拟后台接口
2446
2889
  let $grid = that.getGridTable();
@@ -2610,6 +3053,7 @@ modules = {
2610
3053
  // editRules
2611
3054
  };
2612
3055
  }
3056
+ const clipboardOpts = this.buildTableClipboardConfig();
2613
3057
  let showFooter = this.widget.options.showGridFooter || false;
2614
3058
  let otherConfig = {};
2615
3059
  if (this.widget.options.hideGridCheckBox) {
@@ -2682,14 +3126,17 @@ modules = {
2682
3126
  };
2683
3127
  }
2684
3128
  let hostTreeIdentityConfig = {
3129
+ ...(this.widget.options.treeIdentityConfig || {}),
2685
3130
  ...(dataTableConfig.otherConfig?.treeIdentityConfig || {}),
2686
3131
  ...(dataTableConfig.treeIdentityConfig || {}),
2687
3132
  };
2688
3133
  let hostTreeCopyConfig = {
3134
+ ...(this.widget.options.treeCopyConfig || {}),
2689
3135
  ...(dataTableConfig.otherConfig?.treeCopyConfig || {}),
2690
3136
  ...(dataTableConfig.treeCopyConfig || {}),
2691
3137
  };
2692
3138
  let hostTreeSubmitConfig = {
3139
+ ...(this.widget.options.treeSubmitConfig || {}),
2693
3140
  ...(dataTableConfig.otherConfig?.treeSubmitConfig || {}),
2694
3141
  ...(dataTableConfig.treeSubmitConfig || {}),
2695
3142
  };
@@ -2702,6 +3149,7 @@ modules = {
2702
3149
  columns: columns,
2703
3150
  searchColumns: searchColumns,
2704
3151
  isQueryAllPage,
3152
+ readQueryAllPage: !this.widget.options.isNotReadQueryAllPage,
2705
3153
  exportItemConfig,
2706
3154
  vform: true,
2707
3155
  ...(this.getDynamicColumnSourceType() !== "none"
@@ -2718,11 +3166,14 @@ modules = {
2718
3166
  config: {
2719
3167
  height: height,
2720
3168
  showFooter,
2721
- pagerConfig,
3169
+ pagerConfig: this.widget.options.showPagination
3170
+ ? pagerConfig
3171
+ : false,
2722
3172
  treeConfig,
2723
3173
  rowConfig,
2724
3174
  headerCellStyle,
2725
3175
  ...editOpts,
3176
+ ...clipboardOpts,
2726
3177
  ...hostTableConfig,
2727
3178
 
2728
3179
  /*rowConfig:{
@@ -2820,7 +3271,7 @@ modules = {
2820
3271
  });
2821
3272
  }
2822
3273
 
2823
- if (page.pageSize !== undefined) {
3274
+ if (page && page.pageSize !== undefined) {
2824
3275
  queryParams["size"] = page.pageSize;
2825
3276
  queryParams["current"] = page.currentPage;
2826
3277
  }
@@ -3680,8 +4131,10 @@ modules = {
3680
4131
  async deleteRow(row, rowIndex) {
3681
4132
  let isTreeTable = this.widget.options.isTreeTable;
3682
4133
  if (isTreeTable) {
3683
- this.removeTreeRow({ row });
3684
- return;
4134
+ return this.removeTreeRow({ row, rowIndex });
4135
+ }
4136
+ if (!this.canDeleteTableRow({ row, rowIndex })) {
4137
+ return false;
3685
4138
  }
3686
4139
  delete this.fieldSchemaMap[row._X_ROW_KEY];
3687
4140
  let $grid = this.getGridTable();
@@ -4482,15 +4935,18 @@ modules = {
4482
4935
  /**
4483
4936
  * 保存当前编辑行。持久化行原位合并;新身份以回查结果重建树关系。
4484
4937
  * @param {Object} obj VXE 插槽参数,必须包含 row 和 $table。
4938
+ * @param {Object} options 保存选项;skipConfirm 用于粘贴后的自动保存。
4485
4939
  */
4486
- async saveEditRow(obj) {
4940
+ async saveEditRow(obj, options = {}) {
4487
4941
  let formRef = this.getFormRef();
4488
4942
  let formConfig = this.formConfig;
4489
4943
  let entity = formConfig.entity;
4490
4944
  if (!entity) return;
4491
4945
  let reportTemplate = formRef?.reportTemplate;
4492
4946
  let formCode = reportTemplate?.formCode;
4493
- let scriptCode = formConfig.saveScriptCode || "saveUpdate";
4947
+ let scriptCode = this.widget.options.rowSaveScriptCode
4948
+ || formConfig.saveScriptCode
4949
+ || "saveUpdate";
4494
4950
 
4495
4951
  let $grid = obj.$table.$xegrid;
4496
4952
  if (typeof this.treeSingleSaveReconcileTask === "function") {
@@ -4543,6 +4999,12 @@ modules = {
4543
4999
  const rowValid = controlled ? await this.validateEditRowAsync(obj.row) : this.validateEditRow(obj.row);
4544
5000
  if (!rowValid) return false;
4545
5001
  const fieldControlSnapshot = controlled ? formRef.captureFieldControlSubmission() : null;
5002
+ const beforeSaveResult = this.handleCustomEvent(
5003
+ this.widget.options.beforeRowSave,
5004
+ ["rowData", "tableParam"],
5005
+ [obj.row, obj]
5006
+ );
5007
+ if (beforeSaveResult === false) return false;
4546
5008
 
4547
5009
  let requestRow = obj.row;
4548
5010
  if (treeContext) {
@@ -4612,10 +5074,12 @@ modules = {
4612
5074
  );
4613
5075
  }
4614
5076
 
4615
- try {
4616
- await this.$baseConfirm("您确定要保存吗?");
4617
- } catch (error) {
4618
- return false;
5077
+ if (!options.skipConfirm) {
5078
+ try {
5079
+ await this.$baseConfirm("您确定要保存吗?");
5080
+ } catch (error) {
5081
+ return false;
5082
+ }
4619
5083
  }
4620
5084
 
4621
5085
  await $grid.clearActived();
@@ -4638,8 +5102,8 @@ modules = {
4638
5102
  let urlValueProps = this.getUrlValueAttachmentProps();
4639
5103
  Object.keys(mainData).forEach((key) => {
4640
5104
  if (
4641
- key.startsWith("attachments_") &&
4642
- urlValueProps.indexOf(key) === -1
5105
+ key.startsWith("attachments_")
5106
+ && urlValueProps.indexOf(key) === -1
4643
5107
  ) {
4644
5108
  formData[key] = mainData[key];
4645
5109
  }
@@ -4848,6 +5312,11 @@ modules = {
4848
5312
  await $grid.updateData();
4849
5313
  }
4850
5314
  delete obj.$table.editCloneRow;
5315
+ this.handleCustomEvent(
5316
+ this.widget.options.afterRowSave,
5317
+ ["rowData", "res"],
5318
+ [obj.row, rowResponse]
5319
+ );
4851
5320
  return true;
4852
5321
  };
4853
5322
  let runRowReconcile = async () =>
@@ -4855,6 +5324,7 @@ modules = {
4855
5324
  try {
4856
5325
  return await runRowReconcile();
4857
5326
  } catch (error) {
5327
+ this.handleRowSaveError(error, obj.row);
4858
5328
  keepSaveLocked = true;
4859
5329
  let retryPromise = null;
4860
5330
  this.treeSingleSaveReconcileTask = () => {
@@ -4884,6 +5354,7 @@ modules = {
4884
5354
  return false;
4885
5355
  }
4886
5356
  } catch (error) {
5357
+ this.handleRowSaveError(error, obj.row);
4887
5358
  if (error?.treeReloadFailed) {
4888
5359
  keepSaveLocked = true;
4889
5360
  installReloadRetry();
@@ -4904,6 +5375,18 @@ modules = {
4904
5375
  }
4905
5376
  }
4906
5377
  },
5378
+ /** @param {Error} error 保存异常。@param {Object} rowData 当前行。执行保存失败脚本。 */
5379
+ handleRowSaveError(error, rowData) {
5380
+ try {
5381
+ this.handleCustomEvent(
5382
+ this.widget.options.onRowSaveError,
5383
+ ["error", "rowData"],
5384
+ [error, rowData]
5385
+ );
5386
+ } catch (hookError) {
5387
+ // 自定义失败脚本自身报错时保留原保存异常,由统一错误提示负责展示。
5388
+ }
5389
+ },
4907
5390
  /** @param {*} dataId 行数据 id。@param {Function} callback 查询成功回调。加载保存后的完整行。 */
4908
5391
  getRowData(dataId, callback, fail, error) {
4909
5392
  let reportTemplate = this.getFormRef()?.reportTemplate;
@@ -5235,6 +5718,9 @@ modules = {
5235
5718
  * @param {Object} obj VXE 行参数。
5236
5719
  */
5237
5720
  removeTreeRow(obj) {
5721
+ if (!this.canDeleteTableRow(obj)) {
5722
+ return false;
5723
+ }
5238
5724
  let row = obj.row;
5239
5725
  let tableRows = this.getValue() || [];
5240
5726
  let delKeys = this.collectDescendantKeys(row, tableRows);
@@ -5248,6 +5734,9 @@ modules = {
5248
5734
  * @param {Object} obj VXE 行参数。
5249
5735
  */
5250
5736
  removeChildTreeRows(obj) {
5737
+ if (!this.canDeleteTableRow(obj)) {
5738
+ return false;
5739
+ }
5251
5740
  let row = obj.row;
5252
5741
  let tableRows = this.getValue() || [];
5253
5742
  let delKeys = this.collectDescendantKeys(row, tableRows);
@@ -5281,8 +5770,17 @@ modules = {
5281
5770
  },
5282
5771
  /** @param {Object} obj VXE 单元格参数。@returns {Object} 行新增/编辑按钮配置。 */
5283
5772
  getEditTreeButtonGroupConfig(obj) {
5284
- let columnConfig = obj?.column?.params?.columnConfig || {};
5285
5773
  let tableOptions = this.widget.options || {};
5774
+ let columnConfig = obj?.column?.params?.columnConfig || {};
5775
+ if (columnConfig.formatS !== "editTreeButtonGroup") {
5776
+ let actionColumn = null;
5777
+ this.loodHandleColumns(tableOptions.tableColumns || [], (column) => {
5778
+ if (!actionColumn && column?.formatS === "editTreeButtonGroup") {
5779
+ actionColumn = column;
5780
+ }
5781
+ });
5782
+ columnConfig = actionColumn || columnConfig;
5783
+ }
5286
5784
  return {
5287
5785
  rowAddAuthName: columnConfig.rowAddAuthName,
5288
5786
  rowEditAuthName: columnConfig.rowEditAuthName,
@@ -5314,11 +5812,30 @@ modules = {
5314
5812
  let result = this.handleCustomEvent(script, ["tableParam"], [obj]);
5315
5813
  return result !== false;
5316
5814
  },
5815
+ /** @param {String|Function} script 行级条件脚本。@param {Object} obj VXE 行参数。@returns {Boolean} */
5816
+ isTableRowConditionAllowed(script, obj) {
5817
+ if (!script) {
5818
+ return true;
5819
+ }
5820
+ try {
5821
+ return this.handleCustomEvent(
5822
+ script,
5823
+ ["tableParam"],
5824
+ [obj]
5825
+ ) !== false;
5826
+ } catch (error) {
5827
+ return false;
5828
+ }
5829
+ },
5317
5830
  /** @param {Object} obj VXE 行参数。@returns {Boolean} 是否显示行新增按钮。 */
5318
5831
  canShowRowAdd(obj) {
5319
5832
  return (
5320
- this.hasTableRowPermission(this.getRowAddAuthName(obj)) &&
5321
- this.isRowButtonShow(this.getRowAddShow(obj), obj)
5833
+ this.hasTableRowPermission(this.getRowAddAuthName(obj))
5834
+ && this.isRowButtonShow(this.getRowAddShow(obj), obj)
5835
+ && this.isTableRowConditionAllowed(
5836
+ this.widget.options.rowAddableScript,
5837
+ obj
5838
+ )
5322
5839
  );
5323
5840
  },
5324
5841
  /** @param {Object} obj VXE 行参数。@returns {Boolean} 是否显示行编辑按钮。 */
@@ -5326,10 +5843,50 @@ modules = {
5326
5843
  let isAddRow = !this.hasSaveRow(obj?.row);
5327
5844
  if (isAddRow) return true;
5328
5845
  return (
5329
- this.hasTableRowPermission(this.getRowEditAuthName(obj)) &&
5330
- this.isRowButtonShow(this.getRowEditShow(obj), obj)
5846
+ this.hasTableRowPermission(this.getRowEditAuthName(obj))
5847
+ && this.isRowButtonShow(this.getRowEditShow(obj), obj)
5848
+ && this.isTableRowConditionAllowed(
5849
+ this.widget.options.rowEditableScript,
5850
+ obj
5851
+ )
5331
5852
  );
5332
5853
  },
5854
+ /** @param {Object} obj VXE 行参数。@returns {Boolean} 当前行是否允许删除。 */
5855
+ canDeleteTableRow(obj) {
5856
+ return this.isTableRowConditionAllowed(
5857
+ this.widget.options.rowDeletableScript,
5858
+ obj
5859
+ );
5860
+ },
5861
+ /**
5862
+ * @param {Object} obj VXE 单元格参数。
5863
+ * @param {Boolean} useEditSnapshot true 时按进入编辑时的原始状态判断。
5864
+ * @returns {Boolean} 当前行、当前字段是否允许修改。
5865
+ */
5866
+ isTableCellEditable(obj, useEditSnapshot = false) {
5867
+ const rowParams = useEditSnapshot && obj?.$table?.editCloneRow
5868
+ ? { ...obj, row: obj.$table.editCloneRow }
5869
+ : obj;
5870
+ if (!this.canShowRowEdit(rowParams)) {
5871
+ return false;
5872
+ }
5873
+ const columnConfig = obj?.column?.params?.columnConfig || {};
5874
+ const script = columnConfig.cellEditable;
5875
+ if (!script) {
5876
+ return true;
5877
+ }
5878
+ try {
5879
+ const result = this.handleCustomEvent(
5880
+ script,
5881
+ ["tableParam", "columnConfig"],
5882
+ [rowParams, columnConfig]
5883
+ );
5884
+ return result !== false;
5885
+ } catch (error) {
5886
+ // 条件脚本异常时按不可编辑处理,避免渲染阶段放开字段修改。
5887
+ return false;
5888
+ }
5889
+ },
5333
5890
  /** @param {Object} obj VXE 行参数。保存编辑前快照并激活该行。 */
5334
5891
  editRowEvent(obj) {
5335
5892
  if (!this.canShowRowEdit(obj)) {
@@ -5578,6 +6135,9 @@ modules = {
5578
6135
  * @param {Object} obj VXE 行参数。
5579
6136
  */
5580
6137
  async removeEditRow(obj) {
6138
+ if (!this.canDeleteTableRow(obj)) {
6139
+ return false;
6140
+ }
5581
6141
  let row = obj.row;
5582
6142
  let $grid = this.getGridTable();
5583
6143
  await $grid.remove(row);