@tmagic/editor 1.8.0-beta.10 → 1.8.0-beta.12

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 (111) 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 +4 -0
  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 +5 -2
  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/editorProps.js +6 -1
  8. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +15 -3
  9. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +2 -17
  10. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +2 -1
  11. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +7 -0
  12. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +5 -17
  13. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -0
  14. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +29 -3
  15. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +50 -88
  16. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -5
  17. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +48 -23
  18. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +6 -3
  19. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +28 -22
  20. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +28 -22
  21. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +28 -22
  22. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +6 -3
  23. package/dist/es/fields/UISelect.js +0 -1
  24. package/dist/es/index.js +9 -6
  25. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +21 -6
  26. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +9 -6
  27. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +14 -4
  28. package/dist/es/layouts/history-list/composables.js +1 -1
  29. package/dist/es/layouts/history-list/useHistoryRevert.js +17 -6
  30. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +44 -5
  31. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +15 -8
  32. package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
  33. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +15 -31
  34. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  35. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +18 -3
  36. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +4 -0
  37. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  38. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +18 -3
  39. package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
  40. package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
  41. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +2 -1
  42. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +8 -2
  43. package/dist/es/plugin.js +3 -1
  44. package/dist/es/services/editor.js +104 -5
  45. package/dist/es/services/props.js +2 -1
  46. package/dist/es/services/storage.js +1 -1
  47. package/dist/es/services/ui.js +3 -0
  48. package/dist/es/style.css +104 -55
  49. package/dist/es/utils/data-source/index.js +30 -11
  50. package/dist/es/utils/event.js +123 -0
  51. package/dist/es/utils/monaco-editor.js +22 -6
  52. package/dist/es/utils/props.js +94 -8
  53. package/dist/es/utils/type-match-rules.js +360 -0
  54. package/dist/style.css +104 -55
  55. package/dist/tmagic-editor.umd.cjs +16923 -15771
  56. package/package.json +8 -8
  57. package/src/Editor.vue +3 -1
  58. package/src/components/CodeBlockEditor.vue +3 -0
  59. package/src/components/CompareForm.vue +7 -0
  60. package/src/components/FloatingBox.vue +77 -8
  61. package/src/components/ScrollBar.vue +0 -67
  62. package/src/editorProps.ts +13 -0
  63. package/src/fields/CodeSelect.vue +4 -1
  64. package/src/fields/CondOpSelect.vue +2 -24
  65. package/src/fields/DataSourceFields.vue +5 -0
  66. package/src/fields/DataSourceInput.vue +7 -25
  67. package/src/fields/DataSourceMocks.vue +3 -0
  68. package/src/fields/DisplayConds.vue +14 -0
  69. package/src/fields/EventSelect.vue +84 -129
  70. package/src/fields/StyleSetter/Index.vue +2 -8
  71. package/src/fields/StyleSetter/pro/Background.vue +94 -55
  72. package/src/fields/StyleSetter/pro/Border.vue +9 -11
  73. package/src/fields/StyleSetter/pro/Font.vue +66 -64
  74. package/src/fields/StyleSetter/pro/Layout.vue +140 -138
  75. package/src/fields/StyleSetter/pro/Position.vue +69 -67
  76. package/src/fields/StyleSetter/pro/Transform.vue +23 -25
  77. package/src/fields/UISelect.vue +0 -12
  78. package/src/index.ts +1 -0
  79. package/src/layouts/CodeEditor.vue +32 -6
  80. package/src/layouts/Framework.vue +7 -6
  81. package/src/layouts/history-list/HistoryDiffDialog.vue +23 -5
  82. package/src/layouts/history-list/useHistoryRevert.ts +25 -5
  83. package/src/layouts/props-panel/FormPanel.vue +66 -5
  84. package/src/layouts/props-panel/PropsPanel.vue +20 -6
  85. package/src/layouts/props-panel/use-style-panel.ts +4 -3
  86. package/src/layouts/sidebar/Sidebar.vue +2 -0
  87. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +4 -1
  88. package/src/layouts/sidebar/code-block/useContentMenu.ts +13 -1
  89. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
  90. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +2 -2
  91. package/src/layouts/sidebar/data-source/useContentMenu.ts +13 -1
  92. package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
  93. package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
  94. package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
  95. package/src/plugin.ts +3 -1
  96. package/src/services/editor.ts +168 -3
  97. package/src/services/ui.ts +6 -0
  98. package/src/theme/event.scss +8 -0
  99. package/src/theme/floating-box.scss +9 -1
  100. package/src/theme/layer-node-content.scss +14 -0
  101. package/src/theme/scroll-bar.scss +64 -0
  102. package/src/theme/theme.scss +3 -0
  103. package/src/theme/ui-select.scss +9 -0
  104. package/src/type.ts +72 -2
  105. package/src/utils/data-source/index.ts +30 -12
  106. package/src/utils/event.ts +224 -0
  107. package/src/utils/index.ts +2 -0
  108. package/src/utils/monaco-editor.ts +27 -0
  109. package/src/utils/props.ts +160 -2
  110. package/src/utils/type-match-rules.ts +678 -0
  111. package/types/index.d.ts +391 -85
@@ -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[];
@@ -162,11 +172,19 @@ const hasCurrent = computed(() => payload.value?.currentValue !== undefined && p
162
172
  /** 是否存在该步「修改后的值」:不存在(如仅删除)时「与修改前对比」无意义,置灰禁用。 */
163
173
  const hasValue = computed(() => payload.value?.value !== undefined && payload.value?.value !== null);
164
174
 
175
+ /** 指定模式当前是否可用:before 依赖「修改后的值」,current 依赖「当前值」。 */
176
+ const isModeAvailable = (m: DiffMode): boolean => (m === 'current' ? hasCurrent.value : hasValue.value);
177
+
165
178
  /**
166
179
  * 计算 open 时的初始对比模式:
167
- * 没有「修改后的值」但有当前值时(「与修改前对比」不可用),默认进入「与当前对比」;否则默认「与修改前对比」。
180
+ * - 调用方通过 payload.mode 指定且该模式可用时,优先使用指定模式;
181
+ * - 否则没有「修改后的值」但有当前值时(「与修改前对比」不可用),默认进入「与当前对比」;
182
+ * - 其余情况默认「与修改前对比」。
168
183
  */
169
- const resolveInitialMode = (): DiffMode => (!hasValue.value && hasCurrent.value ? 'current' : 'before');
184
+ const resolveInitialMode = (specified?: DiffMode): DiffMode => {
185
+ if (specified && isModeAvailable(specified)) return specified;
186
+ return !hasValue.value && hasCurrent.value ? 'current' : 'before';
187
+ };
170
188
 
171
189
  /** 左侧(旧/参照)值 */
172
190
  const leftValue = computed<Record<string, any>>(() => {
@@ -222,8 +240,8 @@ const targetText = computed(() => {
222
240
  const open = (p: DiffDialogPayload) => {
223
241
  payload.value = p;
224
242
  // 每次打开按 payload 重置对比模式(hasValue / hasCurrent 依赖 payload,需先赋值):
225
- // 无「修改后的值」但有当前值时默认「与当前对比」,否则默认「与修改前对比」
226
- mode.value = resolveInitialMode();
243
+ // 优先使用 payload.mode 指定的模式;不可用或未指定时按数据自动推断。
244
+ mode.value = resolveInitialMode(p.mode);
227
245
  // 默认回到表单对比形态,避免残留上一次选择的源码模式
228
246
  viewMode.value = 'form';
229
247
  visible.value = true;
@@ -94,6 +94,8 @@ const mountHistoryDiffDialog = async (
94
94
  loadConfig: options.loadConfig,
95
95
  selfDiffFieldTypes: options.selfDiffFieldTypes,
96
96
  compareFormState: options.compareFormState,
97
+ width: options.width,
98
+ size: options.size ?? options.services?.uiService?.get('propsPanelSize'),
97
99
  onClose: options.onClose,
98
100
  });
99
101
  if (options.appContext) {
@@ -192,7 +194,7 @@ const viewHistoryDiffDialog = async (
192
194
  export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
193
195
  // 自动捕获调用方所在组件的 appContext(在 setup 中调用时),业务方亦可显式覆盖。
194
196
  const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
195
- const { extendState, getPropsPanelFormState } = options;
197
+ const { extendState, getPropsPanelFormState, dialogWidth } = options;
196
198
 
197
199
  /** 目标数据已被删除、无法回滚时的统一提示。 */
198
200
  const showRevertTargetMissing = () => {
@@ -216,7 +218,14 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
216
218
  */
217
219
  const runRevert = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<boolean> => {
218
220
  if (payload) {
219
- return confirmRevertWithDiffDialog(payload, { appContext, extendState, services, ...extra });
221
+ return confirmRevertWithDiffDialog(payload, {
222
+ appContext,
223
+ extendState,
224
+ services,
225
+ ...extra,
226
+ width: extra?.width ?? dialogWidth,
227
+ size: extra?.size,
228
+ });
220
229
  }
221
230
  return confirmRevert();
222
231
  };
@@ -334,6 +343,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
334
343
  appContext,
335
344
  extendState,
336
345
  services,
346
+ width: dialogWidth,
337
347
  compareFormState: getPropsPanelFormState?.(),
338
348
  });
339
349
  }
@@ -341,12 +351,12 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
341
351
 
342
352
  const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
343
353
  const payload = buildDataSourceDiffPayload(id, index);
344
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
354
+ if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
345
355
  };
346
356
 
347
357
  const onCodeBlockDiff = (id: string | number, index: number): Promise<void> | void => {
348
358
  const payload = buildCodeBlockDiffPayload(id, index);
349
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
359
+ if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
350
360
  };
351
361
 
352
362
  /**
@@ -373,6 +383,8 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
373
383
  const confirmed = await runRevert(revertOptions.diffPayload ?? null, {
374
384
  loadConfig: revertOptions.loadConfig,
375
385
  selfDiffFieldTypes: revertOptions.selfDiffFieldTypes,
386
+ width: revertOptions.width,
387
+ size: revertOptions.size,
376
388
  });
377
389
  if (!confirmed) return null;
378
390
  return await revertOptions.revert();
@@ -383,7 +395,15 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
383
395
  * 可透传 `loadConfig` / `selfDiffFieldTypes`。payload 为 null(不可对比)时静默返回。
384
396
  */
385
397
  const viewDiff = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<void> | void => {
386
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, ...extra });
398
+ if (payload)
399
+ return viewHistoryDiffDialog(payload, {
400
+ appContext,
401
+ extendState,
402
+ services,
403
+ ...extra,
404
+ width: extra?.width ?? dialogWidth,
405
+ size: extra?.size,
406
+ });
387
407
  };
388
408
 
389
409
  return {
@@ -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 });
@@ -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);