@tmagic/editor 1.8.0-beta.24 → 1.8.0-beta.26

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 (113) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +10 -16
  2. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +4 -4
  3. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +5 -8
  4. package/dist/es/components/ViewForm.vue_vue_type_script_setup_true_lang.js +3 -5
  5. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +3 -11
  6. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +29 -124
  7. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +5 -6
  8. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +3 -2
  9. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  10. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +7 -6
  11. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +6 -10
  12. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +5 -7
  13. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +1 -4
  14. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -6
  15. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +9 -137
  17. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +51 -326
  18. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +2 -4
  19. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +3 -34
  20. package/dist/es/fields/StyleSetter/configs.js +663 -0
  21. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +5 -108
  22. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +3 -9
  23. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +6 -103
  24. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +5 -265
  25. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +5 -85
  26. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +3 -22
  27. package/dist/es/fields/configs/codeSelect.js +98 -0
  28. package/dist/es/fields/configs/displayConds.js +158 -0
  29. package/dist/es/fields/configs/eventSelect.js +225 -0
  30. package/dist/es/fields/configs/stickyAddButton.js +20 -0
  31. package/dist/es/fields/headless-validation.js +139 -0
  32. package/dist/es/headless.js +2 -0
  33. package/dist/es/hooks/use-compare-form.js +6 -23
  34. package/dist/es/hooks/use-form-context.js +19 -0
  35. package/dist/es/index.js +3 -2
  36. package/dist/es/initService.js +17 -1
  37. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +1 -7
  38. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +1 -11
  39. package/dist/es/layouts/history-list/useHistoryRevert.js +3 -11
  40. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +10 -23
  41. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +3 -13
  42. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  43. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
  44. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  45. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
  46. package/dist/es/plugin.js +33 -32
  47. package/dist/es/style.css +120 -60
  48. package/dist/es/utils/data-source/index.js +2 -1
  49. package/dist/es/utils/props.js +1 -4
  50. package/dist/es/utils/type-match-rules.js +23 -22
  51. package/dist/style.css +120 -60
  52. package/dist/themes/magic-admin.css +132 -67
  53. package/dist/tmagic-editor-headless.umd.cjs +10867 -0
  54. package/dist/tmagic-editor.umd.cjs +2151 -2034
  55. package/package.json +12 -7
  56. package/src/Editor.vue +12 -16
  57. package/src/components/CodeParams.vue +10 -4
  58. package/src/components/CompareForm.vue +4 -5
  59. package/src/components/ViewForm.vue +2 -3
  60. package/src/editorProps.ts +1 -2
  61. package/src/fields/Code.vue +2 -10
  62. package/src/fields/CodeSelect.vue +20 -116
  63. package/src/fields/CodeSelectCol.vue +3 -5
  64. package/src/fields/CondOpSelect.vue +3 -2
  65. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +1 -1
  66. package/src/fields/DataSourceFieldSelect/Index.vue +7 -6
  67. package/src/fields/DataSourceFields.vue +0 -7
  68. package/src/fields/DataSourceMethodSelect.vue +2 -5
  69. package/src/fields/DataSourceMethods.vue +1 -6
  70. package/src/fields/DataSourceMocks.vue +1 -10
  71. package/src/fields/DataSourceSelect.vue +1 -1
  72. package/src/fields/DisplayConds.vue +17 -153
  73. package/src/fields/EventSelect.vue +28 -375
  74. package/src/fields/KeyValue.vue +3 -4
  75. package/src/fields/StyleSetter/components/Border.vue +4 -38
  76. package/src/fields/StyleSetter/configs.ts +690 -0
  77. package/src/fields/StyleSetter/pro/Background.vue +4 -104
  78. package/src/fields/StyleSetter/pro/Border.vue +3 -12
  79. package/src/fields/StyleSetter/pro/Font.vue +5 -124
  80. package/src/fields/StyleSetter/pro/Layout.vue +4 -219
  81. package/src/fields/StyleSetter/pro/Position.vue +6 -110
  82. package/src/fields/StyleSetter/pro/Transform.vue +4 -30
  83. package/src/fields/configs/codeSelect.ts +127 -0
  84. package/src/fields/configs/displayConds.ts +184 -0
  85. package/src/fields/configs/eventSelect.ts +309 -0
  86. package/src/fields/configs/stickyAddButton.ts +31 -0
  87. package/src/fields/headless-validation.ts +177 -0
  88. package/src/headless.ts +33 -0
  89. package/src/hooks/use-compare-form.ts +7 -25
  90. package/src/hooks/use-form-context.ts +40 -0
  91. package/src/index.ts +2 -0
  92. package/src/initService.ts +23 -1
  93. package/src/layouts/history-list/HistoryDiffDialog.vue +0 -10
  94. package/src/layouts/history-list/HistoryListPanel.vue +1 -13
  95. package/src/layouts/history-list/useHistoryRevert.ts +7 -13
  96. package/src/layouts/props-panel/FormPanel.vue +17 -28
  97. package/src/layouts/props-panel/PropsPanel.vue +1 -4
  98. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
  99. package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
  100. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
  101. package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
  102. package/src/plugin.ts +32 -33
  103. package/src/theme/code-block.scss +0 -37
  104. package/src/theme/display-conds.scss +4 -0
  105. package/src/theme/event.scss +22 -45
  106. package/src/theme/props-panel.scss +17 -3
  107. package/src/theme/themes/magic-admin/index.scss +12 -1
  108. package/src/type.ts +10 -29
  109. package/src/utils/data-source/index.ts +1 -0
  110. package/src/utils/props.ts +1 -4
  111. package/src/utils/type-match-rules.ts +37 -31
  112. package/types/headless.d.ts +18 -0
  113. package/types/index.d.ts +155 -180
@@ -0,0 +1,31 @@
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
+ /**
20
+ * 属性面板列表字段共用的吸底全宽「添加」按钮,展开到 group-list 配置上。
21
+ *
22
+ * 吸底按钮会盖住列表底部,新增的项必须同时滚进视口才看得见,两者一起给出避免漏配。
23
+ */
24
+ export const stickyAddButton = (text: string) => ({
25
+ scrollLastItemIntoView: true as const,
26
+ addButtonConfig: {
27
+ sticky: true as const,
28
+ text,
29
+ props: { type: 'primary', plain: true, text: false },
30
+ },
31
+ });
@@ -0,0 +1,177 @@
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 {
20
+ type DisplayCondsConfig,
21
+ type EventSelectConfig,
22
+ type FieldInnerConfig,
23
+ type FieldMountValueEffect,
24
+ filterFunction,
25
+ type FormItemConfig,
26
+ type HeadlessFieldOptions,
27
+ } from '@tmagic/form/headless';
28
+ import type { StyleSchema } from '@tmagic/schema';
29
+
30
+ import { editorTypeMatchRules, validateDataSourceFieldSelect } from '@editor/utils/type-match-rules';
31
+
32
+ import { createCodeSelectConfig, normalizeCodeSelectValue } from './configs/codeSelect';
33
+ import { createDisplayCondsConfig } from './configs/displayConds';
34
+ import { createEventSelectConfig, isLegacyEventValue } from './configs/eventSelect';
35
+ import { createStyleSetterConfig } from './StyleSetter/configs';
36
+
37
+ const getName = (config: FormItemConfig): string => `${(config as any).name ?? ''}`;
38
+
39
+ /** `code-select` 旧数据兼容:空值改写成 `{ hookType, hookData }`。 */
40
+ const codeSelectEffect: FieldMountValueEffect = ({ config, model }) => {
41
+ normalizeCodeSelectValue(model, getName(config));
42
+ };
43
+
44
+ /** `event-select`:表单 init 对未声明 defaultValue 的自定义 type 会写成空串,统一收成数组。 */
45
+ const eventSelectEffect: FieldMountValueEffect = ({ config, model }) => {
46
+ const name = getName(config);
47
+ if (model && !Array.isArray(model[name])) {
48
+ model[name] = [];
49
+ }
50
+ };
51
+
52
+ /**
53
+ * `code-select` 的内部配置。
54
+ *
55
+ * 对应 `fields/CodeSelect.vue` 内部
56
+ * `<MContainer :config="codeConfig" :model="model[name]" :prop="prop">`。
57
+ *
58
+ * @param ctx - innerConfig 回调入参
59
+ * @returns 内部容器的 config / model / prop
60
+ */
61
+ const codeSelectInnerConfig: FieldInnerConfig = ({ config, model, prop }) => {
62
+ const name = getName(config);
63
+
64
+ return {
65
+ config: createCodeSelectConfig(config as any),
66
+ model: model?.[name],
67
+ prop,
68
+ };
69
+ };
70
+
71
+ /**
72
+ * `display-conds` 的内部配置。
73
+ *
74
+ * 对应 `fields/DisplayConds.vue` 内部把同一份 groupList 配置交给 `MGroupList`。
75
+ *
76
+ * @param ctx - innerConfig 回调入参
77
+ * @returns 内部 group-list 配置;prop 基准为 parentProp,避免 name 被拼两次
78
+ */
79
+ const displayCondsInnerConfig: FieldInnerConfig = ({ config, model, prop, parentProp, mForm }) => {
80
+ const name = getName(config);
81
+ const parentFields =
82
+ filterFunction<string[]>(mForm, (config as DisplayCondsConfig).parentFields, {
83
+ model,
84
+ config,
85
+ prop,
86
+ }) || [];
87
+
88
+ return {
89
+ config: createDisplayCondsConfig(config as DisplayCondsConfig, name, parentFields),
90
+ // 内部 group-list 复用了字段自身的 name,prop 基准要退回父级,否则 name 会被拼两次
91
+ prop: parentProp,
92
+ };
93
+ };
94
+
95
+ /**
96
+ * `event-select` 的内部配置。
97
+ *
98
+ * 对应 `fields/EventSelect.vue`:列表走 group-list,事件名渲染在 title slot,
99
+ * 无渲染校验仍把事件名当作列表项字段,路径为 `<prop>.<index>.name`。
100
+ *
101
+ * @param ctx - innerConfig 回调入参
102
+ * @returns 合成的 group-list 配置;旧数据格式返回 null,不参与校验
103
+ */
104
+ const eventSelectInnerConfig: FieldInnerConfig = ({ config, model, parentProp }) => {
105
+ const name = getName(config);
106
+ const events = model?.[name];
107
+
108
+ // 旧数据格式走的是另一套表格配置,其中不含任何 rules,不参与校验
109
+ if (!Array.isArray(events) || isLegacyEventValue(events)) return null;
110
+
111
+ return {
112
+ config: createEventSelectConfig(config as EventSelectConfig, name, { includeEventName: true }),
113
+ prop: parentProp,
114
+ };
115
+ };
116
+
117
+ /**
118
+ * `style-setter` 的内部配置。
119
+ *
120
+ * 对应 `fields/StyleSetter/Index.vue`:6 个面板共用 `:values="model[name]"`、`:prop="prop || name"`。
121
+ * `theme` 按 `useTheme` 缺省空串传入,只改 flexWrap 的 UI childType,不影响校验字段。
122
+ *
123
+ * @param ctx - innerConfig 回调入参
124
+ * @returns style 面板配置及 styleModel
125
+ */
126
+ const styleSetterInnerConfig: FieldInnerConfig = ({ config, model, prop }) => {
127
+ const name = getName(config);
128
+ const styleModel = (model?.[name] ?? {}) as Partial<StyleSchema>;
129
+
130
+ return {
131
+ config: createStyleSetterConfig(styleModel, '', validateDataSourceFieldSelect),
132
+ model: styleModel,
133
+ prop,
134
+ };
135
+ };
136
+
137
+ /**
138
+ * 编辑器字段的无渲染登记表(不含 Vue 组件)。
139
+ *
140
+ * Node 里 `registerFields(editorFields)` 即可配合 `validateForm` / `submitForm`。
141
+ * 安装编辑器时由 plugin 把 `component` 叠上去再传给 `@tmagic/form`。
142
+ *
143
+ * - 叶子:内部只渲染叶子 UI,或把子表单渲染在独立的 MForm / MFormBox 实例里
144
+ * - innerConfig:内部再渲染 MContainer / MPanel / MGroupList,把运行期配置交出来
145
+ * - typeMatch:该 type 自身的类型匹配校验
146
+ *
147
+ * innerConfig 返回的 config / model / prop 与组件模板里传给内部容器的那一组保持一一对应,
148
+ * 且配置本身与组件共用同一份工厂(`fields/configs/`)。
149
+ */
150
+ export const editorFields: Record<string, HeadlessFieldOptions> = {
151
+ 'vs-code': {},
152
+ 'code-link': {},
153
+ 'ui-select': { typeMatch: editorTypeMatchRules['ui-select'] },
154
+ 'cond-op-select': { typeMatch: editorTypeMatchRules['cond-op-select'] },
155
+ 'page-fragment-select': { typeMatch: editorTypeMatchRules['page-fragment-select'] },
156
+ 'data-source-select': { typeMatch: editorTypeMatchRules['data-source-select'] },
157
+ 'data-source-input': { typeMatch: editorTypeMatchRules['data-source-input'] },
158
+ 'key-value': { typeMatch: editorTypeMatchRules['key-value'] },
159
+ 'code-select-col': { typeMatch: editorTypeMatchRules['code-select-col'] },
160
+ 'data-source-fields': { typeMatch: editorTypeMatchRules['data-source-fields'] },
161
+ 'data-source-mocks': { typeMatch: editorTypeMatchRules['data-source-mocks'] },
162
+ 'data-source-methods': { typeMatch: editorTypeMatchRules['data-source-methods'] },
163
+ 'data-source-method-select': { typeMatch: editorTypeMatchRules['data-source-method-select'] },
164
+ 'data-source-field-select': { typeMatch: editorTypeMatchRules['data-source-field-select'] },
165
+ 'code-select': {
166
+ effect: codeSelectEffect,
167
+ innerConfig: codeSelectInnerConfig,
168
+ typeMatch: editorTypeMatchRules['code-select'],
169
+ },
170
+ 'display-conds': { innerConfig: displayCondsInnerConfig, typeMatch: editorTypeMatchRules['display-conds'] },
171
+ 'event-select': {
172
+ effect: eventSelectEffect,
173
+ innerConfig: eventSelectInnerConfig,
174
+ typeMatch: editorTypeMatchRules['event-select'],
175
+ },
176
+ 'style-setter': { innerConfig: styleSetterInnerConfig, typeMatch: editorTypeMatchRules['style-setter'] },
177
+ };
@@ -0,0 +1,33 @@
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
+ /**
20
+ * @fileoverview `@tmagic/editor/headless`:编辑器字段的无渲染登记表。
21
+ *
22
+ * 不加载编辑器界面(Editor.vue、各面板、monaco 等),可在无 DOM 的 Node 里 import。
23
+ * 配合 `@tmagic/form/headless`:`registerBuiltInFields(builtInFields); registerFields(editorFields);`
24
+ *
25
+ * 注意这里不是「零 Vue 依赖」:`style-setter` 的字段配置与编辑器 UI 共用同一份工厂
26
+ * (`fields/StyleSetter/configs.ts`),配置里的 `icon` / `component` 会带进来若干图标
27
+ * SFC;typeMatch 的提示文案要读画布与数据源,也会带进 editor services。这些都只在
28
+ * import 时定义、不访问 DOM,但产物体积明显大于 `@tmagic/form/headless`。
29
+ *
30
+ * @module @tmagic/editor/headless
31
+ */
32
+
33
+ export { editorFields } from './fields/headless-validation';
@@ -16,11 +16,12 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { computed, type ComputedRef, inject, provide, type Ref, ref, useTemplateRef, watch, watchEffect } from 'vue';
19
+ import { computed, type ComputedRef, inject, provide, type Ref, ref, useTemplateRef, watch } from 'vue';
20
20
 
21
21
  import type { CodeBlockContent, MNode } from '@tmagic/core';
22
- import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
22
+ import { type FormConfig, type FormContext, type FormValue, MForm } from '@tmagic/form';
23
23
 
24
+ import { useEditorFormContext } from '@editor/hooks/use-form-context';
24
25
  import type { CompareFormBaseProps } from '@editor/type';
25
26
  import { getCodeBlockFormConfig } from '@editor/utils/code-block';
26
27
 
@@ -28,7 +29,7 @@ export interface UseCompareFormReturn {
28
29
  config: Ref<FormConfig>;
29
30
  currentValues: ComputedRef<FormValue>;
30
31
  wrapperStyle: ComputedRef<Record<string, string> | undefined>;
31
- mergedExtendState: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
32
+ formContext: ComputedRef<FormContext>;
32
33
  loadConfig: () => Promise<void>;
33
34
  formRef: Readonly<Ref<InstanceType<typeof MForm> | null>>;
34
35
  normalizeCodeBlockValue: (v: Partial<CodeBlockContent> | Record<string, any> | undefined) => Record<string, any>;
@@ -39,7 +40,7 @@ export interface UseCompareFormReturn {
39
40
  * - 按 `category`(node / data-source / code-block) 加载 FormConfig(支持自定义 `loadConfig`);
40
41
  * - 代码块 `content` 归一化为字符串;
41
42
  * - 外层容器固定高度 + 内部滚动的 `wrapperStyle`;
42
- * - 将 services / stage 注入 MForm.formState,保证 filterFunction 上下文一致。
43
+ * - 将 services / stage 作为 form context 传给 MForm,保证 filterFunction 上下文一致。
43
44
  *
44
45
  * 两个组件的差异仅在于是否做新旧值对比,这部分逻辑保留在各自组件中。
45
46
  */
@@ -87,10 +88,7 @@ export const useCompareForm = (props: CompareFormBaseProps): UseCompareFormRetur
87
88
  return style;
88
89
  });
89
90
 
90
- const mergedExtendState = (state: FormState) => {
91
- const extendState = props.extendState ?? ((s: FormState) => s);
92
- return extendState(props.baseFormState || state);
93
- };
91
+ const formContext = useEditorFormContext(() => props.services);
94
92
 
95
93
  /**
96
94
  * 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
@@ -156,27 +154,11 @@ export const useCompareForm = (props: CompareFormBaseProps): UseCompareFormRetur
156
154
 
157
155
  const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
158
156
 
159
- /**
160
- * 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
161
- * 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
162
- * 在执行时拿不到 `formState.services` 而报错。
163
- *
164
- * 与 props-panel/FormPanel.vue 中的注入方式保持一致:
165
- * - services:整个 useServices() 返回的服务集合;
166
- * - stage:当前 editorService.get('stage') 的最新值。
167
- */
168
- watchEffect(() => {
169
- if (formRef.value && props.services) {
170
- formRef.value.formState.stage = props.services.editorService.get('stage');
171
- formRef.value.formState.services = props.services;
172
- }
173
- });
174
-
175
157
  return {
176
158
  config,
177
159
  currentValues,
178
160
  wrapperStyle,
179
- mergedExtendState,
161
+ formContext,
180
162
  loadConfig,
181
163
  formRef,
182
164
  normalizeCodeBlockValue,
@@ -0,0 +1,40 @@
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 } from 'vue';
20
+
21
+ import type { FormContext } from '@tmagic/form';
22
+
23
+ import type { Services } from '@editor/type';
24
+
25
+ /**
26
+ * 编辑器注入给表单的业务上下文:`services` 与当前画布 `stage`。
27
+ *
28
+ * 由 `Editor.vue`(provide `FORM_CONTEXT_KEY`)、`FormPanel.vue` 与 `useCompareForm`
29
+ * 共用。`stage` 走 computed 而非快照,保证切换画布后配置回调读到的是最新实例。
30
+ *
31
+ * 字段类型见 `@editor/type` 里对 `@tmagic/form-schema` 的 `FormContext` 模块增强。
32
+ */
33
+ export const useEditorFormContext = (getServices: () => Services | undefined): ComputedRef<FormContext> =>
34
+ computed(() => {
35
+ const services = getServices();
36
+ return {
37
+ services,
38
+ stage: services?.editorService.get('stage'),
39
+ };
40
+ });
package/src/index.ts CHANGED
@@ -91,4 +91,6 @@ export { default as DisplayConds } from './fields/DisplayConds.vue';
91
91
  export { default as CondOpSelect } from './fields/CondOpSelect.vue';
92
92
  export { default as StyleSetter } from './fields/StyleSetter/Index.vue';
93
93
 
94
+ export { editorFields } from './fields/headless-validation';
95
+
94
96
  export { default } from './plugin';
@@ -29,6 +29,7 @@ import { getDepNodeIds, getNodes, isPage, isValueIncludeDataSource } from '@tmag
29
29
 
30
30
  import PropsPanel from './layouts/PropsPanel.vue';
31
31
  import { isIncludeDataSource } from './utils/editor';
32
+ import { error as logError } from './utils/logger';
32
33
  import { EditorProps } from './editorProps';
33
34
  import { Services } from './type';
34
35
 
@@ -36,6 +37,19 @@ export declare type LooseRequired<T> = {
36
37
  [P in string & keyof T]: T[P];
37
38
  };
38
39
 
40
+ /**
41
+ * select 为异步串行执行,节点不可选中(如已从 DSL 中移除)时会抛错。
42
+ * 在 watch 回调中调用不能把拒绝抛回调用方,否则会变成 Vue 的「Unhandled error during execution of watcher callback」,
43
+ * 这里统一兜底成日志。
44
+ */
45
+ const selectNode = async (editorService: Services['editorService'], id: Id) => {
46
+ try {
47
+ await editorService.select(id);
48
+ } catch (e) {
49
+ logError(e);
50
+ }
51
+ };
52
+
39
53
  export const initServiceState = (
40
54
  props: EditorProps,
41
55
  {
@@ -193,9 +207,17 @@ export const initServiceState = (
193
207
  },
194
208
  );
195
209
 
210
+ // defaultSelected 可能早于 modelValue 到位(immediate 首次执行时 root 还是空的,或节点尚未出现在 DSL 中),
211
+ // 此时 select 会因为「获取不到组件信息」而抛错,交由 root-change 中的选中逻辑在 root 就绪后补上即可。
196
212
  watch(
197
213
  () => props.defaultSelected,
198
- (defaultSelected) => defaultSelected && editorService.select(defaultSelected),
214
+ (defaultSelected) => {
215
+ if (!defaultSelected || !editorService.getNodeById(defaultSelected)) {
216
+ return;
217
+ }
218
+
219
+ selectNode(editorService, defaultSelected);
220
+ },
199
221
  {
200
222
  immediate: true,
201
223
  },
@@ -43,8 +43,6 @@
43
43
  :data-source-type="payload.dataSourceType"
44
44
  :value="rightValue"
45
45
  :last-value="leftValue"
46
- :base-form-state="compareFormState"
47
- :extend-state="extendState"
48
46
  :load-config="loadConfig"
49
47
  :self-diff-field-types="selfDiffFieldTypes"
50
48
  :services="props.services"
@@ -86,7 +84,6 @@ import {
86
84
  TMagicRadioGroup,
87
85
  TMagicTag,
88
86
  } from '@tmagic/design';
89
- import type { FormState } from '@tmagic/form';
90
87
 
91
88
  import CompareForm from '@editor/components/CompareForm.vue';
92
89
  import CodeEditor from '@editor/layouts/CodeEditor.vue';
@@ -100,12 +97,6 @@ const props = withDefaults(
100
97
  defineProps<{
101
98
  /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
102
99
  services?: Services;
103
- /**
104
- * 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
105
- * 透传给 CompareForm,从而让差异对比时表单 item 中依赖业务上下文的
106
- * `display` / `disabled` 等 filterFunction 正常工作。
107
- */
108
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
109
100
  /**
110
101
  * 自定义 FormConfig 加载逻辑,透传给 CompareForm。传入后将接管内置的按 `category`
111
102
  * 取配置逻辑,可通过 `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
@@ -117,7 +108,6 @@ const props = withDefaults(
117
108
  isConfirm?: boolean;
118
109
  onConfirm?: () => void;
119
110
  selfDiffFieldTypes?: string[];
120
- compareFormState?: FormState;
121
111
  }>(),
122
112
  {
123
113
  width: '900px',
@@ -133,7 +133,6 @@ import { computed, inject, markRaw, ref, watch } from 'vue';
133
133
  import { Clock, Close } from '@element-plus/icons-vue';
134
134
 
135
135
  import { getDesignConfig, TMagicButton, tMagicMessage, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
136
- import type { FormState } from '@tmagic/form';
137
136
 
138
137
  import MIcon from '@editor/components/Icon.vue';
139
138
  import { useServices } from '@editor/hooks/use-services';
@@ -193,17 +192,6 @@ watch([disabledDataSource, disabledCodeBlock], ([dsDisabled, cbDisabled]) => {
193
192
  }
194
193
  });
195
194
 
196
- /**
197
- * 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
198
- * 内部的 CompareForm,使差异对比表单的 filterFunction 能拿到完整的业务上下文。
199
- * 未提供时为 undefined,CompareForm/MForm 会跳过 extendState 处理。
200
- */
201
- const extendFormState = inject<((_state: FormState) => Record<string, any> | Promise<Record<string, any>>) | undefined>(
202
- 'extendFormState',
203
- undefined,
204
- );
205
- const getPropsPanelFormState = inject<(() => FormState | undefined) | undefined>('getPropsPanelFormState', undefined);
206
-
207
195
  const {
208
196
  expanded,
209
197
  toggleGroup,
@@ -316,7 +304,7 @@ const onCodeBlockGotoInitial = (id: string | number) => {
316
304
  * 业务方亦可直接 import useHistoryRevert(options, services) 调用,无需自行挂载任何弹窗。
317
305
  */
318
306
  const { onPageRevert, onDataSourceRevert, onCodeBlockRevert, onPageDiff, onDataSourceDiff, onCodeBlockDiff } =
319
- useHistoryRevert({ extendState: extendFormState, getPropsPanelFormState }, services);
307
+ useHistoryRevert({}, services);
320
308
 
321
309
  /**
322
310
  * 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
@@ -74,7 +74,7 @@ interface MountedDiffDialog {
74
74
  * 弹窗组件动态 import,避免拖累其它消费者。供「确认回滚」与「查看差异」两种交互共用。
75
75
  */
76
76
  const mountHistoryDiffDialog = async (
77
- options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
77
+ options: Pick<UseHistoryRevertOptions, 'appContext'> &
78
78
  CustomDiffFormOptions & {
79
79
  services?: Services;
80
80
  isConfirm?: boolean;
@@ -90,10 +90,8 @@ const mountHistoryDiffDialog = async (
90
90
  const app = createApp(historyDiffDialog, {
91
91
  services: options.services,
92
92
  isConfirm: options.isConfirm,
93
- extendState: options.extendState,
94
93
  loadConfig: options.loadConfig,
95
94
  selfDiffFieldTypes: options.selfDiffFieldTypes,
96
- compareFormState: options.compareFormState,
97
95
  width: options.width,
98
96
  size: options.size ?? options.services?.uiService?.get('propsPanelSize'),
99
97
  onClose: options.onClose,
@@ -125,7 +123,7 @@ const mountHistoryDiffDialog = async (
125
123
  */
126
124
  const confirmRevertWithDiffDialog = async (
127
125
  payload: DiffDialogPayload,
128
- options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
126
+ options: Pick<UseHistoryRevertOptions, 'appContext'> &
129
127
  CustomDiffFormOptions & {
130
128
  services?: Services;
131
129
  },
@@ -147,7 +145,7 @@ const confirmRevertWithDiffDialog = async (
147
145
  */
148
146
  const viewHistoryDiffDialog = async (
149
147
  payload: DiffDialogPayload,
150
- options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
148
+ options: Pick<UseHistoryRevertOptions, 'appContext'> &
151
149
  CustomDiffFormOptions & {
152
150
  services?: Services;
153
151
  },
@@ -194,7 +192,7 @@ const viewHistoryDiffDialog = async (
194
192
  export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
195
193
  // 自动捕获调用方所在组件的 appContext(在 setup 中调用时),业务方亦可显式覆盖。
196
194
  const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
197
- const { extendState, getPropsPanelFormState, dialogWidth } = options;
195
+ const { dialogWidth } = options;
198
196
 
199
197
  /** 目标数据已被删除、无法回滚时的统一提示。 */
200
198
  const showRevertTargetMissing = () => {
@@ -220,7 +218,6 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
220
218
  if (payload) {
221
219
  return confirmRevertWithDiffDialog(payload, {
222
220
  appContext,
223
- extendState,
224
221
  services,
225
222
  ...extra,
226
223
  width: extra?.width ?? dialogWidth,
@@ -307,7 +304,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
307
304
  showRevertTargetMissing();
308
305
  return Promise.resolve(null);
309
306
  }
310
- return runRevert(buildPageDiffPayload(index), { compareFormState: getPropsPanelFormState?.() }).then((result) =>
307
+ return runRevert(buildPageDiffPayload(index), {}).then((result) =>
311
308
  result ? services?.editorService.revertPageStep(index) : null,
312
309
  );
313
310
  };
@@ -341,22 +338,20 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
341
338
  if (payload) {
342
339
  return viewHistoryDiffDialog(payload, {
343
340
  appContext,
344
- extendState,
345
341
  services,
346
342
  width: dialogWidth,
347
- compareFormState: getPropsPanelFormState?.(),
348
343
  });
349
344
  }
350
345
  };
351
346
 
352
347
  const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
353
348
  const payload = buildDataSourceDiffPayload(id, index);
354
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
349
+ if (payload) return viewHistoryDiffDialog(payload, { appContext, services, width: dialogWidth });
355
350
  };
356
351
 
357
352
  const onCodeBlockDiff = (id: string | number, index: number): Promise<void> | void => {
358
353
  const payload = buildCodeBlockDiffPayload(id, index);
359
- if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
354
+ if (payload) return viewHistoryDiffDialog(payload, { appContext, services, width: dialogWidth });
360
355
  };
361
356
 
362
357
  /**
@@ -398,7 +393,6 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
398
393
  if (payload)
399
394
  return viewHistoryDiffDialog(payload, {
400
395
  appContext,
401
- extendState,
402
396
  services,
403
397
  ...extra,
404
398
  width: extra?.width ?? dialogWidth,