@tmagic/editor 1.8.0-beta.12 → 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.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.12",
2
+ "version": "1.8.0-beta.13",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -58,11 +58,11 @@
58
58
  "moveable": "^0.53.0",
59
59
  "serialize-javascript": "^7.0.0",
60
60
  "sortablejs": "^1.15.6",
61
- "@tmagic/stage": "1.8.0-beta.12",
62
- "@tmagic/design": "1.8.0-beta.12",
63
- "@tmagic/utils": "1.8.0-beta.12",
64
- "@tmagic/form": "1.8.0-beta.12",
65
- "@tmagic/table": "1.8.0-beta.12"
61
+ "@tmagic/form": "1.8.0-beta.13",
62
+ "@tmagic/design": "1.8.0-beta.13",
63
+ "@tmagic/stage": "1.8.0-beta.13",
64
+ "@tmagic/utils": "1.8.0-beta.13",
65
+ "@tmagic/table": "1.8.0-beta.13"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/events": "^3.0.3",
@@ -76,7 +76,7 @@
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
78
  "vue": "^3.5.40",
79
- "@tmagic/core": "1.8.0-beta.12"
79
+ "@tmagic/core": "1.8.0-beta.13"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -19,107 +19,36 @@
19
19
  </template>
20
20
 
21
21
  <script lang="ts" setup>
22
- import { computed, inject, provide, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
22
+ import { computed, type Ref, type ShallowRef } from 'vue';
23
23
  import { isEqual } from 'lodash-es';
24
24
 
25
- import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
26
- import { type FieldSize } from '@tmagic/design';
27
- import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
25
+ import { type CodeBlockContent, HookType } from '@tmagic/core';
26
+ import { type FormConfig, type FormValue, MForm } from '@tmagic/form';
28
27
 
29
- import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
30
- import { getCodeBlockFormConfig } from '@editor/utils/code-block';
28
+ import { useCompareForm } from '@editor/hooks/use-compare-form';
29
+ import type { CompareFormBaseProps } from '@editor/type';
31
30
 
32
31
  defineOptions({
33
32
  name: 'MEditorCompareForm',
34
33
  });
35
34
 
36
35
  const props = withDefaults(
37
- defineProps<{
38
- /** 当前值(修改后的值) */
39
- value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
40
- /** 用于对比的旧值(修改前的值) */
41
- lastValue?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
42
- /**
43
- * 类型说明:
44
- * - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
45
- * - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
46
- * - `category` 为 `code-block` 时,`type` 可不传
47
- */
48
- type?: string;
49
- /** 表单配置类别,决定从哪里取 FormConfig */
50
- category?: CompareCategory;
51
- /** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
52
- dataSourceType?: string;
53
- labelWidth?: string;
54
- /**
55
- * 外层容器高度。设置后表单内容超出时会在 CompareForm 内部出现滚动条,
56
- * 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
57
- */
58
- height?: string;
59
- /**
60
- * 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
61
- * PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
62
- * filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
63
- * 因此在差异对比场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
64
- */
65
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
66
- /**
67
- * 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
68
- * CompareForm 会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
69
- */
70
- baseFormState?: FormState;
71
- /** 需要走 self diff 的字段类型(例如 mod-cond)。 */
72
- selfDiffFieldTypes?: string[];
73
- /**
74
- * 表单内组件的尺寸(透传给 MForm 的 `size`),可选 'large' | 'default' | 'small'。
75
- * 缺省时使用 MForm 内置默认尺寸。
76
- */
77
- size?: FieldSize;
78
- /**
79
- * 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
80
- * 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
81
- * `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。返回的 config 直接用于对比展示。
82
- */
83
- loadConfig?: CompareFormLoadConfig;
84
- /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
85
- services?: Services;
86
- }>(),
36
+ defineProps<
37
+ CompareFormBaseProps & {
38
+ /** 用于对比的旧值(修改前的值) */
39
+ lastValue?: CompareFormBaseProps['value'];
40
+ /** 需要走 self diff 的字段类型(例如 mod-cond)。 */
41
+ selfDiffFieldTypes?: string[];
42
+ }
43
+ >(),
87
44
  {
88
45
  category: 'node',
89
46
  labelWidth: '120px',
90
- extendState: (state: FormState) => state,
91
47
  },
92
48
  );
93
49
 
94
- provide('services', props.services);
95
-
96
- const config = ref<FormConfig>([]);
97
-
98
- /** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
99
- const codeOptions = inject<Record<string, any>>('codeOptions', {});
100
-
101
- /** 将代码块的 content 字段统一成字符串,便于在表单/对比中展示 */
102
- const normalizeCodeBlockValue = (
103
- v: Partial<CodeBlockContent> | Record<string, any> | undefined,
104
- ): Record<string, any> => {
105
- if (!v) return {};
106
- const next: Record<string, any> = { ...v };
107
- if (next.content && typeof next.content !== 'string') {
108
- try {
109
- next.content = next.content.toString();
110
- } catch {
111
- next.content = '';
112
- }
113
- }
114
- return next;
115
- };
116
-
117
- const currentValues = computed<FormValue>(() => {
118
- if (props.category === 'code-block') {
119
- return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
120
- }
121
- return (props.value || {}) as FormValue;
122
- });
50
+ const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef, normalizeCodeBlockValue } =
51
+ useCompareForm(props);
123
52
 
124
53
  const lastValuesProcessed = computed<FormValue>(() => {
125
54
  if (props.category === 'code-block') {
@@ -128,18 +57,6 @@ const lastValuesProcessed = computed<FormValue>(() => {
128
57
  return (props.lastValue || {}) as FormValue;
129
58
  });
130
59
 
131
- /**
132
- * 外层包裹层的样式:当传入 `height` 时启用固定高度 + 内部滚动,
133
- * 这样滚动条会出现在 CompareForm 内部,避免父容器(如 Dialog)自身也产生滚动。
134
- */
135
- const wrapperStyle = computed(() => {
136
- if (!props.height) return undefined;
137
- return {
138
- height: props.height,
139
- overflow: 'auto',
140
- } as Record<string, string>;
141
- });
142
-
143
60
  /**
144
61
  * `code-select` 字段在历史数据中存在两种"语义为空"的形态:
145
62
  * - 字符串 `''`(旧数据 / 用户从未配置过钩子);
@@ -168,110 +85,6 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
168
85
  return !isEqual(curValue, lastValue);
169
86
  };
170
87
 
171
- const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
172
- formConfig.map((item) => {
173
- if (!('type' in item)) return item;
174
- if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
175
-
176
- return {
177
- ...item,
178
- items: item.items.map((tabPane) => {
179
- if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
180
-
181
- return {
182
- ...tabPane,
183
- display: true,
184
- };
185
- }),
186
- };
187
- });
188
-
189
- const mergedExtendState = (state: FormState) => {
190
- return props.extendState(props.baseFormState || state);
191
- };
192
-
193
- /**
194
- * 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
195
- * 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
196
- */
197
- const defaultLoadConfig = async (): Promise<FormConfig> => {
198
- if (!props.services) {
199
- return [];
200
- }
201
-
202
- switch (props.category) {
203
- case 'node': {
204
- if (!props.type) {
205
- return [];
206
- }
207
- return removeStyleDisplayConfig(
208
- await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
209
- );
210
- }
211
- case 'data-source': {
212
- const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
213
- // 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
214
- // 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
215
- // 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
216
- return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
217
- }
218
- case 'code-block': {
219
- return getCodeBlockFormConfig({
220
- paramColConfig: props.services.codeBlockService.getParamsColConfig(),
221
- // 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比场景下 props.dataSourceType
222
- // 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
223
- isDataSource: () => Boolean(props.dataSourceType),
224
- dataSourceType: () => props.dataSourceType,
225
- codeOptions,
226
- // 对比模式只读,不需要校验/语法检查
227
- editable: false,
228
- });
229
- }
230
- default:
231
- return [];
232
- }
233
- };
234
-
235
- const loadConfig = async () => {
236
- if (props.loadConfig) {
237
- config.value = await props.loadConfig({
238
- category: props.category,
239
- type: props.type,
240
- dataSourceType: props.dataSourceType,
241
- defaultLoadConfig,
242
- });
243
- return;
244
- }
245
-
246
- config.value = await defaultLoadConfig();
247
- };
248
-
249
- watch(
250
- [() => props.category, () => props.type, () => props.dataSourceType, () => props.loadConfig],
251
- () => {
252
- loadConfig();
253
- },
254
- { immediate: true },
255
- );
256
-
257
- const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
258
-
259
- /**
260
- * 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
261
- * 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
262
- * 在执行时拿不到 `formState.services` 而报错。
263
- *
264
- * 与 props-panel/FormPanel.vue 中的注入方式保持一致:
265
- * - services:整个 useServices() 返回的服务集合;
266
- * - stage:当前 editorService.get('stage') 的最新值。
267
- */
268
- watchEffect(() => {
269
- if (formRef.value && props.services) {
270
- formRef.value.formState.stage = props.services.editorService.get('stage');
271
- formRef.value.formState.services = props.services;
272
- }
273
- });
274
-
275
88
  defineExpose<{
276
89
  form: ShallowRef<InstanceType<typeof MForm> | null>;
277
90
  config: Ref<FormConfig>;
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="m-editor-view-form-wrapper" :style="wrapperStyle">
3
+ <MForm
4
+ v-if="config.length"
5
+ ref="form"
6
+ class="m-editor-view-form"
7
+ :config="config"
8
+ :init-values="currentValues"
9
+ :disabled="disabled"
10
+ :label-width="labelWidth"
11
+ :extend-state="mergedExtendState"
12
+ :size="size"
13
+ ></MForm>
14
+ </div>
15
+ </template>
16
+
17
+ <script lang="ts" setup>
18
+ import { type Ref, type ShallowRef } from 'vue';
19
+
20
+ import { type FormConfig, MForm } from '@tmagic/form';
21
+
22
+ import { useCompareForm } from '@editor/hooks/use-compare-form';
23
+ import type { CompareFormBaseProps } from '@editor/type';
24
+
25
+ defineOptions({
26
+ name: 'MEditorViewForm',
27
+ });
28
+
29
+ const props = withDefaults(
30
+ defineProps<
31
+ CompareFormBaseProps & {
32
+ /** 是否禁用表单(默认只读展示)。 */
33
+ disabled?: boolean;
34
+ }
35
+ >(),
36
+ {
37
+ category: 'node',
38
+ labelWidth: '120px',
39
+ disabled: true,
40
+ // extendState 的默认值由 useCompareForm 内部兜底(props.extendState ?? ...),此处无需重复提供
41
+ },
42
+ );
43
+
44
+ const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef } = useCompareForm(props);
45
+
46
+ defineExpose<{
47
+ form: ShallowRef<InstanceType<typeof MForm> | null>;
48
+ config: Ref<FormConfig>;
49
+ reload: () => Promise<void>;
50
+ }>({
51
+ form: formRef,
52
+ config,
53
+ reload: loadConfig,
54
+ });
55
+ </script>
@@ -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
@@ -74,6 +74,7 @@ export { default as SplitView } from './components/SplitView.vue';
74
74
  export { default as Resizer } from './components/Resizer.vue';
75
75
  export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
76
76
  export { default as CompareForm } from './components/CompareForm.vue';
77
+ export { default as ViewForm } from './components/ViewForm.vue';
77
78
  export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
78
79
  export { default as HistoryListBucketTab } from './layouts/history-list/BucketTab.vue';
79
80
  export { default as HistoryDiffDialog } from './layouts/history-list/HistoryDiffDialog.vue';
package/src/type.ts CHANGED
@@ -566,6 +566,58 @@ export interface CompareFormLoadConfigContext {
566
566
  * 可通过 `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
567
567
  */
568
568
  export type CompareFormLoadConfig = (ctx: CompareFormLoadConfigContext) => FormConfig | Promise<FormConfig>;
569
+
570
+ /**
571
+ * CompareForm / ViewForm 共用的基础 props。
572
+ * 两者都基于同一套「按 category 加载 FormConfig + 注入 services/stage」的逻辑(见 useCompareForm),
573
+ * 差异仅在于是否做新旧值对比。这里抽出公共字段避免重复定义。
574
+ */
575
+ export interface CompareFormBaseProps {
576
+ /** 当前值(对比场景下为修改后的值) */
577
+ value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
578
+ /**
579
+ * 类型说明:
580
+ * - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
581
+ * - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
582
+ * - `category` 为 `code-block` 时,`type` 可不传
583
+ */
584
+ type?: string;
585
+ /** 表单配置类别,决定从哪里取 FormConfig */
586
+ category?: CompareCategory;
587
+ /** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
588
+ dataSourceType?: string;
589
+ labelWidth?: string;
590
+ /**
591
+ * 外层容器高度。设置后表单内容超出时会在组件内部出现滚动条,
592
+ * 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
593
+ */
594
+ height?: string;
595
+ /**
596
+ * 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
597
+ * PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
598
+ * filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
599
+ * 因此在对比 / 展示场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
600
+ */
601
+ extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
602
+ /**
603
+ * 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
604
+ * 组件会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
605
+ */
606
+ baseFormState?: FormState;
607
+ /**
608
+ * 表单内组件的尺寸(透传给 MForm 的 `size`),可选 'large' | 'default' | 'small'。
609
+ * 缺省时使用 MForm 内置默认尺寸。
610
+ */
611
+ size?: FieldSize;
612
+ /**
613
+ * 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
614
+ * 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
615
+ * `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
616
+ */
617
+ loadConfig?: CompareFormLoadConfig;
618
+ /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
619
+ services?: Services;
620
+ }
569
621
  // #endregion CompareForm
570
622
 
571
623
  // #region SideItemKey
@@ -362,6 +362,35 @@ export const fillConfig = (
362
362
  return [tabConfig];
363
363
  };
364
364
 
365
+ /**
366
+ * 将属性表单配置中「样式」tab-pane 的 `display` 强制置为 `true`。
367
+ *
368
+ * `propsService.getPropsConfig` 返回的样式 tab 默认带有
369
+ * `display: ({ services }) => !(services?.uiService?.get('showStylePanel') ?? true)`,
370
+ * 在对比 / 只读展示场景(CompareForm / ViewForm)下并不需要跟随 uiService 状态隐藏,
371
+ * 这里统一放开,保证样式 tab 始终可见。
372
+ *
373
+ * @param formConfig 组件属性表单配置
374
+ * @returns 处理后的表单配置(不修改入参,返回浅拷贝)
375
+ */
376
+ export const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
377
+ formConfig.map((item) => {
378
+ if (!('type' in item)) return item;
379
+ if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
380
+
381
+ return {
382
+ ...item,
383
+ items: item.items.map((tabPane) => {
384
+ if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
385
+
386
+ return {
387
+ ...tabPane,
388
+ display: true,
389
+ };
390
+ }),
391
+ };
392
+ });
393
+
365
394
  // #region ValidatePropsFormOptions
366
395
  /**
367
396
  * validatePropsForm 参数