@tmagic/editor 1.8.0-beta.11 → 1.8.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +7 -4
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +5 -1
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +2 -1
  4. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +18 -123
  5. package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +60 -14
  6. package/dist/es/components/ScrollBar.js +0 -1
  7. package/dist/es/components/ViewForm.js +5 -0
  8. package/dist/es/components/ViewForm.vue_vue_type_script_setup_true_lang.js +58 -0
  9. package/dist/es/editorProps.js +6 -1
  10. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +15 -3
  11. package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +2 -17
  12. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +2 -1
  13. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +7 -0
  14. package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +5 -17
  15. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -0
  16. package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +29 -3
  17. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +50 -88
  18. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -5
  19. package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +48 -23
  20. package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +6 -3
  21. package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +28 -22
  22. package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +28 -22
  23. package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +28 -22
  24. package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +6 -3
  25. package/dist/es/fields/UISelect.js +0 -1
  26. package/dist/es/hooks/use-compare-form.js +119 -0
  27. package/dist/es/index.js +13 -8
  28. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +21 -6
  29. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +9 -6
  30. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +4 -1
  31. package/dist/es/layouts/history-list/composables.js +1 -1
  32. package/dist/es/layouts/history-list/useHistoryRevert.js +7 -3
  33. package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +44 -5
  34. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +15 -8
  35. package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
  36. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +15 -31
  37. package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  38. package/dist/es/layouts/sidebar/code-block/useContentMenu.js +18 -3
  39. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +4 -0
  40. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
  41. package/dist/es/layouts/sidebar/data-source/useContentMenu.js +18 -3
  42. package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
  43. package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
  44. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +2 -1
  45. package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +8 -2
  46. package/dist/es/plugin.js +3 -1
  47. package/dist/es/services/codeBlock.js +1 -1
  48. package/dist/es/services/editor.js +104 -5
  49. package/dist/es/services/history.js +1 -1
  50. package/dist/es/services/props.js +3 -2
  51. package/dist/es/services/storage.js +2 -2
  52. package/dist/es/services/ui.js +3 -0
  53. package/dist/es/style.css +104 -55
  54. package/dist/es/utils/data-source/index.js +30 -11
  55. package/dist/es/utils/event.js +123 -0
  56. package/dist/es/utils/monaco-editor.js +22 -6
  57. package/dist/es/utils/props.js +119 -8
  58. package/dist/es/utils/type-match-rules.js +360 -0
  59. package/dist/style.css +104 -55
  60. package/dist/tmagic-editor.umd.cjs +16996 -15806
  61. package/package.json +8 -8
  62. package/src/Editor.vue +3 -1
  63. package/src/components/CodeBlockEditor.vue +3 -0
  64. package/src/components/CompareForm.vue +16 -196
  65. package/src/components/FloatingBox.vue +77 -8
  66. package/src/components/ScrollBar.vue +0 -67
  67. package/src/components/ViewForm.vue +55 -0
  68. package/src/editorProps.ts +13 -0
  69. package/src/fields/CodeSelect.vue +4 -1
  70. package/src/fields/CondOpSelect.vue +2 -24
  71. package/src/fields/DataSourceFields.vue +5 -0
  72. package/src/fields/DataSourceInput.vue +7 -25
  73. package/src/fields/DataSourceMocks.vue +3 -0
  74. package/src/fields/DisplayConds.vue +14 -0
  75. package/src/fields/EventSelect.vue +84 -129
  76. package/src/fields/StyleSetter/Index.vue +2 -8
  77. package/src/fields/StyleSetter/pro/Background.vue +94 -55
  78. package/src/fields/StyleSetter/pro/Border.vue +9 -11
  79. package/src/fields/StyleSetter/pro/Font.vue +66 -64
  80. package/src/fields/StyleSetter/pro/Layout.vue +140 -138
  81. package/src/fields/StyleSetter/pro/Position.vue +69 -67
  82. package/src/fields/StyleSetter/pro/Transform.vue +23 -25
  83. package/src/fields/UISelect.vue +0 -12
  84. package/src/hooks/index.ts +1 -0
  85. package/src/hooks/use-compare-form.ts +187 -0
  86. package/src/index.ts +2 -0
  87. package/src/layouts/CodeEditor.vue +32 -6
  88. package/src/layouts/Framework.vue +7 -6
  89. package/src/layouts/history-list/HistoryDiffDialog.vue +11 -1
  90. package/src/layouts/history-list/useHistoryRevert.ts +4 -0
  91. package/src/layouts/props-panel/FormPanel.vue +66 -5
  92. package/src/layouts/props-panel/PropsPanel.vue +20 -6
  93. package/src/layouts/props-panel/use-style-panel.ts +4 -3
  94. package/src/layouts/sidebar/Sidebar.vue +2 -0
  95. package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +4 -1
  96. package/src/layouts/sidebar/code-block/useContentMenu.ts +13 -1
  97. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
  98. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +2 -2
  99. package/src/layouts/sidebar/data-source/useContentMenu.ts +13 -1
  100. package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
  101. package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
  102. package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
  103. package/src/plugin.ts +3 -1
  104. package/src/services/editor.ts +168 -3
  105. package/src/services/ui.ts +6 -0
  106. package/src/theme/event.scss +8 -0
  107. package/src/theme/floating-box.scss +9 -1
  108. package/src/theme/layer-node-content.scss +14 -0
  109. package/src/theme/scroll-bar.scss +64 -0
  110. package/src/theme/theme.scss +3 -0
  111. package/src/theme/ui-select.scss +9 -0
  112. package/src/type.ts +105 -2
  113. package/src/utils/data-source/index.ts +30 -12
  114. package/src/utils/event.ts +224 -0
  115. package/src/utils/index.ts +2 -0
  116. package/src/utils/monaco-editor.ts +27 -0
  117. package/src/utils/props.ts +189 -2
  118. package/src/utils/type-match-rules.ts +678 -0
  119. package/types/index.d.ts +558 -213
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.11",
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/form": "1.8.0-beta.11",
62
- "@tmagic/design": "1.8.0-beta.11",
63
- "@tmagic/stage": "1.8.0-beta.11",
64
- "@tmagic/utils": "1.8.0-beta.11",
65
- "@tmagic/table": "1.8.0-beta.11"
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",
@@ -75,8 +75,8 @@
75
75
  "monaco-editor": "^0.55.1 ",
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
- "vue": "^3.5.38",
79
- "@tmagic/core": "1.8.0-beta.11"
78
+ "vue": "^3.5.40",
79
+ "@tmagic/core": "1.8.0-beta.13"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
package/src/Editor.vue CHANGED
@@ -158,7 +158,7 @@ import stageOverlayService from './services/stageOverlay';
158
158
  import storageService from './services/storage';
159
159
  import uiService from './services/ui';
160
160
  import keybindingConfig from './utils/keybinding-config';
161
- import { defaultEditorProps, EditorProps } from './editorProps';
161
+ import { defaultEditorProps, EditorProps, ENABLE_PROPS_FORM_VALIDATE } from './editorProps';
162
162
  import { initServiceEvents, initServiceState } from './initService';
163
163
  import type { TreeNodeData } from './type';
164
164
  import type { EditorSlots, EventBus, Services, StageOptions } from './type';
@@ -233,6 +233,8 @@ provide('services', services);
233
233
 
234
234
  provide('codeOptions', props.codeOptions);
235
235
  provide('stageOptions', stageOptions);
236
+ /** 是否启用「属性配置表单校验」联动能力,供 PropsPanel / FormPanel 判断校验失败时是否仍更新节点并记录错误 */
237
+ provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
236
238
  /**
237
239
  * 把顶层 `extendFormState` 提供给非 PropsPanel 链路上的组件使用(例如历史差异对话框 HistoryDiffDialog
238
240
  * 内部的 CompareForm)。这样所有依赖业务上下文的表单 filterFunction 都能拿到一致的扩展状态,
@@ -4,7 +4,9 @@
4
4
  v-model:visible="boxVisible"
5
5
  v-model:width="width"
6
6
  v-model:height="codeBlockEditorHeight"
7
+ :body-style="{ padding: '0 16px' }"
7
8
  :title="content.name ? `${disabled ? '查看' : '编辑'}${content.name}` : '新增代码'"
9
+ :framework-width="frameworkWidth"
8
10
  :position="boxPosition"
9
11
  :before-close="beforeClose"
10
12
  >
@@ -206,6 +208,7 @@ const closedHandler = () => {
206
208
 
207
209
  const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
208
210
  const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
211
+ const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
209
212
 
210
213
  watch(boxVisible, (visible) => {
211
214
  nextTick(() => {
@@ -13,106 +13,42 @@
13
13
  :extend-state="mergedExtendState"
14
14
  :show-diff="showDiff"
15
15
  :self-diff-field-types="selfDiffFieldTypes"
16
+ :size="size"
16
17
  ></MForm>
17
18
  </div>
18
19
  </template>
19
20
 
20
21
  <script lang="ts" setup>
21
- import { computed, inject, provide, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
22
+ import { computed, type Ref, type ShallowRef } from 'vue';
22
23
  import { isEqual } from 'lodash-es';
23
24
 
24
- import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
25
- 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';
26
27
 
27
- import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
28
- import { getCodeBlockFormConfig } from '@editor/utils/code-block';
28
+ import { useCompareForm } from '@editor/hooks/use-compare-form';
29
+ import type { CompareFormBaseProps } from '@editor/type';
29
30
 
30
31
  defineOptions({
31
32
  name: 'MEditorCompareForm',
32
33
  });
33
34
 
34
35
  const props = withDefaults(
35
- defineProps<{
36
- /** 当前值(修改后的值) */
37
- value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
38
- /** 用于对比的旧值(修改前的值) */
39
- lastValue?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
40
- /**
41
- * 类型说明:
42
- * - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
43
- * - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
44
- * - `category` 为 `code-block` 时,`type` 可不传
45
- */
46
- type?: string;
47
- /** 表单配置类别,决定从哪里取 FormConfig */
48
- category?: CompareCategory;
49
- /** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
50
- dataSourceType?: string;
51
- labelWidth?: string;
52
- /**
53
- * 外层容器高度。设置后表单内容超出时会在 CompareForm 内部出现滚动条,
54
- * 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
55
- */
56
- height?: string;
57
- /**
58
- * 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
59
- * PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
60
- * filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
61
- * 因此在差异对比场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
62
- */
63
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
64
- /**
65
- * 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
66
- * CompareForm 会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
67
- */
68
- baseFormState?: FormState;
69
- /** 需要走 self diff 的字段类型(例如 mod-cond)。 */
70
- selfDiffFieldTypes?: string[];
71
- /**
72
- * 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
73
- * 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
74
- * `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。返回的 config 直接用于对比展示。
75
- */
76
- loadConfig?: CompareFormLoadConfig;
77
- /** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
78
- services?: Services;
79
- }>(),
36
+ defineProps<
37
+ CompareFormBaseProps & {
38
+ /** 用于对比的旧值(修改前的值) */
39
+ lastValue?: CompareFormBaseProps['value'];
40
+ /** 需要走 self diff 的字段类型(例如 mod-cond)。 */
41
+ selfDiffFieldTypes?: string[];
42
+ }
43
+ >(),
80
44
  {
81
45
  category: 'node',
82
46
  labelWidth: '120px',
83
- extendState: (state: FormState) => state,
84
47
  },
85
48
  );
86
49
 
87
- provide('services', props.services);
88
-
89
- const config = ref<FormConfig>([]);
90
-
91
- /** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
92
- const codeOptions = inject<Record<string, any>>('codeOptions', {});
93
-
94
- /** 将代码块的 content 字段统一成字符串,便于在表单/对比中展示 */
95
- const normalizeCodeBlockValue = (
96
- v: Partial<CodeBlockContent> | Record<string, any> | undefined,
97
- ): Record<string, any> => {
98
- if (!v) return {};
99
- const next: Record<string, any> = { ...v };
100
- if (next.content && typeof next.content !== 'string') {
101
- try {
102
- next.content = next.content.toString();
103
- } catch {
104
- next.content = '';
105
- }
106
- }
107
- return next;
108
- };
109
-
110
- const currentValues = computed<FormValue>(() => {
111
- if (props.category === 'code-block') {
112
- return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
113
- }
114
- return (props.value || {}) as FormValue;
115
- });
50
+ const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef, normalizeCodeBlockValue } =
51
+ useCompareForm(props);
116
52
 
117
53
  const lastValuesProcessed = computed<FormValue>(() => {
118
54
  if (props.category === 'code-block') {
@@ -121,18 +57,6 @@ const lastValuesProcessed = computed<FormValue>(() => {
121
57
  return (props.lastValue || {}) as FormValue;
122
58
  });
123
59
 
124
- /**
125
- * 外层包裹层的样式:当传入 `height` 时启用固定高度 + 内部滚动,
126
- * 这样滚动条会出现在 CompareForm 内部,避免父容器(如 Dialog)自身也产生滚动。
127
- */
128
- const wrapperStyle = computed(() => {
129
- if (!props.height) return undefined;
130
- return {
131
- height: props.height,
132
- overflow: 'auto',
133
- } as Record<string, string>;
134
- });
135
-
136
60
  /**
137
61
  * `code-select` 字段在历史数据中存在两种"语义为空"的形态:
138
62
  * - 字符串 `''`(旧数据 / 用户从未配置过钩子);
@@ -161,110 +85,6 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
161
85
  return !isEqual(curValue, lastValue);
162
86
  };
163
87
 
164
- const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
165
- formConfig.map((item) => {
166
- if (!('type' in item)) return item;
167
- if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
168
-
169
- return {
170
- ...item,
171
- items: item.items.map((tabPane) => {
172
- if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
173
-
174
- return {
175
- ...tabPane,
176
- display: true,
177
- };
178
- }),
179
- };
180
- });
181
-
182
- const mergedExtendState = (state: FormState) => {
183
- return props.extendState(props.baseFormState || state);
184
- };
185
-
186
- /**
187
- * 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
188
- * 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
189
- */
190
- const defaultLoadConfig = async (): Promise<FormConfig> => {
191
- if (!props.services) {
192
- return [];
193
- }
194
-
195
- switch (props.category) {
196
- case 'node': {
197
- if (!props.type) {
198
- return [];
199
- }
200
- return removeStyleDisplayConfig(
201
- await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
202
- );
203
- }
204
- case 'data-source': {
205
- const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
206
- // 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
207
- // 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
208
- // 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
209
- return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
210
- }
211
- case 'code-block': {
212
- return getCodeBlockFormConfig({
213
- paramColConfig: props.services.codeBlockService.getParamsColConfig(),
214
- // 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比场景下 props.dataSourceType
215
- // 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
216
- isDataSource: () => Boolean(props.dataSourceType),
217
- dataSourceType: () => props.dataSourceType,
218
- codeOptions,
219
- // 对比模式只读,不需要校验/语法检查
220
- editable: false,
221
- });
222
- }
223
- default:
224
- return [];
225
- }
226
- };
227
-
228
- const loadConfig = async () => {
229
- if (props.loadConfig) {
230
- config.value = await props.loadConfig({
231
- category: props.category,
232
- type: props.type,
233
- dataSourceType: props.dataSourceType,
234
- defaultLoadConfig,
235
- });
236
- return;
237
- }
238
-
239
- config.value = await defaultLoadConfig();
240
- };
241
-
242
- watch(
243
- [() => props.category, () => props.type, () => props.dataSourceType, () => props.loadConfig],
244
- () => {
245
- loadConfig();
246
- },
247
- { immediate: true },
248
- );
249
-
250
- const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
251
-
252
- /**
253
- * 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
254
- * 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
255
- * 在执行时拿不到 `formState.services` 而报错。
256
- *
257
- * 与 props-panel/FormPanel.vue 中的注入方式保持一致:
258
- * - services:整个 useServices() 返回的服务集合;
259
- * - stage:当前 editorService.get('stage') 的最新值。
260
- */
261
- watchEffect(() => {
262
- if (formRef.value && props.services) {
263
- formRef.value.formState.stage = props.services.editorService.get('stage');
264
- formRef.value.formState.services = props.services;
265
- }
266
- });
267
-
268
88
  defineExpose<{
269
89
  form: ShallowRef<InstanceType<typeof MForm> | null>;
270
90
  config: Ref<FormConfig>;
@@ -1,6 +1,12 @@
1
1
  <template>
2
2
  <Teleport to="body" v-if="visible">
3
- <div ref="target" class="m-editor-float-box" :style="{ ...style, zIndex: curZIndex }" @mousedown="nextZIndex">
3
+ <div
4
+ ref="target"
5
+ class="m-editor-float-box"
6
+ v-bind="$attrs"
7
+ :style="{ ...style, zIndex: curZIndex }"
8
+ @mousedown="nextZIndex"
9
+ >
4
10
  <div ref="title" class="m-editor-float-box-title">
5
11
  <slot name="title">
6
12
  <span>{{ title }}</span>
@@ -9,7 +15,7 @@
9
15
  <TMagicButton link size="small" @click="closeHandler"><MIcon :icon="Close"></MIcon></TMagicButton>
10
16
  </div>
11
17
  </div>
12
- <div class="m-editor-float-box-body" :style="{ height: `${bodyHeight}px` }">
18
+ <div class="m-editor-float-box-body" :style="{ height: `${bodyHeight}px`, ...bodyStyle }">
13
19
  <slot name="body"></slot>
14
20
  </div>
15
21
  </div>
@@ -17,14 +23,13 @@
17
23
  </template>
18
24
 
19
25
  <script setup lang="ts">
20
- import { computed, nextTick, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
26
+ import { computed, type CSSProperties, nextTick, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
21
27
  import { Close } from '@element-plus/icons-vue';
22
28
  import VanillaMoveable from 'moveable';
23
29
 
24
30
  import { TMagicButton, useZIndex } from '@tmagic/design';
25
31
 
26
32
  import MIcon from '@editor/components/Icon.vue';
27
- import { useServices } from '@editor/hooks/use-services';
28
33
 
29
34
  interface Position {
30
35
  left: number;
@@ -39,11 +44,18 @@ const props = withDefaults(
39
44
  defineProps<{
40
45
  position?: Position;
41
46
  title?: string;
47
+ bodyStyle?: CSSProperties;
48
+ /** 浮窗初始样式,会与内部计算样式合并,外部传入优先 */
49
+ initialStyle?: CSSProperties;
50
+ /** 用于约束浮窗 left 的容器宽度,传入时按宽度收敛 left,避免超出右边界;默认取视窗宽度 */
51
+ frameworkWidth?: number;
42
52
  beforeClose?: (_done: (_cancel?: boolean) => void) => void;
43
53
  }>(),
44
54
  {
45
55
  title: '',
46
56
  position: () => ({ left: 0, top: 0 }),
57
+ initialStyle: () => ({}),
58
+ frameworkWidth: 0,
47
59
  },
48
60
  );
49
61
 
@@ -66,12 +78,11 @@ const bodyHeight = computed(() => {
66
78
  return 'auto';
67
79
  });
68
80
 
69
- const { uiService } = useServices();
70
- const frameworkWidth = computed(() => uiService.get('frameworkRect').width || 0);
71
81
  const style = computed(() => {
72
82
  let { left } = props.position;
73
- if (width.value) {
74
- left = left + width.value > frameworkWidth.value ? frameworkWidth.value - width.value : left;
83
+ const frameworkWidth = props.frameworkWidth || globalThis.window?.innerWidth || 0;
84
+ if (width.value && frameworkWidth) {
85
+ left = left + width.value > frameworkWidth ? frameworkWidth - width.value : left;
75
86
  }
76
87
 
77
88
  return {
@@ -79,11 +90,60 @@ const style = computed(() => {
79
90
  top: `${props.position.top}px`,
80
91
  width: width.value ? `${width.value}px` : 'auto',
81
92
  height: height.value ? `${height.value}px` : 'auto',
93
+ ...props.initialStyle,
82
94
  };
83
95
  });
84
96
 
85
97
  let moveable: VanillaMoveable | null = null;
86
98
 
99
+ // 拖拽/缩放时用于覆盖 iframe 的遮罩,防止鼠标进入 iframe 区域后事件被 iframe 吞掉导致拖拽丢失
100
+ let dragMask: HTMLDivElement | null = null;
101
+ let dragMaskVisible = false;
102
+
103
+ // 兜底:正常情况下遮罩由 moveable 的 dragEnd/resizeEnd 移除;但某些极端场景
104
+ // (拖拽中鼠标移出窗口后松开、alt-tab 切走窗口失焦、标签页被隐藏等)结束事件不会触发,
105
+ // 会导致全屏透明遮罩残留、整个编辑器无法点击。这里挂全局 pointerup/blur/visibilitychange 强制收尾。
106
+ const bindDragMaskSafety = () => {
107
+ globalThis.window?.addEventListener('pointerup', hideDragMask, true);
108
+ globalThis.window?.addEventListener('blur', hideDragMask);
109
+ globalThis.document?.addEventListener('visibilitychange', hideDragMask);
110
+ };
111
+
112
+ const unbindDragMaskSafety = () => {
113
+ globalThis.window?.removeEventListener('pointerup', hideDragMask, true);
114
+ globalThis.window?.removeEventListener('blur', hideDragMask);
115
+ globalThis.document?.removeEventListener('visibilitychange', hideDragMask);
116
+ };
117
+
118
+ const showDragMask = () => {
119
+ if (dragMaskVisible) {
120
+ return;
121
+ }
122
+ if (!dragMask) {
123
+ dragMask = globalThis.document.createElement('div');
124
+ dragMask.className = 'm-editor-float-box-drag-mask';
125
+ }
126
+ globalThis.document.body.appendChild(dragMask);
127
+ dragMaskVisible = true;
128
+ bindDragMaskSafety();
129
+
130
+ // 拖拽标题时,root 上的 @mousedown="nextZIndex" 会把浮窗 z-index 抬高,
131
+ // 若此时才读取会拿到旧值导致遮罩被浮窗(及其内部 iframe)盖住,故用 nextTick 在 z-index 稳定后再设置到浮窗之上
132
+ const setMaskZIndex = () => {
133
+ if (dragMask) {
134
+ dragMask.style.zIndex = `${curZIndex.value + 1}`;
135
+ }
136
+ };
137
+ setMaskZIndex();
138
+ nextTick(setMaskZIndex);
139
+ };
140
+
141
+ const hideDragMask = () => {
142
+ unbindDragMaskSafety();
143
+ dragMask?.parentNode?.removeChild(dragMask);
144
+ dragMaskVisible = false;
145
+ };
146
+
87
147
  const initMoveable = () => {
88
148
  moveable = new VanillaMoveable(globalThis.document.body, {
89
149
  className: 'm-editor-floating-box-moveable',
@@ -101,6 +161,14 @@ const initMoveable = () => {
101
161
  bounds: { left: 0, top: 0, right: 0, bottom: 0, position: 'css' },
102
162
  });
103
163
 
164
+ // 仅在真正发生拖拽/缩放位移时插入遮罩:moveable 的 dragStart/resizeStart 在 mousedown 时即触发,
165
+ // 若此时就盖遮罩,会盖住浮窗本身导致 mouseup 落在遮罩上、关闭按钮的 click 无法触发(点击关闭不了)。
166
+ // 改为在 drag/resize(实际位移)时才显示,纯点击不再触发遮罩。
167
+ moveable.on('drag', showDragMask);
168
+ moveable.on('resize', showDragMask);
169
+ moveable.on('dragEnd', hideDragMask);
170
+ moveable.on('resizeEnd', hideDragMask);
171
+
104
172
  moveable.on('drag', (e) => {
105
173
  e.target.style.transform = e.transform;
106
174
  });
@@ -115,6 +183,7 @@ const initMoveable = () => {
115
183
  };
116
184
 
117
185
  const destroyMoveable = () => {
186
+ hideDragMask();
118
187
  moveable?.destroy();
119
188
  moveable = null;
120
189
  };
@@ -87,70 +87,3 @@ const scrollBy = (delta: number) => {
87
87
  }
88
88
  };
89
89
  </script>
90
-
91
- <style lang="scss">
92
- .m-editor-scroll-bar {
93
- position: absolute;
94
- background-color: transparent;
95
- opacity: 0.3;
96
- transition:
97
- background-color 0.2s linear,
98
- opacity 0.2s linear;
99
-
100
- .m-editor-scroll-bar-thumb {
101
- background-color: #aaa;
102
- border-radius: 6px;
103
- position: absolute;
104
- }
105
-
106
- &.horizontal {
107
- width: 100%;
108
- height: 15px;
109
- bottom: 0;
110
-
111
- .m-editor-scroll-bar-thumb {
112
- height: 6px;
113
- transition:
114
- background-color 0.2s linear,
115
- height 0.2s ease-in-out;
116
- bottom: 2px;
117
- }
118
- }
119
-
120
- &.vertical {
121
- height: 100%;
122
- width: 15px;
123
- right: 5px;
124
-
125
- .m-editor-scroll-bar-thumb {
126
- width: 6px;
127
- transition:
128
- background-color 0.2s linear,
129
- width 0.2s ease-in-out;
130
- right: 2px;
131
- }
132
- }
133
-
134
- &:hover,
135
- &:focus {
136
- background-color: #eee;
137
- opacity: 0.9;
138
-
139
- .m-editor-scroll-bar-thumb {
140
- background-color: #999;
141
- }
142
-
143
- &.horizontal {
144
- .m-editor-scroll-bar-thumb {
145
- height: 11px;
146
- }
147
- }
148
-
149
- &.vertical {
150
- .m-editor-scroll-bar-thumb {
151
- width: 11px;
152
- }
153
- }
154
- }
155
- }
156
- </style>
@@ -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>
@@ -1,3 +1,5 @@
1
+ import type { InjectionKey } from 'vue';
2
+
1
3
  import type { DataSourceSchema, EventOption, Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
2
4
  import type { FormConfig, FormState } from '@tmagic/form';
3
5
  import StageCore, {
@@ -26,6 +28,12 @@ import type {
26
28
  TreeNodeData,
27
29
  } from './type';
28
30
 
31
+ /**
32
+ * 「属性配置表单校验」联动能力的 provide/inject 注入键。
33
+ * 使用 Symbol 避免与其它字符串键冲突,供 PropsPanel / FormPanel 注入判断校验失败时是否仍更新节点并记录错误。
34
+ */
35
+ export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<boolean> = Symbol('enablePropsFormValidate');
36
+
29
37
  export interface EditorProps {
30
38
  /** 页面初始值 */
31
39
  modelValue?: MApp;
@@ -91,6 +99,11 @@ export interface EditorProps {
91
99
  disabledFlashTip?: boolean;
92
100
  /** 禁用双击在浮层中单独编辑选中组件 */
93
101
  disabledStageOverlay?: boolean;
102
+ /**
103
+ * 是否启用「属性配置表单校验」联动能力:开启后属性/样式表单校验失败时仍更新节点,
104
+ * 并把错误信息集中记录到 editorService,用于组件树标红提示与保存拦截;默认 false(关闭)。
105
+ */
106
+ enablePropsFormValidate?: boolean;
94
107
  /** 禁用属性配置面板右下角显示源码的按钮 */
95
108
  disabledShowSrc?: boolean;
96
109
  /** 禁用数据源 */
@@ -93,7 +93,8 @@ const codeConfig = computed<GroupListConfig>(() => ({
93
93
  { value: HookCodeType.CODE, text: '代码块' },
94
94
  { value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
95
95
  ],
96
- defaultValue: 'code',
96
+ rules: [{ typeMatch: true, trigger: 'change' }],
97
+ defaultValue: HookCodeType.CODE,
97
98
  onChange: (_mForm, v: HookCodeType, { setModel }) => {
98
99
  if (v === HookCodeType.DATA_SOURCE_METHOD) {
99
100
  setModel('codeId', []);
@@ -111,6 +112,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
111
112
  labelWidth: 0,
112
113
  display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
113
114
  notEditable: () => !codeBlockService.getEditStatus(),
115
+ rules: [{ typeMatch: true, trigger: 'change' }],
114
116
  },
115
117
  {
116
118
  type: 'data-source-method-select',
@@ -119,6 +121,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
119
121
  labelWidth: 0,
120
122
  display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
121
123
  notEditable: () => !dataSourceService.get('editable'),
124
+ rules: [{ typeMatch: true, trigger: 'change' }],
122
125
  },
123
126
  ],
124
127
  },