@tmagic/editor 1.8.0-beta.11 → 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 +4 -1
  28. package/dist/es/layouts/history-list/composables.js +1 -1
  29. package/dist/es/layouts/history-list/useHistoryRevert.js +7 -3
  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 +16895 -15757
  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 +11 -1
  82. package/src/layouts/history-list/useHistoryRevert.ts +4 -0
  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 +53 -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 +372 -85
@@ -0,0 +1,224 @@
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 type { EventOption, Id, MComponent, MContainer } from '@tmagic/core';
20
+ import type { CascaderOption } from '@tmagic/form';
21
+ import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
22
+
23
+ import dataSourceService from '@editor/services/dataSource';
24
+ import editorService from '@editor/services/editor';
25
+ import eventsService from '@editor/services/events';
26
+ import { getCascaderOptionsFromFields } from '@editor/utils/data-source';
27
+
28
+ export const EVENT_NAME_VALUE_SEPARATOR = '.';
29
+
30
+ export type EventNameSelectOption = { text: string; value: string };
31
+ export type EventNameOption = EventOption | CascaderOption | EventNameSelectOption;
32
+
33
+ /** 与 EventSelect 中 checkStrictly 一致:component 为 false,其余为 true */
34
+ export const isEventNameCheckStrictly = (src?: string) => src !== 'component';
35
+
36
+ /** 将动作 method 值规范为与 collectEventNameOptionValues 一致的字符串(cascader 无 valueSeparator 时存数组) */
37
+ export const normalizeCompActionValue = (value: unknown): string => {
38
+ if (Array.isArray(value)) {
39
+ return value.map((item) => `${item}`).join(EVENT_NAME_VALUE_SEPARATOR);
40
+ }
41
+ if (value === null || value === undefined || value === '') {
42
+ return '';
43
+ }
44
+ return `${value}`;
45
+ };
46
+
47
+ /**
48
+ * 组装事件名称下拉/级联 options,与 EventSelect 默认 eventNameConfig.options 逻辑一致。
49
+ */
50
+ export const getEventNameOptions = (
51
+ src: string | undefined,
52
+ formValue: Record<string, any> = {},
53
+ ): EventNameOption[] => {
54
+ if (!formValue.type) {
55
+ return [];
56
+ }
57
+
58
+ if (src === 'component') {
59
+ let events: EventOption[] | CascaderOption[] = eventsService.getEvent(formValue.type) || [];
60
+
61
+ if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
62
+ const pageFragment = editorService.get('root')?.items?.find((page) => page.id === formValue.pageFragmentId);
63
+ if (!pageFragment) {
64
+ return [];
65
+ }
66
+
67
+ events = [
68
+ {
69
+ label: pageFragment.name || '页面片容器',
70
+ value: pageFragment.id,
71
+ children: events,
72
+ },
73
+ ];
74
+
75
+ (pageFragment.items || []).forEach((node) => {
76
+ traverseNode<MComponent | MContainer>(node, (current) => {
77
+ const nodeEvents = (current.type && eventsService.getEvent(current.type)) || [];
78
+ (events as CascaderOption[]).push({
79
+ label: `${current.name}_${current.id}`,
80
+ value: `${current.id}`,
81
+ children: nodeEvents,
82
+ });
83
+ });
84
+ });
85
+
86
+ return events;
87
+ }
88
+
89
+ return (events as EventOption[]).map((option) => ({
90
+ text: option.label,
91
+ value: option.value,
92
+ }));
93
+ }
94
+
95
+ if (src === 'datasource') {
96
+ const events: EventNameOption[] = dataSourceService.getFormEvent(formValue.type) || [];
97
+ const dataSource = dataSourceService.getDataSourceById(formValue.id);
98
+ const fields = dataSource?.fields || [];
99
+
100
+ if (fields.length > 0) {
101
+ return [
102
+ ...events,
103
+ {
104
+ label: '数据变化',
105
+ value: DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX,
106
+ children: getCascaderOptionsFromFields(fields),
107
+ },
108
+ ];
109
+ }
110
+
111
+ return events;
112
+ }
113
+
114
+ return [];
115
+ };
116
+
117
+ /** 将 select / cascader options 展平为最终写入 name 的字符串集合(cascader 用 `.` 拼接) */
118
+ export const collectEventNameOptionValues = (
119
+ options: EventNameOption[],
120
+ checkStrictly: boolean,
121
+ prefix: any[] = [],
122
+ result: Set<string> = new Set(),
123
+ ): Set<string> => {
124
+ options.forEach((option) => {
125
+ if (typeof option?.value === 'undefined') {
126
+ return;
127
+ }
128
+
129
+ const path = [...prefix, option.value];
130
+ const joined = path.map((item) => `${item}`).join(EVENT_NAME_VALUE_SEPARATOR);
131
+ const children = 'children' in option ? option.children : undefined;
132
+
133
+ if (Array.isArray(children) && children.length) {
134
+ if (checkStrictly) {
135
+ result.add(joined);
136
+ }
137
+ collectEventNameOptionValues(children as EventNameOption[], checkStrictly, path, result);
138
+ return;
139
+ }
140
+
141
+ result.add(joined);
142
+ });
143
+
144
+ return result;
145
+ };
146
+
147
+ /**
148
+ * 解析 event-select 允许的 name 集合。
149
+ * 自定义 eventNameConfig.options 时返回 null(跳过枚举校验)。
150
+ */
151
+ export const getEventNameAllowedValues = (
152
+ config: { src?: string; eventNameConfig?: { options?: unknown } } = {},
153
+ formValue: Record<string, any> = {},
154
+ ): Set<string> | null => {
155
+ if (typeof config.eventNameConfig?.options !== 'undefined') {
156
+ return null;
157
+ }
158
+
159
+ if (config.src !== 'component' && config.src !== 'datasource') {
160
+ return null;
161
+ }
162
+
163
+ return collectEventNameOptionValues(getEventNameOptions(config.src, formValue), isEventNameCheckStrictly(config.src));
164
+ };
165
+
166
+ /**
167
+ * 组装联动组件动作下拉/级联 options,与 EventSelect 默认 compActionConfig.options 逻辑一致。
168
+ */
169
+ export const getCompActionOptions = (toId?: Id): EventNameOption[] => {
170
+ if (typeof toId === 'undefined' || toId === null || toId === '') {
171
+ return [];
172
+ }
173
+
174
+ const node = editorService.getNodeById(toId);
175
+ if (!node?.type) {
176
+ return [];
177
+ }
178
+
179
+ let methods: EventOption[] | CascaderOption[] = eventsService.getMethod(node.type, toId) || [];
180
+
181
+ if (node.type === 'page-fragment-container' && node.pageFragmentId) {
182
+ const pageFragment = editorService.get('root')?.items?.find((page) => page.id === node.pageFragmentId);
183
+ if (!pageFragment) {
184
+ return [];
185
+ }
186
+
187
+ methods = [];
188
+ (pageFragment.items || []).forEach((item: MComponent | MContainer) => {
189
+ traverseNode<MComponent | MContainer>(item, (current) => {
190
+ const nodeMethods = (current.type && eventsService.getMethod(current.type, current.id)) || [];
191
+
192
+ if (nodeMethods.length) {
193
+ (methods as CascaderOption[]).push({
194
+ label: `${current.name}_${current.id}`,
195
+ value: `${current.id}`,
196
+ children: nodeMethods,
197
+ });
198
+ }
199
+ });
200
+ });
201
+
202
+ return methods;
203
+ }
204
+
205
+ return (methods as EventOption[]).map((method) => ({
206
+ text: method.label,
207
+ value: method.value,
208
+ }));
209
+ };
210
+
211
+ /**
212
+ * 解析 event-select 联动组件动作允许的 method 集合。
213
+ * 自定义 compActionConfig.options 时返回 null(跳过枚举校验)。
214
+ */
215
+ export const getCompActionAllowedValues = (
216
+ config: { src?: string; compActionConfig?: { options?: unknown } } = {},
217
+ model: { to?: Id } = {},
218
+ ): Set<string> | null => {
219
+ if (typeof config.compActionConfig?.options !== 'undefined') {
220
+ return null;
221
+ }
222
+
223
+ return collectEventNameOptionValues(getCompActionOptions(model.to), isEventNameCheckStrictly(config.src));
224
+ };
@@ -30,4 +30,6 @@ export * from './undo-redo';
30
30
  export * from './indexed-db';
31
31
  export * from './history';
32
32
  export * from './const';
33
+ export * from './type-match-rules';
34
+ export * from './event';
33
35
  export { default as loadMonaco } from './monaco-editor';
@@ -1,7 +1,34 @@
1
1
  let cached: Promise<typeof import('monaco-editor')> | undefined;
2
2
 
3
+ // 是否为 Monaco 内部抛出的取消错误(Canceled)
4
+ // 销毁编辑器时,WordHighlighter 内部挂起的 Delayer 会被取消,
5
+ // 取消时 reject 出的 Canceled 错误没有 catch,会变成 "Uncaught (in promise) Canceled",
6
+ // 该错误本身无害,这里做一次性、全局的兜底处理,仅吞掉这类取消错误。
7
+ const isMonacoCanceledError = (reason: any): boolean => {
8
+ if (!reason) return false;
9
+ if (reason instanceof Error) {
10
+ return reason.name === 'Canceled' || reason.message === 'Canceled';
11
+ }
12
+ return reason === 'Canceled';
13
+ };
14
+
15
+ let canceledRejectionHandlerInstalled = false;
16
+ const installCanceledRejectionHandler = () => {
17
+ if (canceledRejectionHandlerInstalled || typeof globalThis.addEventListener !== 'function') return;
18
+ canceledRejectionHandlerInstalled = true;
19
+
20
+ globalThis.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
21
+ if (isMonacoCanceledError(event.reason)) {
22
+ // 阻止其冒泡到控制台,避免无意义的报错噪音
23
+ event.preventDefault();
24
+ }
25
+ });
26
+ };
27
+
3
28
  export default () => {
4
29
  if (!cached) {
30
+ installCanceledRejectionHandler();
31
+
5
32
  cached = Promise.all([import('emmet-monaco-es'), import('monaco-editor')]).then(([emmet, monaco]) => {
6
33
  const { emmetHTML, emmetCSS } = emmet;
7
34
  emmetHTML(monaco);
@@ -17,14 +17,28 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
+ import type { AppContext } from 'vue';
21
+
20
22
  import {
23
+ HookType,
21
24
  NODE_CONDS_KEY,
22
25
  NODE_CONDS_RESULT_KEY,
23
26
  NODE_DISABLE_CODE_BLOCK_KEY,
24
27
  NODE_DISABLE_DATA_SOURCE_KEY,
25
28
  } from '@tmagic/core';
26
29
  import { tMagicMessage } from '@tmagic/design';
27
- import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
30
+ import type {
31
+ ChildConfig,
32
+ DisplayCondsConfig,
33
+ FormConfig,
34
+ FormState,
35
+ FormValue,
36
+ TabConfig,
37
+ TabPaneConfig,
38
+ } from '@tmagic/form';
39
+ import { validateForm } from '@tmagic/form';
40
+
41
+ import type { Services } from '@editor/type';
28
42
 
29
43
  export const arrayOptions = [
30
44
  { text: '包含', value: 'include' },
@@ -45,10 +59,36 @@ export const numberOptions = [
45
59
  { text: '不在范围内', value: 'not_between' },
46
60
  ];
47
61
 
62
+ export const booleanOptions = [
63
+ { text: '是', value: 'is' },
64
+ { text: '不是', value: 'not' },
65
+ ];
66
+
67
+ /** 按字段类型返回条件运算符选项(UI 与 typeMatch 校验共用) */
68
+ export const getCondOpOptionsByFieldType = (type: string) => {
69
+ if (type === 'array') {
70
+ return arrayOptions;
71
+ }
72
+
73
+ if (type === 'boolean' || type === 'null') {
74
+ return booleanOptions;
75
+ }
76
+
77
+ if (type === 'number') {
78
+ return [...eqOptions, ...numberOptions];
79
+ }
80
+
81
+ if (type === 'string') {
82
+ return [...arrayOptions, ...eqOptions];
83
+ }
84
+
85
+ return [...arrayOptions, ...eqOptions, ...numberOptions];
86
+ };
87
+
48
88
  export const styleTabConfig: TabPaneConfig = {
49
89
  title: '样式',
50
90
  lazy: true,
51
- display: ({ services }: any) => !(services.uiService.get('showStylePanel') ?? true),
91
+ display: ({ services }: any) => !(services?.uiService?.get('showStylePanel') ?? true),
52
92
  items: [
53
93
  {
54
94
  name: 'style',
@@ -127,6 +167,7 @@ export const eventTabConfig: TabPaneConfig = {
127
167
  src: 'component',
128
168
  labelWidth: '100px',
129
169
  type: 'event-select',
170
+ rules: [{ typeMatch: true }],
130
171
  },
131
172
  ],
132
173
  };
@@ -155,6 +196,17 @@ export const advancedTabConfig: TabPaneConfig = {
155
196
  labelPosition: 'top',
156
197
  type: 'code-select',
157
198
  extra: '组件初始化时执行',
199
+ rules: [
200
+ { typeMatch: true, trigger: 'change' },
201
+ {
202
+ validator: ({ value, callback }: any) => {
203
+ if (value && value.hookType !== HookType.CODE) {
204
+ return callback('hookType 必须是 code');
205
+ }
206
+ callback();
207
+ },
208
+ },
209
+ ],
158
210
  },
159
211
  {
160
212
  name: 'mounted',
@@ -162,6 +214,17 @@ export const advancedTabConfig: TabPaneConfig = {
162
214
  labelPosition: 'top',
163
215
  type: 'code-select',
164
216
  extra: '组件挂载到dom时执行',
217
+ rules: [
218
+ { typeMatch: true, trigger: 'change' },
219
+ {
220
+ validator: ({ value, callback }: any) => {
221
+ if (value && value.hookType !== HookType.CODE) {
222
+ return callback('hookType 必须是 code');
223
+ }
224
+ callback();
225
+ },
226
+ },
227
+ ],
165
228
  },
166
229
  {
167
230
  name: 'display',
@@ -169,6 +232,17 @@ export const advancedTabConfig: TabPaneConfig = {
169
232
  extra: '控制组件是否渲染,关系的代码块返回值为false时不渲染',
170
233
  labelPosition: 'top',
171
234
  type: 'code-select',
235
+ rules: [
236
+ { typeMatch: true, trigger: 'change' },
237
+ {
238
+ validator: ({ value, callback }: any) => {
239
+ if (value && value.hookType !== HookType.CODE) {
240
+ return callback('hookType 必须是 code');
241
+ }
242
+ callback();
243
+ },
244
+ },
245
+ ],
172
246
  },
173
247
  ],
174
248
  };
@@ -194,6 +268,7 @@ export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
194
268
  name: NODE_CONDS_KEY,
195
269
  titlePrefix: '条件组',
196
270
  defaultValue: [],
271
+ rules: [{ typeMatch: true }],
197
272
  },
198
273
  ],
199
274
  };
@@ -286,3 +361,86 @@ export const fillConfig = (
286
361
 
287
362
  return [tabConfig];
288
363
  };
364
+
365
+ // #region ValidatePropsFormOptions
366
+ /**
367
+ * validatePropsForm 参数
368
+ */
369
+ export interface ValidatePropsFormOptions {
370
+ /** 组件属性表单配置 */
371
+ config: FormConfig;
372
+ /** 待校验的表单值 */
373
+ values: FormValue;
374
+ /**
375
+ * 当前组件实例的 appContext(通常为 `getCurrentInstance()?.appContext`)。
376
+ * 会与 services 一并合入临时 MForm 的 appContext,使编辑器字段组件(DataSourceInput 等)能正常 inject。
377
+ */
378
+ appContext?: AppContext | null;
379
+ /** 编辑器服务集合,注入到临时表单的 formState */
380
+ services?: Services;
381
+ /** stage 实例,注入到临时表单的 formState */
382
+ stage?: any;
383
+ /** 外部扩展的 formState */
384
+ extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
385
+ /**
386
+ * 调试模式,默认 `true`:以弹层形式可见地渲染表单,点击「确定」才触发校验。
387
+ * 置为 `false` 时静默挂载后自动校验。
388
+ */
389
+ debug?: boolean;
390
+ typeMatchValid?: boolean;
391
+ }
392
+ // #endregion ValidatePropsFormOptions
393
+
394
+ /**
395
+ * 对一份「组件属性表单配置 + 值」做一次独立的校验,**不复用也不污染页面上正在展示的表单**。
396
+ *
397
+ * 内部基于 `@tmagic/form` 的 `validateForm` 另建一个独立的 MForm 实例完成校验,并统一处理
398
+ * 编辑器场景所需的上下文注入:将当前组件实例的 provides 合入 appContext,并向 formState 注入
399
+ * stage / services 及外部扩展状态,保证校验规则依赖的上下文可用。
400
+ *
401
+ * 常用于源码编辑器保存后,对最新配置做一次校验,并将校验结果(错误信息)随提交一并抛给上层记录,
402
+ * 使源码保存的错误状态与表单编辑保持一致。
403
+ *
404
+ * @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
405
+ * 仅在初始化超时或挂载失败等异常情况下才会 reject。
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const error = await validatePropsForm({
410
+ * config,
411
+ * values,
412
+ * appContext: getCurrentInstance()?.appContext,
413
+ * services,
414
+ * stage: editorService.get('stage'),
415
+ * extendState,
416
+ * });
417
+ * if (error) {
418
+ * // 配置不合法,error 为错误文案
419
+ * }
420
+ * ```
421
+ */
422
+ export const validatePropsForm = ({
423
+ config,
424
+ values,
425
+ appContext = null,
426
+ services,
427
+ stage,
428
+ extendState,
429
+ debug,
430
+ typeMatchValid,
431
+ }: ValidatePropsFormOptions): Promise<string> =>
432
+ validateForm({
433
+ config,
434
+ debug,
435
+ typeMatchValid,
436
+ initValues: values,
437
+ // 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
438
+ // 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
439
+ appContext: appContext ? { ...appContext, provides: { services } } : null,
440
+ // 与页面表单保持一致:注入 stage/services 及外部扩展状态,保证校验规则依赖的上下文可用
441
+ extendState: async (state) => ({
442
+ ...((await extendState?.(state)) || {}),
443
+ stage,
444
+ services,
445
+ }),
446
+ });