mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-30 04:08:04 +00:00
feat(editor): 历史记录面板支持差异对比
- 新增 HistoryDiffDialog 历史差异对比弹窗 - 新增 CompareForm 表单对比组件 - 抽取 code-block 工具函数到 utils/code-block.ts - 历史列表面板支持选择两个版本进行对比
This commit is contained in:
parent
0f8abf7298
commit
59f4e0edac
@ -229,6 +229,12 @@ provide('services', services);
|
||||
|
||||
provide('codeOptions', props.codeOptions);
|
||||
provide('stageOptions', stageOptions);
|
||||
/**
|
||||
* 把顶层 `extendFormState` 提供给非 PropsPanel 链路上的组件使用(例如历史差异对话框 HistoryDiffDialog
|
||||
* 内部的 CompareForm)。这样所有依赖业务上下文的表单 filterFunction 都能拿到一致的扩展状态,
|
||||
* 与 PropsPanel 通过 `:extend-state` 显式传入的方式保持等价。
|
||||
*/
|
||||
provide('extendFormState', props.extendFormState);
|
||||
|
||||
provide<EventBus>('eventBus', new EventEmitter());
|
||||
|
||||
|
||||
@ -63,14 +63,7 @@ import { computed, inject, nextTick, Ref, ref, useTemplateRef, watch } from 'vue
|
||||
|
||||
import type { CodeBlockContent } from '@tmagic/core';
|
||||
import { TMagicButton, TMagicDialog, tMagicMessage, tMagicMessageBox, TMagicTag } from '@tmagic/design';
|
||||
import {
|
||||
type ContainerChangeEventData,
|
||||
defineFormConfig,
|
||||
defineFormItem,
|
||||
type FormConfig,
|
||||
MFormBox,
|
||||
type TableColumnConfig,
|
||||
} from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, type FormConfig, MFormBox } from '@tmagic/form';
|
||||
|
||||
import FloatingBox from '@editor/components/FloatingBox.vue';
|
||||
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
|
||||
@ -78,6 +71,7 @@ import { useNextFloatBoxPosition } from '@editor/hooks/use-next-float-box-positi
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { useWindowRect } from '@editor/hooks/use-window-rect';
|
||||
import CodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
|
||||
import { getEditorConfig } from '@editor/utils/config';
|
||||
|
||||
defineOptions({
|
||||
@ -119,106 +113,23 @@ const diffChange = () => {
|
||||
difVisible.value = false;
|
||||
};
|
||||
|
||||
const defaultParamColConfig = defineFormItem<TableColumnConfig>({
|
||||
type: 'row',
|
||||
label: '参数类型',
|
||||
items: [
|
||||
{
|
||||
text: '参数类型',
|
||||
labelWidth: '70px',
|
||||
type: 'select',
|
||||
name: 'type',
|
||||
options: [
|
||||
{
|
||||
text: '数字',
|
||||
label: '数字',
|
||||
value: 'number',
|
||||
},
|
||||
{
|
||||
text: '字符串',
|
||||
label: '字符串',
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
text: '组件',
|
||||
label: '组件',
|
||||
value: 'ui-select',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const codeOptions = inject<Record<string, any>>('codeOptions', {});
|
||||
|
||||
const functionConfig = computed(
|
||||
() =>
|
||||
defineFormConfig([
|
||||
{
|
||||
text: '名称',
|
||||
name: 'name',
|
||||
rules: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
text: '描述',
|
||||
name: 'desc',
|
||||
},
|
||||
{
|
||||
text: '执行时机',
|
||||
name: 'timing',
|
||||
type: 'select',
|
||||
options: () => {
|
||||
const options = [
|
||||
{ text: '初始化前', value: 'beforeInit' },
|
||||
{ text: '初始化后', value: 'afterInit' },
|
||||
];
|
||||
if (props.dataSourceType !== 'base') {
|
||||
options.push({ text: '请求前', value: 'beforeRequest' });
|
||||
options.push({ text: '请求后', value: 'afterRequest' });
|
||||
}
|
||||
return options;
|
||||
},
|
||||
display: () => props.isDataSource,
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
border: true,
|
||||
text: '参数',
|
||||
enableFullscreen: false,
|
||||
enableToggleMode: false,
|
||||
name: 'params',
|
||||
dropSort: false,
|
||||
items: [
|
||||
{
|
||||
type: 'text',
|
||||
label: '参数名',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: '描述',
|
||||
name: 'extra',
|
||||
},
|
||||
codeBlockService.getParamsColConfig() || defaultParamColConfig,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'vs-code',
|
||||
options: inject('codeOptions', {}),
|
||||
autosize: { minRows: 10, maxRows: 30 },
|
||||
onChange: (_formState, code: string) => {
|
||||
try {
|
||||
// 检测js代码是否存在语法错误
|
||||
getEditorConfig('parseDSL')(code);
|
||||
|
||||
return code;
|
||||
} catch (error: any) {
|
||||
tMagicMessage.error(error.message);
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
]) as FormConfig,
|
||||
/**
|
||||
* 代码块编辑表单配置。统一委托到 utils/code-block 的 `getCodeBlockFormConfig`,
|
||||
* 与 CompareForm 等其它使用方共享同一份 schema,避免双份维护。
|
||||
*
|
||||
* 这里以 computed 包裹是为了让 `props.isDataSource` / `props.dataSourceType` 变化时
|
||||
* "执行时机"字段的可见性与可选项实时刷新。
|
||||
*/
|
||||
const functionConfig = computed<FormConfig>(() =>
|
||||
getCodeBlockFormConfig({
|
||||
paramColConfig: codeBlockService.getParamsColConfig(),
|
||||
isDataSource: () => Boolean(props.isDataSource),
|
||||
dataSourceType: () => props.dataSourceType,
|
||||
codeOptions,
|
||||
editable: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const parseContent = (content: any) => {
|
||||
|
||||
192
packages/editor/src/components/CompareForm.vue
Normal file
192
packages/editor/src/components/CompareForm.vue
Normal file
@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="m-editor-compare-form-wrapper" :style="wrapperStyle">
|
||||
<MForm
|
||||
v-if="config.length"
|
||||
ref="form"
|
||||
class="m-editor-compare-form"
|
||||
:config="config"
|
||||
:init-values="currentValues"
|
||||
:last-values="lastValuesProcessed"
|
||||
:is-compare="true"
|
||||
:disabled="true"
|
||||
:label-width="labelWidth"
|
||||
:extend-state="extendState"
|
||||
></MForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref, useTemplateRef, watch, watchEffect } from 'vue';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
|
||||
import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorCompareForm',
|
||||
});
|
||||
|
||||
/**
|
||||
* 对比类型:
|
||||
* - node: 节点组件,按 `type` 从 propsService 获取属性表单配置
|
||||
* - data-source: 数据源,按 `type`(base/http/...) 从 dataSourceService 获取数据源表单配置
|
||||
* - code-block: 数据源代码块,使用内置的代码块表单配置
|
||||
*/
|
||||
export type CompareCategory = 'node' | 'data-source' | 'code-block';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 当前值(修改后的值) */
|
||||
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
|
||||
/** 用于对比的旧值(修改前的值) */
|
||||
lastValue?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
|
||||
/**
|
||||
* 类型说明:
|
||||
* - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
|
||||
* - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
|
||||
* - `category` 为 `code-block` 时,`type` 可不传
|
||||
*/
|
||||
type?: string;
|
||||
/** 表单配置类别,决定从哪里取 FormConfig */
|
||||
category?: CompareCategory;
|
||||
/** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
|
||||
dataSourceType?: string;
|
||||
labelWidth?: string;
|
||||
/**
|
||||
* 外层容器高度。设置后表单内容超出时会在 CompareForm 内部出现滚动条,
|
||||
* 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
|
||||
*/
|
||||
height?: string;
|
||||
/**
|
||||
* 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
|
||||
* PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
|
||||
* filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
|
||||
* 因此在差异对比场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
|
||||
*/
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
}>(),
|
||||
{
|
||||
category: 'node',
|
||||
labelWidth: '120px',
|
||||
},
|
||||
);
|
||||
|
||||
const { propsService, dataSourceService, codeBlockService, editorService } = useServices();
|
||||
const services = useServices();
|
||||
|
||||
const config = ref<FormConfig>([]);
|
||||
|
||||
/** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
|
||||
const codeOptions = inject<Record<string, any>>('codeOptions', {});
|
||||
|
||||
/** 将代码块的 content 字段统一成字符串,便于在表单/对比中展示 */
|
||||
const normalizeCodeBlockValue = (
|
||||
v: Partial<CodeBlockContent> | Record<string, any> | undefined,
|
||||
): Record<string, any> => {
|
||||
if (!v) return {};
|
||||
const next: Record<string, any> = { ...v };
|
||||
if (next.content && typeof next.content !== 'string') {
|
||||
try {
|
||||
next.content = next.content.toString();
|
||||
} catch {
|
||||
next.content = '';
|
||||
}
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const currentValues = computed<FormValue>(() => {
|
||||
if (props.category === 'code-block') {
|
||||
return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
|
||||
}
|
||||
return (props.value || {}) as FormValue;
|
||||
});
|
||||
|
||||
const lastValuesProcessed = computed<FormValue>(() => {
|
||||
if (props.category === 'code-block') {
|
||||
return normalizeCodeBlockValue(props.lastValue as Partial<CodeBlockContent>);
|
||||
}
|
||||
return (props.lastValue || {}) as FormValue;
|
||||
});
|
||||
|
||||
/**
|
||||
* 外层包裹层的样式:当传入 `height` 时启用固定高度 + 内部滚动,
|
||||
* 这样滚动条会出现在 CompareForm 内部,避免父容器(如 Dialog)自身也产生滚动。
|
||||
*/
|
||||
const wrapperStyle = computed(() => {
|
||||
if (!props.height) return undefined;
|
||||
return {
|
||||
height: props.height,
|
||||
overflow: 'auto',
|
||||
} as Record<string, string>;
|
||||
});
|
||||
|
||||
const loadConfig = async () => {
|
||||
switch (props.category) {
|
||||
case 'node': {
|
||||
if (!props.type) {
|
||||
config.value = [];
|
||||
return;
|
||||
}
|
||||
config.value = await propsService.getPropsConfig(props.type);
|
||||
break;
|
||||
}
|
||||
case 'data-source': {
|
||||
config.value = dataSourceService.getFormConfig(props.type || 'base');
|
||||
break;
|
||||
}
|
||||
case 'code-block': {
|
||||
config.value = getCodeBlockFormConfig({
|
||||
paramColConfig: codeBlockService.getParamsColConfig(),
|
||||
// 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比场景下 props.dataSourceType
|
||||
// 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
|
||||
isDataSource: () => Boolean(props.dataSourceType),
|
||||
dataSourceType: () => props.dataSourceType,
|
||||
codeOptions,
|
||||
// 对比模式只读,不需要校验/语法检查
|
||||
editable: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
config.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => props.category, () => props.type, () => props.dataSourceType],
|
||||
() => {
|
||||
loadConfig();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
|
||||
|
||||
/**
|
||||
* 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
|
||||
* 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
|
||||
* 在执行时拿不到 `formState.services` 而报错。
|
||||
*
|
||||
* 与 props-panel/FormPanel.vue 中的注入方式保持一致:
|
||||
* - services:整个 useServices() 返回的服务集合;
|
||||
* - stage:当前 editorService.get('stage') 的最新值。
|
||||
*/
|
||||
const stage = computed(() => editorService.get('stage'));
|
||||
|
||||
watchEffect(() => {
|
||||
if (formRef.value) {
|
||||
formRef.value.formState.stage = stage.value;
|
||||
formRef.value.formState.services = services;
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
form: formRef,
|
||||
config,
|
||||
reload: loadConfig,
|
||||
});
|
||||
</script>
|
||||
@ -70,6 +70,7 @@ export { default as LayoutContainer } from './components/SplitView.vue';
|
||||
export { default as SplitView } from './components/SplitView.vue';
|
||||
export { default as Resizer } from './components/Resizer.vue';
|
||||
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
|
||||
export { default as CompareForm } from './components/CompareForm.vue';
|
||||
export { default as FloatingBox } from './components/FloatingBox.vue';
|
||||
export { default as Tree } from './components/Tree.vue';
|
||||
export { default as TreeNode } from './components/TreeNode.vue';
|
||||
|
||||
@ -22,12 +22,14 @@
|
||||
applied: s.applied,
|
||||
isCurrent: s.isCurrent,
|
||||
desc: describeStep(s.step),
|
||||
diffable: isStepDiffable ? isStepDiffable(s.step) : false,
|
||||
}))
|
||||
"
|
||||
:is-current="group.isCurrent"
|
||||
:expanded="!!expanded[`${prefix}-${bucketId}-${gIdx}`]"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(index: number) => $emit('goto', bucketId, index)"
|
||||
@diff-step="(index: number) => $emit('diff-step', bucketId, index)"
|
||||
/>
|
||||
<!--
|
||||
初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。
|
||||
@ -68,6 +70,8 @@ const props = defineProps<{
|
||||
describeGroup: (_group: any) => string;
|
||||
/** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
|
||||
describeStep: (_step: any) => string;
|
||||
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
|
||||
isStepDiffable?: (_step: any) => boolean;
|
||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
||||
expanded: Record<string, boolean>;
|
||||
}>();
|
||||
@ -82,6 +86,8 @@ defineEmits<{
|
||||
(_e: 'goto', _bucketId: string | number, _index: number): void;
|
||||
/** 用户点击初始项希望该 bucket 回到未修改状态;携带 bucketId 用于上层路由到正确的 service。 */
|
||||
(_e: 'goto-initial', _bucketId: string | number): void;
|
||||
/** 用户点击"查看差异",携带 bucketId 与 step 索引。 */
|
||||
(_e: 'diff-step', _bucketId: string | number, _index: number): void;
|
||||
}>();
|
||||
|
||||
/** 该 bucket 是否处于初始状态(栈 cursor=0),等价于全部 group 都未 applied。 */
|
||||
|
||||
@ -10,10 +10,12 @@
|
||||
:groups="bucket.groups"
|
||||
:describe-group="describeCodeBlockGroup"
|
||||
:describe-step="describeCodeBlockStep"
|
||||
:is-step-diffable="isCodeBlockStepDiffable"
|
||||
:expanded="expanded"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
||||
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
||||
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
||||
/>
|
||||
</TMagicScrollbar>
|
||||
</template>
|
||||
@ -21,7 +23,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { TMagicScrollbar } from '@tmagic/design';
|
||||
|
||||
import type { CodeBlockHistoryGroup } from '@editor/type';
|
||||
import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type';
|
||||
|
||||
import Bucket from './Bucket.vue';
|
||||
import { describeCodeBlockGroup, describeCodeBlockStep } from './composables';
|
||||
@ -47,5 +49,10 @@ defineEmits<{
|
||||
(_e: 'goto', _codeBlockId: string | number, _index: number): void;
|
||||
/** 透传 Bucket 的 goto-initial 事件,携带 codeBlock id(回到该代码块未修改时的状态)。 */
|
||||
(_e: 'goto-initial', _codeBlockId: string | number): void;
|
||||
/** 透传 Bucket 的 diff-step 事件,携带 codeBlock id 与 step 索引。 */
|
||||
(_e: 'diff-step', _codeBlockId: string | number, _index: number): void;
|
||||
}>();
|
||||
|
||||
/** 仅 update(前后 content 都存在)时可查看差异。 */
|
||||
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
|
||||
</script>
|
||||
|
||||
@ -10,10 +10,12 @@
|
||||
:groups="bucket.groups"
|
||||
:describe-group="describeDataSourceGroup"
|
||||
:describe-step="describeDataSourceStep"
|
||||
:is-step-diffable="isDataSourceStepDiffable"
|
||||
:expanded="expanded"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
||||
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
||||
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
||||
/>
|
||||
</TMagicScrollbar>
|
||||
</template>
|
||||
@ -21,7 +23,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { TMagicScrollbar } from '@tmagic/design';
|
||||
|
||||
import type { DataSourceHistoryGroup } from '@editor/type';
|
||||
import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type';
|
||||
|
||||
import Bucket from './Bucket.vue';
|
||||
import { describeDataSourceGroup, describeDataSourceStep } from './composables';
|
||||
@ -47,5 +49,10 @@ defineEmits<{
|
||||
(_e: 'goto', _dataSourceId: string | number, _index: number): void;
|
||||
/** 透传 Bucket 的 goto-initial 事件,携带 dataSource id(回到该数据源未修改时的状态)。 */
|
||||
(_e: 'goto-initial', _dataSourceId: string | number): void;
|
||||
/** 透传 Bucket 的 diff-step 事件,携带 dataSource id 与 step 索引。 */
|
||||
(_e: 'diff-step', _dataSourceId: string | number, _index: number): void;
|
||||
}>();
|
||||
|
||||
/** 仅 update(前后 schema 都存在)时可查看差异。 */
|
||||
const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
|
||||
</script>
|
||||
|
||||
@ -12,6 +12,13 @@
|
||||
<span class="m-editor-history-list-item-op" :class="`op-${opType}`">{{ opLabel(opType) }}</span>
|
||||
<span class="m-editor-history-list-item-desc">{{ desc }}</span>
|
||||
<span v-if="isCurrent" class="m-editor-history-list-item-current">当前</span>
|
||||
<span
|
||||
v-if="!merged && headDiffable"
|
||||
class="m-editor-history-list-item-diff"
|
||||
title="查看修改差异"
|
||||
@click.stop="onDiffClick(subSteps[0].index)"
|
||||
>查看差异</span
|
||||
>
|
||||
<span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
|
||||
<span v-if="merged" class="m-editor-history-list-group-toggle" :class="{ 'is-expanded': expanded }">▾</span>
|
||||
</div>
|
||||
@ -25,8 +32,15 @@
|
||||
@click="onSubStepClick(s)"
|
||||
>
|
||||
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
|
||||
<span>{{ s.desc }}</span>
|
||||
<span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
|
||||
<span v-if="s.isCurrent" class="m-editor-history-list-item-current">当前</span>
|
||||
<span
|
||||
v-if="s.diffable"
|
||||
class="m-editor-history-list-item-diff"
|
||||
title="查看修改差异"
|
||||
@click.stop="onDiffClick(s.index)"
|
||||
>查看差异</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@ -57,7 +71,7 @@ const props = defineProps<{
|
||||
/** 组内的 step 总数,仅在 merged 为 true 时显示为 "合并 N 步"。 */
|
||||
stepCount: number;
|
||||
/** 子步列表,用于在展开状态下逐条展示每个 step 的索引、应用状态与描述文案。 */
|
||||
subSteps: { index: number; applied: boolean; desc: string; isCurrent?: boolean }[];
|
||||
subSteps: { index: number; applied: boolean; desc: string; isCurrent?: boolean; diffable?: boolean }[];
|
||||
/** 当前组是否处于展开状态。仅在 merged 为 true 时生效,控制子步列表是否渲染。 */
|
||||
expanded: boolean;
|
||||
/** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组),UI 高亮展示。 */
|
||||
@ -79,6 +93,12 @@ const emit = defineEmits<{
|
||||
* 当前所在的步骤(isCurrent)始终不会触发 goto。
|
||||
*/
|
||||
(_e: 'goto', _index: number): void;
|
||||
/**
|
||||
* 用户希望查看该 step 的修改差异(旧值 vs 新值)。
|
||||
* 只在 step 满足"前后值都存在"(如 update / 数据源、代码块的 update)时由父级标记 `diffable=true`。
|
||||
* payload 为该 step 在所属栈中的索引,由上层根据 index 取 step 内容并展示对比。
|
||||
*/
|
||||
(_e: 'diff-step', _index: number): void;
|
||||
}>();
|
||||
|
||||
/** 单步组:头部可点击 goto;合并组:头部可点击切换展开。当前组(isCurrent)的单步组头部不可点击。 */
|
||||
@ -112,4 +132,11 @@ const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => {
|
||||
if (s.isCurrent) return;
|
||||
emit('goto', s.index);
|
||||
};
|
||||
|
||||
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
|
||||
const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
|
||||
|
||||
const onDiffClick = (index: number) => {
|
||||
emit('diff-step', index);
|
||||
};
|
||||
</script>
|
||||
|
||||
157
packages/editor/src/layouts/history-list/HistoryDiffDialog.vue
Normal file
157
packages/editor/src/layouts/history-list/HistoryDiffDialog.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<TMagicDialog
|
||||
v-model="visible"
|
||||
class="m-editor-history-diff-dialog"
|
||||
title="查看修改差异"
|
||||
width="900px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="payload" class="m-editor-history-diff-dialog-body">
|
||||
<div class="m-editor-history-diff-dialog-header">
|
||||
<span class="m-editor-history-diff-dialog-target">{{ targetText }}</span>
|
||||
<TMagicRadioGroup v-model="mode" size="small" class="m-editor-history-diff-dialog-mode">
|
||||
<TMagicRadioButton value="before">与修改前对比</TMagicRadioButton>
|
||||
<TMagicRadioButton value="current" :disabled="!hasCurrent">与当前对比</TMagicRadioButton>
|
||||
</TMagicRadioGroup>
|
||||
</div>
|
||||
|
||||
<div class="m-editor-history-diff-dialog-legend">
|
||||
<TMagicTag size="small" type="info">{{ leftLabel }}</TMagicTag>
|
||||
<span class="m-editor-history-diff-dialog-arrow">→</span>
|
||||
<TMagicTag size="small" type="success">{{ rightLabel }}</TMagicTag>
|
||||
<span v-if="mode === 'current' && isSameAsCurrent" class="m-editor-history-diff-dialog-tip">
|
||||
当前值与该步修改后一致,无差异
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<CompareForm
|
||||
:category="payload.category"
|
||||
:type="payload.type"
|
||||
:data-source-type="payload.dataSourceType"
|
||||
:value="rightValue"
|
||||
:last-value="leftValue"
|
||||
:extend-state="extendState"
|
||||
height="60vh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<TMagicButton size="small" @click="visible = false">关闭</TMagicButton>
|
||||
</template>
|
||||
</TMagicDialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { TMagicButton, TMagicDialog, TMagicRadioButton, TMagicRadioGroup, TMagicTag } from '@tmagic/design';
|
||||
import type { FormState } from '@tmagic/form';
|
||||
|
||||
import CompareForm, { type CompareCategory } from '@editor/components/CompareForm.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorHistoryDiffDialog',
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
/**
|
||||
* 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
|
||||
* 透传给 CompareForm,从而让差异对比时表单 item 中依赖业务上下文的
|
||||
* `display` / `disabled` 等 filterFunction 正常工作。
|
||||
*/
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
}>();
|
||||
|
||||
/** 差异对话框的入参 */
|
||||
export interface DiffDialogPayload {
|
||||
/** 表单类别 */
|
||||
category: CompareCategory;
|
||||
/** 节点类型 / 数据源类型 */
|
||||
type?: string;
|
||||
/** 代码块场景下的数据源类型 */
|
||||
dataSourceType?: string;
|
||||
/** 该 step 修改前的值(oldNode / oldSchema / oldContent) */
|
||||
lastValue: Record<string, any>;
|
||||
/** 该 step 修改后的值(newNode / newSchema / newContent) */
|
||||
value: Record<string, any>;
|
||||
/** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */
|
||||
currentValue?: Record<string, any> | null;
|
||||
/** 用于标题展示的目标名称 */
|
||||
targetLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 差异对比模式:
|
||||
* - before:该步骤修改前 vs 该步骤修改后(默认行为,体现这一步带来的变化)
|
||||
* - current:该步骤修改后 vs 当前最新值(用于查看「该步骤之后是否还被改过」)
|
||||
*/
|
||||
type DiffMode = 'before' | 'current';
|
||||
|
||||
const visible = ref(false);
|
||||
const payload = ref<DiffDialogPayload | null>(null);
|
||||
const mode = ref<DiffMode>('before');
|
||||
|
||||
const hasCurrent = computed(() => payload.value?.currentValue !== undefined && payload.value?.currentValue !== null);
|
||||
|
||||
/** 左侧(旧/参照)值 */
|
||||
const leftValue = computed<Record<string, any>>(() => {
|
||||
if (!payload.value) return {};
|
||||
if (mode.value === 'current') return payload.value.value;
|
||||
return payload.value.lastValue;
|
||||
});
|
||||
|
||||
/** 右侧(新/对比)值 */
|
||||
const rightValue = computed<Record<string, any>>(() => {
|
||||
if (!payload.value) return {};
|
||||
if (mode.value === 'current') return payload.value.currentValue || {};
|
||||
return payload.value.value;
|
||||
});
|
||||
|
||||
const leftLabel = computed(() => (mode.value === 'current' ? '该步修改后' : '修改前'));
|
||||
const rightLabel = computed(() => (mode.value === 'current' ? '当前' : '修改后'));
|
||||
|
||||
/** 「与当前对比」模式下,若当前值与该步修改后值相等,则展示提示 */
|
||||
const isSameAsCurrent = computed(() => {
|
||||
if (mode.value !== 'current' || !payload.value) return false;
|
||||
return isEqual(payload.value.value, payload.value.currentValue);
|
||||
});
|
||||
|
||||
const targetText = computed(() => {
|
||||
if (!payload.value) return '';
|
||||
const categoryText: Record<CompareCategory, string> = {
|
||||
node: '节点',
|
||||
'data-source': '数据源',
|
||||
'code-block': '代码块',
|
||||
};
|
||||
const prefix = categoryText[payload.value.category] || '';
|
||||
const label = payload.value.targetLabel || payload.value.type || '';
|
||||
return [prefix, label].filter(Boolean).join(':');
|
||||
});
|
||||
|
||||
const open = (p: DiffDialogPayload) => {
|
||||
payload.value = p;
|
||||
// 每次打开按需重置默认模式:有当前值时优先「与当前对比」更贴近"看现在差什么",否则回退到默认
|
||||
mode.value = 'before';
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 关闭后清理 payload,避免下一次打开时残留旧值闪现
|
||||
watch(visible, (v) => {
|
||||
if (!v) {
|
||||
payload.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
@ -12,6 +12,7 @@
|
||||
@toggle="toggleGroup"
|
||||
@goto="onPageGoto"
|
||||
@goto-initial="onPageGotoInitial"
|
||||
@diff-step="onPageDiff"
|
||||
/>
|
||||
</component>
|
||||
|
||||
@ -25,6 +26,7 @@
|
||||
@toggle="toggleGroup"
|
||||
@goto="onDataSourceGoto"
|
||||
@goto-initial="onDataSourceGotoInitial"
|
||||
@diff-step="onDataSourceDiff"
|
||||
/>
|
||||
</component>
|
||||
|
||||
@ -38,6 +40,7 @@
|
||||
@toggle="toggleGroup"
|
||||
@goto="onCodeBlockGoto"
|
||||
@goto-initial="onCodeBlockGotoInitial"
|
||||
@diff-step="onCodeBlockDiff"
|
||||
/>
|
||||
</component>
|
||||
</TMagicTabs>
|
||||
@ -53,6 +56,8 @@
|
||||
</TMagicTooltip>
|
||||
</template>
|
||||
</TMagicPopover>
|
||||
|
||||
<HistoryDiffDialog ref="diffDialog" :extend-state="extendFormState" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -70,13 +75,17 @@
|
||||
*
|
||||
* 这里的 targetCursor = 用户点击的 step.index + 1,即"应用至此步完成的状态"。
|
||||
*
|
||||
* 此外每条 step 上提供"查看差异"入口(仅在前后值都存在的 update 步骤显示),
|
||||
* 点击后弹出 HistoryDiffDialog,使用 CompareForm 组件以表单形式展示新旧值差异。
|
||||
*
|
||||
* 各 tab 的内容拆分为独立的 SFC(PageTab / DataSourceTab / CodeBlockTab),
|
||||
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
||||
*/
|
||||
import { markRaw, ref } from 'vue';
|
||||
import { inject, markRaw, ref, useTemplateRef } from 'vue';
|
||||
import { Clock } from '@element-plus/icons-vue';
|
||||
|
||||
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
||||
import type { FormState } from '@tmagic/form';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
@ -84,6 +93,7 @@ import { useServices } from '@editor/hooks/use-services';
|
||||
import CodeBlockTab from './CodeBlockTab.vue';
|
||||
import { useHistoryList } from './composables';
|
||||
import DataSourceTab from './DataSourceTab.vue';
|
||||
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
||||
import PageTab from './PageTab.vue';
|
||||
|
||||
defineOptions({
|
||||
@ -95,7 +105,17 @@ const activeTab = ref<'page' | 'data-source' | 'code-block'>('page');
|
||||
|
||||
const tabPaneComponent = getDesignConfig('components')?.tabPane;
|
||||
|
||||
const { editorService, dataSourceService, codeBlockService } = useServices();
|
||||
const { editorService, dataSourceService, codeBlockService, historyService } = useServices();
|
||||
|
||||
/**
|
||||
* 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
|
||||
* 内部的 CompareForm,使差异对比表单的 filterFunction 能拿到完整的业务上下文。
|
||||
* 未提供时为 undefined,CompareForm/MForm 会跳过 extendState 处理。
|
||||
*/
|
||||
const extendFormState = inject<((_state: FormState) => Record<string, any> | Promise<Record<string, any>>) | undefined>(
|
||||
'extendFormState',
|
||||
undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
expanded,
|
||||
@ -138,4 +158,74 @@ const onDataSourceGotoInitial = (id: string | number) => {
|
||||
const onCodeBlockGotoInitial = (id: string | number) => {
|
||||
codeBlockService.goto(id, 0);
|
||||
};
|
||||
|
||||
const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
|
||||
|
||||
/**
|
||||
* 页面 step 差异:仅 update 单节点修改可对比,传入旧/新节点。
|
||||
* 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
|
||||
* `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
|
||||
*/
|
||||
const onPageDiff = (index: number) => {
|
||||
const groups = historyService.getPageHistoryGroups();
|
||||
for (const group of groups) {
|
||||
const entry = group.steps.find((s) => s.index === index);
|
||||
if (!entry) continue;
|
||||
const item = entry.step.updatedItems?.[0];
|
||||
if (!item?.oldNode || !item?.newNode) return;
|
||||
const type = (item.newNode.type as string) || (item.oldNode.type as string) || '';
|
||||
const nodeId = item.newNode.id ?? item.oldNode.id;
|
||||
const currentNode = nodeId !== undefined ? editorService.getNodeById(nodeId) : null;
|
||||
diffDialogRef.value?.open({
|
||||
category: 'node',
|
||||
type,
|
||||
lastValue: item.oldNode as Record<string, any>,
|
||||
value: item.newNode as Record<string, any>,
|
||||
currentValue: (currentNode as Record<string, any>) || null,
|
||||
targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onDataSourceDiff = (id: string | number, index: number) => {
|
||||
const groups = historyService.getDataSourceHistoryGroups();
|
||||
for (const group of groups) {
|
||||
if (group.id !== id) continue;
|
||||
const entry = group.steps.find((s) => s.index === index);
|
||||
if (!entry) continue;
|
||||
const { oldSchema, newSchema } = entry.step;
|
||||
if (!oldSchema || !newSchema) return;
|
||||
const currentSchema = dataSourceService.getDataSourceById(`${id}`);
|
||||
diffDialogRef.value?.open({
|
||||
category: 'data-source',
|
||||
type: newSchema.type || oldSchema.type || 'base',
|
||||
lastValue: oldSchema as Record<string, any>,
|
||||
value: newSchema as Record<string, any>,
|
||||
currentValue: (currentSchema as Record<string, any>) || null,
|
||||
targetLabel: newSchema.title || oldSchema.title || `${id}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onCodeBlockDiff = (id: string | number, index: number) => {
|
||||
const groups = historyService.getCodeBlockHistoryGroups();
|
||||
for (const group of groups) {
|
||||
if (group.id !== id) continue;
|
||||
const entry = group.steps.find((s) => s.index === index);
|
||||
if (!entry) continue;
|
||||
const { oldContent, newContent } = entry.step;
|
||||
if (!oldContent || !newContent) return;
|
||||
const currentContent = codeBlockService.getCodeContentById(id);
|
||||
diffDialogRef.value?.open({
|
||||
category: 'code-block',
|
||||
lastValue: oldContent as Record<string, any>,
|
||||
value: newContent as Record<string, any>,
|
||||
currentValue: (currentContent as Record<string, any>) || null,
|
||||
targetLabel: newContent.name || oldContent.name || `${id}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -17,12 +17,14 @@
|
||||
applied: s.applied,
|
||||
isCurrent: s.isCurrent,
|
||||
desc: describePageStep(s.step),
|
||||
diffable: isPageStepDiffable(s.step),
|
||||
}))
|
||||
"
|
||||
:is-current="group.isCurrent"
|
||||
:expanded="!!expanded[`pg-${gIdx}`]"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(index: number) => $emit('goto', index)"
|
||||
@diff-step="(index: number) => $emit('diff-step', index)"
|
||||
/>
|
||||
<!--
|
||||
初始状态项:永远位于列表底部(页面 tab 倒序展示,最底部=最早),
|
||||
@ -38,7 +40,7 @@ import { computed } from 'vue';
|
||||
|
||||
import { TMagicScrollbar } from '@tmagic/design';
|
||||
|
||||
import type { PageHistoryGroup } from '@editor/type';
|
||||
import type { PageHistoryGroup, StepValue } from '@editor/type';
|
||||
|
||||
import { describePageGroup, describePageStep } from './composables';
|
||||
import GroupRow from './GroupRow.vue';
|
||||
@ -62,8 +64,23 @@ defineEmits<{
|
||||
(_e: 'goto', _index: number): void;
|
||||
/** 用户点击初始项希望回到未修改的状态(cursor=0)。 */
|
||||
(_e: 'goto-initial'): void;
|
||||
/** 用户点击"查看差异"按钮,携带目标 step 在栈中的索引。 */
|
||||
(_e: 'diff-step', _index: number): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 当前 step 是否可查看差异:
|
||||
* - 仅 update 操作;
|
||||
* - 单节点更新(updatedItems.length === 1),且 oldNode / newNode 都存在。
|
||||
* 多节点更新难以选定单一对比目标,统一不展示差异入口。
|
||||
*/
|
||||
const isPageStepDiffable = (step: StepValue): boolean => {
|
||||
if (step.opType !== 'update') return false;
|
||||
const items = step.updatedItems ?? [];
|
||||
if (items.length !== 1) return false;
|
||||
return Boolean(items[0]?.oldNode && items[0]?.newNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否处于"初始状态"——即对应页面历史栈 cursor===0:
|
||||
* 当 list 中所有 group 的 applied 都为 false 时即为该状态。
|
||||
|
||||
@ -213,6 +213,29 @@
|
||||
background-color: rgba(230, 162, 60, 0.12);
|
||||
}
|
||||
|
||||
.m-editor-history-list-item-diff {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor-history-list-substep-desc {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m-editor-history-list-bucket {
|
||||
margin-bottom: 8px;
|
||||
|
||||
@ -250,3 +273,54 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog {
|
||||
.m-editor-history-diff-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog-mode {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog-target {
|
||||
flex: 1 1 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog-arrow {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.m-editor-history-diff-dialog-tip {
|
||||
margin-left: 8px;
|
||||
color: #e6a23c;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
150
packages/editor/src/utils/code-block.ts
Normal file
150
packages/editor/src/utils/code-block.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import { defineFormConfig, defineFormItem, type FormConfig, type TableColumnConfig } from '@tmagic/form';
|
||||
|
||||
import { getEditorConfig } from './config';
|
||||
|
||||
/**
|
||||
* 代码块编辑表单配置的统一入口。
|
||||
*
|
||||
* 用于:
|
||||
* - `CodeBlockEditor.vue`:作为代码块新增/编辑的表单结构;
|
||||
* - `CompareForm.vue`:在历史差异对比中以"代码块"形态展示前后值。
|
||||
*
|
||||
* 通过参数控制两端的细微差异(是否启用必填校验 / vs-code onChange 校验 / dataSource 相关字段显示规则等),
|
||||
* 避免两边同步两份相同的 schema 而造成漂移。
|
||||
*/
|
||||
export interface GetCodeBlockFormConfigOptions {
|
||||
/**
|
||||
* 自定义 `params` 表格的"参数类型"列配置。优先级高于内置默认值;
|
||||
* 通常由 `codeBlockService.getParamsColConfig()` 注入业务侧的扩展。
|
||||
*/
|
||||
paramColConfig?: TableColumnConfig;
|
||||
/**
|
||||
* 是否在「数据源代码块」语境下使用:
|
||||
* - true:`执行时机` 字段会展示,`请求前 / 请求后` 选项仅在 `dataSourceType !== 'base'` 时出现;
|
||||
* - false:`执行时机` 字段隐藏(普通代码块没有时机概念)。
|
||||
*
|
||||
* 注意这里以函数形式传入是为了让 `display` / `options` 在每次渲染时实时取值,
|
||||
* 例如外部 `props.isDataSource` 切换或 `dataSourceType` 变更都能立即生效。
|
||||
*/
|
||||
isDataSource?: () => boolean;
|
||||
/** 当 isDataSource 为 true 时使用:返回当前数据源类型(`base` / `http` / ...),决定时机选项是否包含请求前后。 */
|
||||
dataSourceType?: () => string | undefined;
|
||||
/** vs-code 编辑器的额外 monaco options。一般传 `inject('codeOptions', {})` 的结果。 */
|
||||
codeOptions?: Record<string, any>;
|
||||
/**
|
||||
* 是否启用编辑态特性:
|
||||
* - true(默认):`name` 字段加必填校验;vs-code 的 `onChange` 会调用 `parseDSL` 检查语法,出错时弹消息并抛错;
|
||||
* - false:纯只读/对比场景,不加校验逻辑。
|
||||
*/
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
/** 默认的"参数类型"列配置:数字 / 字符串 / 组件 三选一。 */
|
||||
const defaultParamColConfig = () =>
|
||||
defineFormItem<TableColumnConfig>({
|
||||
type: 'row',
|
||||
label: '参数类型',
|
||||
items: [
|
||||
{
|
||||
text: '参数类型',
|
||||
labelWidth: '70px',
|
||||
type: 'select',
|
||||
name: 'type',
|
||||
options: [
|
||||
{ text: '数字', label: '数字', value: 'number' },
|
||||
{ text: '字符串', label: '字符串', value: 'text' },
|
||||
{ text: '组件', label: '组件', value: 'ui-select' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成代码块表单配置。返回的是普通对象数组,可直接传给 `<MForm :config>`;
|
||||
* 如需响应式的 props 变化(如切换 dataSourceType),调用方在 `computed` 中再次调用本函数即可。
|
||||
*/
|
||||
export const getCodeBlockFormConfig = (options: GetCodeBlockFormConfigOptions = {}): FormConfig => {
|
||||
const { paramColConfig, isDataSource, dataSourceType, codeOptions = {}, editable = true } = options;
|
||||
|
||||
return defineFormConfig([
|
||||
{
|
||||
text: '名称',
|
||||
name: 'name',
|
||||
...(editable ? { rules: [{ required: true, message: '请输入名称', trigger: 'blur' }] } : {}),
|
||||
},
|
||||
{
|
||||
text: '描述',
|
||||
name: 'desc',
|
||||
},
|
||||
{
|
||||
text: '执行时机',
|
||||
name: 'timing',
|
||||
type: 'select',
|
||||
options: () => {
|
||||
const list = [
|
||||
{ text: '初始化前', value: 'beforeInit' },
|
||||
{ text: '初始化后', value: 'afterInit' },
|
||||
];
|
||||
if (dataSourceType?.() !== 'base') {
|
||||
list.push({ text: '请求前', value: 'beforeRequest' });
|
||||
list.push({ text: '请求后', value: 'afterRequest' });
|
||||
}
|
||||
return list;
|
||||
},
|
||||
display: () => Boolean(isDataSource?.()),
|
||||
},
|
||||
{
|
||||
type: 'table',
|
||||
border: true,
|
||||
text: '参数',
|
||||
enableFullscreen: false,
|
||||
enableToggleMode: false,
|
||||
name: 'params',
|
||||
dropSort: false,
|
||||
items: [
|
||||
{ type: 'text', label: '参数名', name: 'name' },
|
||||
{ type: 'text', label: '描述', name: 'extra' },
|
||||
paramColConfig || defaultParamColConfig(),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'vs-code',
|
||||
options: codeOptions,
|
||||
autosize: { minRows: 10, maxRows: 30 },
|
||||
...(editable
|
||||
? {
|
||||
onChange: (_formState: any, code: string) => {
|
||||
try {
|
||||
// 检测 js 代码是否存在语法错误
|
||||
getEditorConfig('parseDSL')(code);
|
||||
return code;
|
||||
} catch (error: any) {
|
||||
tMagicMessage.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
]) as FormConfig;
|
||||
};
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
export * from './config';
|
||||
export * from './props';
|
||||
export * from './code-block';
|
||||
export * from './logger';
|
||||
export * from './editor';
|
||||
export * from './operator';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user