@tmagic/editor 1.8.0-beta.10 → 1.8.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +7 -4
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +60 -14
- package/dist/es/components/ScrollBar.js +0 -1
- package/dist/es/editorProps.js +6 -1
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +15 -3
- package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +2 -17
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +7 -0
- package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +5 -17
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +29 -3
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +50 -88
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -5
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +48 -23
- package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +28 -22
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +28 -22
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +28 -22
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/UISelect.js +0 -1
- package/dist/es/index.js +9 -6
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +21 -6
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +9 -6
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +14 -4
- package/dist/es/layouts/history-list/composables.js +1 -1
- package/dist/es/layouts/history-list/useHistoryRevert.js +17 -6
- package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +44 -5
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +15 -8
- package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +15 -31
- package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/sidebar/code-block/useContentMenu.js +18 -3
- package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/sidebar/data-source/useContentMenu.js +18 -3
- package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
- package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/es/plugin.js +3 -1
- package/dist/es/services/editor.js +104 -5
- package/dist/es/services/props.js +2 -1
- package/dist/es/services/storage.js +1 -1
- package/dist/es/services/ui.js +3 -0
- package/dist/es/style.css +104 -55
- package/dist/es/utils/data-source/index.js +30 -11
- package/dist/es/utils/event.js +123 -0
- package/dist/es/utils/monaco-editor.js +22 -6
- package/dist/es/utils/props.js +94 -8
- package/dist/es/utils/type-match-rules.js +360 -0
- package/dist/style.css +104 -55
- package/dist/tmagic-editor.umd.cjs +16923 -15771
- package/package.json +8 -8
- package/src/Editor.vue +3 -1
- package/src/components/CodeBlockEditor.vue +3 -0
- package/src/components/CompareForm.vue +7 -0
- package/src/components/FloatingBox.vue +77 -8
- package/src/components/ScrollBar.vue +0 -67
- package/src/editorProps.ts +13 -0
- package/src/fields/CodeSelect.vue +4 -1
- package/src/fields/CondOpSelect.vue +2 -24
- package/src/fields/DataSourceFields.vue +5 -0
- package/src/fields/DataSourceInput.vue +7 -25
- package/src/fields/DataSourceMocks.vue +3 -0
- package/src/fields/DisplayConds.vue +14 -0
- package/src/fields/EventSelect.vue +84 -129
- package/src/fields/StyleSetter/Index.vue +2 -8
- package/src/fields/StyleSetter/pro/Background.vue +94 -55
- package/src/fields/StyleSetter/pro/Border.vue +9 -11
- package/src/fields/StyleSetter/pro/Font.vue +66 -64
- package/src/fields/StyleSetter/pro/Layout.vue +140 -138
- package/src/fields/StyleSetter/pro/Position.vue +69 -67
- package/src/fields/StyleSetter/pro/Transform.vue +23 -25
- package/src/fields/UISelect.vue +0 -12
- package/src/index.ts +1 -0
- package/src/layouts/CodeEditor.vue +32 -6
- package/src/layouts/Framework.vue +7 -6
- package/src/layouts/history-list/HistoryDiffDialog.vue +23 -5
- package/src/layouts/history-list/useHistoryRevert.ts +25 -5
- package/src/layouts/props-panel/FormPanel.vue +66 -5
- package/src/layouts/props-panel/PropsPanel.vue +20 -6
- package/src/layouts/props-panel/use-style-panel.ts +4 -3
- package/src/layouts/sidebar/Sidebar.vue +2 -0
- package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +4 -1
- package/src/layouts/sidebar/code-block/useContentMenu.ts +13 -1
- package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +2 -2
- package/src/layouts/sidebar/data-source/useContentMenu.ts +13 -1
- package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
- package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
- package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
- package/src/plugin.ts +3 -1
- package/src/services/editor.ts +168 -3
- package/src/services/ui.ts +6 -0
- package/src/theme/event.scss +8 -0
- package/src/theme/floating-box.scss +9 -1
- package/src/theme/layer-node-content.scss +14 -0
- package/src/theme/scroll-bar.scss +64 -0
- package/src/theme/theme.scss +3 -0
- package/src/theme/ui-select.scss +9 -0
- package/src/type.ts +72 -2
- package/src/utils/data-source/index.ts +30 -12
- package/src/utils/event.ts +224 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/monaco-editor.ts +27 -0
- package/src/utils/props.ts +160 -2
- package/src/utils/type-match-rules.ts +678 -0
- package/types/index.d.ts +391 -85
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
right-class="m-editor-framework-right"
|
|
26
26
|
:left="hideSidebar ? undefined : columnWidth.left"
|
|
27
27
|
:right="columnWidth.right"
|
|
28
|
-
:min-left="hideSidebar ? 0 :
|
|
29
|
-
:min-right="
|
|
30
|
-
:min-center="
|
|
28
|
+
:min-left="hideSidebar ? 0 : minLeftColumnWidth"
|
|
29
|
+
:min-right="minRightColumnWidth"
|
|
30
|
+
:min-center="minCenterColumnWidth"
|
|
31
31
|
:width="frameworkRect.width"
|
|
32
32
|
@change="columnWidthChange"
|
|
33
33
|
>
|
|
@@ -78,9 +78,6 @@ import { getEditorConfig } from '@editor/utils/config';
|
|
|
78
78
|
import {
|
|
79
79
|
DEFAULT_LEFT_COLUMN_WIDTH,
|
|
80
80
|
LEFT_COLUMN_WIDTH_STORAGE_KEY,
|
|
81
|
-
MIN_CENTER_COLUMN_WIDTH,
|
|
82
|
-
MIN_LEFT_COLUMN_WIDTH,
|
|
83
|
-
MIN_RIGHT_COLUMN_WIDTH,
|
|
84
81
|
RIGHT_COLUMN_WIDTH_STORAGE_KEY,
|
|
85
82
|
} from '@editor/utils/const';
|
|
86
83
|
|
|
@@ -116,6 +113,10 @@ const showSrc = computed(() => uiService.get('showSrc'));
|
|
|
116
113
|
|
|
117
114
|
const columnWidth = computed(() => uiService.get('columnWidth'));
|
|
118
115
|
|
|
116
|
+
const minLeftColumnWidth = computed(() => uiService.get('minLeftColumnWidth'));
|
|
117
|
+
const minCenterColumnWidth = computed(() => uiService.get('minCenterColumnWidth'));
|
|
118
|
+
const minRightColumnWidth = computed(() => uiService.get('minRightColumnWidth'));
|
|
119
|
+
|
|
119
120
|
watch(pageLength, () => {
|
|
120
121
|
splitViewRef.value?.updateWidth();
|
|
121
122
|
});
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
:load-config="loadConfig"
|
|
49
49
|
:self-diff-field-types="selfDiffFieldTypes"
|
|
50
50
|
:services="props.services"
|
|
51
|
+
:size="props.size"
|
|
51
52
|
height="70vh"
|
|
52
53
|
/>
|
|
53
54
|
|
|
@@ -77,7 +78,14 @@
|
|
|
77
78
|
import { computed, ref, watch } from 'vue';
|
|
78
79
|
import { isEqual } from 'lodash-es';
|
|
79
80
|
|
|
80
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
type FieldSize,
|
|
83
|
+
TMagicButton,
|
|
84
|
+
TMagicDialog,
|
|
85
|
+
TMagicRadioButton,
|
|
86
|
+
TMagicRadioGroup,
|
|
87
|
+
TMagicTag,
|
|
88
|
+
} from '@tmagic/design';
|
|
81
89
|
import type { FormState } from '@tmagic/form';
|
|
82
90
|
|
|
83
91
|
import CompareForm from '@editor/components/CompareForm.vue';
|
|
@@ -104,6 +112,8 @@ const props = withDefaults(
|
|
|
104
112
|
*/
|
|
105
113
|
loadConfig?: CompareFormLoadConfig;
|
|
106
114
|
width?: string;
|
|
115
|
+
/** 差异对比表单内组件的尺寸(透传给 CompareForm 的 `size`),可选 'large' | 'default' | 'small'。 */
|
|
116
|
+
size?: FieldSize;
|
|
107
117
|
isConfirm?: boolean;
|
|
108
118
|
onConfirm?: () => void;
|
|
109
119
|
selfDiffFieldTypes?: string[];
|
|
@@ -162,11 +172,19 @@ const hasCurrent = computed(() => payload.value?.currentValue !== undefined && p
|
|
|
162
172
|
/** 是否存在该步「修改后的值」:不存在(如仅删除)时「与修改前对比」无意义,置灰禁用。 */
|
|
163
173
|
const hasValue = computed(() => payload.value?.value !== undefined && payload.value?.value !== null);
|
|
164
174
|
|
|
175
|
+
/** 指定模式当前是否可用:before 依赖「修改后的值」,current 依赖「当前值」。 */
|
|
176
|
+
const isModeAvailable = (m: DiffMode): boolean => (m === 'current' ? hasCurrent.value : hasValue.value);
|
|
177
|
+
|
|
165
178
|
/**
|
|
166
179
|
* 计算 open 时的初始对比模式:
|
|
167
|
-
*
|
|
180
|
+
* - 调用方通过 payload.mode 指定且该模式可用时,优先使用指定模式;
|
|
181
|
+
* - 否则没有「修改后的值」但有当前值时(「与修改前对比」不可用),默认进入「与当前对比」;
|
|
182
|
+
* - 其余情况默认「与修改前对比」。
|
|
168
183
|
*/
|
|
169
|
-
const resolveInitialMode = (): DiffMode =>
|
|
184
|
+
const resolveInitialMode = (specified?: DiffMode): DiffMode => {
|
|
185
|
+
if (specified && isModeAvailable(specified)) return specified;
|
|
186
|
+
return !hasValue.value && hasCurrent.value ? 'current' : 'before';
|
|
187
|
+
};
|
|
170
188
|
|
|
171
189
|
/** 左侧(旧/参照)值 */
|
|
172
190
|
const leftValue = computed<Record<string, any>>(() => {
|
|
@@ -222,8 +240,8 @@ const targetText = computed(() => {
|
|
|
222
240
|
const open = (p: DiffDialogPayload) => {
|
|
223
241
|
payload.value = p;
|
|
224
242
|
// 每次打开按 payload 重置对比模式(hasValue / hasCurrent 依赖 payload,需先赋值):
|
|
225
|
-
//
|
|
226
|
-
mode.value = resolveInitialMode();
|
|
243
|
+
// 优先使用 payload.mode 指定的模式;不可用或未指定时按数据自动推断。
|
|
244
|
+
mode.value = resolveInitialMode(p.mode);
|
|
227
245
|
// 默认回到表单对比形态,避免残留上一次选择的源码模式
|
|
228
246
|
viewMode.value = 'form';
|
|
229
247
|
visible.value = true;
|
|
@@ -94,6 +94,8 @@ const mountHistoryDiffDialog = async (
|
|
|
94
94
|
loadConfig: options.loadConfig,
|
|
95
95
|
selfDiffFieldTypes: options.selfDiffFieldTypes,
|
|
96
96
|
compareFormState: options.compareFormState,
|
|
97
|
+
width: options.width,
|
|
98
|
+
size: options.size ?? options.services?.uiService?.get('propsPanelSize'),
|
|
97
99
|
onClose: options.onClose,
|
|
98
100
|
});
|
|
99
101
|
if (options.appContext) {
|
|
@@ -192,7 +194,7 @@ const viewHistoryDiffDialog = async (
|
|
|
192
194
|
export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
|
|
193
195
|
// 自动捕获调用方所在组件的 appContext(在 setup 中调用时),业务方亦可显式覆盖。
|
|
194
196
|
const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
|
|
195
|
-
const { extendState, getPropsPanelFormState } = options;
|
|
197
|
+
const { extendState, getPropsPanelFormState, dialogWidth } = options;
|
|
196
198
|
|
|
197
199
|
/** 目标数据已被删除、无法回滚时的统一提示。 */
|
|
198
200
|
const showRevertTargetMissing = () => {
|
|
@@ -216,7 +218,14 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
|
|
216
218
|
*/
|
|
217
219
|
const runRevert = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<boolean> => {
|
|
218
220
|
if (payload) {
|
|
219
|
-
return confirmRevertWithDiffDialog(payload, {
|
|
221
|
+
return confirmRevertWithDiffDialog(payload, {
|
|
222
|
+
appContext,
|
|
223
|
+
extendState,
|
|
224
|
+
services,
|
|
225
|
+
...extra,
|
|
226
|
+
width: extra?.width ?? dialogWidth,
|
|
227
|
+
size: extra?.size,
|
|
228
|
+
});
|
|
220
229
|
}
|
|
221
230
|
return confirmRevert();
|
|
222
231
|
};
|
|
@@ -334,6 +343,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
|
|
334
343
|
appContext,
|
|
335
344
|
extendState,
|
|
336
345
|
services,
|
|
346
|
+
width: dialogWidth,
|
|
337
347
|
compareFormState: getPropsPanelFormState?.(),
|
|
338
348
|
});
|
|
339
349
|
}
|
|
@@ -341,12 +351,12 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
|
|
341
351
|
|
|
342
352
|
const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
|
|
343
353
|
const payload = buildDataSourceDiffPayload(id, index);
|
|
344
|
-
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
|
|
354
|
+
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
|
|
345
355
|
};
|
|
346
356
|
|
|
347
357
|
const onCodeBlockDiff = (id: string | number, index: number): Promise<void> | void => {
|
|
348
358
|
const payload = buildCodeBlockDiffPayload(id, index);
|
|
349
|
-
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
|
|
359
|
+
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
|
|
350
360
|
};
|
|
351
361
|
|
|
352
362
|
/**
|
|
@@ -373,6 +383,8 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
|
|
373
383
|
const confirmed = await runRevert(revertOptions.diffPayload ?? null, {
|
|
374
384
|
loadConfig: revertOptions.loadConfig,
|
|
375
385
|
selfDiffFieldTypes: revertOptions.selfDiffFieldTypes,
|
|
386
|
+
width: revertOptions.width,
|
|
387
|
+
size: revertOptions.size,
|
|
376
388
|
});
|
|
377
389
|
if (!confirmed) return null;
|
|
378
390
|
return await revertOptions.revert();
|
|
@@ -383,7 +395,15 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
|
|
383
395
|
* 可透传 `loadConfig` / `selfDiffFieldTypes`。payload 为 null(不可对比)时静默返回。
|
|
384
396
|
*/
|
|
385
397
|
const viewDiff = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<void> | void => {
|
|
386
|
-
if (payload)
|
|
398
|
+
if (payload)
|
|
399
|
+
return viewHistoryDiffDialog(payload, {
|
|
400
|
+
appContext,
|
|
401
|
+
extendState,
|
|
402
|
+
services,
|
|
403
|
+
...extra,
|
|
404
|
+
width: extra?.width ?? dialogWidth,
|
|
405
|
+
size: extra?.size,
|
|
406
|
+
});
|
|
387
407
|
};
|
|
388
408
|
|
|
389
409
|
return {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
:size="propsPanelSize"
|
|
13
13
|
:init-values="values"
|
|
14
14
|
:config="config"
|
|
15
|
+
:type-match-valid="true"
|
|
15
16
|
:extend-state="extendState"
|
|
16
17
|
@change="submit"
|
|
17
18
|
@error="errorHandler"
|
|
@@ -46,13 +47,16 @@
|
|
|
46
47
|
import { computed, getCurrentInstance, inject, onMounted, onUnmounted, ref, useTemplateRef, watchEffect } from 'vue';
|
|
47
48
|
import { Document as DocumentIcon } from '@element-plus/icons-vue';
|
|
48
49
|
|
|
49
|
-
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
|
|
50
|
+
import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
|
|
50
51
|
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
|
|
51
52
|
import { MForm } from '@tmagic/form';
|
|
53
|
+
import { filterXSS } from '@tmagic/utils';
|
|
52
54
|
|
|
53
55
|
import MIcon from '@editor/components/Icon.vue';
|
|
56
|
+
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
|
54
57
|
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
|
|
55
58
|
import { useServices } from '@editor/hooks/use-services';
|
|
59
|
+
import { validatePropsForm } from '@editor/utils/props';
|
|
56
60
|
|
|
57
61
|
import CodeEditor from '../CodeEditor.vue';
|
|
58
62
|
|
|
@@ -75,13 +79,15 @@ const props = defineProps<{
|
|
|
75
79
|
}>();
|
|
76
80
|
|
|
77
81
|
const emit = defineEmits<{
|
|
78
|
-
submit: [values: any, eventData?: ContainerChangeEventData];
|
|
82
|
+
submit: [values: any, eventData?: ContainerChangeEventData, error?: any];
|
|
79
83
|
'submit-error': [e: any];
|
|
80
84
|
'form-error': [e: any];
|
|
81
85
|
mounted: [internalInstance: any];
|
|
82
86
|
unmounted: [];
|
|
83
87
|
}>();
|
|
84
88
|
|
|
89
|
+
const enablePropsFormValidate = inject(ENABLE_PROPS_FORM_VALIDATE, false);
|
|
90
|
+
|
|
85
91
|
const services = useServices();
|
|
86
92
|
const { editorService, uiService } = services;
|
|
87
93
|
|
|
@@ -113,9 +119,16 @@ onUnmounted(() => {
|
|
|
113
119
|
const submit = async (v: FormValue, eventData: ContainerChangeEventData) => {
|
|
114
120
|
try {
|
|
115
121
|
const values = await configFormRef.value?.submitForm();
|
|
122
|
+
// 校验成功:正常更新节点(第三个参数 error 为空,表示清除该来源的错误记录)
|
|
116
123
|
emit('submit', values, eventData);
|
|
117
124
|
} catch (e: any) {
|
|
118
|
-
|
|
125
|
+
if (enablePropsFormValidate) {
|
|
126
|
+
// 启用校验联动:校验失败时仍以当前表单值更新节点,并把错误信息一并抛给上层记录
|
|
127
|
+
emit('submit', v, eventData, e);
|
|
128
|
+
} else {
|
|
129
|
+
// 未启用:保持原行为,校验失败丢弃本次改动
|
|
130
|
+
emit('submit-error', e);
|
|
131
|
+
}
|
|
119
132
|
}
|
|
120
133
|
};
|
|
121
134
|
|
|
@@ -123,8 +136,56 @@ const errorHandler = (e: any) => {
|
|
|
123
136
|
emit('form-error', e);
|
|
124
137
|
};
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
/**
|
|
140
|
+
* 将校验错误文案安全地转为用于弹窗展示的 HTML。
|
|
141
|
+
*
|
|
142
|
+
* 文案形如 `字段 -> 主文案\n\n建议`,多条以结构性 `<br>` 拼接。为在保留换行排版的同时
|
|
143
|
+
* 避免 `${value}`/字段名中可能夹带的 HTML 造成 XSS:先按结构性 `<br>` 拆分,对每段做 HTML
|
|
144
|
+
* 转义后再把段内换行 `\n` 替换为 `<br>`,最后用 `<br>` 拼回。
|
|
145
|
+
*/
|
|
146
|
+
const formatValidateErrorHtml = (error: string): string =>
|
|
147
|
+
error
|
|
148
|
+
.split(/<br\s*\/?>/i)
|
|
149
|
+
.map((segment) => filterXSS(segment).replaceAll('\n', '<br>'))
|
|
150
|
+
.join('<br>');
|
|
151
|
+
|
|
152
|
+
const saveCode = async (values: any) => {
|
|
153
|
+
const newValues = props.codeValueKey ? { [props.codeValueKey]: values } : values;
|
|
154
|
+
|
|
155
|
+
if (!enablePropsFormValidate) {
|
|
156
|
+
// 未启用校验联动:保持原行为,直接提交源码保存的值
|
|
157
|
+
emit('submit', newValues);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 启用校验联动:源码编辑器保存的值未经过表单交互,这里另建一个独立的 MForm 实例对最新配置
|
|
162
|
+
// 做一次静默校验(不复用、也不污染页面上正在展示的表单),并将校验结果(错误信息)随提交
|
|
163
|
+
// 一并抛给上层记录,使源码保存的错误状态与表单编辑保持一致。
|
|
164
|
+
try {
|
|
165
|
+
const error = await validatePropsForm({
|
|
166
|
+
config: props.config,
|
|
167
|
+
values: newValues,
|
|
168
|
+
appContext: internalInstance?.appContext ?? null,
|
|
169
|
+
services,
|
|
170
|
+
stage: stage.value,
|
|
171
|
+
typeMatchValid: true,
|
|
172
|
+
extendState: props.extendState,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (error) {
|
|
176
|
+
tMagicMessage({
|
|
177
|
+
type: 'error',
|
|
178
|
+
message: formatValidateErrorHtml(error),
|
|
179
|
+
dangerouslyUseHTMLString: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
emit('submit', newValues, undefined, error ? new Error(error) : undefined);
|
|
184
|
+
} catch (e: any) {
|
|
185
|
+
console.log('validateForm error', e);
|
|
186
|
+
// 静默校验本身出现异常(如初始化超时)时,退回到不携带错误信息的提交,避免阻塞源码保存
|
|
187
|
+
emit('submit', newValues);
|
|
188
|
+
}
|
|
128
189
|
};
|
|
129
190
|
|
|
130
191
|
defineExpose({ configForm: configFormRef, submit });
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
:values="values"
|
|
10
10
|
:disabledShowSrc="disabledShowSrc"
|
|
11
11
|
:extendState="extendState"
|
|
12
|
-
@submit="submit"
|
|
12
|
+
@submit="(v, eventData, error) => submit(v, eventData, error, 'props')"
|
|
13
13
|
@submit-error="errorHandler"
|
|
14
14
|
@form-error="errorHandler"
|
|
15
15
|
@mounted="mountedHandler"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
:values="values"
|
|
28
28
|
:disabledShowSrc="disabledShowSrc"
|
|
29
29
|
:extendState="extendState"
|
|
30
|
-
@submit="submit"
|
|
30
|
+
@submit="(v, eventData, error) => submit(v, eventData, error, 'style')"
|
|
31
31
|
@submit-error="errorHandler"
|
|
32
32
|
@form-error="errorHandler"
|
|
33
33
|
>
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
</template>
|
|
56
56
|
|
|
57
57
|
<script lang="ts" setup>
|
|
58
|
-
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, watchEffect } from 'vue';
|
|
58
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, watchEffect } from 'vue';
|
|
59
59
|
import { Close, Sugar } from '@element-plus/icons-vue';
|
|
60
60
|
import type { OnDrag } from 'gesto';
|
|
61
61
|
|
|
@@ -66,9 +66,10 @@ import { setValueByKeyPath } from '@tmagic/utils';
|
|
|
66
66
|
|
|
67
67
|
import MIcon from '@editor/components/Icon.vue';
|
|
68
68
|
import Resizer from '@editor/components/Resizer.vue';
|
|
69
|
+
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
|
69
70
|
import { useServices } from '@editor/hooks/use-services';
|
|
70
71
|
import { Protocol } from '@editor/services/storage';
|
|
71
|
-
import type { PropsPanelSlots } from '@editor/type';
|
|
72
|
+
import type { NodeInvalidSource, PropsPanelSlots } from '@editor/type';
|
|
72
73
|
import { styleTabConfig } from '@editor/utils';
|
|
73
74
|
import { PROPS_PANEL_WIDTH_STORAGE_KEY } from '@editor/utils/const';
|
|
74
75
|
|
|
@@ -95,6 +96,8 @@ const emit = defineEmits<{
|
|
|
95
96
|
|
|
96
97
|
const { editorService, uiService, propsService, storageService } = useServices();
|
|
97
98
|
|
|
99
|
+
const enablePropsFormValidate = inject(ENABLE_PROPS_FORM_VALIDATE, false);
|
|
100
|
+
|
|
98
101
|
const values = ref<FormValue>({});
|
|
99
102
|
// ts类型应该是FormConfig, 但是打包时会出错,所以暂时用any
|
|
100
103
|
const curFormConfig = ref<any>([]);
|
|
@@ -126,7 +129,12 @@ onBeforeUnmount(() => {
|
|
|
126
129
|
propsService.off('props-configs-change', init);
|
|
127
130
|
});
|
|
128
131
|
|
|
129
|
-
const submit = async (
|
|
132
|
+
const submit = async (
|
|
133
|
+
v: MNode,
|
|
134
|
+
eventData?: ContainerChangeEventData,
|
|
135
|
+
error?: any,
|
|
136
|
+
source: NodeInvalidSource = 'props',
|
|
137
|
+
) => {
|
|
130
138
|
try {
|
|
131
139
|
if (!v.id) {
|
|
132
140
|
v.id = values.value.id;
|
|
@@ -155,7 +163,13 @@ const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
|
|
|
155
163
|
// 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
|
|
156
164
|
const historySource = eventData ? 'props' : 'code';
|
|
157
165
|
|
|
158
|
-
editorService.update(newValue, {
|
|
166
|
+
editorService.update(newValue, {
|
|
167
|
+
changeRecords: eventData?.changeRecords,
|
|
168
|
+
historySource,
|
|
169
|
+
// 启用校验联动时,仅校验失败(error 存在)才把错误信息随更新传入 editorService 记录;
|
|
170
|
+
// 其余情况(含表单校验成功、CodeEditor 源码保存)不携带 invalidInfo,由 editorService 在执行 update 时统一清除该节点错误。
|
|
171
|
+
...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}),
|
|
172
|
+
});
|
|
159
173
|
} catch (e: any) {
|
|
160
174
|
emit('submit-error', e);
|
|
161
175
|
}
|
|
@@ -2,7 +2,7 @@ import { computed, type Ref, watch } from 'vue';
|
|
|
2
2
|
|
|
3
3
|
import { Protocol } from '@editor/services/storage';
|
|
4
4
|
import { Services } from '@editor/type';
|
|
5
|
-
import {
|
|
5
|
+
import { RIGHT_COLUMN_WIDTH_STORAGE_KEY } from '@editor/utils/const';
|
|
6
6
|
|
|
7
7
|
export const useStylePanel = (
|
|
8
8
|
{ uiService, storageService }: Pick<Services, 'uiService' | 'storageService'>,
|
|
@@ -47,8 +47,9 @@ export const useStylePanel = (
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (columnWidth.center < 0) {
|
|
50
|
-
|
|
51
|
-
columnWidth.
|
|
50
|
+
const minCenterColumnWidth = uiService.get('minCenterColumnWidth');
|
|
51
|
+
columnWidth.right = columnWidth.right + columnWidth.center - minCenterColumnWidth;
|
|
52
|
+
columnWidth.center = minCenterColumnWidth;
|
|
52
53
|
|
|
53
54
|
propsPanelWidth.value = columnWidth.right / 2;
|
|
54
55
|
}
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
v-model:height="columnLeftHeight"
|
|
133
133
|
:width="columnLeftWidth"
|
|
134
134
|
:title="config.text"
|
|
135
|
+
:framework-width="frameworkWidth"
|
|
135
136
|
:position="{
|
|
136
137
|
left: floatBoxStates[config.$key].left,
|
|
137
138
|
top: floatBoxStates[config.$key].top,
|
|
@@ -221,6 +222,7 @@ const taskLength = computed(() => depService.get('taskLength'));
|
|
|
221
222
|
const tipsBarVisible = ref(true);
|
|
222
223
|
|
|
223
224
|
const columnLeftWidth = computed(() => uiService.get('columnWidth')[ColumnLayout.LEFT]);
|
|
225
|
+
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
|
|
224
226
|
const { height: editorContentHeight } = useEditorContentHeight();
|
|
225
227
|
const columnLeftHeight = ref(0);
|
|
226
228
|
|
|
@@ -121,8 +121,11 @@ const {
|
|
|
121
121
|
nodeContentMenuHandler,
|
|
122
122
|
menuData: contentMenuData,
|
|
123
123
|
contentMenuHideHandler,
|
|
124
|
+
getTarget,
|
|
124
125
|
} = useContentMenu((id: string) => {
|
|
125
126
|
codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
|
|
126
127
|
});
|
|
127
|
-
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
128
|
+
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
129
|
+
props.customContentMenu(contentMenuData, 'code-block', getTarget),
|
|
130
|
+
);
|
|
128
131
|
</script>
|
|
@@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
|
|
|
3
3
|
import { cloneDeep } from 'lodash-es';
|
|
4
4
|
|
|
5
5
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
|
6
|
-
import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
|
6
|
+
import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
|
7
7
|
|
|
8
8
|
export const useContentMenu = (deleteCode: (id: string) => void) => {
|
|
9
9
|
const eventBus = inject<EventBus>('eventBus');
|
|
10
10
|
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
|
|
11
11
|
|
|
12
12
|
let selectId = '';
|
|
13
|
+
let selectData: TreeNodeData | null = null;
|
|
14
|
+
|
|
15
|
+
const getTarget = (): ContentMenuTarget | null => {
|
|
16
|
+
if (!selectId) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return { id: selectId, data: selectData ?? undefined };
|
|
20
|
+
};
|
|
13
21
|
|
|
14
22
|
const menuData: (MenuButton | MenuComponent)[] = [
|
|
15
23
|
{
|
|
@@ -65,19 +73,23 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
|
|
|
65
73
|
menuRef.value?.show(event);
|
|
66
74
|
if (data.id) {
|
|
67
75
|
selectId = `${data.id}`;
|
|
76
|
+
selectData = data;
|
|
68
77
|
} else {
|
|
69
78
|
selectId = '';
|
|
79
|
+
selectData = null;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
};
|
|
73
83
|
|
|
74
84
|
const contentMenuHideHandler = () => {
|
|
75
85
|
selectId = '';
|
|
86
|
+
selectData = null;
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
return {
|
|
79
90
|
menuData,
|
|
80
91
|
nodeContentMenuHandler,
|
|
81
92
|
contentMenuHideHandler,
|
|
93
|
+
getTarget,
|
|
82
94
|
};
|
|
83
95
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<FloatingBox
|
|
3
|
+
:body-style="{ padding: '0 16px' }"
|
|
3
4
|
v-model:visible="boxVisible"
|
|
4
5
|
v-model:width="width"
|
|
5
6
|
v-model:height="editorHeight"
|
|
6
7
|
:title="title"
|
|
8
|
+
:framework-width="frameworkWidth"
|
|
7
9
|
:position="boxPosition"
|
|
8
10
|
>
|
|
9
11
|
<template #body>
|
|
@@ -65,6 +67,7 @@ const { height: editorHeight } = useEditorContentHeight();
|
|
|
65
67
|
|
|
66
68
|
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
|
|
67
69
|
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
|
|
70
|
+
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
|
|
68
71
|
|
|
69
72
|
/** 供「方法定义」tab 内的字段消费,用于打开数据源详情后自动打开指定方法 */
|
|
70
73
|
provide(
|
|
@@ -171,8 +171,8 @@ eventBus?.on('remove-data-source', (id: string) => {
|
|
|
171
171
|
removeHandler(id);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler } = useContentMenu();
|
|
174
|
+
const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler, getTarget } = useContentMenu();
|
|
175
175
|
const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
176
|
-
props.customContentMenu(contentMenuData, 'data-source'),
|
|
176
|
+
props.customContentMenu(contentMenuData, 'data-source', getTarget),
|
|
177
177
|
);
|
|
178
178
|
</script>
|
|
@@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue';
|
|
|
3
3
|
import { cloneDeep } from 'lodash-es';
|
|
4
4
|
|
|
5
5
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
|
6
|
-
import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
|
6
|
+
import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
|
|
7
7
|
|
|
8
8
|
export const useContentMenu = () => {
|
|
9
9
|
const eventBus = inject<EventBus>('eventBus');
|
|
10
10
|
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('menu');
|
|
11
11
|
|
|
12
12
|
let selectId = '';
|
|
13
|
+
let selectData: TreeNodeData | null = null;
|
|
14
|
+
|
|
15
|
+
const getTarget = (): ContentMenuTarget | null => {
|
|
16
|
+
if (!selectId) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return { id: selectId, data: selectData ?? undefined };
|
|
20
|
+
};
|
|
13
21
|
|
|
14
22
|
const menuData: (MenuButton | MenuComponent)[] = [
|
|
15
23
|
{
|
|
@@ -63,19 +71,23 @@ export const useContentMenu = () => {
|
|
|
63
71
|
menuRef.value?.show(event);
|
|
64
72
|
if (data.id) {
|
|
65
73
|
selectId = `${data.id}`;
|
|
74
|
+
selectData = data;
|
|
66
75
|
} else {
|
|
67
76
|
selectId = '';
|
|
77
|
+
selectData = null;
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
};
|
|
71
81
|
|
|
72
82
|
const contentMenuHideHandler = () => {
|
|
73
83
|
selectId = '';
|
|
84
|
+
selectData = null;
|
|
74
85
|
};
|
|
75
86
|
|
|
76
87
|
return {
|
|
77
88
|
menuData,
|
|
78
89
|
nodeContentMenuHandler,
|
|
79
90
|
contentMenuHideHandler,
|
|
91
|
+
getTarget,
|
|
80
92
|
};
|
|
81
93
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="m-editor-layer-node-content" :class="{ 'is-invalid': hasError }">
|
|
3
|
+
<span class="m-editor-layer-node-label">{{ label }}</span>
|
|
4
|
+
<TMagicTooltip v-if="hasError" placement="top">
|
|
5
|
+
<template #content>
|
|
6
|
+
<span v-html="errorMessage"></span>
|
|
7
|
+
</template>
|
|
8
|
+
<MIcon class="m-editor-layer-node-error-icon" :icon="WarningFilled"></MIcon>
|
|
9
|
+
</TMagicTooltip>
|
|
10
|
+
</span>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { computed } from 'vue';
|
|
15
|
+
import { WarningFilled } from '@element-plus/icons-vue';
|
|
16
|
+
|
|
17
|
+
import { stripValidateSuggestion, TMagicTooltip } from '@tmagic/design';
|
|
18
|
+
|
|
19
|
+
import MIcon from '@editor/components/Icon.vue';
|
|
20
|
+
import { useServices } from '@editor/hooks/use-services';
|
|
21
|
+
import type { TreeNodeData } from '@editor/type';
|
|
22
|
+
|
|
23
|
+
defineOptions({
|
|
24
|
+
name: 'MEditorLayerNodeContent',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const props = defineProps<{
|
|
28
|
+
data: TreeNodeData;
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
const { editorService } = useServices();
|
|
32
|
+
|
|
33
|
+
const label = computed(() => `${props.data.name} (${props.data.id})`);
|
|
34
|
+
|
|
35
|
+
/** 读取集中存储的校验错误状态,建立响应式依赖 */
|
|
36
|
+
const invalidInfo = computed(() => editorService.get('invalidNodeIds').get(props.data.id));
|
|
37
|
+
|
|
38
|
+
const hasError = computed(() => Boolean(invalidInfo.value?.props || invalidInfo.value?.style));
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 去掉单条校验文案中的「修改建议」部分。
|
|
42
|
+
*
|
|
43
|
+
* 错误文案可能是多条错误以 `<br>` 拼接的 HTML,每条本身形如 `主文案\n\n建议`。
|
|
44
|
+
* 组件树 tooltip 仅展示错误描述,不展示修改建议(建议仅在错误汇总面板展示),
|
|
45
|
+
* 故先按 `<br>` 拆分,每段用 `stripValidateSuggestion` 截断建议后再拼回。
|
|
46
|
+
*/
|
|
47
|
+
const stripSuggestion = (text?: string): string =>
|
|
48
|
+
String(text ?? '')
|
|
49
|
+
.split(/<br\s*\/?>/i)
|
|
50
|
+
.map((segment) => stripValidateSuggestion(segment))
|
|
51
|
+
.join('<br>');
|
|
52
|
+
|
|
53
|
+
/** 合并属性表单与样式表单的错误文案(去掉建议,本身可能仍是含 <br> 的 HTML) */
|
|
54
|
+
const errorMessage = computed(() =>
|
|
55
|
+
[stripSuggestion(invalidInfo.value?.props), stripSuggestion(invalidInfo.value?.style)].filter(Boolean).join('<br>'),
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
</template>
|
|
34
34
|
|
|
35
35
|
<template #tree-node-label="{ data: nodeData }">
|
|
36
|
-
<slot name="layer-node-label" :data="nodeData"
|
|
36
|
+
<slot name="layer-node-label" :data="nodeData">
|
|
37
|
+
<LayerNodeContent :data="nodeData"></LayerNodeContent>
|
|
38
|
+
</slot>
|
|
37
39
|
</template>
|
|
38
40
|
</Tree>
|
|
39
41
|
|
|
@@ -69,6 +71,7 @@ import type {
|
|
|
69
71
|
} from '@editor/type';
|
|
70
72
|
|
|
71
73
|
import LayerMenu from './LayerMenu.vue';
|
|
74
|
+
import LayerNodeContent from './LayerNodeContent.vue';
|
|
72
75
|
import LayerNodeTool from './LayerNodeTool.vue';
|
|
73
76
|
import { useClick } from './use-click';
|
|
74
77
|
import { useDrag } from './use-drag';
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
ref="box"
|
|
9
9
|
v-model:visible="visible"
|
|
10
10
|
title="当前位置下的组件"
|
|
11
|
+
:framework-width="frameworkWidth"
|
|
11
12
|
:position="menuPosition"
|
|
12
13
|
>
|
|
13
14
|
<template #body>
|
|
@@ -36,7 +37,9 @@ import { useNodeStatus } from '@editor/layouts/sidebar/layer/use-node-status';
|
|
|
36
37
|
import type { TreeNodeData } from '@editor/type';
|
|
37
38
|
|
|
38
39
|
const services = useServices();
|
|
39
|
-
const { editorService } = services;
|
|
40
|
+
const { editorService, uiService } = services;
|
|
41
|
+
|
|
42
|
+
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
|
|
40
43
|
|
|
41
44
|
const visible = ref(false);
|
|
42
45
|
const buttonVisible = ref(false);
|