@tmagic/editor 1.8.0 → 1.8.1

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.
@@ -1,4 +1,5 @@
1
1
  import { useServices } from "../../hooks/use-services.js";
2
+ import { resolveFieldByPath } from "../../utils/data-source/index.js";
2
3
  import Icon_default from "../../components/Icon.js";
3
4
  import DatasourceIcon_default from "../../icons/DatasourceIcon.js";
4
5
  import FieldSelect_default from "./FieldSelect.js";
@@ -86,15 +87,11 @@ var Index_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComp
86
87
  emit("change", value, eventData);
87
88
  return;
88
89
  }
89
- let fields = dataSource.fields || [];
90
- let field;
91
- (keys || []).forEach((key) => {
92
- field = fields.find((f) => f.name === key);
93
- fields = field?.fields || [];
94
- });
90
+ const { field, ok, untypedArrayElement } = resolveFieldByPath(dataSource.fields, keys, { allowArrayIndex: true });
95
91
  const dataSourceFieldType = props.config.dataSourceFieldType || ["any"];
96
92
  if (!dataSourceFieldType.length) dataSourceFieldType.push("any");
97
- if (!keys.length || field?.type && (field.type === "any" || dataSourceFieldType.includes("any") || dataSourceFieldType.includes(field.type))) emit("change", value, eventData);
93
+ const typeMatches = ok && !!field?.type && (dataSourceFieldType.includes("any") || !untypedArrayElement && (field.type === "any" || dataSourceFieldType.includes(field.type)));
94
+ if (!keys.length || typeMatches) emit("change", value, eventData);
98
95
  else {
99
96
  tMagicMessage.error(`请选择类型为${dataSourceFieldType.join("或")}的字段`);
100
97
  emit("change", [dsId], eventData);
@@ -1,4 +1,4 @@
1
- import { getCascaderOptionsFromFields, getFieldType } from "../../utils/data-source/index.js";
1
+ import { getCascaderOptionsFromFields, getFieldType, resolveFieldByPath } from "../../utils/data-source/index.js";
2
2
  import dataSource_default from "../../services/dataSource.js";
3
3
  import { stickyAddButton } from "./stickyAddButton.js";
4
4
  import { removeDataSourceFieldPrefix } from "@tmagic/utils";
@@ -34,11 +34,8 @@ var createDisplayCondsConfig = (config, name, parentFields) => {
34
34
  options: () => {
35
35
  const { ds, fieldNames } = resolveFieldPath(parentFields);
36
36
  if (!ds) return [];
37
- let fields = ds.fields || [];
38
- fieldNames.forEach((key) => {
39
- fields = fields.find((f) => f.name === key)?.fields || [];
40
- });
41
- return getCascaderOptionsFromFields(fields, [
37
+ const resolved = resolveFieldByPath(ds.fields, fieldNames, { allowArrayIndex: true });
38
+ return getCascaderOptionsFromFields(resolved.ok ? resolved.fields : [], [
42
39
  "string",
43
40
  "number",
44
41
  "boolean",
@@ -1,6 +1,6 @@
1
1
  import base_default from "./formConfigs/base.js";
2
2
  import http_default from "./formConfigs/http.js";
3
- import { dataSourceTemplateRegExp, getKeysArray, isNumber } from "@tmagic/utils";
3
+ import { dataSourceTemplateRegExp, getKeysArray, isArrayIndex, isNumber } from "@tmagic/utils";
4
4
  //#region packages/editor/src/utils/data-source/index.ts
5
5
  var dataSourceFormConfig = {
6
6
  type: "tab",
@@ -192,25 +192,59 @@ var getCascaderOptionsFromFields = (fields = [], dataSourceFieldType = ["any"])
192
192
  /**
193
193
  * 按字段名路径下钻 DataSchema。
194
194
  * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
195
+ * @param allowArrayIndex 为 true 时识别数组下标。`number` 只能是下标;数字字符串先按当前层字段名匹配,没有同名字段时才当中下标
195
196
  */
196
197
  var resolveFieldByPath = (fields, fieldNames, options = {}) => {
197
198
  let currentFields = fields || [];
198
199
  let field;
200
+ /** 刚消费了数组下标,路径若在此结束则当前值是数组元素 */
201
+ let atArrayElement = false;
202
+ let arrayIndexName = "";
199
203
  for (const name of fieldNames) {
200
204
  if (options.skipNumberIndices && isNumber(name)) continue;
205
+ const segment = `${name}`;
206
+ if (options.allowArrayIndex && isArrayIndex(name) && field?.type === "array" && !atArrayElement) {
207
+ if (!(typeof name === "string" ? currentFields.find((item) => item.name === segment) : void 0)) {
208
+ atArrayElement = true;
209
+ arrayIndexName = segment;
210
+ continue;
211
+ }
212
+ }
213
+ if (options.allowArrayIndex && typeof name === "number") return {
214
+ ok: false,
215
+ fields: currentFields,
216
+ failedName: segment,
217
+ invalidArrayIndex: true
218
+ };
219
+ atArrayElement = false;
201
220
  if (!currentFields.length) return {
202
221
  ok: false,
203
222
  fields: currentFields,
204
- failedName: name
223
+ failedName: segment
205
224
  };
206
- field = currentFields.find((item) => item.name === name);
225
+ field = currentFields.find((item) => item.name === segment);
207
226
  if (!field) return {
208
227
  ok: false,
209
228
  fields: currentFields,
210
- failedName: name
229
+ failedName: segment
211
230
  };
212
231
  currentFields = field.fields || [];
213
232
  }
233
+ if (atArrayElement && field?.type === "array") {
234
+ const elementFields = field.fields || [];
235
+ const untypedArrayElement = elementFields.length === 0;
236
+ return {
237
+ ok: true,
238
+ arrayElement: true,
239
+ untypedArrayElement,
240
+ field: {
241
+ name: arrayIndexName,
242
+ type: untypedArrayElement ? "any" : "object",
243
+ fields: elementFields
244
+ },
245
+ fields: elementFields
246
+ };
247
+ }
214
248
  return {
215
249
  field,
216
250
  ok: true,
@@ -218,8 +252,8 @@ var resolveFieldByPath = (fields, fieldNames, options = {}) => {
218
252
  };
219
253
  };
220
254
  var getFieldType = (ds, fieldNames) => {
221
- const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
222
- if (!ok) return "";
255
+ const { ok, field, untypedArrayElement } = resolveFieldByPath(ds?.fields, fieldNames, { allowArrayIndex: true });
256
+ if (!ok || untypedArrayElement) return "";
223
257
  return field?.type || "";
224
258
  };
225
259
  //#endregion
@@ -3,7 +3,7 @@ import editor_default from "../services/editor.js";
3
3
  import { getFieldType, resolveFieldByPath } from "./data-source/index.js";
4
4
  import codeBlock_default from "../services/codeBlock.js";
5
5
  import dataSource_default from "../services/dataSource.js";
6
- import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_SET_DATA_METHOD_NAME, dataSourceTemplateRegExp, getKeysArray, removeDataSourceFieldPrefix } from "@tmagic/utils";
6
+ import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_SET_DATA_METHOD_NAME, dataSourceTemplateRegExp, getKeysArray, isArrayIndex, removeDataSourceFieldPrefix } from "@tmagic/utils";
7
7
  import { NodeType } from "@tmagic/core";
8
8
  import { appendValidateSuggestion } from "@tmagic/design/headless";
9
9
  import { MAX_SUGGESTION_OPTIONS, optionSuggestion, stringifyExampleValue, validateTypeMatch } from "@tmagic/form/headless";
@@ -157,14 +157,18 @@ var validateDataSourceFieldPath = (path, options = {}) => {
157
157
  const ds = findDataSource(`${dsId}`);
158
158
  if (!ds) return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
159
159
  if (!path.length) return;
160
- const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path);
161
- if (!ok) return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
160
+ const { field, ok, fields, failedName, invalidArrayIndex, arrayElement, untypedArrayElement } = resolveFieldByPath(ds.fields, path, { allowArrayIndex: true });
161
+ if (!ok) {
162
+ if (invalidArrayIndex) return defaultMessage(options.message, `数组下标(${failedName})只能接在数组字段后面`);
163
+ return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
164
+ }
162
165
  const allowedTypes = options.dataSourceFieldType || ["any"];
163
166
  if (!allowedTypes.length || allowedTypes.includes("any")) return;
167
+ if (untypedArrayElement) return defaultMessage(options.message, `数组下标(${field?.name ?? path[path.length - 1]})的元素类型未定义,请选择具体字段`);
164
168
  const leafType = field?.type || "any";
165
169
  if (leafType !== "any" && !allowedTypes.includes(leafType)) {
166
170
  const fieldName = field?.name || path[path.length - 1];
167
- return defaultMessage(options.message, `请选择类型为${allowedTypes.join("或")}的字段,字段(${fieldName})的类型为${leafType}`);
171
+ return defaultMessage(options.message, arrayElement ? `请选择类型为${allowedTypes.join("或")}的字段,数组元素的类型为${leafType}` : `请选择类型为${allowedTypes.join("或")}的字段,字段(${fieldName})的类型为${leafType}`);
168
172
  }
169
173
  };
170
174
  var validateDataSourceMethodTuple = (value, message) => {
@@ -238,8 +242,10 @@ var validateDataSourceInput = (value, { message }) => {
238
242
  return validateDataSourceTemplateBindings(value, message);
239
243
  };
240
244
  var validateDataSourceMethodSelect = (value, { message }) => validateDataSourceMethodTuple(value, message);
245
+ /** 字段名,或非负整数下标(number)。数字字符串本身是 string,走字段名分支。 */
246
+ var isDataSourceFieldPathSegment = (item) => typeof item === "string" || typeof item === "number" && isArrayIndex(item);
241
247
  var isDataSourceFieldPathValue = (value, config) => {
242
- if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== "string")) return false;
248
+ if (!Array.isArray(value) || !value.length || value.some((item) => !isDataSourceFieldPathSegment(item))) return false;
243
249
  if (config.dataSourceId) return true;
244
250
  if (config.value === "key") return true;
245
251
  return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
@@ -260,11 +266,13 @@ var validateDataSourceFieldSelect = (value, context, options) => {
260
266
  }
261
267
  }, message);
262
268
  }
263
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
269
+ if (!Array.isArray(value) || value.some((item) => !isDataSourceFieldPathSegment(item))) return defaultMessage(message, `${value}类型应为字符串数组,数组字段后可接数字下标`, dataSourceFieldPathSuggestion());
264
270
  let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : void 0;
265
271
  let fieldNames = value;
266
272
  if (!dataSourceId) {
267
- dataSourceId = value.length ? removeDataSourceFieldPrefix(value[0]) : void 0;
273
+ const dsId = value[0];
274
+ if (value.length && typeof dsId !== "string") return defaultMessage(message, `${value}类型应为字符串数组,数组字段后可接数字下标`, dataSourceFieldPathSuggestion());
275
+ dataSourceId = value.length ? removeDataSourceFieldPrefix(dsId) : void 0;
268
276
  fieldNames = value.slice(1);
269
277
  }
270
278
  return validateDataSourceFieldPath(fieldNames, {
@@ -8023,25 +8023,59 @@
8023
8023
  /**
8024
8024
  * 按字段名路径下钻 DataSchema。
8025
8025
  * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
8026
+ * @param allowArrayIndex 为 true 时识别数组下标。`number` 只能是下标;数字字符串先按当前层字段名匹配,没有同名字段时才当中下标
8026
8027
  */
8027
8028
  var resolveFieldByPath = (fields, fieldNames, options = {}) => {
8028
8029
  let currentFields = fields || [];
8029
8030
  let field;
8031
+ /** 刚消费了数组下标,路径若在此结束则当前值是数组元素 */
8032
+ let atArrayElement = false;
8033
+ let arrayIndexName = "";
8030
8034
  for (const name of fieldNames) {
8031
8035
  if (options.skipNumberIndices && (0, _tmagic_utils.isNumber)(name)) continue;
8036
+ const segment = `${name}`;
8037
+ if (options.allowArrayIndex && (0, _tmagic_utils.isArrayIndex)(name) && field?.type === "array" && !atArrayElement) {
8038
+ if (!(typeof name === "string" ? currentFields.find((item) => item.name === segment) : void 0)) {
8039
+ atArrayElement = true;
8040
+ arrayIndexName = segment;
8041
+ continue;
8042
+ }
8043
+ }
8044
+ if (options.allowArrayIndex && typeof name === "number") return {
8045
+ ok: false,
8046
+ fields: currentFields,
8047
+ failedName: segment,
8048
+ invalidArrayIndex: true
8049
+ };
8050
+ atArrayElement = false;
8032
8051
  if (!currentFields.length) return {
8033
8052
  ok: false,
8034
8053
  fields: currentFields,
8035
- failedName: name
8054
+ failedName: segment
8036
8055
  };
8037
- field = currentFields.find((item) => item.name === name);
8056
+ field = currentFields.find((item) => item.name === segment);
8038
8057
  if (!field) return {
8039
8058
  ok: false,
8040
8059
  fields: currentFields,
8041
- failedName: name
8060
+ failedName: segment
8042
8061
  };
8043
8062
  currentFields = field.fields || [];
8044
8063
  }
8064
+ if (atArrayElement && field?.type === "array") {
8065
+ const elementFields = field.fields || [];
8066
+ const untypedArrayElement = elementFields.length === 0;
8067
+ return {
8068
+ ok: true,
8069
+ arrayElement: true,
8070
+ untypedArrayElement,
8071
+ field: {
8072
+ name: arrayIndexName,
8073
+ type: untypedArrayElement ? "any" : "object",
8074
+ fields: elementFields
8075
+ },
8076
+ fields: elementFields
8077
+ };
8078
+ }
8045
8079
  return {
8046
8080
  field,
8047
8081
  ok: true,
@@ -8049,8 +8083,8 @@
8049
8083
  };
8050
8084
  };
8051
8085
  var getFieldType = (ds, fieldNames) => {
8052
- const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
8053
- if (!ok) return "";
8086
+ const { ok, field, untypedArrayElement } = resolveFieldByPath(ds?.fields, fieldNames, { allowArrayIndex: true });
8087
+ if (!ok || untypedArrayElement) return "";
8054
8088
  return field?.type || "";
8055
8089
  };
8056
8090
  //#endregion
@@ -8628,14 +8662,18 @@
8628
8662
  const ds = findDataSource(`${dsId}`);
8629
8663
  if (!ds) return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
8630
8664
  if (!path.length) return;
8631
- const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path);
8632
- if (!ok) return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
8665
+ const { field, ok, fields, failedName, invalidArrayIndex, arrayElement, untypedArrayElement } = resolveFieldByPath(ds.fields, path, { allowArrayIndex: true });
8666
+ if (!ok) {
8667
+ if (invalidArrayIndex) return defaultMessage(options.message, `数组下标(${failedName})只能接在数组字段后面`);
8668
+ return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
8669
+ }
8633
8670
  const allowedTypes = options.dataSourceFieldType || ["any"];
8634
8671
  if (!allowedTypes.length || allowedTypes.includes("any")) return;
8672
+ if (untypedArrayElement) return defaultMessage(options.message, `数组下标(${field?.name ?? path[path.length - 1]})的元素类型未定义,请选择具体字段`);
8635
8673
  const leafType = field?.type || "any";
8636
8674
  if (leafType !== "any" && !allowedTypes.includes(leafType)) {
8637
8675
  const fieldName = field?.name || path[path.length - 1];
8638
- return defaultMessage(options.message, `请选择类型为${allowedTypes.join("或")}的字段,字段(${fieldName})的类型为${leafType}`);
8676
+ return defaultMessage(options.message, arrayElement ? `请选择类型为${allowedTypes.join("或")}的字段,数组元素的类型为${leafType}` : `请选择类型为${allowedTypes.join("或")}的字段,字段(${fieldName})的类型为${leafType}`);
8639
8677
  }
8640
8678
  };
8641
8679
  var validateDataSourceMethodTuple = (value, message) => {
@@ -8708,8 +8746,10 @@
8708
8746
  return validateDataSourceTemplateBindings(value, message);
8709
8747
  };
8710
8748
  var validateDataSourceMethodSelect = (value, { message }) => validateDataSourceMethodTuple(value, message);
8749
+ /** 字段名,或非负整数下标(number)。数字字符串本身是 string,走字段名分支。 */
8750
+ var isDataSourceFieldPathSegment = (item) => typeof item === "string" || typeof item === "number" && (0, _tmagic_utils.isArrayIndex)(item);
8711
8751
  var isDataSourceFieldPathValue = (value, config) => {
8712
- if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== "string")) return false;
8752
+ if (!Array.isArray(value) || !value.length || value.some((item) => !isDataSourceFieldPathSegment(item))) return false;
8713
8753
  if (config.dataSourceId) return true;
8714
8754
  if (config.value === "key") return true;
8715
8755
  return `${value[0]}`.startsWith(_tmagic_utils.DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
@@ -8730,11 +8770,13 @@
8730
8770
  }
8731
8771
  }, message);
8732
8772
  }
8733
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
8773
+ if (!Array.isArray(value) || value.some((item) => !isDataSourceFieldPathSegment(item))) return defaultMessage(message, `${value}类型应为字符串数组,数组字段后可接数字下标`, dataSourceFieldPathSuggestion());
8734
8774
  let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : void 0;
8735
8775
  let fieldNames = value;
8736
8776
  if (!dataSourceId) {
8737
- dataSourceId = value.length ? (0, _tmagic_utils.removeDataSourceFieldPrefix)(value[0]) : void 0;
8777
+ const dsId = value[0];
8778
+ if (value.length && typeof dsId !== "string") return defaultMessage(message, `${value}类型应为字符串数组,数组字段后可接数字下标`, dataSourceFieldPathSuggestion());
8779
+ dataSourceId = value.length ? (0, _tmagic_utils.removeDataSourceFieldPrefix)(dsId) : void 0;
8738
8780
  fieldNames = value.slice(1);
8739
8781
  }
8740
8782
  return validateDataSourceFieldPath(fieldNames, {
@@ -8967,11 +9009,8 @@
8967
9009
  options: () => {
8968
9010
  const { ds, fieldNames } = resolveFieldPath(parentFields);
8969
9011
  if (!ds) return [];
8970
- let fields = ds.fields || [];
8971
- fieldNames.forEach((key) => {
8972
- fields = fields.find((f) => f.name === key)?.fields || [];
8973
- });
8974
- return getCascaderOptionsFromFields(fields, [
9012
+ const resolved = resolveFieldByPath(ds.fields, fieldNames, { allowArrayIndex: true });
9013
+ return getCascaderOptionsFromFields(resolved.ok ? resolved.fields : [], [
8975
9014
  "string",
8976
9015
  "number",
8977
9016
  "boolean",
@@ -9630,25 +9630,59 @@
9630
9630
  /**
9631
9631
  * 按字段名路径下钻 DataSchema。
9632
9632
  * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
9633
+ * @param allowArrayIndex 为 true 时识别数组下标。`number` 只能是下标;数字字符串先按当前层字段名匹配,没有同名字段时才当中下标
9633
9634
  */
9634
9635
  var resolveFieldByPath = (fields, fieldNames, options = {}) => {
9635
9636
  let currentFields = fields || [];
9636
9637
  let field;
9638
+ /** 刚消费了数组下标,路径若在此结束则当前值是数组元素 */
9639
+ let atArrayElement = false;
9640
+ let arrayIndexName = "";
9637
9641
  for (const name of fieldNames) {
9638
9642
  if (options.skipNumberIndices && (0, _tmagic_utils.isNumber)(name)) continue;
9643
+ const segment = `${name}`;
9644
+ if (options.allowArrayIndex && (0, _tmagic_utils.isArrayIndex)(name) && field?.type === "array" && !atArrayElement) {
9645
+ if (!(typeof name === "string" ? currentFields.find((item) => item.name === segment) : void 0)) {
9646
+ atArrayElement = true;
9647
+ arrayIndexName = segment;
9648
+ continue;
9649
+ }
9650
+ }
9651
+ if (options.allowArrayIndex && typeof name === "number") return {
9652
+ ok: false,
9653
+ fields: currentFields,
9654
+ failedName: segment,
9655
+ invalidArrayIndex: true
9656
+ };
9657
+ atArrayElement = false;
9639
9658
  if (!currentFields.length) return {
9640
9659
  ok: false,
9641
9660
  fields: currentFields,
9642
- failedName: name
9661
+ failedName: segment
9643
9662
  };
9644
- field = currentFields.find((item) => item.name === name);
9663
+ field = currentFields.find((item) => item.name === segment);
9645
9664
  if (!field) return {
9646
9665
  ok: false,
9647
9666
  fields: currentFields,
9648
- failedName: name
9667
+ failedName: segment
9649
9668
  };
9650
9669
  currentFields = field.fields || [];
9651
9670
  }
9671
+ if (atArrayElement && field?.type === "array") {
9672
+ const elementFields = field.fields || [];
9673
+ const untypedArrayElement = elementFields.length === 0;
9674
+ return {
9675
+ ok: true,
9676
+ arrayElement: true,
9677
+ untypedArrayElement,
9678
+ field: {
9679
+ name: arrayIndexName,
9680
+ type: untypedArrayElement ? "any" : "object",
9681
+ fields: elementFields
9682
+ },
9683
+ fields: elementFields
9684
+ };
9685
+ }
9652
9686
  return {
9653
9687
  field,
9654
9688
  ok: true,
@@ -9656,8 +9690,8 @@
9656
9690
  };
9657
9691
  };
9658
9692
  var getFieldType = (ds, fieldNames) => {
9659
- const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
9660
- if (!ok) return "";
9693
+ const { ok, field, untypedArrayElement } = resolveFieldByPath(ds?.fields, fieldNames, { allowArrayIndex: true });
9694
+ if (!ok || untypedArrayElement) return "";
9661
9695
  return field?.type || "";
9662
9696
  };
9663
9697
  //#endregion
@@ -10993,14 +11027,18 @@
10993
11027
  const ds = findDataSource(`${dsId}`);
10994
11028
  if (!ds) return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
10995
11029
  if (!path.length) return;
10996
- const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path);
10997
- if (!ok) return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
11030
+ const { field, ok, fields, failedName, invalidArrayIndex, arrayElement, untypedArrayElement } = resolveFieldByPath(ds.fields, path, { allowArrayIndex: true });
11031
+ if (!ok) {
11032
+ if (invalidArrayIndex) return defaultMessage(options.message, `数组下标(${failedName})只能接在数组字段后面`);
11033
+ return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
11034
+ }
10998
11035
  const allowedTypes = options.dataSourceFieldType || ["any"];
10999
11036
  if (!allowedTypes.length || allowedTypes.includes("any")) return;
11037
+ if (untypedArrayElement) return defaultMessage(options.message, `数组下标(${field?.name ?? path[path.length - 1]})的元素类型未定义,请选择具体字段`);
11000
11038
  const leafType = field?.type || "any";
11001
11039
  if (leafType !== "any" && !allowedTypes.includes(leafType)) {
11002
11040
  const fieldName = field?.name || path[path.length - 1];
11003
- return defaultMessage(options.message, `请选择类型为${allowedTypes.join("或")}的字段,字段(${fieldName})的类型为${leafType}`);
11041
+ return defaultMessage(options.message, arrayElement ? `请选择类型为${allowedTypes.join("或")}的字段,数组元素的类型为${leafType}` : `请选择类型为${allowedTypes.join("或")}的字段,字段(${fieldName})的类型为${leafType}`);
11004
11042
  }
11005
11043
  };
11006
11044
  var validateDataSourceMethodTuple = (value, message) => {
@@ -11073,8 +11111,10 @@
11073
11111
  return validateDataSourceTemplateBindings(value, message);
11074
11112
  };
11075
11113
  var validateDataSourceMethodSelect = (value, { message }) => validateDataSourceMethodTuple(value, message);
11114
+ /** 字段名,或非负整数下标(number)。数字字符串本身是 string,走字段名分支。 */
11115
+ var isDataSourceFieldPathSegment = (item) => typeof item === "string" || typeof item === "number" && (0, _tmagic_utils.isArrayIndex)(item);
11076
11116
  var isDataSourceFieldPathValue = (value, config) => {
11077
- if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== "string")) return false;
11117
+ if (!Array.isArray(value) || !value.length || value.some((item) => !isDataSourceFieldPathSegment(item))) return false;
11078
11118
  if (config.dataSourceId) return true;
11079
11119
  if (config.value === "key") return true;
11080
11120
  return `${value[0]}`.startsWith(_tmagic_utils.DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
@@ -11095,11 +11135,13 @@
11095
11135
  }
11096
11136
  }, message);
11097
11137
  }
11098
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
11138
+ if (!Array.isArray(value) || value.some((item) => !isDataSourceFieldPathSegment(item))) return defaultMessage(message, `${value}类型应为字符串数组,数组字段后可接数字下标`, dataSourceFieldPathSuggestion());
11099
11139
  let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : void 0;
11100
11140
  let fieldNames = value;
11101
11141
  if (!dataSourceId) {
11102
- dataSourceId = value.length ? (0, _tmagic_utils.removeDataSourceFieldPrefix)(value[0]) : void 0;
11142
+ const dsId = value[0];
11143
+ if (value.length && typeof dsId !== "string") return defaultMessage(message, `${value}类型应为字符串数组,数组字段后可接数字下标`, dataSourceFieldPathSuggestion());
11144
+ dataSourceId = value.length ? (0, _tmagic_utils.removeDataSourceFieldPrefix)(dsId) : void 0;
11103
11145
  fieldNames = value.slice(1);
11104
11146
  }
11105
11147
  return validateDataSourceFieldPath(fieldNames, {
@@ -22885,15 +22927,11 @@
22885
22927
  emit("change", value, eventData);
22886
22928
  return;
22887
22929
  }
22888
- let fields = dataSource.fields || [];
22889
- let field;
22890
- (keys || []).forEach((key) => {
22891
- field = fields.find((f) => f.name === key);
22892
- fields = field?.fields || [];
22893
- });
22930
+ const { field, ok, untypedArrayElement } = resolveFieldByPath(dataSource.fields, keys, { allowArrayIndex: true });
22894
22931
  const dataSourceFieldType = props.config.dataSourceFieldType || ["any"];
22895
22932
  if (!dataSourceFieldType.length) dataSourceFieldType.push("any");
22896
- if (!keys.length || field?.type && (field.type === "any" || dataSourceFieldType.includes("any") || dataSourceFieldType.includes(field.type))) emit("change", value, eventData);
22933
+ const typeMatches = ok && !!field?.type && (dataSourceFieldType.includes("any") || !untypedArrayElement && (field.type === "any" || dataSourceFieldType.includes(field.type)));
22934
+ if (!keys.length || typeMatches) emit("change", value, eventData);
22897
22935
  else {
22898
22936
  _tmagic_design.tMagicMessage.error(`请选择类型为${dataSourceFieldType.join("或")}的字段`);
22899
22937
  emit("change", [dsId], eventData);
@@ -23650,11 +23688,8 @@
23650
23688
  options: () => {
23651
23689
  const { ds, fieldNames } = resolveFieldPath(parentFields);
23652
23690
  if (!ds) return [];
23653
- let fields = ds.fields || [];
23654
- fieldNames.forEach((key) => {
23655
- fields = fields.find((f) => f.name === key)?.fields || [];
23656
- });
23657
- return getCascaderOptionsFromFields(fields, [
23691
+ const resolved = resolveFieldByPath(ds.fields, fieldNames, { allowArrayIndex: true });
23692
+ return getCascaderOptionsFromFields(resolved.ok ? resolved.fields : [], [
23658
23693
  "string",
23659
23694
  "number",
23660
23695
  "boolean",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0",
2
+ "version": "1.8.1",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -63,11 +63,11 @@
63
63
  "moveable": "^0.53.0",
64
64
  "serialize-javascript": "^7.1.1",
65
65
  "sortablejs": "^1.15.7",
66
- "@tmagic/design": "1.8.0",
67
- "@tmagic/form": "1.8.0",
68
- "@tmagic/stage": "1.8.0",
69
- "@tmagic/utils": "1.8.0",
70
- "@tmagic/table": "1.8.0"
66
+ "@tmagic/design": "1.8.1",
67
+ "@tmagic/form": "1.8.1",
68
+ "@tmagic/stage": "1.8.1",
69
+ "@tmagic/table": "1.8.1",
70
+ "@tmagic/utils": "1.8.1"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/events": "^3.0.3",
@@ -81,7 +81,7 @@
81
81
  "type-fest": "^5.2.0",
82
82
  "typescript": "^6.0.3",
83
83
  "vue": "^3.5.43",
84
- "@tmagic/core": "1.8.0"
84
+ "@tmagic/core": "1.8.1"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "typescript": {
@@ -51,7 +51,6 @@
51
51
  <script setup lang="ts">
52
52
  import { computed, inject, ref, resolveComponent, watch } from 'vue';
53
53
 
54
- import { DataSchema } from '@tmagic/core';
55
54
  import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
56
55
  import {
57
56
  type ContainerChangeEventData,
@@ -64,6 +63,7 @@ import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } f
64
63
 
65
64
  import MIcon from '@editor/components/Icon.vue';
66
65
  import { useServices } from '@editor/hooks/use-services';
66
+ import { resolveFieldByPath } from '@editor/utils/data-source';
67
67
 
68
68
  import dataSourceIcon from '../../icons/DatasourceIcon.vue';
69
69
 
@@ -175,11 +175,8 @@ const onChangeHandler = (value: string[], eventData?: ContainerChangeEventData)
175
175
  return;
176
176
  }
177
177
 
178
- let fields = dataSource.fields || [];
179
- let field: DataSchema | undefined;
180
- (keys || []).forEach((key) => {
181
- field = fields.find((f) => f.name === key);
182
- fields = field?.fields || [];
178
+ const { field, ok, untypedArrayElement } = resolveFieldByPath(dataSource.fields, keys, {
179
+ allowArrayIndex: true,
183
180
  });
184
181
 
185
182
  const dataSourceFieldType = props.config.dataSourceFieldType || ['any'];
@@ -187,11 +184,14 @@ const onChangeHandler = (value: string[], eventData?: ContainerChangeEventData)
187
184
  dataSourceFieldType.push('any');
188
185
  }
189
186
 
190
- if (
191
- !keys.length ||
192
- (field?.type &&
193
- (field.type === 'any' || dataSourceFieldType.includes('any') || dataSourceFieldType.includes(field.type)))
194
- ) {
187
+ // 未声明元素结构的下标不是显式 any,限定了具体类型时不放行
188
+ const typeMatches =
189
+ ok &&
190
+ !!field?.type &&
191
+ (dataSourceFieldType.includes('any') ||
192
+ (!untypedArrayElement && (field.type === 'any' || dataSourceFieldType.includes(field.type))));
193
+
194
+ if (!keys.length || typeMatches) {
195
195
  emit('change', value, eventData);
196
196
  } else {
197
197
  tMagicMessage.error(`请选择类型为${dataSourceFieldType.join('或')}的字段`);
@@ -20,7 +20,7 @@ import type { DisplayCondsConfig, FormState, GroupListConfig } from '@tmagic/for
20
20
  import { removeDataSourceFieldPrefix } from '@tmagic/utils';
21
21
 
22
22
  import dataSourceService from '@editor/services/dataSource';
23
- import { getCascaderOptionsFromFields, getFieldType } from '@editor/utils/data-source';
23
+ import { getCascaderOptionsFromFields, getFieldType, resolveFieldByPath } from '@editor/utils/data-source';
24
24
 
25
25
  import { stickyAddButton } from './stickyAddButton';
26
26
 
@@ -38,7 +38,7 @@ export const createDisplayCondsConfig = (
38
38
  name: string,
39
39
  parentFields: string[],
40
40
  ): GroupListConfig => {
41
- const resolveFieldPath = (path: string[]) => {
41
+ const resolveFieldPath = (path: Array<string | number>) => {
42
42
  const [id, ...fieldNames] = path;
43
43
  const ds = id ? dataSourceService.getDataSourceById(removeDataSourceFieldPrefix(`${id}`)) : undefined;
44
44
  return { ds, fieldNames };
@@ -69,13 +69,13 @@ export const createDisplayCondsConfig = (
69
69
  return [];
70
70
  }
71
71
 
72
- let fields = ds.fields || [];
73
- fieldNames.forEach((key) => {
74
- const field = fields.find((f) => f.name === key);
75
- fields = field?.fields || [];
76
- });
77
-
78
- return getCascaderOptionsFromFields(fields, ['string', 'number', 'boolean', 'any']);
72
+ const resolved = resolveFieldByPath(ds.fields, fieldNames, { allowArrayIndex: true });
73
+ return getCascaderOptionsFromFields(resolved.ok ? resolved.fields : [], [
74
+ 'string',
75
+ 'number',
76
+ 'boolean',
77
+ 'any',
78
+ ]);
79
79
  },
80
80
  name: 'field',
81
81
  value: 'key',
@@ -1,6 +1,6 @@
1
1
  import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
2
2
  import { type CascaderOption, type FormConfig, type TabConfig } from '@tmagic/form';
3
- import { dataSourceTemplateRegExp, getKeysArray, isNumber } from '@tmagic/utils';
3
+ import { dataSourceTemplateRegExp, getKeysArray, isArrayIndex, isNumber } from '@tmagic/utils';
4
4
 
5
5
  import BaseFormConfig from './formConfigs/base';
6
6
  import HttpFormConfig from './formConfigs/http';
@@ -239,34 +239,86 @@ export const getCascaderOptionsFromFields = (
239
239
  /**
240
240
  * 按字段名路径下钻 DataSchema。
241
241
  * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
242
+ * @param allowArrayIndex 为 true 时识别数组下标。`number` 只能是下标;数字字符串先按当前层字段名匹配,没有同名字段时才当中下标
242
243
  */
243
244
  export const resolveFieldByPath = (
244
245
  fields: DataSchema[] | undefined,
245
- fieldNames: string[],
246
- options: { skipNumberIndices?: boolean } = {},
247
- ): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => {
246
+ fieldNames: Array<string | number>,
247
+ options: { skipNumberIndices?: boolean; allowArrayIndex?: boolean } = {},
248
+ ): {
249
+ ok: boolean;
250
+ field?: DataSchema;
251
+ fields: DataSchema[];
252
+ failedName?: string;
253
+ /** number 下标没有紧跟在数组字段后面 */
254
+ invalidArrayIndex?: boolean;
255
+ /** 路径停在数组下标上,当前值是数组元素 */
256
+ arrayElement?: boolean;
257
+ /** 数组没有子字段,元素类型未知 */
258
+ untypedArrayElement?: boolean;
259
+ } => {
248
260
  let currentFields = fields || [];
249
261
  let field: DataSchema | undefined;
262
+ /** 刚消费了数组下标,路径若在此结束则当前值是数组元素 */
263
+ let atArrayElement = false;
264
+ let arrayIndexName = '';
250
265
 
251
266
  for (const name of fieldNames) {
252
267
  if (options.skipNumberIndices && isNumber(name)) {
253
268
  continue;
254
269
  }
270
+
271
+ const segment = `${name}`;
272
+ // number 不能当字段名。数字字符串仅在当前层没有同名字段时才当中下标,避免盖住名为 "0" 的子字段。
273
+ if (options.allowArrayIndex && isArrayIndex(name) && field?.type === 'array' && !atArrayElement) {
274
+ const namedField = typeof name === 'string' ? currentFields.find((item) => item.name === segment) : undefined;
275
+ if (!namedField) {
276
+ atArrayElement = true;
277
+ arrayIndexName = segment;
278
+ continue;
279
+ }
280
+ }
281
+
282
+ if (options.allowArrayIndex && typeof name === 'number') {
283
+ return { ok: false, fields: currentFields, failedName: segment, invalidArrayIndex: true };
284
+ }
285
+
286
+ atArrayElement = false;
287
+
255
288
  if (!currentFields.length) {
256
- return { ok: false, fields: currentFields, failedName: name };
289
+ return { ok: false, fields: currentFields, failedName: segment };
257
290
  }
258
- field = currentFields.find((item) => item.name === name);
291
+ field = currentFields.find((item) => item.name === segment);
259
292
  if (!field) {
260
- return { ok: false, fields: currentFields, failedName: name };
293
+ return { ok: false, fields: currentFields, failedName: segment };
261
294
  }
262
295
  currentFields = field.fields || [];
263
296
  }
264
297
 
298
+ if (atArrayElement && field?.type === 'array') {
299
+ const elementFields = field.fields || [];
300
+ const untypedArrayElement = elementFields.length === 0;
301
+ return {
302
+ ok: true,
303
+ arrayElement: true,
304
+ untypedArrayElement,
305
+ field: {
306
+ name: arrayIndexName,
307
+ // 有子字段时元素是这些字段组成的对象;没有子字段时类型未知,不能当成显式 any
308
+ type: untypedArrayElement ? 'any' : 'object',
309
+ fields: elementFields,
310
+ },
311
+ fields: elementFields,
312
+ };
313
+ }
314
+
265
315
  return { field, ok: true, fields: currentFields };
266
316
  };
267
317
 
268
- export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
269
- const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
270
- if (!ok) return '';
318
+ export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: Array<string | number>) => {
319
+ const { ok, field, untypedArrayElement } = resolveFieldByPath(ds?.fields, fieldNames, {
320
+ allowArrayIndex: true,
321
+ });
322
+ if (!ok || untypedArrayElement) return '';
271
323
  return field?.type || '';
272
324
  };
@@ -31,6 +31,7 @@ import {
31
31
  DATA_SOURCE_SET_DATA_METHOD_NAME,
32
32
  dataSourceTemplateRegExp,
33
33
  getKeysArray,
34
+ isArrayIndex,
34
35
  removeDataSourceFieldPrefix,
35
36
  } from '@tmagic/utils';
36
37
 
@@ -211,7 +212,7 @@ const displayCondSuggestion = (): string => {
211
212
  };
212
213
 
213
214
  const validateDataSourceFieldPath = (
214
- path: string[],
215
+ path: Array<string | number>,
215
216
  options: {
216
217
  dataSourceId?: string;
217
218
  dataSourceFieldType?: DataSourceFieldType[];
@@ -239,8 +240,17 @@ const validateDataSourceFieldPath = (
239
240
  return undefined;
240
241
  }
241
242
 
242
- const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path);
243
+ const { field, ok, fields, failedName, invalidArrayIndex, arrayElement, untypedArrayElement } = resolveFieldByPath(
244
+ ds.fields,
245
+ path,
246
+ {
247
+ allowArrayIndex: true,
248
+ },
249
+ );
243
250
  if (!ok) {
251
+ if (invalidArrayIndex) {
252
+ return defaultMessage(options.message, `数组下标(${failedName})只能接在数组字段后面`);
253
+ }
244
254
  return defaultMessage(
245
255
  options.message,
246
256
  `数据源字段(${failedName})不存在`,
@@ -253,6 +263,13 @@ const validateDataSourceFieldPath = (
253
263
  return undefined;
254
264
  }
255
265
 
266
+ if (untypedArrayElement) {
267
+ return defaultMessage(
268
+ options.message,
269
+ `数组下标(${field?.name ?? path[path.length - 1]})的元素类型未定义,请选择具体字段`,
270
+ );
271
+ }
272
+
256
273
  const leafType = field?.type || 'any';
257
274
  if (leafType !== 'any' && !allowedTypes.includes(leafType)) {
258
275
  const fieldName = field?.name || path[path.length - 1];
@@ -260,7 +277,9 @@ const validateDataSourceFieldPath = (
260
277
  // 文案已点明当前字段类型与要求类型,无需再列同级可选字段
261
278
  return defaultMessage(
262
279
  options.message,
263
- `请选择类型为${allowedTypes.join('或')}的字段,字段(${fieldName})的类型为${leafType}`,
280
+ arrayElement
281
+ ? `请选择类型为${allowedTypes.join('或')}的字段,数组元素的类型为${leafType}`
282
+ : `请选择类型为${allowedTypes.join('或')}的字段,字段(${fieldName})的类型为${leafType}`,
264
283
  );
265
284
  }
266
285
  };
@@ -437,8 +456,12 @@ const validateDataSourceInput: TypeMatchValidator = (value, { message }) => {
437
456
  const validateDataSourceMethodSelect: TypeMatchValidator = (value, { message }) =>
438
457
  validateDataSourceMethodTuple(value, message);
439
458
 
440
- const isDataSourceFieldPathValue = (value: any, config: any): value is string[] => {
441
- if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== 'string')) {
459
+ /** 字段名,或非负整数下标(number)。数字字符串本身是 string,走字段名分支。 */
460
+ const isDataSourceFieldPathSegment = (item: unknown): item is string | number =>
461
+ typeof item === 'string' || (typeof item === 'number' && isArrayIndex(item));
462
+
463
+ const isDataSourceFieldPathValue = (value: any, config: any): value is Array<string | number> => {
464
+ if (!Array.isArray(value) || !value.length || value.some((item) => !isDataSourceFieldPathSegment(item))) {
442
465
  return false;
443
466
  }
444
467
  if (config.dataSourceId) {
@@ -488,16 +511,29 @@ export const validateDataSourceFieldSelect = (
488
511
  }
489
512
  }
490
513
 
491
- if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
492
- return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
514
+ if (!Array.isArray(value) || value.some((item) => !isDataSourceFieldPathSegment(item))) {
515
+ return defaultMessage(
516
+ message,
517
+ `${value}类型应为字符串数组,数组字段后可接数字下标`,
518
+ dataSourceFieldPathSuggestion(),
519
+ );
493
520
  }
494
521
 
495
522
  // 未指定 dataSourceId 时,路径首项为数据源 id,其余为字段名。
496
523
  // value 模式的首项带 ds-field:: 前缀、key 模式不带,removeDataSourceFieldPrefix 对后者是幂等的,无需分支。
524
+ // 数组下标只出现在字段段,数据源 id 仍必须是字符串。
497
525
  let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : undefined;
498
- let fieldNames = value;
526
+ let fieldNames: Array<string | number> = value;
499
527
  if (!dataSourceId) {
500
- dataSourceId = value.length ? removeDataSourceFieldPrefix(value[0]) : undefined;
528
+ const dsId = value[0];
529
+ if (value.length && typeof dsId !== 'string') {
530
+ return defaultMessage(
531
+ message,
532
+ `${value}类型应为字符串数组,数组字段后可接数字下标`,
533
+ dataSourceFieldPathSuggestion(),
534
+ );
535
+ }
536
+ dataSourceId = value.length ? removeDataSourceFieldPrefix(dsId) : undefined;
501
537
  fieldNames = value.slice(1);
502
538
  }
503
539
 
package/types/index.d.ts CHANGED
@@ -3261,16 +3261,24 @@ export declare const getCascaderOptionsFromFields: (fields?: DataSchema[], dataS
3261
3261
  /**
3262
3262
  * 按字段名路径下钻 DataSchema。
3263
3263
  * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
3264
+ * @param allowArrayIndex 为 true 时识别数组下标。`number` 只能是下标;数字字符串先按当前层字段名匹配,没有同名字段时才当中下标
3264
3265
  */
3265
- export declare const resolveFieldByPath: (fields: DataSchema[] | undefined, fieldNames: string[], options?: {
3266
+ export declare const resolveFieldByPath: (fields: DataSchema[] | undefined, fieldNames: Array<string | number>, options?: {
3266
3267
  skipNumberIndices?: boolean;
3268
+ allowArrayIndex?: boolean;
3267
3269
  }) => {
3268
3270
  ok: boolean;
3269
3271
  field?: DataSchema;
3270
3272
  fields: DataSchema[];
3271
3273
  failedName?: string;
3274
+ /** number 下标没有紧跟在数组字段后面 */
3275
+ invalidArrayIndex?: boolean;
3276
+ /** 路径停在数组下标上,当前值是数组元素 */
3277
+ arrayElement?: boolean;
3278
+ /** 数组没有子字段,元素类型未知 */
3279
+ untypedArrayElement?: boolean;
3272
3280
  };
3273
- export declare const getFieldType: (ds: DataSourceSchema | undefined, fieldNames: string[]) => "" | DataSourceFieldType;
3281
+ export declare const getFieldType: (ds: DataSourceSchema | undefined, fieldNames: Array<string | number>) => "" | DataSourceFieldType;
3274
3282
  //#endregion
3275
3283
  //#region temp/packages/editor/src/utils/dep/idle-task.d.ts
3276
3284
  export interface IdleTaskEvents {