@tmagic/editor 1.8.0-beta.11 → 1.8.0-beta.13

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 (119) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +7 -4
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +5 -1
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +2 -1
  4. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +18 -123
  5. package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +60 -14
  6. package/dist/es/components/ScrollBar.js +0 -1
  7. package/dist/es/components/ViewForm.js +5 -0
  8. package/dist/es/components/ViewForm.vue_vue_type_script_setup_true_lang.js +58 -0
  9. package/dist/es/editorProps.js +6 -1
  10. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +15 -3
  11. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +2 -17
  12. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +2 -1
  13. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +7 -0
  14. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +5 -17
  15. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -0
  16. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +29 -3
  17. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +50 -88
  18. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -5
  19. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +48 -23
  20. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +6 -3
  21. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +28 -22
  22. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +28 -22
  23. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +28 -22
  24. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +6 -3
  25. package/dist/es/fields/UISelect.js +0 -1
  26. package/dist/es/hooks/use-compare-form.js +119 -0
  27. package/dist/es/index.js +13 -8
  28. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +21 -6
  29. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +9 -6
  30. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +4 -1
  31. package/dist/es/layouts/history-list/composables.js +1 -1
  32. package/dist/es/layouts/history-list/useHistoryRevert.js +7 -3
  33. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +44 -5
  34. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +15 -8
  35. package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
  36. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +15 -31
  37. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  38. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +18 -3
  39. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +4 -0
  40. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  41. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +18 -3
  42. package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
  43. package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
  44. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +2 -1
  45. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +8 -2
  46. package/dist/es/plugin.js +3 -1
  47. package/dist/es/services/codeBlock.js +1 -1
  48. package/dist/es/services/editor.js +104 -5
  49. package/dist/es/services/history.js +1 -1
  50. package/dist/es/services/props.js +3 -2
  51. package/dist/es/services/storage.js +2 -2
  52. package/dist/es/services/ui.js +3 -0
  53. package/dist/es/style.css +104 -55
  54. package/dist/es/utils/data-source/index.js +30 -11
  55. package/dist/es/utils/event.js +123 -0
  56. package/dist/es/utils/monaco-editor.js +22 -6
  57. package/dist/es/utils/props.js +119 -8
  58. package/dist/es/utils/type-match-rules.js +360 -0
  59. package/dist/style.css +104 -55
  60. package/dist/tmagic-editor.umd.cjs +16996 -15806
  61. package/package.json +8 -8
  62. package/src/Editor.vue +3 -1
  63. package/src/components/CodeBlockEditor.vue +3 -0
  64. package/src/components/CompareForm.vue +16 -196
  65. package/src/components/FloatingBox.vue +77 -8
  66. package/src/components/ScrollBar.vue +0 -67
  67. package/src/components/ViewForm.vue +55 -0
  68. package/src/editorProps.ts +13 -0
  69. package/src/fields/CodeSelect.vue +4 -1
  70. package/src/fields/CondOpSelect.vue +2 -24
  71. package/src/fields/DataSourceFields.vue +5 -0
  72. package/src/fields/DataSourceInput.vue +7 -25
  73. package/src/fields/DataSourceMocks.vue +3 -0
  74. package/src/fields/DisplayConds.vue +14 -0
  75. package/src/fields/EventSelect.vue +84 -129
  76. package/src/fields/StyleSetter/Index.vue +2 -8
  77. package/src/fields/StyleSetter/pro/Background.vue +94 -55
  78. package/src/fields/StyleSetter/pro/Border.vue +9 -11
  79. package/src/fields/StyleSetter/pro/Font.vue +66 -64
  80. package/src/fields/StyleSetter/pro/Layout.vue +140 -138
  81. package/src/fields/StyleSetter/pro/Position.vue +69 -67
  82. package/src/fields/StyleSetter/pro/Transform.vue +23 -25
  83. package/src/fields/UISelect.vue +0 -12
  84. package/src/hooks/index.ts +1 -0
  85. package/src/hooks/use-compare-form.ts +187 -0
  86. package/src/index.ts +2 -0
  87. package/src/layouts/CodeEditor.vue +32 -6
  88. package/src/layouts/Framework.vue +7 -6
  89. package/src/layouts/history-list/HistoryDiffDialog.vue +11 -1
  90. package/src/layouts/history-list/useHistoryRevert.ts +4 -0
  91. package/src/layouts/props-panel/FormPanel.vue +66 -5
  92. package/src/layouts/props-panel/PropsPanel.vue +20 -6
  93. package/src/layouts/props-panel/use-style-panel.ts +4 -3
  94. package/src/layouts/sidebar/Sidebar.vue +2 -0
  95. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +4 -1
  96. package/src/layouts/sidebar/code-block/useContentMenu.ts +13 -1
  97. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
  98. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +2 -2
  99. package/src/layouts/sidebar/data-source/useContentMenu.ts +13 -1
  100. package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
  101. package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
  102. package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
  103. package/src/plugin.ts +3 -1
  104. package/src/services/editor.ts +168 -3
  105. package/src/services/ui.ts +6 -0
  106. package/src/theme/event.scss +8 -0
  107. package/src/theme/floating-box.scss +9 -1
  108. package/src/theme/layer-node-content.scss +14 -0
  109. package/src/theme/scroll-bar.scss +64 -0
  110. package/src/theme/theme.scss +3 -0
  111. package/src/theme/ui-select.scss +9 -0
  112. package/src/type.ts +105 -2
  113. package/src/utils/data-source/index.ts +30 -12
  114. package/src/utils/event.ts +224 -0
  115. package/src/utils/index.ts +2 -0
  116. package/src/utils/monaco-editor.ts +27 -0
  117. package/src/utils/props.ts +189 -2
  118. package/src/utils/type-match-rules.ts +678 -0
  119. package/types/index.d.ts +558 -213
@@ -9,7 +9,7 @@
9
9
  :values="values"
10
10
  :disabledShowSrc="disabledShowSrc"
11
11
  :extendState="extendState"
12
- @submit="submit"
12
+ @submit="(v, eventData, error) => submit(v, eventData, error, 'props')"
13
13
  @submit-error="errorHandler"
14
14
  @form-error="errorHandler"
15
15
  @mounted="mountedHandler"
@@ -27,7 +27,7 @@
27
27
  :values="values"
28
28
  :disabledShowSrc="disabledShowSrc"
29
29
  :extendState="extendState"
30
- @submit="submit"
30
+ @submit="(v, eventData, error) => submit(v, eventData, error, 'style')"
31
31
  @submit-error="errorHandler"
32
32
  @form-error="errorHandler"
33
33
  >
@@ -55,7 +55,7 @@
55
55
  </template>
56
56
 
57
57
  <script lang="ts" setup>
58
- import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, watchEffect } from 'vue';
58
+ import { computed, inject, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, watchEffect } from 'vue';
59
59
  import { Close, Sugar } from '@element-plus/icons-vue';
60
60
  import type { OnDrag } from 'gesto';
61
61
 
@@ -66,9 +66,10 @@ import { setValueByKeyPath } from '@tmagic/utils';
66
66
 
67
67
  import MIcon from '@editor/components/Icon.vue';
68
68
  import Resizer from '@editor/components/Resizer.vue';
69
+ import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
69
70
  import { useServices } from '@editor/hooks/use-services';
70
71
  import { Protocol } from '@editor/services/storage';
71
- import type { PropsPanelSlots } from '@editor/type';
72
+ import type { NodeInvalidSource, PropsPanelSlots } from '@editor/type';
72
73
  import { styleTabConfig } from '@editor/utils';
73
74
  import { PROPS_PANEL_WIDTH_STORAGE_KEY } from '@editor/utils/const';
74
75
 
@@ -95,6 +96,8 @@ const emit = defineEmits<{
95
96
 
96
97
  const { editorService, uiService, propsService, storageService } = useServices();
97
98
 
99
+ const enablePropsFormValidate = inject(ENABLE_PROPS_FORM_VALIDATE, false);
100
+
98
101
  const values = ref<FormValue>({});
99
102
  // ts类型应该是FormConfig, 但是打包时会出错,所以暂时用any
100
103
  const curFormConfig = ref<any>([]);
@@ -126,7 +129,12 @@ onBeforeUnmount(() => {
126
129
  propsService.off('props-configs-change', init);
127
130
  });
128
131
 
129
- const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
132
+ const submit = async (
133
+ v: MNode,
134
+ eventData?: ContainerChangeEventData,
135
+ error?: any,
136
+ source: NodeInvalidSource = 'props',
137
+ ) => {
130
138
  try {
131
139
  if (!v.id) {
132
140
  v.id = values.value.id;
@@ -155,7 +163,13 @@ const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
155
163
  // 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
156
164
  const historySource = eventData ? 'props' : 'code';
157
165
 
158
- editorService.update(newValue, { changeRecords: eventData?.changeRecords, historySource });
166
+ editorService.update(newValue, {
167
+ changeRecords: eventData?.changeRecords,
168
+ historySource,
169
+ // 启用校验联动时,仅校验失败(error 存在)才把错误信息随更新传入 editorService 记录;
170
+ // 其余情况(含表单校验成功、CodeEditor 源码保存)不携带 invalidInfo,由 editorService 在执行 update 时统一清除该节点错误。
171
+ ...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}),
172
+ });
159
173
  } catch (e: any) {
160
174
  emit('submit-error', e);
161
175
  }
@@ -2,7 +2,7 @@ import { computed, type Ref, watch } from 'vue';
2
2
 
3
3
  import { Protocol } from '@editor/services/storage';
4
4
  import { Services } from '@editor/type';
5
- import { MIN_CENTER_COLUMN_WIDTH, RIGHT_COLUMN_WIDTH_STORAGE_KEY } from '@editor/utils/const';
5
+ import { RIGHT_COLUMN_WIDTH_STORAGE_KEY } from '@editor/utils/const';
6
6
 
7
7
  export const useStylePanel = (
8
8
  { uiService, storageService }: Pick<Services, 'uiService' | 'storageService'>,
@@ -47,8 +47,9 @@ export const useStylePanel = (
47
47
  }
48
48
 
49
49
  if (columnWidth.center < 0) {
50
- columnWidth.right = columnWidth.right + columnWidth.center - MIN_CENTER_COLUMN_WIDTH;
51
- columnWidth.center = MIN_CENTER_COLUMN_WIDTH;
50
+ const minCenterColumnWidth = uiService.get('minCenterColumnWidth');
51
+ columnWidth.right = columnWidth.right + columnWidth.center - minCenterColumnWidth;
52
+ columnWidth.center = minCenterColumnWidth;
52
53
 
53
54
  propsPanelWidth.value = columnWidth.right / 2;
54
55
  }
@@ -132,6 +132,7 @@
132
132
  v-model:height="columnLeftHeight"
133
133
  :width="columnLeftWidth"
134
134
  :title="config.text"
135
+ :framework-width="frameworkWidth"
135
136
  :position="{
136
137
  left: floatBoxStates[config.$key].left,
137
138
  top: floatBoxStates[config.$key].top,
@@ -221,6 +222,7 @@ const taskLength = computed(() => depService.get('taskLength'));
221
222
  const tipsBarVisible = ref(true);
222
223
 
223
224
  const columnLeftWidth = computed(() => uiService.get('columnWidth')[ColumnLayout.LEFT]);
225
+ const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
224
226
  const { height: editorContentHeight } = useEditorContentHeight();
225
227
  const columnLeftHeight = ref(0);
226
228
 
@@ -121,8 +121,11 @@ const {
121
121
  nodeContentMenuHandler,
122
122
  menuData: contentMenuData,
123
123
  contentMenuHideHandler,
124
+ getTarget,
124
125
  } = useContentMenu((id: string) => {
125
126
  codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
126
127
  });
127
- const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
128
+ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
129
+ props.customContentMenu(contentMenuData, 'code-block', getTarget),
130
+ );
128
131
  </script>
@@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
3
3
  import { cloneDeep } from 'lodash-es';
4
4
 
5
5
  import ContentMenu from '@editor/components/ContentMenu.vue';
6
- import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
6
+ import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
7
7
 
8
8
  export const useContentMenu = (deleteCode: (id: string) => void) => {
9
9
  const eventBus = inject<EventBus>('eventBus');
10
10
  const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
11
11
 
12
12
  let selectId = '';
13
+ let selectData: TreeNodeData | null = null;
14
+
15
+ const getTarget = (): ContentMenuTarget | null => {
16
+ if (!selectId) {
17
+ return null;
18
+ }
19
+ return { id: selectId, data: selectData ?? undefined };
20
+ };
13
21
 
14
22
  const menuData: (MenuButton | MenuComponent)[] = [
15
23
  {
@@ -65,19 +73,23 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
65
73
  menuRef.value?.show(event);
66
74
  if (data.id) {
67
75
  selectId = `${data.id}`;
76
+ selectData = data;
68
77
  } else {
69
78
  selectId = '';
79
+ selectData = null;
70
80
  }
71
81
  }
72
82
  };
73
83
 
74
84
  const contentMenuHideHandler = () => {
75
85
  selectId = '';
86
+ selectData = null;
76
87
  };
77
88
 
78
89
  return {
79
90
  menuData,
80
91
  nodeContentMenuHandler,
81
92
  contentMenuHideHandler,
93
+ getTarget,
82
94
  };
83
95
  };
@@ -1,9 +1,11 @@
1
1
  <template>
2
2
  <FloatingBox
3
+ :body-style="{ padding: '0 16px' }"
3
4
  v-model:visible="boxVisible"
4
5
  v-model:width="width"
5
6
  v-model:height="editorHeight"
6
7
  :title="title"
8
+ :framework-width="frameworkWidth"
7
9
  :position="boxPosition"
8
10
  >
9
11
  <template #body>
@@ -65,6 +67,7 @@ const { height: editorHeight } = useEditorContentHeight();
65
67
 
66
68
  const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
67
69
  const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
70
+ const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
68
71
 
69
72
  /** 供「方法定义」tab 内的字段消费,用于打开数据源详情后自动打开指定方法 */
70
73
  provide(
@@ -171,8 +171,8 @@ eventBus?.on('remove-data-source', (id: string) => {
171
171
  removeHandler(id);
172
172
  });
173
173
 
174
- const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler } = useContentMenu();
174
+ const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler, getTarget } = useContentMenu();
175
175
  const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
176
- props.customContentMenu(contentMenuData, 'data-source'),
176
+ props.customContentMenu(contentMenuData, 'data-source', getTarget),
177
177
  );
178
178
  </script>
@@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
3
3
  import { cloneDeep } from 'lodash-es';
4
4
 
5
5
  import ContentMenu from '@editor/components/ContentMenu.vue';
6
- import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
6
+ import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
7
7
 
8
8
  export const useContentMenu = () => {
9
9
  const eventBus = inject<EventBus>('eventBus');
10
10
  const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
11
11
 
12
12
  let selectId = '';
13
+ let selectData: TreeNodeData | null = null;
14
+
15
+ const getTarget = (): ContentMenuTarget | null => {
16
+ if (!selectId) {
17
+ return null;
18
+ }
19
+ return { id: selectId, data: selectData ?? undefined };
20
+ };
13
21
 
14
22
  const menuData: (MenuButton | MenuComponent)[] = [
15
23
  {
@@ -63,19 +71,23 @@ export const useContentMenu = () => {
63
71
  menuRef.value?.show(event);
64
72
  if (data.id) {
65
73
  selectId = `${data.id}`;
74
+ selectData = data;
66
75
  } else {
67
76
  selectId = '';
77
+ selectData = null;
68
78
  }
69
79
  }
70
80
  };
71
81
 
72
82
  const contentMenuHideHandler = () => {
73
83
  selectId = '';
84
+ selectData = null;
74
85
  };
75
86
 
76
87
  return {
77
88
  menuData,
78
89
  nodeContentMenuHandler,
79
90
  contentMenuHideHandler,
91
+ getTarget,
80
92
  };
81
93
  };
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <span class="m-editor-layer-node-content" :class="{ 'is-invalid': hasError }">
3
+ <span class="m-editor-layer-node-label">{{ label }}</span>
4
+ <TMagicTooltip v-if="hasError" placement="top">
5
+ <template #content>
6
+ <span v-html="errorMessage"></span>
7
+ </template>
8
+ <MIcon class="m-editor-layer-node-error-icon" :icon="WarningFilled"></MIcon>
9
+ </TMagicTooltip>
10
+ </span>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed } from 'vue';
15
+ import { WarningFilled } from '@element-plus/icons-vue';
16
+
17
+ import { stripValidateSuggestion, TMagicTooltip } from '@tmagic/design';
18
+
19
+ import MIcon from '@editor/components/Icon.vue';
20
+ import { useServices } from '@editor/hooks/use-services';
21
+ import type { TreeNodeData } from '@editor/type';
22
+
23
+ defineOptions({
24
+ name: 'MEditorLayerNodeContent',
25
+ });
26
+
27
+ const props = defineProps<{
28
+ data: TreeNodeData;
29
+ }>();
30
+
31
+ const { editorService } = useServices();
32
+
33
+ const label = computed(() => `${props.data.name} (${props.data.id})`);
34
+
35
+ /** 读取集中存储的校验错误状态,建立响应式依赖 */
36
+ const invalidInfo = computed(() => editorService.get('invalidNodeIds').get(props.data.id));
37
+
38
+ const hasError = computed(() => Boolean(invalidInfo.value?.props || invalidInfo.value?.style));
39
+
40
+ /**
41
+ * 去掉单条校验文案中的「修改建议」部分。
42
+ *
43
+ * 错误文案可能是多条错误以 `<br>` 拼接的 HTML,每条本身形如 `主文案\n\n建议`。
44
+ * 组件树 tooltip 仅展示错误描述,不展示修改建议(建议仅在错误汇总面板展示),
45
+ * 故先按 `<br>` 拆分,每段用 `stripValidateSuggestion` 截断建议后再拼回。
46
+ */
47
+ const stripSuggestion = (text?: string): string =>
48
+ String(text ?? '')
49
+ .split(/<br\s*\/?>/i)
50
+ .map((segment) => stripValidateSuggestion(segment))
51
+ .join('<br>');
52
+
53
+ /** 合并属性表单与样式表单的错误文案(去掉建议,本身可能仍是含 <br> 的 HTML) */
54
+ const errorMessage = computed(() =>
55
+ [stripSuggestion(invalidInfo.value?.props), stripSuggestion(invalidInfo.value?.style)].filter(Boolean).join('<br>'),
56
+ );
57
+ </script>
@@ -33,7 +33,9 @@
33
33
  </template>
34
34
 
35
35
  <template #tree-node-label="{ data: nodeData }">
36
- <slot name="layer-node-label" :data="nodeData"></slot>
36
+ <slot name="layer-node-label" :data="nodeData">
37
+ <LayerNodeContent :data="nodeData"></LayerNodeContent>
38
+ </slot>
37
39
  </template>
38
40
  </Tree>
39
41
 
@@ -69,6 +71,7 @@ import type {
69
71
  } from '@editor/type';
70
72
 
71
73
  import LayerMenu from './LayerMenu.vue';
74
+ import LayerNodeContent from './LayerNodeContent.vue';
72
75
  import LayerNodeTool from './LayerNodeTool.vue';
73
76
  import { useClick } from './use-click';
74
77
  import { useDrag } from './use-drag';
@@ -8,6 +8,7 @@
8
8
  ref="box"
9
9
  v-model:visible="visible"
10
10
  title="当前位置下的组件"
11
+ :framework-width="frameworkWidth"
11
12
  :position="menuPosition"
12
13
  >
13
14
  <template #body>
@@ -36,7 +37,9 @@ import { useNodeStatus } from '@editor/layouts/sidebar/layer/use-node-status';
36
37
  import type { TreeNodeData } from '@editor/type';
37
38
 
38
39
  const services = useServices();
39
- const { editorService } = services;
40
+ const { editorService, uiService } = services;
41
+
42
+ const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
40
43
 
41
44
  const visible = ref(false);
42
45
  const buttonVisible = ref(false);
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 from '@tmagic/form';
24
+ import formPlugin, { registerTypeMatchRules } from '@tmagic/form';
25
25
  import tablePlugin from '@tmagic/table';
26
26
 
27
27
  import Code from './fields/Code.vue';
@@ -44,6 +44,7 @@ import StyleSetter from './fields/StyleSetter/Index.vue';
44
44
  import uiSelect from './fields/UISelect.vue';
45
45
  import CodeEditor from './layouts/CodeEditor.vue';
46
46
  import { setEditorConfig } from './utils/config';
47
+ import { editorTypeMatchRules } from './utils/type-match-rules';
47
48
  import Editor from './Editor.vue';
48
49
  import type { EditorInstallOptions } from './type';
49
50
 
@@ -64,6 +65,7 @@ export default {
64
65
  app.use(designPlugin, opt || {});
65
66
  app.use(formPlugin, opt || {});
66
67
  app.use(tablePlugin);
68
+ registerTypeMatchRules(editorTypeMatchRules);
67
69
 
68
70
  app.config.globalProperties.$TMAGIC_EDITOR = option;
69
71
  setEditorConfig(option);
@@ -31,6 +31,7 @@ import {
31
31
  isPage,
32
32
  isPageFragment,
33
33
  setValueByKeyPath,
34
+ traverseNode,
34
35
  } from '@tmagic/utils';
35
36
 
36
37
  import BaseService from '@editor/services//BaseService';
@@ -47,6 +48,8 @@ import type {
47
48
  EditorNodeInfo,
48
49
  HistoryOpSource,
49
50
  HistoryOpType,
51
+ NodeInvalidInfo,
52
+ NodeInvalidSource,
50
53
  PastePosition,
51
54
  StepDiffItem,
52
55
  StepValue,
@@ -104,12 +107,18 @@ class Editor extends BaseService {
104
107
  stageLoading: true,
105
108
  highlightNode: null,
106
109
  modifiedNodeIds: new Map(),
110
+ invalidNodeIds: new Map(),
107
111
  pageLength: 0,
108
112
  pageFragmentLength: 0,
109
113
  disabledMultiSelect: false,
110
114
  alwaysMultiSelect: false,
111
115
  });
112
116
  private selectionBeforeOp: Id[] | null = null;
117
+ /**
118
+ * 操作前的节点校验错误快照,与 selectionBeforeOp 同时在 captureSelectionBeforeOp 中捕获,
119
+ * 供 pushOpHistory 写入 step.extra.invalidNodeIdsBefore,用于撤销时还原到「操作前」的错误状态。
120
+ */
121
+ private invalidNodeIdsBeforeOp: Map<Id, NodeInvalidInfo> | null = null;
113
122
  /**
114
123
  * 最近一次 pushOpHistory 写入的历史记录 uuid。
115
124
  * 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
@@ -127,7 +136,7 @@ class Editor extends BaseService {
127
136
 
128
137
  /**
129
138
  * 设置当前指点节点配置
130
- * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
139
+ * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength
131
140
  * @param value MNode
132
141
  * @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
133
142
  */
@@ -180,12 +189,15 @@ class Editor extends BaseService {
180
189
  }
181
190
 
182
191
  this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
192
+
193
+ // 整体替换 DSL 后,清理不再存在于新 DSL 中的失效节点错误记录,避免残留误报。
194
+ this.pruneInvalidNodeIds();
183
195
  }
184
196
  }
185
197
 
186
198
  /**
187
199
  * 获取当前指点节点配置
188
- * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength'
200
+ * @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength'
189
201
  * @returns MNode
190
202
  */
191
203
  public get<K extends StoreStateKey>(name: K): StoreState[K] {
@@ -663,6 +675,9 @@ class Editor extends BaseService {
663
675
 
664
676
  await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
665
677
 
678
+ // 删除节点时同步清理其(含子树)的校验错误记录;置于 pushOpHistory 之前,使历史快照与本次删除对齐。
679
+ this.removeInvalidNodesBySubtree(nodes);
680
+
666
681
  if (removedItems.length > 0 && pageForOp) {
667
682
  if (!doNotPushHistory) {
668
683
  this.pushOpHistory('remove', {
@@ -770,11 +785,23 @@ class Editor extends BaseService {
770
785
  doNotPushHistory?: boolean;
771
786
  historyDescription?: string;
772
787
  historySource?: HistoryOpSource;
788
+ /**
789
+ * 属性面板提交时携带的校验错误信息,在写入历史记录之前落库,
790
+ * 使历史快照与本次变更对齐,从而 undo/redo 能正确还原错误标记。
791
+ */
792
+ invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string };
773
793
  } = {},
774
794
  ): Promise<MNode | MNode[]> {
775
795
  this.captureSelectionBeforeOp();
776
796
 
777
- const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
797
+ const {
798
+ doNotPushHistory = false,
799
+ changeRecordList,
800
+ changeRecords,
801
+ historyDescription,
802
+ historySource,
803
+ invalidInfo,
804
+ } = data;
778
805
 
779
806
  const nodes = Array.isArray(config) ? config : [config];
780
807
 
@@ -787,6 +814,9 @@ class Editor extends BaseService {
787
814
  }),
788
815
  );
789
816
 
817
+ // 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。
818
+ this.applyInvalidInfo(config, invalidInfo);
819
+
790
820
  if (updateData[0].oldNode?.type !== NodeType.ROOT) {
791
821
  const curNodes = this.get('nodes');
792
822
  if (curNodes.length) {
@@ -1542,6 +1572,7 @@ class Editor extends BaseService {
1542
1572
  this.set('stage', null);
1543
1573
  this.set('highlightNode', null);
1544
1574
  this.set('modifiedNodeIds', new Map());
1575
+ this.set('invalidNodeIds', new Map());
1545
1576
  this.set('pageLength', 0);
1546
1577
  }
1547
1578
 
@@ -1555,6 +1586,62 @@ class Editor extends BaseService {
1555
1586
  this.get('modifiedNodeIds').clear();
1556
1587
  }
1557
1588
 
1589
+ /**
1590
+ * 记录(或覆盖)某个节点在指定来源(属性表单 / 样式表单)上的校验错误信息。
1591
+ * @param id 节点 id
1592
+ * @param source 错误来源:'props'(属性表单)| 'style'(样式表单)
1593
+ * @param message 错误文案(可能是包含 <br> 的 HTML)
1594
+ */
1595
+ public setInvalidNode(id: Id, source: NodeInvalidSource, message: string) {
1596
+ const map = this.get('invalidNodeIds');
1597
+ const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
1598
+ info[source] = message;
1599
+ map.set(id, info);
1600
+ this.emit('invalid-node-change', map);
1601
+ }
1602
+
1603
+ /**
1604
+ * 删除节点的校验错误记录。
1605
+ * @param id 节点 id
1606
+ * @param source 指定来源则仅删除该来源;不传则删除该节点全部来源的错误
1607
+ */
1608
+ public deleteInvalidNode(id: Id, source?: NodeInvalidSource) {
1609
+ const map = this.get('invalidNodeIds');
1610
+ if (!map.has(id)) return;
1611
+
1612
+ if (!source) {
1613
+ map.delete(id);
1614
+ } else {
1615
+ const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
1616
+ delete info[source];
1617
+ if (info.props || info.style) {
1618
+ map.set(id, info);
1619
+ } else {
1620
+ map.delete(id);
1621
+ }
1622
+ }
1623
+
1624
+ this.emit('invalid-node-change', map);
1625
+ }
1626
+
1627
+ /** 获取当前存在校验错误的节点错误 Map(key 为节点 id) */
1628
+ public getInvalidNodeIds(): Map<Id, NodeInvalidInfo> {
1629
+ return this.get('invalidNodeIds');
1630
+ }
1631
+
1632
+ /** 获取指定节点的校验错误信息 */
1633
+ public getInvalidNodeInfo(id: Id): NodeInvalidInfo | undefined {
1634
+ return this.get('invalidNodeIds').get(id);
1635
+ }
1636
+
1637
+ /** 清空全部校验错误记录 */
1638
+ public resetInvalidNodeId() {
1639
+ const map = this.get('invalidNodeIds');
1640
+ if (map.size === 0) return;
1641
+ map.clear();
1642
+ this.emit('invalid-node-change', map);
1643
+ }
1644
+
1558
1645
  public usePlugin(options: AsyncHookPlugin<AsyncMethodName, Editor>): void {
1559
1646
  super.usePlugin(options);
1560
1647
  }
@@ -1577,6 +1664,68 @@ class Editor extends BaseService {
1577
1664
  return super.emit(eventName, ...args);
1578
1665
  }
1579
1666
 
1667
+ /**
1668
+ * 应用一次属性面板提交携带的校验错误信息(在写入历史记录之前调用,使历史快照与本次变更对齐)。
1669
+ * - invalidInfo 为空(调用方未携带):认为本次更新对应的节点已无校验错误,清除其全部来源的错误;
1670
+ * - error 非空则记录错误,为空则清除对应来源的错误。
1671
+ */
1672
+ private applyInvalidInfo(
1673
+ config: MNode | MNode[],
1674
+ invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string },
1675
+ ) {
1676
+ if (!invalidInfo) {
1677
+ // 调用方未携带 invalidInfo:本次更新对应节点不应再保留校验错误,清除其全部来源。
1678
+ const ids = (Array.isArray(config) ? config : [config])
1679
+ .map((node) => node.id)
1680
+ .filter((id): id is Id => id !== undefined);
1681
+ ids.forEach((id) => this.deleteInvalidNode(id));
1682
+ return;
1683
+ }
1684
+ const { id, source, error } = invalidInfo;
1685
+ if (error) {
1686
+ this.setInvalidNode(id, source, error);
1687
+ } else {
1688
+ this.deleteInvalidNode(id, source);
1689
+ }
1690
+ }
1691
+
1692
+ /** 删除被移除节点(含其子树)的校验错误记录,避免残留误报 */
1693
+ private removeInvalidNodesBySubtree(nodes: MNode[]) {
1694
+ const map = this.get('invalidNodeIds');
1695
+ if (map.size === 0) return;
1696
+
1697
+ let changed = false;
1698
+ nodes.forEach((node) => {
1699
+ traverseNode(node, (n) => {
1700
+ if (n.id !== undefined && map.delete(n.id)) {
1701
+ changed = true;
1702
+ }
1703
+ });
1704
+ });
1705
+
1706
+ if (changed) {
1707
+ this.emit('invalid-node-change', map);
1708
+ }
1709
+ }
1710
+
1711
+ /** 清理不在当前 DSL 中的失效节点错误记录(用于整体替换 root 后) */
1712
+ private pruneInvalidNodeIds() {
1713
+ const map = this.get('invalidNodeIds');
1714
+ if (map.size === 0) return;
1715
+
1716
+ let changed = false;
1717
+ for (const id of [...map.keys()]) {
1718
+ if (!this.getNodeById(id, false)) {
1719
+ map.delete(id);
1720
+ changed = true;
1721
+ }
1722
+ }
1723
+
1724
+ if (changed) {
1725
+ this.emit('invalid-node-change', map);
1726
+ }
1727
+ }
1728
+
1580
1729
  private addModifiedNodeId(id: Id) {
1581
1730
  this.get('modifiedNodeIds').set(id, id);
1582
1731
  }
@@ -1598,6 +1747,9 @@ class Editor extends BaseService {
1598
1747
  private captureSelectionBeforeOp() {
1599
1748
  if (this.selectionBeforeOp) return;
1600
1749
  this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
1750
+ // 与选区快照同步捕获「操作前」的校验错误状态;因 applyInvalidInfo 会在 pushOpHistory 前修改 invalidNodeIds,
1751
+ // 必须在此(操作最开始)留存操作前快照,供 undo 还原。
1752
+ this.invalidNodeIdsBeforeOp = new Map(this.get('invalidNodeIds'));
1601
1753
  }
1602
1754
 
1603
1755
  /**
@@ -1704,6 +1856,9 @@ class Editor extends BaseService {
1704
1856
  selectedBefore: this.selectionBeforeOp ?? [],
1705
1857
  selectedAfter: this.get('nodes').map((n) => n.id),
1706
1858
  modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
1859
+ // 方向性双快照:undo 还原「操作前」错误状态,redo 还原「操作后」错误状态。
1860
+ invalidNodeIdsBefore: new Map(this.invalidNodeIdsBeforeOp ?? this.get('invalidNodeIds')),
1861
+ invalidNodeIdsAfter: new Map(this.get('invalidNodeIds')),
1707
1862
  },
1708
1863
  diff,
1709
1864
  };
@@ -1716,6 +1871,7 @@ class Editor extends BaseService {
1716
1871
  const historyId = pushed ? step.uuid : null;
1717
1872
  this.lastPushedHistoryId = historyId;
1718
1873
  this.selectionBeforeOp = null;
1874
+ this.invalidNodeIdsBeforeOp = null;
1719
1875
  return historyId;
1720
1876
  }
1721
1877
 
@@ -1841,6 +1997,15 @@ class Editor extends BaseService {
1841
1997
 
1842
1998
  this.set('modifiedNodeIds', step.extra?.modifiedNodeIds ?? new Map());
1843
1999
 
2000
+ // 还原校验错误标记:因 undo/redo 复用同一 step,需按方向取「操作前 / 操作后」快照——
2001
+ // undo(reverse=true) 还原到操作前的错误状态(撤销一个「校验失败」的改动后错误消失),
2002
+ // redo(reverse=false) 还原到操作后的错误状态(重做后错误恢复)。
2003
+ // 浅拷贝一份以隔离历史快照,避免后续 set/deleteInvalidNode 反向污染 step.extra。
2004
+ const invalidToRestore =
2005
+ (reverse ? step.extra?.invalidNodeIdsBefore : step.extra?.invalidNodeIdsAfter) ?? new Map();
2006
+ this.set('invalidNodeIds', new Map(invalidToRestore));
2007
+ this.emit('invalid-node-change', this.get('invalidNodeIds'));
2008
+
1844
2009
  const page = toRaw(this.get('page'));
1845
2010
  if (page) {
1846
2011
  const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];