@tmagic/editor 1.8.0-beta.14 → 1.8.0-beta.16

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.
@@ -5182,7 +5182,7 @@
5182
5182
  }));
5183
5183
  //#endregion
5184
5184
  //#region packages/editor/src/utils/props.ts
5185
- var arrayOptions, eqOptions, numberOptions, booleanOptions, getCondOpOptionsByFieldType, styleTabConfig, eventTabConfig, advancedTabConfig, displayTabConfig, fillConfig, removeStyleDisplayConfig, validatePropsForm;
5185
+ var arrayOptions, eqOptions, numberOptions, booleanOptions, getCondOpOptionsByFieldType, styleTabConfig, eventTabConfig, advancedTabConfig, displayTabConfig, fillConfig, removeStyleDisplayConfig;
5186
5186
  var init_props = __esmMin((() => {
5187
5187
  arrayOptions = [{
5188
5188
  text: "包含",
@@ -5479,21 +5479,6 @@
5479
5479
  })
5480
5480
  };
5481
5481
  });
5482
- validatePropsForm = ({ config, values, appContext = null, services, stage, extendState, debug, typeMatchValid }) => (0, _tmagic_form.validateForm)({
5483
- config,
5484
- debug,
5485
- typeMatchValid,
5486
- initValues: values,
5487
- appContext: appContext ? {
5488
- ...appContext,
5489
- provides: { services }
5490
- } : null,
5491
- extendState: async (state) => ({
5492
- ...await extendState?.(state) || {},
5493
- stage,
5494
- services
5495
- })
5496
- });
5497
5482
  }));
5498
5483
  //#endregion
5499
5484
  //#region packages/editor/src/hooks/use-compare-form.ts
@@ -6605,6 +6590,22 @@
6605
6590
  });
6606
6591
  }
6607
6592
  /**
6593
+ * 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
6594
+ *
6595
+ * 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
6596
+ * 对 isPage / isPageFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
6597
+ * 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
6598
+ *
6599
+ * 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,
6600
+ * 使一次操作只派发一个事件;`add` / `remove` 分别为本次新增与删除的页面列表(其一可为空数组)。
6601
+ *
6602
+ * @param change 本次结构变更:{ add: 新增的页面列表, remove: 删除的页面列表 }
6603
+ */
6604
+ notifyPageStructureChange(change) {
6605
+ if (!change.add.length && !change.remove.length) return;
6606
+ this.emit("page-structure-change", change);
6607
+ }
6608
+ /**
6608
6609
  * 把当前内存中的全部历史栈(页面 / 代码块 / 数据源 / 扩展类型)序列化后写入本地 IndexedDB。
6609
6610
  *
6610
6611
  * - 每个 UndoRedo 栈连同其游标、容量一并保存,恢复后可继续 undo/redo;
@@ -7701,6 +7702,11 @@
7701
7702
  historySource,
7702
7703
  doNotPushHistory
7703
7704
  });
7705
+ const addedPages = newNodes.filter((node) => (0, _tmagic_utils.isPage)(node) || (0, _tmagic_utils.isPageFragment)(node));
7706
+ if (addedPages.length) history_default.notifyPageStructureChange({
7707
+ add: addedPages,
7708
+ remove: []
7709
+ });
7704
7710
  return Array.isArray(addNode) ? newNodes : newNodes[0];
7705
7711
  }
7706
7712
  async doRemove(node, { doNotSelect = false, doNotSwitchPage = false } = {}) {
@@ -7804,6 +7810,11 @@
7804
7810
  historySource,
7805
7811
  doNotPushHistory
7806
7812
  });
7813
+ const removedPages = nodes.filter((node) => (0, _tmagic_utils.isPage)(node) || (0, _tmagic_utils.isPageFragment)(node));
7814
+ if (removedPages.length) history_default.notifyPageStructureChange({
7815
+ add: [],
7816
+ remove: removedPages
7817
+ });
7807
7818
  }
7808
7819
  async doUpdate(config, { changeRecords = [], historySource } = {}) {
7809
7820
  if (!this.get("root")) throw new Error("root为空");
@@ -8588,24 +8599,35 @@
8588
8599
  const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
8589
8600
  const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
8590
8601
  const indexInItems = (root, id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
8602
+ const addedPages = [];
8603
+ const removedPages = [];
8591
8604
  nextPages.forEach((nextPage) => {
8592
8605
  const prevPage = prevMap.get(`${nextPage.id}`);
8593
- if (!prevPage) this.pushPageDiffStep("add", nextPage, {
8594
- newSchema: cloneDeep$1((0, vue.toRaw)(nextPage)),
8595
- parentId: nextRoot.id,
8596
- index: indexInItems(nextRoot, nextPage.id)
8597
- }, source);
8598
- else if (!isEqual((0, vue.toRaw)(prevPage), (0, vue.toRaw)(nextPage))) this.pushPageDiffStep("update", nextPage, {
8606
+ if (!prevPage) {
8607
+ this.pushPageDiffStep("add", nextPage, {
8608
+ newSchema: cloneDeep$1((0, vue.toRaw)(nextPage)),
8609
+ parentId: nextRoot.id,
8610
+ index: indexInItems(nextRoot, nextPage.id)
8611
+ }, source);
8612
+ addedPages.push(nextPage);
8613
+ } else if (!isEqual((0, vue.toRaw)(prevPage), (0, vue.toRaw)(nextPage))) this.pushPageDiffStep("update", nextPage, {
8599
8614
  oldSchema: cloneDeep$1((0, vue.toRaw)(prevPage)),
8600
8615
  newSchema: cloneDeep$1((0, vue.toRaw)(nextPage))
8601
8616
  }, source);
8602
8617
  });
8603
8618
  prevPages.forEach((prevPage) => {
8604
- if (!nextMap.has(`${prevPage.id}`)) this.pushPageDiffStep("remove", prevPage, {
8605
- oldSchema: cloneDeep$1((0, vue.toRaw)(prevPage)),
8606
- parentId: preRoot.id,
8607
- index: indexInItems(preRoot, prevPage.id)
8608
- }, source);
8619
+ if (!nextMap.has(`${prevPage.id}`)) {
8620
+ this.pushPageDiffStep("remove", prevPage, {
8621
+ oldSchema: cloneDeep$1((0, vue.toRaw)(prevPage)),
8622
+ parentId: preRoot.id,
8623
+ index: indexInItems(preRoot, prevPage.id)
8624
+ }, source);
8625
+ removedPages.push(prevPage);
8626
+ }
8627
+ });
8628
+ if (addedPages.length || removedPages.length) history_default.notifyPageStructureChange({
8629
+ add: addedPages,
8630
+ remove: removedPages
8609
8631
  });
8610
8632
  }
8611
8633
  /**
@@ -9598,12 +9620,14 @@
9598
9620
  if (options.skipNumberIndices && (0, _tmagic_utils.isNumber)(name)) continue;
9599
9621
  if (!currentFields.length) return {
9600
9622
  ok: false,
9601
- fields: currentFields
9623
+ fields: currentFields,
9624
+ failedName: name
9602
9625
  };
9603
9626
  field = currentFields.find((item) => item.name === name);
9604
9627
  if (!field) return {
9605
9628
  ok: false,
9606
- fields: currentFields
9629
+ fields: currentFields,
9630
+ failedName: name
9607
9631
  };
9608
9632
  currentFields = field.fields || [];
9609
9633
  }
@@ -10768,7 +10792,7 @@
10768
10792
  }
10769
10793
  return String(value);
10770
10794
  };
10771
- var MAX_SUGGESTION_OPTIONS = 5;
10795
+ var MAX_SUGGESTION_OPTIONS = 20;
10772
10796
  /**
10773
10797
  * 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
10774
10798
  */
@@ -10929,10 +10953,10 @@
10929
10953
  fieldNames = path.slice(1);
10930
10954
  }
10931
10955
  const ds = findDataSource(`${dsId}`);
10932
- if (!ds) return defaultMessage(options.message, "值不在可选项中", dataSourceIdSuggestion());
10956
+ if (!ds) return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
10933
10957
  if (!fieldNames.length) return;
10934
- const { field, ok } = resolveFieldByPath(ds.fields, fieldNames);
10935
- if (!ok) return defaultMessage(options.message, "值不在可选项中", dataSourceIdSuggestion());
10958
+ const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, fieldNames);
10959
+ if (!ok) return defaultMessage(options.message, `数据源字段(${failedName})不存在`, listSuggestion(fields.map((item) => item.name)));
10936
10960
  const allowedTypes = options.dataSourceFieldType || ["any"];
10937
10961
  if (!allowedTypes.length || allowedTypes.includes("any")) return;
10938
10962
  const leafType = field?.type || "any";
@@ -11015,9 +11039,15 @@
11015
11039
  if (config.value === "key") return true;
11016
11040
  return `${value[0]}`.startsWith(_tmagic_utils.DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
11017
11041
  };
11018
- var validateDataSourceFieldSelect = (value, { message, props }) => {
11042
+ var validateDataSourceFieldSelect = (value, { mForm, message, props }) => {
11019
11043
  const config = props.config || {};
11020
- if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) return;
11044
+ if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) return (0, _tmagic_form.validateTypeMatch)(value, mForm, {
11045
+ ...props,
11046
+ config: {
11047
+ name: config.name,
11048
+ ...config.fieldConfig
11049
+ }
11050
+ }, message);
11021
11051
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
11022
11052
  return validateDataSourceFieldPath(value, {
11023
11053
  dataSourceId: config.dataSourceId,
@@ -14795,7 +14825,6 @@
14795
14825
  //#endregion
14796
14826
  //#region packages/editor/src/layouts/props-panel/FormPanel.vue?vue&type=script&setup=true&lang.ts
14797
14827
  init_Icon();
14798
- init_props();
14799
14828
  init_CodeEditor();
14800
14829
  var _hoisted_1$59 = { class: "m-editor-props-form-panel" };
14801
14830
  var FormPanel_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.defineComponent)({
@@ -14869,14 +14898,18 @@
14869
14898
  return;
14870
14899
  }
14871
14900
  try {
14872
- const error = await validatePropsForm({
14901
+ const error = await (0, _tmagic_form.validateForm)({
14873
14902
  config: props.config,
14874
- values: newValues,
14875
- appContext: internalInstance?.appContext ?? null,
14876
- services,
14877
- stage: stage.value,
14878
14903
  typeMatchValid: true,
14879
- extendState: props.extendState
14904
+ initValues: newValues,
14905
+ appContext: internalInstance?.appContext ? {
14906
+ ...internalInstance?.appContext,
14907
+ provides: { services }
14908
+ } : null,
14909
+ extendState: (state) => {
14910
+ if (configFormRef.value?.formState) return { ...configFormRef.value?.formState || {} };
14911
+ return props.extendState?.(state) || {};
14912
+ }
14880
14913
  });
14881
14914
  if (error) (0, _tmagic_design.tMagicMessage)({
14882
14915
  type: "error",
@@ -15055,14 +15088,14 @@
15055
15088
  ...v,
15056
15089
  style: {}
15057
15090
  };
15058
- if (v.style) {
15091
+ if (v.style) if (eventData) {
15059
15092
  Object.entries(v.style).forEach(([key, value]) => {
15060
15093
  if (value !== "" && newValue.style) newValue.style[key] = value;
15061
15094
  });
15062
- eventData?.changeRecords?.forEach((record) => {
15095
+ eventData.changeRecords?.forEach((record) => {
15063
15096
  if (record.propPath?.startsWith("style") && record.value === "") (0, _tmagic_utils.setValueByKeyPath)(record.propPath, record.value, newValue);
15064
15097
  });
15065
- }
15098
+ } else newValue.style = { ...v.style };
15066
15099
  const historySource = eventData ? "props" : "code";
15067
15100
  editorService.update(newValue, {
15068
15101
  changeRecords: eventData?.changeRecords,
@@ -15700,15 +15733,29 @@
15700
15733
  if (contains(target)) return;
15701
15734
  hide();
15702
15735
  };
15736
+ const fixPosition = () => {
15737
+ const menu = menuEl.value;
15738
+ if (!menu || !visible.value) return;
15739
+ const menuHeight = menu.clientHeight;
15740
+ const menuWidth = menu.clientWidth;
15741
+ let { top, left } = menuPosition.value;
15742
+ if (top + menuHeight > document.body.clientHeight) top = Math.max(0, document.body.clientHeight - menuHeight);
15743
+ if (left + menuWidth > document.body.clientWidth) left = Math.max(0, document.body.clientWidth - menuWidth);
15744
+ if (top !== menuPosition.value.top || left !== menuPosition.value.left) menuPosition.value = {
15745
+ top,
15746
+ left
15747
+ };
15748
+ };
15703
15749
  const setPosition = (e) => {
15704
- const menuHeight = menuEl.value?.clientHeight || 0;
15705
- let top = e.clientY;
15706
- if (menuHeight + e.clientY > document.body.clientHeight) top = document.body.clientHeight - menuHeight;
15707
15750
  menuPosition.value = {
15708
- top,
15751
+ top: e.clientY,
15709
15752
  left: e.clientX
15710
15753
  };
15754
+ fixPosition();
15711
15755
  };
15756
+ const resizeObserver = new ResizeObserver(() => {
15757
+ fixPosition();
15758
+ });
15712
15759
  const show = (e) => {
15713
15760
  visible.value = true;
15714
15761
  (0, vue.nextTick)(() => {
@@ -15737,10 +15784,12 @@
15737
15784
  emit("mouseenter");
15738
15785
  };
15739
15786
  (0, vue.onMounted)(() => {
15787
+ if (menuEl.value) resizeObserver.observe(menuEl.value);
15740
15788
  if (props.isSubMenu) return;
15741
15789
  globalThis.addEventListener("mousedown", outsideClickHideHandler, true);
15742
15790
  });
15743
15791
  (0, vue.onBeforeUnmount)(() => {
15792
+ resizeObserver.disconnect();
15744
15793
  if (props.isSubMenu) return;
15745
15794
  globalThis.removeEventListener("mousedown", outsideClickHideHandler, true);
15746
15795
  });
@@ -25323,6 +25372,12 @@
25323
25372
  },
25324
25373
  emits: ["change"],
25325
25374
  setup(__props, { emit: __emit }) {
25375
+ /**
25376
+ * 静默模式(submitForm/validateForm 隐藏挂载)下跳过 monaco 渲染:
25377
+ * 校验由 FormItem 针对 model 中的值完成,与编辑器实例无关;本组件挂载无值副作用
25378
+ * (save 仅由用户操作/编辑器内容变更触发),跳过可省去 monaco worker/model 的无谓实例化。
25379
+ */
25380
+ const silentMode = (0, vue.inject)(_tmagic_form.FORM_SILENT_MODE_KEY, false);
25326
25381
  const emit = __emit;
25327
25382
  const props = __props;
25328
25383
  /**
@@ -25340,7 +25395,8 @@
25340
25395
  emit("change", v);
25341
25396
  };
25342
25397
  return (_ctx, _cache) => {
25343
- return (0, vue.openBlock)(), (0, vue.createBlock)(CodeEditor_default, {
25398
+ return !(0, vue.unref)(silentMode) ? ((0, vue.openBlock)(), (0, vue.createBlock)(CodeEditor_default, {
25399
+ key: 0,
25344
25400
  height: __props.config.height,
25345
25401
  type: diffMode.value ? "diff" : void 0,
25346
25402
  "init-values": diffMode.value ? (__props.lastValues || {})[__props.name] : __props.model[__props.name],
@@ -25364,7 +25420,7 @@
25364
25420
  "autosize",
25365
25421
  "parse",
25366
25422
  "editor-custom-type"
25367
- ]);
25423
+ ])) : (0, vue.createCommentVNode)("v-if", true);
25368
25424
  };
25369
25425
  }
25370
25426
  });
@@ -25862,7 +25918,6 @@
25862
25918
  exports.useServices = useServices;
25863
25919
  exports.useStage = useStage;
25864
25920
  exports.useWindowRect = useWindowRect;
25865
- exports.validatePropsForm = validatePropsForm;
25866
25921
  exports.warn = warn;
25867
25922
  Object.keys(_tmagic_form).forEach(function(k) {
25868
25923
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.14",
2
+ "version": "1.8.0-beta.16",
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.0",
60
60
  "sortablejs": "^1.15.6",
61
- "@tmagic/design": "1.8.0-beta.14",
62
- "@tmagic/form": "1.8.0-beta.14",
63
- "@tmagic/stage": "1.8.0-beta.14",
64
- "@tmagic/table": "1.8.0-beta.14",
65
- "@tmagic/utils": "1.8.0-beta.14"
61
+ "@tmagic/design": "1.8.0-beta.16",
62
+ "@tmagic/stage": "1.8.0-beta.16",
63
+ "@tmagic/form": "1.8.0-beta.16",
64
+ "@tmagic/utils": "1.8.0-beta.16",
65
+ "@tmagic/table": "1.8.0-beta.16"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/events": "^3.0.3",
@@ -76,7 +76,7 @@
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
78
  "vue": "^3.5.40",
79
- "@tmagic/core": "1.8.0-beta.14"
79
+ "@tmagic/core": "1.8.0-beta.16"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -128,20 +128,42 @@ const outsideClickHideHandler = (e: MouseEvent) => {
128
128
  hide();
129
129
  };
130
130
 
131
- const setPosition = (e: { clientY: number; clientX: number }) => {
132
- const menuHeight = menuEl.value?.clientHeight || 0;
131
+ // 根据菜单实际尺寸修正位置,避免超出可视范围
132
+ const fixPosition = () => {
133
+ const menu = menuEl.value;
134
+ if (!menu || !visible.value) return;
135
+
136
+ const menuHeight = menu.clientHeight;
137
+ const menuWidth = menu.clientWidth;
138
+
139
+ let { top, left } = menuPosition.value;
140
+
141
+ if (top + menuHeight > document.body.clientHeight) {
142
+ top = Math.max(0, document.body.clientHeight - menuHeight);
143
+ }
144
+
145
+ if (left + menuWidth > document.body.clientWidth) {
146
+ left = Math.max(0, document.body.clientWidth - menuWidth);
147
+ }
133
148
 
134
- let top = e.clientY;
135
- if (menuHeight + e.clientY > document.body.clientHeight) {
136
- top = document.body.clientHeight - menuHeight;
149
+ if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
150
+ menuPosition.value = { top, left };
137
151
  }
152
+ };
138
153
 
154
+ const setPosition = (e: { clientY: number; clientX: number }) => {
139
155
  menuPosition.value = {
140
- top,
156
+ top: e.clientY,
141
157
  left: e.clientX,
142
158
  };
159
+ fixPosition();
143
160
  };
144
161
 
162
+ // 菜单大小动态变化(如菜单项更新)后重新修正位置
163
+ const resizeObserver = new ResizeObserver(() => {
164
+ fixPosition();
165
+ });
166
+
145
167
  const show = (e?: { clientY: number; clientX: number }) => {
146
168
  visible.value = true;
147
169
 
@@ -186,12 +208,18 @@ const mouseenterHandler = () => {
186
208
  };
187
209
 
188
210
  onMounted(() => {
211
+ if (menuEl.value) {
212
+ resizeObserver.observe(menuEl.value);
213
+ }
214
+
189
215
  if (props.isSubMenu) return;
190
216
 
191
217
  globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
192
218
  });
193
219
 
194
220
  onBeforeUnmount(() => {
221
+ resizeObserver.disconnect();
222
+
195
223
  if (props.isSubMenu) return;
196
224
 
197
225
  globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <MagicCodeEditor
3
+ v-if="!silentMode"
3
4
  :height="config.height"
4
5
  :type="diffMode ? 'diff' : undefined"
5
6
  :init-values="diffMode ? (lastValues || {})[name] : model[name]"
@@ -17,9 +18,9 @@
17
18
  </template>
18
19
 
19
20
  <script lang="ts" setup>
20
- import { computed } from 'vue';
21
+ import { computed, inject } from 'vue';
21
22
 
22
- import type { CodeConfig, FieldProps } from '@tmagic/form';
23
+ import { type CodeConfig, type FieldProps, FORM_SILENT_MODE_KEY } from '@tmagic/form';
23
24
 
24
25
  import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
25
26
 
@@ -27,6 +28,13 @@ defineOptions({
27
28
  name: 'MFieldsVsCode',
28
29
  });
29
30
 
31
+ /**
32
+ * 静默模式(submitForm/validateForm 隐藏挂载)下跳过 monaco 渲染:
33
+ * 校验由 FormItem 针对 model 中的值完成,与编辑器实例无关;本组件挂载无值副作用
34
+ * (save 仅由用户操作/编辑器内容变更触发),跳过可省去 monaco worker/model 的无谓实例化。
35
+ */
36
+ const silentMode = inject(FORM_SILENT_MODE_KEY, false);
37
+
30
38
  const emit = defineEmits<{
31
39
  change: [value: string | any];
32
40
  }>();
@@ -49,14 +49,13 @@ import { Document as DocumentIcon } from '@element-plus/icons-vue';
49
49
 
50
50
  import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
51
51
  import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
52
- import { MForm } from '@tmagic/form';
52
+ import { MForm, validateForm } from '@tmagic/form';
53
53
  import { filterXSS } from '@tmagic/utils';
54
54
 
55
55
  import MIcon from '@editor/components/Icon.vue';
56
56
  import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
57
57
  import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
58
58
  import { useServices } from '@editor/hooks/use-services';
59
- import { validatePropsForm } from '@editor/utils/props';
60
59
 
61
60
  import CodeEditor from '../CodeEditor.vue';
62
61
 
@@ -162,14 +161,19 @@ const saveCode = async (values: any) => {
162
161
  // 做一次静默校验(不复用、也不污染页面上正在展示的表单),并将校验结果(错误信息)随提交
163
162
  // 一并抛给上层记录,使源码保存的错误状态与表单编辑保持一致。
164
163
  try {
165
- const error = await validatePropsForm({
164
+ const error = await validateForm({
166
165
  config: props.config,
167
- values: newValues,
168
- appContext: internalInstance?.appContext ?? null,
169
- services,
170
- stage: stage.value,
171
166
  typeMatchValid: true,
172
- extendState: props.extendState,
167
+ initValues: newValues,
168
+ // 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
169
+ // 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
170
+ appContext: internalInstance?.appContext ? { ...internalInstance?.appContext, provides: { services } } : null,
171
+ extendState: (state) => {
172
+ if (configFormRef.value?.formState) {
173
+ return { ...(configFormRef.value?.formState || {}) };
174
+ }
175
+ return props.extendState?.(state) || {};
176
+ },
173
177
  });
174
178
 
175
179
  if (error) {
@@ -164,17 +164,25 @@ const submit = async (
164
164
  };
165
165
 
166
166
  if (v.style) {
167
- Object.entries(v.style).forEach(([key, value]) => {
168
- if (value !== '' && newValue.style) {
169
- newValue.style[key] = value;
170
- }
171
- });
172
-
173
- eventData?.changeRecords?.forEach((record) => {
174
- if (record.propPath?.startsWith('style') && record.value === '') {
175
- setValueByKeyPath(record.propPath, record.value, newValue);
176
- }
177
- });
167
+ // 空字符串样式值表示「清除该样式」,需保留才能在 doUpdate 的 mergeWith 中覆盖旧值。
168
+ if (eventData) {
169
+ // 表单编辑:先过滤掉空字符串(避免表单默认空值污染 DSL),
170
+ // 再按 changeRecords 恢复被主动清空的字段。
171
+ Object.entries(v.style).forEach(([key, value]) => {
172
+ if (value !== '' && newValue.style) {
173
+ newValue.style[key] = value;
174
+ }
175
+ });
176
+
177
+ eventData.changeRecords?.forEach((record) => {
178
+ if (record.propPath?.startsWith('style') && record.value === '') {
179
+ setValueByKeyPath(record.propPath, record.value, newValue);
180
+ }
181
+ });
182
+ } else {
183
+ // 源码编辑器保存(无 eventData):style 原样保留,其中的空字符串视为用户主动清除该样式。
184
+ newValue.style = { ...v.style };
185
+ }
178
186
  }
179
187
 
180
188
  // 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
@@ -554,6 +554,12 @@ class Editor extends BaseService {
554
554
  doNotPushHistory,
555
555
  });
556
556
 
557
+ // 页面 / 页面片新增不入历史栈(见上方 isPage / isPageFragment 分支),这里合并补发一次结构变更通知
558
+ const addedPages = newNodes.filter((node) => isPage(node) || isPageFragment(node)) as (MPage | MPageFragment)[];
559
+ if (addedPages.length) {
560
+ historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
561
+ }
562
+
557
563
  return Array.isArray(addNode) ? newNodes : newNodes[0];
558
564
  }
559
565
 
@@ -695,6 +701,12 @@ class Editor extends BaseService {
695
701
 
696
702
  this.emit('remove', nodes);
697
703
  this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
704
+
705
+ // 页面 / 页面片删除不入历史栈(见上方 isPage / isPageFragment 分支),这里合并补发一次结构变更通知
706
+ const removedPages = nodes.filter((node) => isPage(node) || isPageFragment(node)) as (MPage | MPageFragment)[];
707
+ if (removedPages.length) {
708
+ historyService.notifyPageStructureChange({ add: [], remove: removedPages });
709
+ }
698
710
  }
699
711
 
700
712
  public async doUpdate(
@@ -1776,6 +1788,10 @@ class Editor extends BaseService {
1776
1788
  const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
1777
1789
  const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
1778
1790
 
1791
+ // 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
1792
+ const addedPages: (MPage | MPageFragment)[] = [];
1793
+ const removedPages: (MPage | MPageFragment)[] = [];
1794
+
1779
1795
  nextPages.forEach((nextPage) => {
1780
1796
  const prevPage = prevMap.get(`${nextPage.id}`);
1781
1797
  if (!prevPage) {
@@ -1785,6 +1801,7 @@ class Editor extends BaseService {
1785
1801
  { newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
1786
1802
  source,
1787
1803
  );
1804
+ addedPages.push(nextPage);
1788
1805
  } else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
1789
1806
  this.pushPageDiffStep(
1790
1807
  'update',
@@ -1803,8 +1820,13 @@ class Editor extends BaseService {
1803
1820
  { oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
1804
1821
  source,
1805
1822
  );
1823
+ removedPages.push(prevPage);
1806
1824
  }
1807
1825
  });
1826
+
1827
+ if (addedPages.length || removedPages.length) {
1828
+ historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
1829
+ }
1808
1830
  }
1809
1831
 
1810
1832
  /**
@@ -19,7 +19,7 @@
19
19
  import { reactive } from 'vue';
20
20
  import type { Writable } from 'type-fest';
21
21
 
22
- import type { Id } from '@tmagic/core';
22
+ import type { Id, MPage, MPageFragment } from '@tmagic/core';
23
23
  import { guid } from '@tmagic/utils';
24
24
 
25
25
  import type {
@@ -353,6 +353,27 @@ class History extends BaseService {
353
353
  this.emit('mark-saved', { kind: stepType, id });
354
354
  }
355
355
 
356
+ /**
357
+ * 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
358
+ *
359
+ * 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
360
+ * 对 isPage / isPageFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
361
+ * 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
362
+ *
363
+ * 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,
364
+ * 使一次操作只派发一个事件;`add` / `remove` 分别为本次新增与删除的页面列表(其一可为空数组)。
365
+ *
366
+ * @param change 本次结构变更:{ add: 新增的页面列表, remove: 删除的页面列表 }
367
+ */
368
+ public notifyPageStructureChange(change: {
369
+ add: (MPage | MPageFragment)[];
370
+ remove: (MPage | MPageFragment)[];
371
+ }): void {
372
+ // 增删均为空时不派发,避免无意义通知
373
+ if (!change.add.length && !change.remove.length) return;
374
+ this.emit('page-structure-change', change);
375
+ }
376
+
356
377
  /**
357
378
  * 把当前内存中的全部历史栈(页面 / 代码块 / 数据源 / 扩展类型)序列化后写入本地 IndexedDB。
358
379
  *
package/src/type.ts CHANGED
@@ -1385,6 +1385,12 @@ export interface HistoryEvents {
1385
1385
  'mark-saved': [{ kind: HistoryStepType; id?: Id }];
1386
1386
  clear: [{ id: Id; stepType: HistoryStepType }];
1387
1387
  'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }];
1388
+ /**
1389
+ * 页面 / 页面片结构变更(新增 / 删除)时派发,见 {@link HistoryService.notifyPageStructureChange}。
1390
+ * 一次操作(add / remove / setRoot 整体替换)涉及多个页面时合并为**一个**事件;
1391
+ * `add` / `remove` 分别为本次新增与删除的页面列表(其一可为空数组)。
1392
+ */
1393
+ 'page-structure-change': [change: { add: (MPage | MPageFragment)[]; remove: (MPage | MPageFragment)[] }];
1388
1394
  }
1389
1395
 
1390
1396
  export const canUsePluginMethods = {
@@ -243,7 +243,7 @@ export const resolveFieldByPath = (
243
243
  fields: DataSchema[] | undefined,
244
244
  fieldNames: string[],
245
245
  options: { skipNumberIndices?: boolean } = {},
246
- ): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => {
246
+ ): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => {
247
247
  let currentFields = fields || [];
248
248
  let field: DataSchema | undefined;
249
249
 
@@ -252,11 +252,11 @@ export const resolveFieldByPath = (
252
252
  continue;
253
253
  }
254
254
  if (!currentFields.length) {
255
- return { ok: false, fields: currentFields };
255
+ return { ok: false, fields: currentFields, failedName: name };
256
256
  }
257
257
  field = currentFields.find((item) => item.name === name);
258
258
  if (!field) {
259
- return { ok: false, fields: currentFields };
259
+ return { ok: false, fields: currentFields, failedName: name };
260
260
  }
261
261
  currentFields = field.fields || [];
262
262
  }