@tmagic/editor 1.8.0-beta.23 → 1.8.0-beta.25

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 (43) hide show
  1. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +5 -4
  2. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +3 -2
  3. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +8 -5
  4. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +10 -6
  5. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +6 -4
  6. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +4 -1
  7. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +6 -4
  8. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +12 -7
  9. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +2 -2
  10. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +4 -2
  11. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +2 -2
  12. package/dist/es/hooks/use-compare-form.js +1 -2
  13. package/dist/es/index.js +3 -3
  14. package/dist/es/plugin.js +9 -1
  15. package/dist/es/services/editor.js +43 -37
  16. package/dist/es/services/props.js +1 -1
  17. package/dist/es/style.css +4 -0
  18. package/dist/es/utils/event.js +1 -1
  19. package/dist/es/utils/props.js +1 -29
  20. package/dist/es/utils/type-match-rules.js +21 -20
  21. package/dist/style.css +4 -0
  22. package/dist/themes/magic-admin.css +4 -0
  23. package/dist/tmagic-editor.umd.cjs +412 -398
  24. package/package.json +8 -8
  25. package/src/fields/CodeSelectCol.vue +4 -2
  26. package/src/fields/CondOpSelect.vue +3 -2
  27. package/src/fields/DataSourceFieldSelect/Index.vue +9 -4
  28. package/src/fields/DataSourceFields.vue +6 -0
  29. package/src/fields/DataSourceMethodSelect.vue +4 -1
  30. package/src/fields/DataSourceMethods.vue +11 -2
  31. package/src/fields/DataSourceMocks.vue +10 -1
  32. package/src/fields/DisplayConds.vue +11 -7
  33. package/src/fields/EventSelect.vue +3 -3
  34. package/src/fields/KeyValue.vue +4 -3
  35. package/src/fields/StyleSetter/pro/Font.vue +2 -2
  36. package/src/hooks/use-compare-form.ts +1 -4
  37. package/src/plugin.ts +9 -1
  38. package/src/services/editor.ts +53 -85
  39. package/src/type.ts +23 -0
  40. package/src/utils/event.ts +2 -2
  41. package/src/utils/props.ts +0 -32
  42. package/src/utils/type-match-rules.ts +29 -28
  43. package/types/index.d.ts +39 -61
@@ -56,6 +56,7 @@ import type {
56
56
  StepValue,
57
57
  StoreState,
58
58
  StoreStateKey,
59
+ UpdateOptions,
59
60
  } from '@editor/type';
60
61
  import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
61
62
  import {
@@ -83,6 +84,10 @@ import { beforePaste, getAddParent } from '@editor/utils/operator';
83
84
 
84
85
  type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
85
86
 
87
+ /** 历史插回时把记录的下标收敛到 [0, length],越界(含未记录)一律追加到末尾 */
88
+ const clampIndex = (index: number | undefined, length: number): number =>
89
+ typeof index === 'number' && index >= 0 && index <= length ? index : length;
90
+
86
91
  /**
87
92
  * 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
88
93
  * `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
@@ -126,6 +131,11 @@ class Editor extends BaseService {
126
131
  * 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
127
132
  */
128
133
  private lastPushedHistoryId: string | null = null;
134
+ /**
135
+ * 上一次 doAdd 的插入记忆:nodeId 为插入的节点,selectedId 为当时的选中节点。
136
+ * 仅在选中节点未变化时复用(连续 add / doNotSelect / 批量粘贴),选中变化后自动失效,无需手动清理。
137
+ */
138
+ private lastAdded: { selectedId: Id; nodeId: Id } | null = null;
129
139
 
130
140
  constructor() {
131
141
  super(
@@ -404,7 +414,7 @@ class Editor extends BaseService {
404
414
  this.set('highlightNode', null);
405
415
  }
406
416
 
407
- public async doAdd(node: MNode, parent: MContainer): Promise<MNode> {
417
+ public async doAdd(node: MNode, parent: MContainer, _options: DslOpOptions = {}): Promise<MNode> {
408
418
  const root = this.get('root');
409
419
 
410
420
  if (!root) throw new Error('root为空');
@@ -418,13 +428,16 @@ class Editor extends BaseService {
418
428
  throw new Error('app下不能添加组件');
419
429
  }
420
430
 
421
- if (parent.id !== curNode.id && !isPageOrFragment(node)) {
422
- const index = parent.items.indexOf(curNode);
423
- parent.items?.splice(index + 1, 0, node);
424
- } else {
425
- // 新增节点添加到配置中
426
- parent.items?.push(node);
427
- }
431
+ // 连续 add 时接在上一个新增节点之后,否则接在当前选中节点之后;
432
+ // 锚点节点可能已不在 items 中(被删 / DSL 整体替换),回退到当前选中节点,都不在则追加到末尾
433
+ const anchorId = this.lastAdded?.selectedId === curNode.id ? this.lastAdded.nodeId : curNode.id;
434
+ const anchorIndex = isPageOrFragment(node)
435
+ ? -1
436
+ : Math.max(getNodeIndex(anchorId, parent), getNodeIndex(curNode.id, parent));
437
+ const insertIndex = anchorIndex < 0 ? parent.items.length : anchorIndex + 1;
438
+
439
+ parent.items.splice(insertIndex, 0, node);
440
+ this.lastAdded = { selectedId: curNode.id, nodeId: node.id };
428
441
 
429
442
  const layout = await this.getLayout(toRaw(parent), node as MNode);
430
443
  node.style = getInitPositionStyle(node.style, layout);
@@ -434,6 +447,7 @@ class Editor extends BaseService {
434
447
  parent: cloneDeep(parent),
435
448
  parentId: parent.id,
436
449
  root: cloneDeep(root),
450
+ index: insertIndex,
437
451
  });
438
452
 
439
453
  const newStyle = fixNodePosition(node, parent, stage);
@@ -452,24 +466,23 @@ class Editor extends BaseService {
452
466
  * 向指点容器添加组件节点
453
467
  * @param addConfig 将要添加的组件节点配置
454
468
  * @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
455
- * @param options 可选配置
456
- * @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
457
- * @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
458
- * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
469
+ * @param options 可选配置,见 {@link DslOpOptions}
459
470
  * @returns 添加后的节点
460
471
  */
461
472
  public async add(
462
473
  addNode: AddMNode | MNode[],
463
474
  parent?: MContainer | null,
464
- {
475
+ options: DslOpOptions = {},
476
+ ): Promise<MNode | MNode[]> {
477
+ this.captureSelectionBeforeOp();
478
+
479
+ const {
465
480
  doNotSelect = false,
466
481
  doNotSwitchPage = false,
467
482
  doNotPushHistory = false,
468
483
  historyDescription,
469
484
  historySource,
470
- }: DslOpOptions = {},
471
- ): Promise<MNode | MNode[]> {
472
- this.captureSelectionBeforeOp();
485
+ } = options;
473
486
 
474
487
  const stage = this.get('stage');
475
488
 
@@ -485,17 +498,15 @@ class Editor extends BaseService {
485
498
  addNodes.push(...addNode);
486
499
  }
487
500
 
488
- const newNodes = await Promise.all(
489
- addNodes.map((node) => {
490
- const root = this.get('root');
491
- if (isPageOrFragment(node) && root) {
492
- return this.doAdd(node, root);
493
- }
494
- const parentNode = parent ?? getAddParent(node);
495
- if (!parentNode) throw new Error('未找到父元素');
496
- return this.doAdd(node, parentNode);
497
- }),
498
- );
501
+ // 必须串行:每个节点的插入位置依赖上一个已插入的节点,并行时插入顺序会受
502
+ // doAdd 前置耗时(如异步 beforeDoAdd 插件钩子)影响而错乱
503
+ const newNodes: MNode[] = [];
504
+ for (const node of addNodes) {
505
+ const root = this.get('root');
506
+ const parentNode = isPageOrFragment(node) && root ? root : (parent ?? getAddParent(node));
507
+ if (!parentNode) throw new Error('未找到父元素');
508
+ newNodes.push(await this.doAdd(node, parentNode, options));
509
+ }
499
510
 
500
511
  if (newNodes.length > 1) {
501
512
  // 多选时只要任一新增节点位于非当前页面,触发的 multiSelect 就会引起页面切换
@@ -654,16 +665,8 @@ class Editor extends BaseService {
654
665
  * @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
655
666
  * @param options.doNotPushHistory 是否不写入历史记录(默认 false)
656
667
  */
657
- public async remove(
658
- nodeOrNodeList: MNode | MNode[],
659
- {
660
- doNotSelect = false,
661
- doNotSwitchPage = false,
662
- doNotPushHistory = false,
663
- historyDescription,
664
- historySource,
665
- }: DslOpOptions = {},
666
- ): Promise<void> {
668
+ public async remove(nodeOrNodeList: MNode | MNode[], options: DslOpOptions = {}): Promise<void> {
669
+ const { doNotPushHistory = false, historyDescription, historySource } = options;
667
670
  this.captureSelectionBeforeOp();
668
671
 
669
672
  const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
@@ -690,7 +693,7 @@ class Editor extends BaseService {
690
693
  }
691
694
  }
692
695
 
693
- await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
696
+ await Promise.all(nodes.map((node) => this.doRemove(node, options)));
694
697
 
695
698
  // 删除节点时同步清理其(含子树)的校验错误记录;置于 pushOpHistory 之前,使历史快照与本次删除对齐。
696
699
  this.removeInvalidNodesBySubtree(nodes);
@@ -720,12 +723,9 @@ class Editor extends BaseService {
720
723
 
721
724
  public async doUpdate(
722
725
  config: MNode,
723
- {
724
- changeRecords = [],
725
- historySource,
726
- replace = false,
727
- }: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource; replace?: boolean } = {},
726
+ data: UpdateOptions = {},
728
727
  ): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
728
+ const { changeRecords = [], historySource, replace = false } = data;
729
729
  const root = this.get('root');
730
730
  if (!root) throw new Error('root为空');
731
731
 
@@ -804,34 +804,10 @@ class Editor extends BaseService {
804
804
  * 更新节点
805
805
  * update后会触发依赖收集,收集完后会掉stage.update方法
806
806
  * @param config 新的节点配置,配置中需要有id信息
807
- * @param data 额外数据
808
- * @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
809
- * @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
810
- * @param data.doNotPushHistory 是否不写入历史记录(默认 false)
811
- * @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
812
- * @param data.replace 是否整节点替换:为 true 时跳过 mergeWith / toggleFixedPosition / setChildrenLayout,直接用传入配置覆盖(默认 false)
807
+ * @param data 额外数据,见 {@link UpdateOptions}
813
808
  * @returns 更新后的节点配置
814
809
  */
815
- public async update(
816
- config: MNode | MNode[],
817
- data: {
818
- changeRecords?: ChangeRecord[];
819
- changeRecordList?: ChangeRecord[][];
820
- doNotPushHistory?: boolean;
821
- historyDescription?: string;
822
- historySource?: HistoryOpSource;
823
- /**
824
- * 为 true 时不做深合并等变换,直接用传入配置整节点替换现有节点。
825
- * 适用于源码编辑、历史整节点快照回放等「完整 DSL」场景;默认 false(局部属性更新走 merge)。
826
- */
827
- replace?: boolean;
828
- /**
829
- * 属性面板提交时携带的校验错误信息,在写入历史记录之前落库,
830
- * 使历史快照与本次变更对齐,从而 undo/redo 能正确还原错误标记。
831
- */
832
- invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string };
833
- } = {},
834
- ): Promise<MNode | MNode[]> {
810
+ public async update(config: MNode | MNode[], data: UpdateOptions = {}): Promise<MNode | MNode[]> {
835
811
  this.captureSelectionBeforeOp();
836
812
 
837
813
  const {
@@ -840,7 +816,6 @@ class Editor extends BaseService {
840
816
  changeRecords,
841
817
  historyDescription,
842
818
  historySource,
843
- replace = false,
844
819
  invalidInfo,
845
820
  } = data;
846
821
 
@@ -851,7 +826,7 @@ class Editor extends BaseService {
851
826
  const updateData = await Promise.all(
852
827
  nodes.map((node, index) => {
853
828
  const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
854
- return this.doUpdate(node, { changeRecords: recordsForNode, historySource, replace });
829
+ return this.doUpdate(node, { ...data, changeRecords: recordsForNode });
855
830
  }),
856
831
  );
857
832
 
@@ -1360,14 +1335,7 @@ class Editor extends BaseService {
1360
1335
  /** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
1361
1336
  public async updateAndGetHistoryId(
1362
1337
  config: MNode | MNode[],
1363
- data: {
1364
- changeRecords?: ChangeRecord[];
1365
- changeRecordList?: ChangeRecord[][];
1366
- doNotPushHistory?: boolean;
1367
- historyDescription?: string;
1368
- historySource?: HistoryOpSource;
1369
- replace?: boolean;
1370
- } = {},
1338
+ data: UpdateOptions = {},
1371
1339
  ): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
1372
1340
  this.lastPushedHistoryId = null;
1373
1341
  const result = await this.update(config, data);
@@ -1987,17 +1955,15 @@ class Editor extends BaseService {
1987
1955
  const parent = this.getNodeById(parentId, false) as MContainer | null;
1988
1956
  if (parent?.items) {
1989
1957
  const addedNode = cloneDeep(newSchema);
1990
- if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
1991
- parent.items.splice(index, 0, addedNode);
1992
- } else {
1993
- parent.items.push(addedNode);
1994
- }
1958
+ const insertIndex = clampIndex(index, parent.items.length);
1959
+ parent.items.splice(insertIndex, 0, addedNode);
1995
1960
  addedNodes.push(addedNode);
1996
1961
  await stage?.add({
1997
1962
  config: cloneDeep(newSchema),
1998
1963
  parent: cloneDeep(parent),
1999
1964
  parentId: parent.id,
2000
1965
  root: cloneDeep(root),
1966
+ index: insertIndex,
2001
1967
  });
2002
1968
  }
2003
1969
  }
@@ -2016,13 +1982,15 @@ class Editor extends BaseService {
2016
1982
  const parent = this.getNodeById(parentId, false) as MContainer | null;
2017
1983
  if (parent?.items) {
2018
1984
  const addedNode = cloneDeep(oldSchema);
2019
- parent.items.splice(index ?? parent.items.length, 0, addedNode);
1985
+ const insertIndex = clampIndex(index, parent.items.length);
1986
+ parent.items.splice(insertIndex, 0, addedNode);
2020
1987
  addedNodes.push(addedNode);
2021
1988
  await stage?.add({
2022
1989
  config: cloneDeep(oldSchema),
2023
1990
  parent: cloneDeep(parent),
2024
1991
  parentId,
2025
1992
  root: cloneDeep(root),
1993
+ index: insertIndex,
2026
1994
  });
2027
1995
  }
2028
1996
  }
package/src/type.ts CHANGED
@@ -1464,6 +1464,29 @@ export interface DslOpOptions extends HistoryOpOptions {
1464
1464
  }
1465
1465
  // #endregion DslOpOptions
1466
1466
 
1467
+ // #region UpdateOptions
1468
+ /**
1469
+ * {@link EditorService.update} / {@link EditorService.doUpdate} 的额外数据
1470
+ * - changeRecords: 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
1471
+ * - changeRecordList: 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
1472
+ * - replace: 为 true 时跳过 mergeWith / toggleFixedPosition / setChildrenLayout,直接用传入配置整节点替换
1473
+ * - invalidInfo: 属性面板提交时携带的校验错误信息,在写入历史记录之前落库
1474
+ */
1475
+ export interface UpdateOptions extends HistoryOpOptionsWithChangeRecords {
1476
+ changeRecordList?: ChangeRecord[][];
1477
+ /**
1478
+ * 为 true 时不做深合并等变换,直接用传入配置整节点替换现有节点。
1479
+ * 适用于源码编辑、历史整节点快照回放等「完整 DSL」场景;默认 false(局部属性更新走 merge)。
1480
+ */
1481
+ replace?: boolean;
1482
+ /**
1483
+ * 属性面板提交时携带的校验错误信息,在写入历史记录之前落库,
1484
+ * 使历史快照与本次变更对齐,从而 undo/redo 能正确还原错误标记。
1485
+ */
1486
+ invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string };
1487
+ }
1488
+ // #endregion UpdateOptions
1489
+
1467
1490
  /** 差异对话框的入参 */
1468
1491
  export interface DiffDialogPayload {
1469
1492
  /** 表单类别 */
@@ -16,7 +16,7 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import type { EventOption, Id, MComponent, MContainer } from '@tmagic/core';
19
+ import type { EventOption, Id, MComponent, MContainer, MNode } from '@tmagic/core';
20
20
  import type { CascaderOption } from '@tmagic/form';
21
21
  import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
22
22
 
@@ -56,7 +56,7 @@ export const getEventNameOptions = (
56
56
  }
57
57
 
58
58
  if (src === 'component') {
59
- const sourceNode = editorService.getNodeById(formValue.id);
59
+ const sourceNode = editorService.getNodeById(formValue.id) || (formValue as MNode);
60
60
  let events: EventOption[] | CascaderOption[] = eventsService.getEvent(formValue.type, { node: sourceNode }) || [];
61
61
 
62
62
  if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
@@ -73,7 +73,6 @@ export const getCondOpOptionsByFieldType = (type: string) => {
73
73
 
74
74
  export const styleTabConfig: TabPaneConfig = {
75
75
  title: '样式',
76
- lazy: true,
77
76
  display: ({ services }: any) => !(services?.uiService?.get('showStylePanel') ?? true),
78
77
  items: [
79
78
  {
@@ -146,7 +145,6 @@ export const styleTabConfig: TabPaneConfig = {
146
145
 
147
146
  export const eventTabConfig: TabPaneConfig = {
148
147
  title: '事件',
149
- lazy: true,
150
148
  items: [
151
149
  {
152
150
  name: 'events',
@@ -160,7 +158,6 @@ export const eventTabConfig: TabPaneConfig = {
160
158
 
161
159
  export const advancedTabConfig: TabPaneConfig = {
162
160
  title: '高级',
163
- lazy: true,
164
161
  items: [
165
162
  {
166
163
  name: NODE_DISABLE_CODE_BLOCK_KEY,
@@ -360,32 +357,3 @@ export const fillConfig = (
360
357
 
361
358
  return [tabConfig];
362
359
  };
363
-
364
- /**
365
- * 将属性表单配置中「样式」tab-pane 的 `display` 强制置为 `true`。
366
- *
367
- * `propsService.getPropsConfig` 返回的样式 tab 默认带有
368
- * `display: ({ services }) => !(services?.uiService?.get('showStylePanel') ?? true)`,
369
- * 在对比 / 只读展示场景(CompareForm / ViewForm)下并不需要跟随 uiService 状态隐藏,
370
- * 这里统一放开,保证样式 tab 始终可见。
371
- *
372
- * @param formConfig 组件属性表单配置
373
- * @returns 处理后的表单配置(不修改入参,返回浅拷贝)
374
- */
375
- export const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
376
- formConfig.map((item) => {
377
- if (!('type' in item)) return item;
378
- if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
379
-
380
- return {
381
- ...item,
382
- items: item.items.map((tabPane) => {
383
- if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
384
-
385
- return {
386
- ...tabPane,
387
- display: true,
388
- };
389
- }),
390
- };
391
- });
@@ -209,7 +209,6 @@ const validateDataSourceFieldPath = (
209
209
  path: string[],
210
210
  options: {
211
211
  dataSourceId?: string;
212
- valueMode?: 'key' | 'value';
213
212
  dataSourceFieldType?: DataSourceFieldType[];
214
213
  message?: string;
215
214
  } = {},
@@ -219,19 +218,11 @@ const validateDataSourceFieldPath = (
219
218
  return undefined;
220
219
  }
221
220
 
222
- let dsId = options.dataSourceId;
223
- let fieldNames = path;
224
-
221
+ const dsId = options.dataSourceId;
225
222
  if (!dsId) {
226
- if (!path.length) {
227
- return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion());
228
- }
229
- const rawDsId = path[0];
230
- dsId =
231
- options.valueMode === 'key' || !`${rawDsId}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX)
232
- ? `${rawDsId}`
233
- : removeDataSourceFieldPrefix(`${rawDsId}`);
234
- fieldNames = path.slice(1);
223
+ // 仅当取值为空数组或首项为空串时命中。表单校验管道里 validateTypeMatch 对空数组会短路,
224
+ // 因此这里只可能来自直接调用 validateDataSourceFieldSelect 的自定义 validator
225
+ return defaultMessage(options.message, '数据源不存在', dataSourceIdSuggestion());
235
226
  }
236
227
 
237
228
  const ds = findDataSource(`${dsId}`);
@@ -239,11 +230,11 @@ const validateDataSourceFieldPath = (
239
230
  return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
240
231
  }
241
232
 
242
- if (!fieldNames.length) {
233
+ if (!path.length) {
243
234
  return undefined;
244
235
  }
245
236
 
246
- const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, fieldNames);
237
+ const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path);
247
238
  if (!ok) {
248
239
  return defaultMessage(
249
240
  options.message,
@@ -258,11 +249,15 @@ const validateDataSourceFieldPath = (
258
249
  }
259
250
 
260
251
  const leafType = field?.type || 'any';
261
- if (leafType === 'any' || allowedTypes.includes(leafType)) {
262
- return undefined;
263
- }
252
+ if (leafType !== 'any' && !allowedTypes.includes(leafType)) {
253
+ const fieldName = field?.name || path[path.length - 1];
264
254
 
265
- return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion());
255
+ // 文案已点明当前字段类型与要求类型,无需再列同级可选字段
256
+ return defaultMessage(
257
+ options.message,
258
+ `请选择类型为${allowedTypes.join('或')}的字段,字段(${fieldName})的类型为${leafType}`,
259
+ );
260
+ }
266
261
  };
267
262
 
268
263
  const validateDataSourceMethodTuple = (value: any, message?: string): string | undefined => {
@@ -367,7 +362,7 @@ const validateCondOpSelect: TypeMatchValidator = (value, { message, props }) =>
367
362
  const parentFields = props.config?.parentFields || [];
368
363
  const fieldPath = Array.isArray(props.model?.field) ? props.model.field : [];
369
364
  const [id, ...fieldNames] = [...parentFields, ...fieldPath];
370
- const ds = id ? findDataSource(`${id}`) : undefined;
365
+ const ds = id ? findDataSource(removeDataSourceFieldPrefix(`${id}`)) : undefined;
371
366
  const fieldType = getFieldType(ds, fieldNames);
372
367
  const allowed = getCondOpsByFieldType(fieldType);
373
368
 
@@ -464,7 +459,7 @@ export type ValidateDataSourceFieldSelectOptions = {
464
459
  /**
465
460
  * data-source-field-select 的 typeMatch 校验逻辑,可供自定义 rules.validator 复用。
466
461
  */
467
- export const validateDataSourceFieldSelectValue = (
462
+ export const validateDataSourceFieldSelect = (
468
463
  value: any,
469
464
  context: TypeMatchValidateContext,
470
465
  options?: ValidateDataSourceFieldSelectOptions,
@@ -492,17 +487,22 @@ export const validateDataSourceFieldSelectValue = (
492
487
  return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
493
488
  }
494
489
 
495
- return validateDataSourceFieldPath(value, {
496
- dataSourceId: config.dataSourceId,
497
- valueMode: config.value,
490
+ // 未指定 dataSourceId 时,路径首项为数据源 id,其余为字段名。
491
+ // value 模式的首项带 ds-field:: 前缀、key 模式不带,removeDataSourceFieldPrefix 对后者是幂等的,无需分支。
492
+ let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : undefined;
493
+ let fieldNames = value;
494
+ if (!dataSourceId) {
495
+ dataSourceId = value.length ? removeDataSourceFieldPrefix(value[0]) : undefined;
496
+ fieldNames = value.slice(1);
497
+ }
498
+
499
+ return validateDataSourceFieldPath(fieldNames, {
500
+ dataSourceId,
498
501
  dataSourceFieldType: config.dataSourceFieldType,
499
502
  message,
500
503
  });
501
504
  };
502
505
 
503
- const validateDataSourceFieldSelect: TypeMatchValidator = (value, context) =>
504
- validateDataSourceFieldSelectValue(value, context);
505
-
506
506
  const validateDataSourceSelect: TypeMatchValidator = (value, { message, props }) => {
507
507
  const config = props.config || {};
508
508
  const dataSources = getDataSources();
@@ -678,7 +678,8 @@ export const editorTypeMatchRules: Record<string, TypeMatchValidator> = {
678
678
  'ui-select': validateUiSelect,
679
679
  'data-source-input': validateDataSourceInput,
680
680
  'data-source-method-select': validateDataSourceMethodSelect,
681
- 'data-source-field-select': validateDataSourceFieldSelect,
681
+ /** 注册进 typeMatch 规则表的入口,收窄为 TypeMatchValidator,避免调用方误传第三个参数 */
682
+ 'data-source-field-select': (value, context) => validateDataSourceFieldSelect(value, context),
682
683
  'data-source-select': validateDataSourceSelect,
683
684
  'code-select': validateCodeSelect,
684
685
  'data-source-fields': validateDataSourceFields,