@tmagic/editor 1.8.0-beta.23 → 1.8.0-beta.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 (32) hide show
  1. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +5 -4
  2. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +5 -4
  3. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +10 -6
  4. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +6 -4
  5. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +4 -1
  6. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +6 -4
  7. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +2 -2
  8. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +4 -2
  9. package/dist/es/hooks/use-compare-form.js +1 -2
  10. package/dist/es/index.js +2 -2
  11. package/dist/es/plugin.js +9 -1
  12. package/dist/es/services/editor.js +43 -37
  13. package/dist/es/services/props.js +1 -1
  14. package/dist/es/utils/event.js +1 -1
  15. package/dist/es/utils/props.js +1 -29
  16. package/dist/tmagic-editor.umd.cjs +373 -368
  17. package/package.json +8 -8
  18. package/src/fields/CodeSelectCol.vue +4 -2
  19. package/src/fields/DataSourceFieldSelect/Index.vue +5 -3
  20. package/src/fields/DataSourceFields.vue +6 -0
  21. package/src/fields/DataSourceMethodSelect.vue +4 -1
  22. package/src/fields/DataSourceMethods.vue +11 -2
  23. package/src/fields/DataSourceMocks.vue +10 -1
  24. package/src/fields/EventSelect.vue +3 -3
  25. package/src/fields/KeyValue.vue +4 -3
  26. package/src/hooks/use-compare-form.ts +1 -4
  27. package/src/plugin.ts +9 -1
  28. package/src/services/editor.ts +53 -85
  29. package/src/type.ts +23 -0
  30. package/src/utils/event.ts +2 -2
  31. package/src/utils/props.ts +0 -32
  32. package/types/index.d.ts +38 -60
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.23",
2
+ "version": "1.8.0-beta.24",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -58,11 +58,11 @@
58
58
  "moveable": "^0.53.0",
59
59
  "serialize-javascript": "^7.0.7",
60
60
  "sortablejs": "^1.15.7",
61
- "@tmagic/design": "1.8.0-beta.23",
62
- "@tmagic/stage": "1.8.0-beta.23",
63
- "@tmagic/utils": "1.8.0-beta.23",
64
- "@tmagic/table": "1.8.0-beta.23",
65
- "@tmagic/form": "1.8.0-beta.23"
61
+ "@tmagic/design": "1.8.0-beta.24",
62
+ "@tmagic/form": "1.8.0-beta.24",
63
+ "@tmagic/utils": "1.8.0-beta.24",
64
+ "@tmagic/stage": "1.8.0-beta.24",
65
+ "@tmagic/table": "1.8.0-beta.24"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/events": "^3.0.3",
@@ -75,8 +75,8 @@
75
75
  "monaco-editor": "^0.55.1 ",
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
- "vue": "^3.5.40",
79
- "@tmagic/core": "1.8.0-beta.23"
78
+ "vue": "^3.5.41",
79
+ "@tmagic/core": "1.8.0-beta.24"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -15,7 +15,7 @@
15
15
  :prop="prop"
16
16
  ></MFormContainer>
17
17
  <MSelect
18
- v-else
18
+ v-else-if="!silentMode"
19
19
  class="select"
20
20
  :config="selectConfig"
21
21
  :name="name"
@@ -27,7 +27,7 @@
27
27
 
28
28
  <!-- 查看/编辑按钮:对比模式为只读,不展示 -->
29
29
  <TMagicButton
30
- v-if="!isCompareMode && model[name] && hasCodeBlockSidePanel"
30
+ v-if="!isCompareMode && !silentMode && model[name] && hasCodeBlockSidePanel"
31
31
  class="m-fields-select-action-button"
32
32
  :size="size"
33
33
  @click="editCode(model[name])"
@@ -65,6 +65,7 @@ import {
65
65
  createValues,
66
66
  type FieldProps,
67
67
  filterFunction,
68
+ FORM_SILENT_MODE_KEY,
68
69
  type FormItemConfig,
69
70
  type FormState,
70
71
  MContainer as MFormContainer,
@@ -83,6 +84,7 @@ defineOptions({
83
84
  });
84
85
 
85
86
  const mForm = inject<FormState | undefined>('mForm');
87
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
86
88
  const { codeBlockService, uiService } = useServices();
87
89
  const eventBus = inject<EventBus>('eventBus');
88
90
  const emit = defineEmits<{
@@ -6,7 +6,7 @@
6
6
  }"
7
7
  >
8
8
  <FieldSelect
9
- v-if="isSelectValid"
9
+ v-if="isSelectValid && !silentMode"
10
10
  :model-value="model[name]"
11
11
  :disabled="disabled"
12
12
  :size="size"
@@ -18,7 +18,7 @@
18
18
  ></FieldSelect>
19
19
 
20
20
  <component
21
- v-else
21
+ v-else-if="!silentMode"
22
22
  :is="tagName"
23
23
  :config="config.fieldConfig"
24
24
  :model="model"
@@ -33,7 +33,7 @@
33
33
  ></component>
34
34
 
35
35
  <TMagicTooltip
36
- v-if="config.fieldConfig && !disabledDataSource && !mForm?.isCompare"
36
+ v-if="!silentMode && config.fieldConfig && !disabledDataSource && !mForm?.isCompare"
37
37
  :disabled="showDataSourceFieldSelect"
38
38
  content="选择数据源"
39
39
  >
@@ -57,6 +57,7 @@ import {
57
57
  type ContainerChangeEventData,
58
58
  type DataSourceFieldSelectConfig,
59
59
  type FieldProps,
60
+ FORM_SILENT_MODE_KEY,
60
61
  type FormState,
61
62
  getFormField,
62
63
  } from '@tmagic/form';
@@ -104,6 +105,7 @@ watch(
104
105
 
105
106
  const { dataSourceService, propsService } = useServices();
106
107
  const mForm = inject<FormState | undefined>('mForm');
108
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
107
109
 
108
110
  const dataSources = computed(() => dataSourceService.get('dataSources') || []);
109
111
  const disabledDataSource = computed(() => propsService.getDisabledDataSource());
@@ -8,6 +8,7 @@
8
8
  </div>
9
9
 
10
10
  <FloatingBox
11
+ v-if="!silentMode"
11
12
  :body-style="{ padding: '0 16px' }"
12
13
  v-model:visible="addDialogVisible"
13
14
  v-model:width="width"
@@ -30,6 +31,7 @@
30
31
  </FloatingBox>
31
32
 
32
33
  <FloatingBox
34
+ v-if="!silentMode"
33
35
  :body-style="{ padding: '0 16px' }"
34
36
  v-model:visible="addFromJsonDialogVisible"
35
37
  v-model:width="width"
@@ -60,6 +62,7 @@ import {
60
62
  type ContainerChangeEventData,
61
63
  type DataSourceFieldsConfig,
62
64
  type FieldProps,
65
+ FORM_SILENT_MODE_KEY,
63
66
  type FormConfig,
64
67
  type FormState,
65
68
  MFormBox,
@@ -89,6 +92,7 @@ const emit = defineEmits<{
89
92
 
90
93
  const { uiService } = useServices();
91
94
  const mForm = inject<FormState | undefined>('mForm');
95
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
92
96
 
93
97
  /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
94
98
  const isCompare = computed(() => Boolean(mForm?.isCompare));
@@ -379,6 +383,8 @@ provide(
379
383
  );
380
384
 
381
385
  onMounted(() => {
386
+ if (silentMode) return;
387
+
382
388
  const path = editingFieldPath.value;
383
389
  if (!path.length) return;
384
390
 
@@ -2,6 +2,7 @@
2
2
  <div class="m-fields-data-source-method-select">
3
3
  <div class="data-source-method-select-container">
4
4
  <MCascader
5
+ v-if="!silentMode"
5
6
  class="select"
6
7
  :config="cascaderConfig"
7
8
  :model="model"
@@ -13,7 +14,7 @@
13
14
  ></MCascader>
14
15
 
15
16
  <TMagicTooltip
16
- v-if="model[name] && isCustomMethod && dataSourceSidePanel && !isCompare"
17
+ v-if="!silentMode && model[name] && isCustomMethod && dataSourceSidePanel && !isCompare"
17
18
  :content="notEditable ? '查看' : '编辑'"
18
19
  >
19
20
  <TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler">
@@ -48,6 +49,7 @@ import {
48
49
  type DataSourceMethodSelectConfig,
49
50
  type FieldProps,
50
51
  filterFunction,
52
+ FORM_SILENT_MODE_KEY,
51
53
  type FormItemConfig,
52
54
  type FormState,
53
55
  MCascader,
@@ -67,6 +69,7 @@ defineOptions({
67
69
 
68
70
  const { dataSourceService, uiService } = useServices();
69
71
  const mForm = inject<FormState | undefined>('mForm');
72
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
70
73
  const eventBus = inject<EventBus>('eventBus');
71
74
 
72
75
  const emit = defineEmits(['change']);
@@ -9,7 +9,7 @@
9
9
  </div>
10
10
 
11
11
  <CodeBlockEditor
12
- v-if="codeConfig"
12
+ v-if="codeConfig && !silentMode"
13
13
  ref="codeBlockEditor"
14
14
  :disabled="disabled"
15
15
  :content="codeConfig"
@@ -26,7 +26,13 @@ import { cloneDeep } from 'lodash-es';
26
26
 
27
27
  import type { CodeBlockContent } from '@tmagic/core';
28
28
  import { TMagicButton, tMagicMessageBox } from '@tmagic/design';
29
- import type { ContainerChangeEventData, DataSourceMethodsConfig, FieldProps, FormState } from '@tmagic/form';
29
+ import {
30
+ type ContainerChangeEventData,
31
+ type DataSourceMethodsConfig,
32
+ type FieldProps,
33
+ FORM_SILENT_MODE_KEY,
34
+ type FormState,
35
+ } from '@tmagic/form';
30
36
  import { type ColumnConfig, MagicTable } from '@tmagic/table';
31
37
 
32
38
  import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
@@ -43,6 +49,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodsConfig>>(), {
43
49
  const emit = defineEmits(['change']);
44
50
 
45
51
  const mForm = inject<FormState | undefined>('mForm');
52
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
46
53
 
47
54
  /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
48
55
  const isCompare = computed(() => Boolean(mForm?.isCompare));
@@ -170,6 +177,8 @@ const editingMethodName = inject<ComputedRef<string | undefined>>(
170
177
  );
171
178
 
172
179
  onMounted(() => {
180
+ if (silentMode) return;
181
+
173
182
  const methodName = editingMethodName.value;
174
183
  if (!methodName) return;
175
184
 
@@ -7,6 +7,7 @@
7
7
  </div>
8
8
 
9
9
  <FloatingBox
10
+ v-if="!silentMode"
10
11
  :body-style="{ padding: '0 16px' }"
11
12
  v-model:visible="addDialogVisible"
12
13
  v-model:width="width"
@@ -34,7 +35,14 @@ import { computed, inject, Ref, ref } from 'vue';
34
35
 
35
36
  import type { MockSchema } from '@tmagic/core';
36
37
  import { TMagicButton, tMagicMessageBox, TMagicSwitch } from '@tmagic/design';
37
- import { type DataSourceMocksConfig, type FieldProps, type FormConfig, type FormState, MFormBox } from '@tmagic/form';
38
+ import {
39
+ type DataSourceMocksConfig,
40
+ type FieldProps,
41
+ FORM_SILENT_MODE_KEY,
42
+ type FormConfig,
43
+ type FormState,
44
+ MFormBox,
45
+ } from '@tmagic/form';
38
46
  import { type ColumnConfig, MagicTable } from '@tmagic/table';
39
47
  import { getDefaultValueFromFields } from '@tmagic/utils';
40
48
 
@@ -57,6 +65,7 @@ const emit = defineEmits(['change']);
57
65
 
58
66
  const { uiService } = useServices();
59
67
  const mForm = inject<FormState | undefined>('mForm');
68
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
60
69
 
61
70
  /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
62
71
  const isCompare = computed(() => Boolean(mForm?.isCompare));
@@ -85,7 +85,7 @@ import { Delete } from '@element-plus/icons-vue';
85
85
  import { Plus } from '@element-plus/icons-vue';
86
86
  import { has } from 'lodash-es';
87
87
 
88
- import { ActionType } from '@tmagic/core';
88
+ import { ActionType, MNode } from '@tmagic/core';
89
89
  import { TMagicButton } from '@tmagic/design';
90
90
  import type {
91
91
  CodeSelectColConfig,
@@ -327,8 +327,8 @@ const tableConfig = computed(
327
327
  name: 'method',
328
328
  label: '动作',
329
329
  type: compActionConfig.value.type,
330
- options: (mForm: FormState, { model }: any) => {
331
- const node = editorService.getNodeById(model.to);
330
+ options: (mForm: FormState, { model, formValue }: any) => {
331
+ const node = editorService.getNodeById(model.to) || (formValue as MNode);
332
332
  if (!node?.type) return [];
333
333
 
334
334
  return eventsService.getMethod(node.type, { targetId: model.to, node }).map((option: any) => ({
@@ -44,7 +44,7 @@
44
44
  </div>
45
45
 
46
46
  <MagicCodeEditor
47
- v-if="config.advanced && showCode"
47
+ v-if="config.advanced && showCode && !silentMode"
48
48
  editor-custom-type="m-fields-key-value"
49
49
  language="javascript"
50
50
  :init-values="model[name]"
@@ -60,7 +60,7 @@
60
60
  ></MagicCodeEditor>
61
61
 
62
62
  <TMagicButton
63
- v-if="config.advanced && !isCompare"
63
+ v-if="config.advanced && !isCompare && !silentMode"
64
64
  size="default"
65
65
  :disabled="disabled"
66
66
  link
@@ -75,7 +75,7 @@ import { computed, inject, ref, watchEffect } from 'vue';
75
75
  import { Delete, Plus } from '@element-plus/icons-vue';
76
76
 
77
77
  import { TMagicButton, TMagicInput } from '@tmagic/design';
78
- import type { FieldProps, FormState, KeyValueConfig } from '@tmagic/form';
78
+ import { type FieldProps, FORM_SILENT_MODE_KEY, type FormState, type KeyValueConfig } from '@tmagic/form';
79
79
 
80
80
  import CodeIcon from '@editor/icons/CodeIcon.vue';
81
81
  import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
@@ -93,6 +93,7 @@ const emit = defineEmits<{
93
93
  }>();
94
94
 
95
95
  const mForm = inject<FormState | undefined>('mForm');
96
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
96
97
 
97
98
  /** 对比模式下隐藏增删/代码切换等操作按钮,仅保留只读展示。 */
98
99
  const isCompare = computed(() => Boolean(mForm?.isCompare));
@@ -23,7 +23,6 @@ import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/
23
23
 
24
24
  import type { CompareFormBaseProps } from '@editor/type';
25
25
  import { getCodeBlockFormConfig } from '@editor/utils/code-block';
26
- import { removeStyleDisplayConfig } from '@editor/utils/props';
27
26
 
28
27
  export interface UseCompareFormReturn {
29
28
  config: Ref<FormConfig>;
@@ -107,9 +106,7 @@ export const useCompareForm = (props: CompareFormBaseProps): UseCompareFormRetur
107
106
  if (!props.type) {
108
107
  return [];
109
108
  }
110
- return removeStyleDisplayConfig(
111
- await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
112
- );
109
+ return props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode });
113
110
  }
114
111
  case 'data-source': {
115
112
  const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
package/src/plugin.ts CHANGED
@@ -21,7 +21,7 @@ import { type App } from 'vue';
21
21
  import type { DesignPluginOptions } from '@tmagic/design';
22
22
  import designPlugin from '@tmagic/design';
23
23
  import type { FormInstallOptions } from '@tmagic/form';
24
- import formPlugin, { registerTypeMatchRules } from '@tmagic/form';
24
+ import formPlugin, { registerSilentLeafFieldTypes, registerTypeMatchRules } from '@tmagic/form';
25
25
  import tablePlugin from '@tmagic/table';
26
26
 
27
27
  import Code from './fields/Code.vue';
@@ -68,6 +68,14 @@ export default {
68
68
  app.use(formPlugin, opt || {});
69
69
  app.use(tablePlugin);
70
70
  registerTypeMatchRules(editorTypeMatchRules);
71
+ registerSilentLeafFieldTypes([
72
+ 'vs-code',
73
+ 'ui-select',
74
+ 'cond-op-select',
75
+ 'page-fragment-select',
76
+ 'data-source-select',
77
+ 'data-source-input',
78
+ ]);
71
79
 
72
80
  app.config.globalProperties.$TMAGIC_EDITOR = option;
73
81
  setEditorConfig(option);
@@ -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
  /** 表单类别 */