@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
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <MContainer
3
+ :prop="prop"
3
4
  :config="config"
4
5
  :model="values"
5
6
  :last-values="lastValues"
@@ -21,6 +22,7 @@ defineProps<{
21
22
  isCompare?: boolean;
22
23
  disabled?: boolean;
23
24
  size?: 'large' | 'default' | 'small';
25
+ prop?: string;
24
26
  }>();
25
27
 
26
28
  const emit = defineEmits<{
@@ -29,33 +31,29 @@ const emit = defineEmits<{
29
31
  }>();
30
32
 
31
33
  const config = defineFormItem({
34
+ name: 'transform',
32
35
  items: [
33
36
  {
34
- name: 'transform',
35
- items: [
36
- {
37
- name: 'rotate',
38
- text: '旋转角度',
39
- labelWidth: '68px',
40
- type: 'data-source-field-select',
41
- checkStrictly: false,
42
- dataSourceFieldType: ['string', 'number'],
43
- fieldConfig: {
44
- type: 'text',
45
- },
46
- },
47
- {
48
- name: 'scale',
49
- text: '缩放',
50
- labelWidth: '68px',
51
- type: 'data-source-field-select',
52
- checkStrictly: false,
53
- dataSourceFieldType: ['string', 'number'],
54
- fieldConfig: {
55
- type: 'text',
56
- },
57
- },
58
- ],
37
+ name: 'rotate',
38
+ text: '旋转角度',
39
+ labelWidth: '68px',
40
+ type: 'data-source-field-select',
41
+ checkStrictly: false,
42
+ dataSourceFieldType: ['string', 'number'],
43
+ fieldConfig: {
44
+ type: 'text',
45
+ },
46
+ },
47
+ {
48
+ name: 'scale',
49
+ text: '缩放',
50
+ labelWidth: '68px',
51
+ type: 'data-source-field-select',
52
+ checkStrictly: false,
53
+ dataSourceFieldType: ['string', 'number'],
54
+ fieldConfig: {
55
+ type: 'text',
56
+ },
59
57
  },
60
58
  ],
61
59
  });
@@ -127,15 +127,3 @@ const unhighlight = () => {
127
127
  stageOverlayService.get('stage')?.clearHighlight();
128
128
  };
129
129
  </script>
130
-
131
- <style lang="scss">
132
- .m-fields-ui-select {
133
- cursor: pointer;
134
- i {
135
- margin-right: 3px;
136
- }
137
- span {
138
- color: #2882e0;
139
- }
140
- }
141
- </style>
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  export * from './use-code-block-edit';
20
+ export * from './use-compare-form';
20
21
  export * from './use-stage';
21
22
  export * from './use-float-box';
22
23
  export * from './use-window-rect';
@@ -0,0 +1,187 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import { computed, type ComputedRef, inject, provide, type Ref, ref, useTemplateRef, watch, watchEffect } from 'vue';
20
+
21
+ import type { CodeBlockContent, MNode } from '@tmagic/core';
22
+ import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
23
+
24
+ import type { CompareFormBaseProps } from '@editor/type';
25
+ import { getCodeBlockFormConfig } from '@editor/utils/code-block';
26
+ import { removeStyleDisplayConfig } from '@editor/utils/props';
27
+
28
+ export interface UseCompareFormReturn {
29
+ config: Ref<FormConfig>;
30
+ currentValues: ComputedRef<FormValue>;
31
+ wrapperStyle: ComputedRef<Record<string, string> | undefined>;
32
+ mergedExtendState: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
33
+ loadConfig: () => Promise<void>;
34
+ formRef: Readonly<Ref<InstanceType<typeof MForm> | null>>;
35
+ normalizeCodeBlockValue: (v: Partial<CodeBlockContent> | Record<string, any> | undefined) => Record<string, any>;
36
+ }
37
+
38
+ /**
39
+ * CompareForm / ViewForm 共用逻辑:
40
+ * - 按 `category`(node / data-source / code-block) 加载 FormConfig(支持自定义 `loadConfig`);
41
+ * - 代码块 `content` 归一化为字符串;
42
+ * - 外层容器固定高度 + 内部滚动的 `wrapperStyle`;
43
+ * - 将 services / stage 注入 MForm.formState,保证 filterFunction 上下文一致。
44
+ *
45
+ * 两个组件的差异仅在于是否做新旧值对比,这部分逻辑保留在各自组件中。
46
+ */
47
+ export const useCompareForm = (props: CompareFormBaseProps): UseCompareFormReturn => {
48
+ provide('services', props.services);
49
+
50
+ const config = ref<FormConfig>([]);
51
+
52
+ /** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
53
+ const codeOptions = inject<Record<string, any>>('codeOptions', {});
54
+
55
+ /** 将代码块的 content 字段统一成字符串,便于在表单 / 对比中展示 */
56
+ const normalizeCodeBlockValue = (
57
+ v: Partial<CodeBlockContent> | Record<string, any> | undefined,
58
+ ): Record<string, any> => {
59
+ if (!v) return {};
60
+ const next: Record<string, any> = { ...v };
61
+ if (next.content && typeof next.content !== 'string') {
62
+ try {
63
+ next.content = next.content.toString();
64
+ } catch {
65
+ next.content = '';
66
+ }
67
+ }
68
+ return next;
69
+ };
70
+
71
+ const currentValues = computed<FormValue>(() => {
72
+ if (props.category === 'code-block') {
73
+ return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
74
+ }
75
+ return (props.value || {}) as FormValue;
76
+ });
77
+
78
+ /**
79
+ * 外层包裹层的样式:当传入 `height` 时启用固定高度 + 内部滚动,
80
+ * 这样滚动条会出现在组件内部,避免父容器(如 Dialog)自身也产生滚动。
81
+ */
82
+ const wrapperStyle = computed(() => {
83
+ if (!props.height) return undefined;
84
+ const style: Record<string, string> = {
85
+ height: props.height,
86
+ overflow: 'auto',
87
+ };
88
+ return style;
89
+ });
90
+
91
+ const mergedExtendState = (state: FormState) => {
92
+ const extendState = props.extendState ?? ((s: FormState) => s);
93
+ return extendState(props.baseFormState || state);
94
+ };
95
+
96
+ /**
97
+ * 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
98
+ * 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
99
+ */
100
+ const defaultLoadConfig = async (): Promise<FormConfig> => {
101
+ if (!props.services) {
102
+ return [];
103
+ }
104
+
105
+ switch (props.category) {
106
+ case 'node': {
107
+ if (!props.type) {
108
+ return [];
109
+ }
110
+ return removeStyleDisplayConfig(
111
+ await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
112
+ );
113
+ }
114
+ case 'data-source': {
115
+ const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
116
+ // 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
117
+ // 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
118
+ // 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
119
+ return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
120
+ }
121
+ case 'code-block': {
122
+ return getCodeBlockFormConfig({
123
+ paramColConfig: props.services.codeBlockService.getParamsColConfig(),
124
+ // 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比 / 展示场景下 props.dataSourceType
125
+ // 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
126
+ isDataSource: () => Boolean(props.dataSourceType),
127
+ dataSourceType: () => props.dataSourceType,
128
+ codeOptions,
129
+ // 对比 / 展示模式只读,不需要校验/语法检查
130
+ editable: false,
131
+ });
132
+ }
133
+ default:
134
+ return [];
135
+ }
136
+ };
137
+
138
+ const loadConfig = async () => {
139
+ if (props.loadConfig) {
140
+ config.value = await props.loadConfig({
141
+ category: props.category as string,
142
+ type: props.type,
143
+ dataSourceType: props.dataSourceType,
144
+ defaultLoadConfig,
145
+ });
146
+ return;
147
+ }
148
+
149
+ config.value = await defaultLoadConfig();
150
+ };
151
+
152
+ watch(
153
+ [() => props.category, () => props.type, () => props.dataSourceType, () => props.loadConfig],
154
+ () => {
155
+ loadConfig();
156
+ },
157
+ { immediate: true },
158
+ );
159
+
160
+ const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
161
+
162
+ /**
163
+ * 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
164
+ * 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
165
+ * 在执行时拿不到 `formState.services` 而报错。
166
+ *
167
+ * 与 props-panel/FormPanel.vue 中的注入方式保持一致:
168
+ * - services:整个 useServices() 返回的服务集合;
169
+ * - stage:当前 editorService.get('stage') 的最新值。
170
+ */
171
+ watchEffect(() => {
172
+ if (formRef.value && props.services) {
173
+ formRef.value.formState.stage = props.services.editorService.get('stage');
174
+ formRef.value.formState.services = props.services;
175
+ }
176
+ });
177
+
178
+ return {
179
+ config,
180
+ currentValues,
181
+ wrapperStyle,
182
+ mergedExtendState,
183
+ loadConfig,
184
+ formRef,
185
+ normalizeCodeBlockValue,
186
+ };
187
+ };
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export { default as componentListService } from './services/componentList';
48
48
  export { default as keybindingService } from './services/keybinding';
49
49
  export { default as ComponentListPanel } from './layouts/sidebar/ComponentListPanel.vue';
50
50
  export { default as LayerPanel } from './layouts/sidebar/layer/LayerPanel.vue';
51
+ export { default as LayerNodeContent } from './layouts/sidebar/layer/LayerNodeContent.vue';
51
52
  export { default as CodeSelect } from './fields/CodeSelect.vue';
52
53
  export { default as CodeSelectCol } from './fields/CodeSelectCol.vue';
53
54
  export { default as DataSourceFields } from './fields/DataSourceFields.vue';
@@ -73,6 +74,7 @@ export { default as SplitView } from './components/SplitView.vue';
73
74
  export { default as Resizer } from './components/Resizer.vue';
74
75
  export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
75
76
  export { default as CompareForm } from './components/CompareForm.vue';
77
+ export { default as ViewForm } from './components/ViewForm.vue';
76
78
  export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
77
79
  export { default as HistoryListBucketTab } from './layouts/history-list/BucketTab.vue';
78
80
  export { default as HistoryDiffDialog } from './layouts/history-list/HistoryDiffDialog.vue';
@@ -194,6 +194,9 @@ const values = ref('');
194
194
  const loading = ref(false);
195
195
  const codeEditorEl = useTemplateRef<HTMLDivElement>('codeEditor');
196
196
 
197
+ // 组件是否已卸载,避免异步初始化过程中组件被销毁后继续创建编辑器
198
+ let destroyed = false;
199
+
197
200
  const resizeObserver = new globalThis.ResizeObserver(
198
201
  throttle((): void => {
199
202
  vsEditor?.layout();
@@ -254,11 +257,18 @@ const handleKeyDown = (e: KeyboardEvent) => {
254
257
  }
255
258
  };
256
259
 
260
+ // 判断容器是否仍然有效:组件未卸载、ref 存在且已挂载到文档树中
261
+ // Monaco 在创建编辑器时会向上遍历 parentNode 判断是否处于 shadow DOM,
262
+ // 若容器为 null 或已从文档树脱离,将抛出 "Cannot read properties of null (reading 'parentNode')"
263
+ const isEditorElValid = () => !destroyed && !!codeEditorEl.value && codeEditorEl.value.isConnected;
264
+
257
265
  const init = async () => {
258
- if (!codeEditorEl.value) return;
266
+ if (!isEditorElValid()) return;
259
267
 
260
- if (codeEditorEl.value.clientHeight === 0) {
268
+ if (codeEditorEl.value!.clientHeight === 0) {
261
269
  await nextTick();
270
+ // await 期间组件可能已被卸载或容器被移除
271
+ if (!isEditorElValid()) return;
262
272
  }
263
273
 
264
274
  // 重置缓存的额外高度,因为编辑器重新初始化
@@ -266,6 +276,11 @@ const init = async () => {
266
276
 
267
277
  monaco = await loadMonaco();
268
278
 
279
+ // 加载 monaco 是异步过程,期间组件可能已被卸载或容器被移除
280
+ if (!isEditorElValid()) return;
281
+
282
+ const editorEl = codeEditorEl.value!;
283
+
269
284
  const options = {
270
285
  value: values.value,
271
286
  language: props.language,
@@ -275,7 +290,7 @@ const init = async () => {
275
290
  };
276
291
 
277
292
  if (props.type === 'diff') {
278
- vsDiffEditor = await getEditorConfig('customCreateMonacoDiffEditor')(monaco!, codeEditorEl.value, options);
293
+ vsDiffEditor = await getEditorConfig('customCreateMonacoDiffEditor')(monaco!, editorEl, options);
279
294
 
280
295
  // 监听diff编辑器内容变化
281
296
  vsDiffEditor.getModifiedEditor().onDidChangeModelContent(() => {
@@ -285,7 +300,7 @@ const init = async () => {
285
300
  }
286
301
  });
287
302
  } else {
288
- vsEditor = await getEditorConfig('customCreateMonacoEditor')(monaco!, codeEditorEl.value, options);
303
+ vsEditor = await getEditorConfig('customCreateMonacoEditor')(monaco!, editorEl, options);
289
304
 
290
305
  // 监听编辑器内容变化
291
306
  vsEditor.onDidChangeModelContent(() => {
@@ -296,10 +311,19 @@ const init = async () => {
296
311
  });
297
312
  }
298
313
 
314
+ // 编辑器创建可能是异步的,创建完成后若组件已卸载则立即销毁,避免内存泄漏
315
+ if (destroyed) {
316
+ vsEditor?.dispose();
317
+ vsDiffEditor?.dispose();
318
+ vsEditor = null;
319
+ vsDiffEditor = null;
320
+ return;
321
+ }
322
+
299
323
  setEditorValue(props.initValues, props.modifiedValues);
300
324
 
301
325
  emit('initd', vsEditor);
302
- codeEditorEl.value.addEventListener('keydown', handleKeyDown);
326
+ editorEl.addEventListener('keydown', handleKeyDown);
303
327
 
304
328
  if (props.type !== 'diff' && props.autoSave) {
305
329
  vsEditor?.onDidBlurEditorWidget(() => {
@@ -311,7 +335,7 @@ const init = async () => {
311
335
  });
312
336
  }
313
337
 
314
- resizeObserver.observe(codeEditorEl.value);
338
+ resizeObserver.observe(editorEl);
315
339
  };
316
340
 
317
341
  watch(
@@ -362,6 +386,8 @@ onMounted(async () => {
362
386
  });
363
387
 
364
388
  onBeforeUnmount(() => {
389
+ destroyed = true;
390
+
365
391
  resizeObserver.disconnect();
366
392
 
367
393
  vsEditor?.dispose();
@@ -25,9 +25,9 @@
25
25
  right-class="m-editor-framework-right"
26
26
  :left="hideSidebar ? undefined : columnWidth.left"
27
27
  :right="columnWidth.right"
28
- :min-left="hideSidebar ? 0 : MIN_LEFT_COLUMN_WIDTH"
29
- :min-right="MIN_RIGHT_COLUMN_WIDTH"
30
- :min-center="MIN_CENTER_COLUMN_WIDTH"
28
+ :min-left="hideSidebar ? 0 : minLeftColumnWidth"
29
+ :min-right="minRightColumnWidth"
30
+ :min-center="minCenterColumnWidth"
31
31
  :width="frameworkRect.width"
32
32
  @change="columnWidthChange"
33
33
  >
@@ -78,9 +78,6 @@ import { getEditorConfig } from '@editor/utils/config';
78
78
  import {
79
79
  DEFAULT_LEFT_COLUMN_WIDTH,
80
80
  LEFT_COLUMN_WIDTH_STORAGE_KEY,
81
- MIN_CENTER_COLUMN_WIDTH,
82
- MIN_LEFT_COLUMN_WIDTH,
83
- MIN_RIGHT_COLUMN_WIDTH,
84
81
  RIGHT_COLUMN_WIDTH_STORAGE_KEY,
85
82
  } from '@editor/utils/const';
86
83
 
@@ -116,6 +113,10 @@ const showSrc = computed(() => uiService.get('showSrc'));
116
113
 
117
114
  const columnWidth = computed(() => uiService.get('columnWidth'));
118
115
 
116
+ const minLeftColumnWidth = computed(() => uiService.get('minLeftColumnWidth'));
117
+ const minCenterColumnWidth = computed(() => uiService.get('minCenterColumnWidth'));
118
+ const minRightColumnWidth = computed(() => uiService.get('minRightColumnWidth'));
119
+
119
120
  watch(pageLength, () => {
120
121
  splitViewRef.value?.updateWidth();
121
122
  });
@@ -48,6 +48,7 @@
48
48
  :load-config="loadConfig"
49
49
  :self-diff-field-types="selfDiffFieldTypes"
50
50
  :services="props.services"
51
+ :size="props.size"
51
52
  height="70vh"
52
53
  />
53
54
 
@@ -77,7 +78,14 @@
77
78
  import { computed, ref, watch } from 'vue';
78
79
  import { isEqual } from 'lodash-es';
79
80
 
80
- import { TMagicButton, TMagicDialog, TMagicRadioButton, TMagicRadioGroup, TMagicTag } from '@tmagic/design';
81
+ import {
82
+ type FieldSize,
83
+ TMagicButton,
84
+ TMagicDialog,
85
+ TMagicRadioButton,
86
+ TMagicRadioGroup,
87
+ TMagicTag,
88
+ } from '@tmagic/design';
81
89
  import type { FormState } from '@tmagic/form';
82
90
 
83
91
  import CompareForm from '@editor/components/CompareForm.vue';
@@ -104,6 +112,8 @@ const props = withDefaults(
104
112
  */
105
113
  loadConfig?: CompareFormLoadConfig;
106
114
  width?: string;
115
+ /** 差异对比表单内组件的尺寸(透传给 CompareForm 的 `size`),可选 'large' | 'default' | 'small'。 */
116
+ size?: FieldSize;
107
117
  isConfirm?: boolean;
108
118
  onConfirm?: () => void;
109
119
  selfDiffFieldTypes?: string[];
@@ -95,6 +95,7 @@ const mountHistoryDiffDialog = async (
95
95
  selfDiffFieldTypes: options.selfDiffFieldTypes,
96
96
  compareFormState: options.compareFormState,
97
97
  width: options.width,
98
+ size: options.size ?? options.services?.uiService?.get('propsPanelSize'),
98
99
  onClose: options.onClose,
99
100
  });
100
101
  if (options.appContext) {
@@ -223,6 +224,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
223
224
  services,
224
225
  ...extra,
225
226
  width: extra?.width ?? dialogWidth,
227
+ size: extra?.size,
226
228
  });
227
229
  }
228
230
  return confirmRevert();
@@ -382,6 +384,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
382
384
  loadConfig: revertOptions.loadConfig,
383
385
  selfDiffFieldTypes: revertOptions.selfDiffFieldTypes,
384
386
  width: revertOptions.width,
387
+ size: revertOptions.size,
385
388
  });
386
389
  if (!confirmed) return null;
387
390
  return await revertOptions.revert();
@@ -399,6 +402,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
399
402
  services,
400
403
  ...extra,
401
404
  width: extra?.width ?? dialogWidth,
405
+ size: extra?.size,
402
406
  });
403
407
  };
404
408
 
@@ -12,6 +12,7 @@
12
12
  :size="propsPanelSize"
13
13
  :init-values="values"
14
14
  :config="config"
15
+ :type-match-valid="true"
15
16
  :extend-state="extendState"
16
17
  @change="submit"
17
18
  @error="errorHandler"
@@ -46,13 +47,16 @@
46
47
  import { computed, getCurrentInstance, inject, onMounted, onUnmounted, ref, useTemplateRef, watchEffect } from 'vue';
47
48
  import { Document as DocumentIcon } from '@element-plus/icons-vue';
48
49
 
49
- import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
50
+ import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
50
51
  import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
51
52
  import { MForm } from '@tmagic/form';
53
+ import { filterXSS } from '@tmagic/utils';
52
54
 
53
55
  import MIcon from '@editor/components/Icon.vue';
56
+ import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
54
57
  import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
55
58
  import { useServices } from '@editor/hooks/use-services';
59
+ import { validatePropsForm } from '@editor/utils/props';
56
60
 
57
61
  import CodeEditor from '../CodeEditor.vue';
58
62
 
@@ -75,13 +79,15 @@ const props = defineProps<{
75
79
  }>();
76
80
 
77
81
  const emit = defineEmits<{
78
- submit: [values: any, eventData?: ContainerChangeEventData];
82
+ submit: [values: any, eventData?: ContainerChangeEventData, error?: any];
79
83
  'submit-error': [e: any];
80
84
  'form-error': [e: any];
81
85
  mounted: [internalInstance: any];
82
86
  unmounted: [];
83
87
  }>();
84
88
 
89
+ const enablePropsFormValidate = inject(ENABLE_PROPS_FORM_VALIDATE, false);
90
+
85
91
  const services = useServices();
86
92
  const { editorService, uiService } = services;
87
93
 
@@ -113,9 +119,16 @@ onUnmounted(() => {
113
119
  const submit = async (v: FormValue, eventData: ContainerChangeEventData) => {
114
120
  try {
115
121
  const values = await configFormRef.value?.submitForm();
122
+ // 校验成功:正常更新节点(第三个参数 error 为空,表示清除该来源的错误记录)
116
123
  emit('submit', values, eventData);
117
124
  } catch (e: any) {
118
- emit('submit-error', e);
125
+ if (enablePropsFormValidate) {
126
+ // 启用校验联动:校验失败时仍以当前表单值更新节点,并把错误信息一并抛给上层记录
127
+ emit('submit', v, eventData, e);
128
+ } else {
129
+ // 未启用:保持原行为,校验失败丢弃本次改动
130
+ emit('submit-error', e);
131
+ }
119
132
  }
120
133
  };
121
134
 
@@ -123,8 +136,56 @@ const errorHandler = (e: any) => {
123
136
  emit('form-error', e);
124
137
  };
125
138
 
126
- const saveCode = (values: any) => {
127
- emit('submit', props.codeValueKey ? { [props.codeValueKey]: values } : values);
139
+ /**
140
+ * 将校验错误文案安全地转为用于弹窗展示的 HTML。
141
+ *
142
+ * 文案形如 `字段 -> 主文案\n\n建议`,多条以结构性 `<br>` 拼接。为在保留换行排版的同时
143
+ * 避免 `${value}`/字段名中可能夹带的 HTML 造成 XSS:先按结构性 `<br>` 拆分,对每段做 HTML
144
+ * 转义后再把段内换行 `\n` 替换为 `<br>`,最后用 `<br>` 拼回。
145
+ */
146
+ const formatValidateErrorHtml = (error: string): string =>
147
+ error
148
+ .split(/<br\s*\/?>/i)
149
+ .map((segment) => filterXSS(segment).replaceAll('\n', '<br>'))
150
+ .join('<br>');
151
+
152
+ const saveCode = async (values: any) => {
153
+ const newValues = props.codeValueKey ? { [props.codeValueKey]: values } : values;
154
+
155
+ if (!enablePropsFormValidate) {
156
+ // 未启用校验联动:保持原行为,直接提交源码保存的值
157
+ emit('submit', newValues);
158
+ return;
159
+ }
160
+
161
+ // 启用校验联动:源码编辑器保存的值未经过表单交互,这里另建一个独立的 MForm 实例对最新配置
162
+ // 做一次静默校验(不复用、也不污染页面上正在展示的表单),并将校验结果(错误信息)随提交
163
+ // 一并抛给上层记录,使源码保存的错误状态与表单编辑保持一致。
164
+ try {
165
+ const error = await validatePropsForm({
166
+ config: props.config,
167
+ values: newValues,
168
+ appContext: internalInstance?.appContext ?? null,
169
+ services,
170
+ stage: stage.value,
171
+ typeMatchValid: true,
172
+ extendState: props.extendState,
173
+ });
174
+
175
+ if (error) {
176
+ tMagicMessage({
177
+ type: 'error',
178
+ message: formatValidateErrorHtml(error),
179
+ dangerouslyUseHTMLString: true,
180
+ });
181
+ }
182
+
183
+ emit('submit', newValues, undefined, error ? new Error(error) : undefined);
184
+ } catch (e: any) {
185
+ console.log('validateForm error', e);
186
+ // 静默校验本身出现异常(如初始化超时)时,退回到不携带错误信息的提交,避免阻塞源码保存
187
+ emit('submit', newValues);
188
+ }
128
189
  };
129
190
 
130
191
  defineExpose({ configForm: configFormRef, submit });