roymondchen b02aa75ddc feat(editor): 历史记录面板支持单步回滚(类 git revert)
将目标历史步骤的修改作为一次新操作反向应用,不破坏原有栈结构,
page/dataSource/codeBlock 三类 service 均提供 revert 能力;
面板新增关闭按钮、步骤编号展示与合并组卡片样式优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 14:19:44 +08:00

267 lines
7.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<component
v-model="activeTabName"
v-bind="
tabsComponent?.props({
type: config.tabType,
editable: config.editable || false,
tabPosition: config.tabPosition || 'top',
}) || {}
"
:is="tabsComponent?.component || 'el-tabs'"
:class="`tmagic-design-tabs ${config.dynamic ? 'magic-form-dynamic-tab' : 'magic-form-tab'}`"
@tab-click="tabClickHandler"
@tab-add="onTabAdd"
@tab-remove="onTabRemove"
>
<component
v-for="(tab, tabIndex) in tabs"
:is="tabPaneComponent?.component || 'el-tab-pane'"
:key="tab[mForm?.keyProp || '__key'] ?? tabIndex"
v-bind="
tabPaneComponent?.props({
name: filter(tab.status) || tabIndex.toString(),
lazy: isCompare ? false : tab.lazy || false,
}) || {}
"
>
<template #label>
<span>
{{ filter(tab.title)
}}<TMagicBadge
:hidden="!diffCount[Number(tabIndex)]"
:value="diffCount[Number(tabIndex)]"
class="diff-count-badge"
></TMagicBadge>
</span>
</template>
<Container
v-for="item in tabItems(tab)"
:key="(item as Record<string, any>)[mForm?.keyProp || '__key']"
:config="item"
:disabled="disabled"
:model="
config.dynamic
? (name ? model[name] : model)[tabIndex]
: tab.name
? (name ? model[name] : model)[tab.name]
: name
? model[name]
: model
"
:last-values="
isEmpty(lastValues)
? {}
: config.dynamic
? (name ? lastValues[name] : lastValues)[tabIndex]
: tab.name
? (name ? lastValues[name] : lastValues)[tab.name]
: name
? lastValues[name]
: lastValues
"
:is-compare="isCompare"
:prop="
config.dynamic
? `${prop}${prop ? '.' : ''}${String(tabIndex)}`
: tab.name
? `${prop}${prop ? '.' : ''}${tab.name}`
: prop
"
:size="size"
:label-width="tab.labelWidth || labelWidth"
:expand-more="expandMore"
@change="changeHandler"
@addDiffCount="onAddDiffCount(Number(tabIndex))"
></Container>
</component>
</component>
</template>
<script setup lang="ts">
import { computed, inject, ref, watch, watchEffect } from 'vue';
import { isEmpty } from 'lodash-es';
import { getDesignConfig, TMagicBadge } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, TabConfig, TabPaneConfig } from '../schema';
import { display as displayFunc, filterFunction, initValue } from '../utils/form';
import Container from './Container.vue';
defineOptions({
name: 'MFormTabs',
});
type DiffCount = {
[tabIndex: number]: number;
};
const props = withDefaults(
defineProps<{
model: any;
lastValues?: any;
isCompare?: boolean;
config: TabConfig;
name: string;
size?: string;
labelWidth?: string;
prop?: string;
expandMore?: boolean;
disabled?: boolean;
}>(),
{
lastValues: () => ({}),
isCompare: false,
prop: '',
},
);
const tabPaneComponent = getDesignConfig('components')?.tabPane;
const tabsComponent = getDesignConfig('components')?.tabs;
const getActive = (mForm: FormState | undefined, props: any, activeTabName: string) => {
const { config, model, prop } = props;
const { active } = config;
if (typeof active === 'function') return active(mForm, { model, formValue: mForm?.values, prop });
if (+activeTabName >= props.config.items.length) return '0';
if (typeof active !== 'undefined') return active;
return '0';
};
const tabClick = (mForm: FormState | undefined, tab: any, props: any) => {
const { config, model, prop } = props;
// 兼容vue2的element-ui
tab.name = tab.paneName;
if (typeof config.onTabClick === 'function') {
config.onTabClick(mForm, tab, { model, formValue: mForm?.values, prop, config });
}
const tabConfig = config.items.find((item: TabPaneConfig) => tab.name === item.status);
if (tabConfig && typeof tabConfig.onTabClick === 'function') {
tabConfig.onTabClick(mForm, tab, { model, formValue: mForm?.values, prop, config });
}
};
const emit = defineEmits<{
change: [v: any, eventData?: ContainerChangeEventData];
addDiffCount: [];
}>();
const mForm = inject<FormState | undefined>('mForm');
const activeTabName = ref(getActive(mForm, props, ''));
const diffCount = ref<DiffCount>({});
const tabs = computed(() => {
if (props.config.dynamic) {
if (!props.config.name) throw new Error('dynamic tab 必须配置name');
return props.model[props.config.name] || [];
}
return props.config.items.filter((item) => displayFunc(mForm, item.display, props));
});
const filter = (config: any) => filterFunction(mForm, config, props);
watchEffect(() => {
if (typeof props.config.activeChange === 'function') {
props.config.activeChange(mForm, activeTabName.value, {
model: props.model,
prop: props.prop,
});
}
});
// model 或 lastValues 变化时,重置差异数
watch([() => props.model, () => props.lastValues], () => {
diffCount.value = {};
});
const tabItems = (tab: TabPaneConfig) => (props.config.dynamic ? props.config.items : tab.items);
const tabClickHandler = (tab: any) => {
if (typeof tab === 'object') {
tabClick(mForm, tab, props);
} else {
let item = tabs.value.find((tab: any) => tab.status === tab);
if (!item) {
item = tabs.value[tab];
}
tabClick(mForm, item, props);
}
};
const onTabAdd = async () => {
if (!props.name) throw new Error('dynamic tab 必须配置name');
if (typeof props.config.onTabAdd === 'function') {
props.config.onTabAdd(mForm, {
model: props.model,
prop: props.prop,
config: props.config,
});
emit('change', props.model[props.name]);
} else {
const newObj = await initValue(mForm, {
config: props.config.items,
initValues: {},
});
newObj.title = `标签${tabs.value.length + 1}`;
props.model[props.name].push(newObj);
emit('change', props.model[props.name], {
changeRecords: [
{
propPath: `${props.prop}.${props.model[props.name].length - 1}`,
value: newObj,
},
],
});
}
mForm?.$emit('field-change', props.prop, props.model[props.name]);
};
const onTabRemove = (tabName: string) => {
if (!props.name) throw new Error('dynamic tab 必须配置name');
if (typeof props.config.onTabRemove === 'function') {
props.config.onTabRemove(mForm, tabName, {
model: props.model,
prop: props.prop,
config: props.config,
});
} else {
props.model[props.name].splice(+tabName, 1);
// 防止删除后没有选中的问题
if (tabName < activeTabName.value || activeTabName.value >= props.model[props.name].length) {
activeTabName.value = (+activeTabName.value - 1).toString();
tabClick(mForm, { name: activeTabName.value }, props);
}
}
emit('change', props.model[props.name]);
mForm?.$emit('field-change', props.prop, props.model[props.name]);
};
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.name ? props.model[props.name] : props.model, eventData);
};
// 在tabs组件中收集事件触发次数即该tab下的差异数
const onAddDiffCount = (tabIndex: number) => {
if (!diffCount.value[tabIndex]) {
diffCount.value[tabIndex] = 1;
} else {
diffCount.value[tabIndex] += 1;
}
// 继续抛出给更高层级的组件
emit('addDiffCount');
};
</script>