feat(editor): 样式设置器 StyleSetter 支持表单对比模式

- Index.vue 透传 lastValues/isCompare 给各分类子组件,并冒泡 addDiffCount

- pro 下 6 个分类组件接受新 props 并向 MContainer 传递

- Layout/Border 同时将新 props 传递给内部 Box/Border 组件

- components/Border.vue 接受新 props 并冒泡 MContainer 的 addDiffCount

- components/Box.vue 接受 props 以保持接口一致

- 补充单元测试覆盖透传与事件冒泡
This commit is contained in:
roymondchen 2026-05-26 20:59:43 +08:00
parent 540a2716d8
commit b1193b909e
14 changed files with 324 additions and 22 deletions

View File

@ -7,9 +7,12 @@
v-if="item.component"
:is="item.component"
:values="model[name]"
:last-values="lastValues?.[name]"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></component>
</TMagicCollapseItem>
</template>
@ -36,6 +39,7 @@ const props = defineProps<FieldProps<StyleSchema>>();
const emit = defineEmits<{
change: [v: any, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const list = [
@ -82,4 +86,6 @@ const change = (v: any, eventData: ContainerChangeEventData) => {
});
emit('change', v, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -30,7 +30,16 @@
</div>
</div>
<div class="border-value-container">
<MContainer :config="config" :model="model" :size="size" :disabled="disabled" @change="change"></MContainer>
<MContainer
:config="config"
:model="model"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
</div>
</div>
</template>
@ -86,11 +95,14 @@ const selectDirection = (d?: string) => (direction.value = d || '');
const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
withDefaults(
defineProps<{
model: FormValue;
lastValues?: FormValue;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>(),
@ -104,4 +116,6 @@ const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
});
});
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -64,6 +64,8 @@ withDefaults(
disabled?: boolean;
size?: 'large' | 'default' | 'small';
model: FormValue;
lastValues?: FormValue;
isCompare?: boolean;
}>(),
{},
);

View File

@ -1,5 +1,14 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
<MContainer
:config="config"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
</template>
<script lang="ts" setup>
@ -13,12 +22,15 @@ import { BackgroundNoRepeat, BackgroundRepeat, BackgroundRepeatX, BackgroundRepe
defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const config = defineFormItem({
@ -79,4 +91,6 @@ const config = defineFormItem({
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -1,6 +1,23 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
<Border :model="values" :size="size" :disabled="disabled" @change="change"></Border>
<MContainer
:config="config"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
<Border
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></Border>
</template>
<script lang="ts" setup>
@ -11,12 +28,15 @@ import Border from '../components/Border.vue';
defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const config = defineFormItem({
@ -36,4 +56,6 @@ const config = defineFormItem({
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -1,5 +1,14 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
<MContainer
:config="config"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
</template>
<script lang="ts" setup>
@ -12,12 +21,15 @@ import { AlignCenter, AlignLeft, AlignRight } from '../icons/text-align';
defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const config = defineFormItem({
@ -91,4 +103,6 @@ const config = defineFormItem({
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -1,8 +1,19 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
<MContainer
:config="config"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
<Box
v-show="!['fixed', 'absolute'].includes(values.position)"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@ -34,12 +45,15 @@ import {
defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: string | StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const config = defineFormItem({
@ -185,4 +199,6 @@ const config = defineFormItem({
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -1,5 +1,14 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
<MContainer
:config="config"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
</template>
<script lang="ts" setup>
@ -8,12 +17,15 @@ import type { StyleSchema } from '@tmagic/schema';
const props = defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: string | StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const positionText: Record<string, string> = {
@ -100,4 +112,6 @@ const config = defineFormItem({
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -1,5 +1,14 @@
<template>
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
<MContainer
:config="config"
:model="values"
:last-values="lastValues"
:is-compare="isCompare"
:size="size"
:disabled="disabled"
@change="change"
@add-diff-count="onAddDiffCount"
></MContainer>
</template>
<script lang="ts" setup>
@ -8,12 +17,15 @@ import type { StyleSchema } from '@tmagic/schema';
defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
}>();
const emit = defineEmits<{
change: [v: StyleSchema, eventData: ContainerChangeEventData];
addDiffCount: [];
}>();
const config = defineFormItem({
@ -51,4 +63,6 @@ const config = defineFormItem({
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
emit('change', value, eventData);
};
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -34,13 +34,14 @@ vi.mock('@editor/fields/StyleSetter/pro/index', () => {
const make = (name: string) =>
defineComponent({
name,
props: ['values', 'size', 'disabled'],
emits: ['change'],
props: ['values', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change', 'addDiffCount'],
setup(_p, { emit }) {
return () =>
h('div', {
class: name,
onClick: () => emit('change', { foo: 1 }, { changeRecords: [{ propPath: 'foo', value: 1 }] }),
onDblclick: () => emit('addDiffCount'),
});
},
});
@ -131,4 +132,47 @@ describe('StyleSetter Index', () => {
expect(layout.props('size')).toBe('small');
expect(layout.props('disabled')).toBe(true);
});
test('lastValues/isCompare 正确透传到子组件', () => {
const wrapper = mount(StyleSetter, {
props: {
model: { style: { color: 'red' } },
lastValues: { style: { color: 'blue' } },
isCompare: true,
name: 'style',
prop: 'style',
} as any,
});
const layout = wrapper.findComponent({ name: 'Layout' });
expect(layout.props('lastValues')).toEqual({ color: 'blue' });
expect(layout.props('isCompare')).toBe(true);
});
test('lastValues 为空时透传 undefined / isCompare 默认 false', () => {
const wrapper = mount(StyleSetter, {
props: { model: { style: {} }, name: 'style', prop: 'style' } as any,
});
const layout = wrapper.findComponent({ name: 'Layout' });
expect(layout.props('lastValues')).toBeUndefined();
// Boolean 类型 prop 未传时 Vue 默认为 false
expect(layout.props('isCompare')).toBe(false);
});
test('子组件 addDiffCount 事件向上冒泡', async () => {
const wrapper = mount(StyleSetter, {
props: {
model: { style: {} },
lastValues: { style: {} },
isCompare: true,
name: 'style',
prop: 'style',
} as any,
});
await wrapper.find('.Layout').trigger('dblclick');
expect(wrapper.emitted('addDiffCount')).toBeTruthy();
expect(wrapper.emitted('addDiffCount')?.length).toBe(1);
await wrapper.find('.Background').trigger('dblclick');
expect(wrapper.emitted('addDiffCount')?.length).toBe(2);
});
});

View File

@ -13,8 +13,8 @@ vi.mock('@tmagic/form', () => ({
defineFormItem: (cfg: any) => cfg,
MContainer: defineComponent({
name: 'FakeMContainer',
props: ['config', 'model', 'size', 'disabled'],
emits: ['change'],
props: ['config', 'model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change', 'addDiffCount'],
setup(_p, { emit }) {
return () =>
h(
@ -22,6 +22,7 @@ vi.mock('@tmagic/form', () => ({
{
class: 'fake-mcontainer',
onClick: () => emit('change', 'val', { propPath: 'p' }),
onDblclick: () => emit('addDiffCount'),
},
'mc',
);
@ -32,7 +33,7 @@ vi.mock('@tmagic/form', () => ({
vi.mock('@editor/fields/StyleSetter/components/Box.vue', () => ({
default: defineComponent({
name: 'FakeBox',
props: ['model', 'size', 'disabled'],
props: ['model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change'],
setup(_p, { emit }) {
return () =>
@ -110,4 +111,39 @@ describe('StyleSetter/Layout.vue', () => {
const values = displayItem.options.map((o: any) => o.value);
expect(values).toEqual(['inline', 'flex', 'block', 'inline-block', 'none']);
});
test('lastValues/isCompare 透传到 MContainer 与 Box', () => {
const wrapper = mount(Layout, {
props: {
values: { position: 'static', display: 'flex' },
lastValues: { position: 'relative', display: 'block' },
isCompare: true,
} as any,
});
const container = wrapper.findComponent({ name: 'FakeMContainer' });
expect(container.props('lastValues')).toEqual({ position: 'relative', display: 'block' });
expect(container.props('isCompare')).toBe(true);
const box = wrapper.findComponent({ name: 'FakeBox' });
expect(box.props('lastValues')).toEqual({ position: 'relative', display: 'block' });
expect(box.props('isCompare')).toBe(true);
});
test('未传 lastValues 时为 undefined / isCompare 默认 false', () => {
const wrapper = mount(Layout, {
props: { values: { position: 'static', display: 'flex' } } as any,
});
const container = wrapper.findComponent({ name: 'FakeMContainer' });
expect(container.props('lastValues')).toBeUndefined();
// Boolean 类型 prop 未传时 Vue 默认为 false
expect(container.props('isCompare')).toBe(false);
});
test('MContainer 的 addDiffCount 事件向上冒泡', async () => {
const wrapper = mount(Layout, {
props: { values: { position: 'static', display: 'flex' } } as any,
});
await wrapper.find('.fake-mcontainer').trigger('dblclick');
expect(wrapper.emitted('addDiffCount')).toBeTruthy();
expect(wrapper.emitted('addDiffCount')?.length).toBe(1);
});
});

View File

@ -13,8 +13,8 @@ vi.mock('@tmagic/form', () => ({
defineFormItem: (cfg: any) => cfg,
MContainer: defineComponent({
name: 'FakeMContainer',
props: ['config', 'model', 'size', 'disabled'],
emits: ['change'],
props: ['config', 'model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change', 'addDiffCount'],
setup(props, { emit }) {
return () =>
h(
@ -22,6 +22,7 @@ vi.mock('@tmagic/form', () => ({
{
class: 'fake-mcontainer',
onClick: () => emit('change', 'val', { propPath: 'p' }),
onDblclick: () => emit('addDiffCount'),
},
JSON.stringify(props.config?.items?.length || 0),
);
@ -62,4 +63,26 @@ describe('StyleSetter/Position.vue', () => {
const rowItems = config.items.filter((it: any) => it.type === 'row');
expect(rowItems[0].display()).toBe(true);
});
test('lastValues/isCompare 透传到 MContainer', () => {
const wrapper = mount(Position, {
props: {
values: { position: 'absolute' },
lastValues: { position: 'static' },
isCompare: true,
} as any,
});
const container = wrapper.findComponent({ name: 'FakeMContainer' });
expect(container.props('lastValues')).toEqual({ position: 'static' });
expect(container.props('isCompare')).toBe(true);
});
test('MContainer 的 addDiffCount 事件向上冒泡', async () => {
const wrapper = mount(Position, {
props: { values: { position: 'absolute' } } as any,
});
await wrapper.find('.fake-mcontainer').trigger('dblclick');
expect(wrapper.emitted('addDiffCount')).toBeTruthy();
expect(wrapper.emitted('addDiffCount')?.length).toBe(1);
});
});

View File

@ -13,8 +13,8 @@ vi.mock('@tmagic/form', () => ({
defineFormItem: (cfg: any) => cfg,
MContainer: defineComponent({
name: 'MContainer',
props: ['config', 'model', 'size', 'disabled'],
emits: ['change'],
props: ['config', 'model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change', 'addDiffCount'],
setup(_props, { expose }) {
expose({ trigger: () => null });
return () => h('div', { class: 'm-container' });
@ -61,4 +61,25 @@ describe('StyleSetter Border', () => {
await wrapper.find('.border-icon-right').trigger('click');
expect(wrapper.find('.border-icon-right').classes()).toContain('active');
});
test('lastValues/isCompare 透传到 MContainer', () => {
const wrapper = mount(Border, {
props: {
model: { borderWidth: '2px' },
lastValues: { borderWidth: '1px' },
isCompare: true,
} as any,
});
const container = wrapper.findComponent({ name: 'MContainer' });
expect(container.props('lastValues')).toEqual({ borderWidth: '1px' });
expect(container.props('isCompare')).toBe(true);
});
test('MContainer 的 addDiffCount 事件向上冒泡', () => {
const wrapper = mount(Border, { props: { model: {} } });
const container = wrapper.findComponent({ name: 'MContainer' });
container.vm.$emit('addDiffCount');
expect(wrapper.emitted('addDiffCount')).toBeTruthy();
expect(wrapper.emitted('addDiffCount')?.length).toBe(1);
});
});

View File

@ -17,8 +17,8 @@ vi.mock('@tmagic/form', () => ({
defineFormItem: (cfg: any) => cfg,
MContainer: defineComponent({
name: 'MContainer',
props: ['config', 'model', 'size', 'disabled'],
emits: ['change'],
props: ['config', 'model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change', 'addDiffCount'],
setup() {
return () => h('div', { class: 'm-container' });
},
@ -28,7 +28,7 @@ vi.mock('@tmagic/form', () => ({
vi.mock('@editor/fields/StyleSetter/components/Box.vue', () => ({
default: defineComponent({
name: 'StyleBox',
props: ['model', 'size', 'disabled'],
props: ['model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change'],
setup() {
return () => h('div', { class: 'fake-box' });
@ -39,8 +39,8 @@ vi.mock('@editor/fields/StyleSetter/components/Box.vue', () => ({
vi.mock('@editor/fields/StyleSetter/components/Border.vue', () => ({
default: defineComponent({
name: 'StyleBorder',
props: ['model', 'size', 'disabled'],
emits: ['change'],
props: ['model', 'lastValues', 'isCompare', 'size', 'disabled'],
emits: ['change', 'addDiffCount'],
setup() {
return () => h('div', { class: 'fake-border' });
},
@ -88,4 +88,66 @@ describe('StyleSetter pro 组件', () => {
const wrapper = mount(BorderPro, { props: { values: {} } });
expect(wrapper.find('.fake-border').exists()).toBe(true);
});
test.each([
['Background', Background],
['BorderPro', BorderPro],
['Font', Font],
['Layout', Layout],
['Transform', Transform],
])('%s 透传 lastValues/isCompare 给 MContainer', (_name, comp) => {
const wrapper = mount(comp as any, {
props: {
values: { color: 'red' },
lastValues: { color: 'blue' },
isCompare: true,
},
});
const container = wrapper.findComponent({ name: 'MContainer' });
expect(container.props('lastValues')).toEqual({ color: 'blue' });
expect(container.props('isCompare')).toBe(true);
});
test.each([
['Background', Background],
['BorderPro', BorderPro],
['Font', Font],
['Layout', Layout],
['Transform', Transform],
])('%s 冒泡 MContainer 的 addDiffCount 事件', (_name, comp) => {
const wrapper = mount(comp as any, { props: { values: {} } });
const container = wrapper.findComponent({ name: 'MContainer' });
container.vm.$emit('addDiffCount');
expect(wrapper.emitted('addDiffCount')).toBeTruthy();
expect(wrapper.emitted('addDiffCount')?.length).toBe(1);
});
test('Layout 透传 lastValues/isCompare 给 Box', () => {
const wrapper = mount(Layout, {
props: {
values: { position: 'static' } as any,
lastValues: { position: 'static' } as any,
isCompare: true,
},
});
const box = wrapper.findComponent({ name: 'StyleBox' });
expect(box.props('lastValues')).toEqual({ position: 'static' });
expect(box.props('isCompare')).toBe(true);
});
test('BorderPro 透传 lastValues/isCompare 给 Border 子组件,并冒泡其 addDiffCount', () => {
const wrapper = mount(BorderPro, {
props: {
values: { borderRadius: '4px' } as any,
lastValues: { borderRadius: '2px' } as any,
isCompare: true,
},
});
const border = wrapper.findComponent({ name: 'StyleBorder' });
expect(border.props('lastValues')).toEqual({ borderRadius: '2px' });
expect(border.props('isCompare')).toBe(true);
border.vm.$emit('addDiffCount');
expect(wrapper.emitted('addDiffCount')).toBeTruthy();
});
});