feat(editor): 补全样式面板 CSS 枚举选项并补齐图标

- textAlign/display/justifyContent/alignItems/backgroundRepeat/fontWeight/borderStyle 按 CSS 规范补齐关键字
- 新增 15 个图标组件,新增项统一为图标按钮;textAlign 去掉不等宽文案,避免换行后边框错位
- 修正 opacity 标签由「透明度(%)」改为「透明度(0-1)」
- 新增 configs.spec.ts 并更新 Layout.spec.ts / icons.spec.ts
This commit is contained in:
roymondchen 2026-11-12 20:12:56 +08:00
parent 72664955bf
commit 0e8b676fe7
24 changed files with 479 additions and 16 deletions

View File

@ -8,14 +8,35 @@ import type { validateDataSourceFieldSelect } from '@editor/utils/type-match-rul
import BackgroundPosition from './components/BackgroundPosition.vue';
import {
AlignItemsBaseline,
AlignItemsCenter,
AlignItemsEnd,
AlignItemsFlexEnd,
AlignItemsFlexStart,
AlignItemsNormal,
AlignItemsSpaceAround,
AlignItemsSpaceBetween,
AlignItemsStart,
AlignItemsStretch,
} from './icons/align-items';
import { BackgroundNoRepeat, BackgroundRepeat, BackgroundRepeatX, BackgroundRepeatY } from './icons/background-repeat';
import { DisplayBlock, DisplayFlex, DisplayInline, DisplayInlineBlock, DisplayNone } from './icons/display';
import {
BackgroundNoRepeat,
BackgroundRepeat,
BackgroundRepeatRound,
BackgroundRepeatSpace,
BackgroundRepeatX,
BackgroundRepeatY,
} from './icons/background-repeat';
import {
DisplayBlock,
DisplayFlex,
DisplayGrid,
DisplayInline,
DisplayInlineBlock,
DisplayInlineFlex,
DisplayInlineGrid,
DisplayNone,
} from './icons/display';
import {
FlexDirectionColumn,
FlexDirectionColumnReverse,
@ -28,8 +49,17 @@ import {
JustifyContentFlexStart,
JustifyContentSpaceAround,
JustifyContentSpaceBetween,
JustifyContentSpaceEvenly,
} from './icons/justify-content';
import { AlignCenter, AlignLeft, AlignRight } from './icons/text-align';
import {
AlignCenter,
AlignEnd,
AlignJustify,
AlignLeft,
AlignMatchParent,
AlignRight,
AlignStart,
} from './icons/text-align';
/**
* 「布局」面板配置。
@ -71,6 +101,9 @@ export const createLayoutConfig = (theme: string): FormConfig => [
icon: markRaw(DisplayNone),
tooltip: '隐藏 none',
},
{ value: 'inline-flex', icon: markRaw(DisplayInlineFlex), tooltip: '内联弹性布局 inline-flex' },
{ value: 'grid', icon: markRaw(DisplayGrid), tooltip: '网格布局 grid' },
{ value: 'inline-grid', icon: markRaw(DisplayInlineGrid), tooltip: '内联网格布局 inline-grid' },
],
}),
defineFormItem({
@ -121,6 +154,7 @@ export const createLayoutConfig = (theme: string): FormConfig => [
icon: markRaw(JustifyContentSpaceAround),
tooltip: '横向平分 space-around',
},
{ value: 'space-evenly', icon: markRaw(JustifyContentSpaceEvenly), tooltip: '等间距分布 space-evenly' },
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
}),
@ -145,6 +179,11 @@ export const createLayoutConfig = (theme: string): FormConfig => [
icon: markRaw(AlignItemsSpaceAround),
tooltip: '横向平分 space-around',
},
{ value: 'stretch', icon: markRaw(AlignItemsStretch), tooltip: '拉伸填满 stretch' },
{ value: 'baseline', icon: markRaw(AlignItemsBaseline), tooltip: '基线对齐 baseline' },
{ value: 'normal', icon: markRaw(AlignItemsNormal), tooltip: '默认 normal' },
{ value: 'start', icon: markRaw(AlignItemsStart), tooltip: '起始对齐 start' },
{ value: 'end', icon: markRaw(AlignItemsEnd), tooltip: '结束对齐 end' },
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
}),
@ -222,9 +261,9 @@ export const createLayoutConfig = (theme: string): FormConfig => [
items: [
{
type: 'data-source-field-select',
text: '透明度(%)',
text: '透明度(0-1)',
name: 'opacity',
labelWidth: '90px',
labelWidth: '100px',
dataSourceFieldType: ['string', 'number'],
fieldConfig: {
type: 'text',
@ -445,6 +484,8 @@ export const createBackgroundConfig = (): FormConfig => [
icon: markRaw(BackgroundRepeat),
tooltip: '垂直和水平方向重复 repeat',
},
{ value: 'space', icon: markRaw(BackgroundRepeatSpace), tooltip: '不裁剪、均匀留白 space' },
{ value: 'round', icon: markRaw(BackgroundRepeatRound), tooltip: '缩放铺满 round' },
],
}),
defineFormItem({
@ -502,9 +543,9 @@ export const createFontConfig = (validateDataSourceField: typeof validateDataSou
fieldConfig: {
type: 'select',
allowCreate: true,
options: ['normal', 'bold']
options: ['normal', 'bold', 'bolder', 'lighter']
.concat(
Array(7)
Array(9)
.fill(1)
.map((x, i) => `${i + 1}00`),
)
@ -575,10 +616,16 @@ export const createFontConfig = (validateDataSourceField: typeof validateDataSou
type: 'radioGroup',
childType: 'button',
labelWidth: '68px',
// 7 个值统一用图标按钮:带中文文案会因字数不同撑出不同宽度,换行后列线和边框错开。
// 中文说明放在 tooltip 里,与 display / justifyContent 等布局选项保持一致。
options: [
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row', text: '左对齐' },
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center', text: '居中对齐' },
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right', text: '右对齐' },
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 left' },
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center' },
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right' },
{ value: 'start', icon: markRaw(AlignStart), tooltip: '起始对齐 start' },
{ value: 'end', icon: markRaw(AlignEnd), tooltip: '结束对齐 end' },
{ value: 'justify', icon: markRaw(AlignJustify), tooltip: '两端对齐 justify' },
{ value: 'match-parent', icon: markRaw(AlignMatchParent), tooltip: '按父级方向对齐 match-parent' },
],
}),
];
@ -680,10 +727,12 @@ export const createBorderDirectionConfig = (direction: string) =>
type: 'data-source-field-select',
fieldConfig: {
type: 'select',
options: ['solid', 'dashed', 'dotted'].map((item) => ({
value: item,
text: item,
})),
options: ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'].map(
(item) => ({
value: item,
text: item,
}),
),
},
},
],

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="2" y="3" width="2.5" height="8" rx="0.5" />
<rect x="6.75" y="6" width="2.5" height="5" rx="0.5" />
<rect x="11.5" y="1" width="2.5" height="10" rx="0.5" />
<rect x="0.75" y="11.25" width="14.5" height="0.75" rx="0.375" />
</svg>
</template>

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="2" y="2" width="9" height="1" rx="0.5" />
<path d="M11 0.75L14 2.5 11 4.25Z" />
<rect x="3" y="10" width="10" height="4" rx="0.5" fill="none" stroke="currentColor" />
<rect x="1" y="14.5" width="14" height="0.5" rx="0.25" fill-opacity="0.3" />
</svg>
</template>

View File

@ -0,0 +1,6 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="1" y="1" width="14" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.3" />
<rect x="3" y="5.5" width="10" height="5" rx="0.5" fill="none" stroke="currentColor" />
</svg>
</template>

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="2" y="2" width="9" height="1" rx="0.5" />
<path d="M11 0.75L14 2.5 11 4.25Z" />
<rect x="3" y="6" width="10" height="4" rx="0.5" fill="none" stroke="currentColor" />
<rect x="1" y="5" width="14" height="0.5" rx="0.25" fill-opacity="0.3" />
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="1" y="1" width="14" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.3" />
<rect x="3" y="2" width="10" height="12" rx="0.5" fill="none" stroke="currentColor" />
<path d="M8 4.5L9.75 6.5H8.75V9.5H9.75L8 11.5 6.25 9.5H7.25V6.5H6.25Z" />
</svg>
</template>

View File

@ -1,5 +1,10 @@
export { default as AlignItemsBaseline } from './Baseline.vue';
export { default as AlignItemsCenter } from './Center.vue';
export { default as AlignItemsEnd } from './End.vue';
export { default as AlignItemsFlexEnd } from './FlexEnd.vue';
export { default as AlignItemsFlexStart } from './FlexStart.vue';
export { default as AlignItemsNormal } from './Normal.vue';
export { default as AlignItemsSpaceAround } from './SpaceAround.vue';
export { default as AlignItemsSpaceBetween } from './SpaceBetween.vue';
export { default as AlignItemsStart } from './Start.vue';
export { default as AlignItemsStretch } from './Stretch.vue';

View File

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="1" y="1" width="6.5" height="6.5" rx="0.5" />
<rect x="8.5" y="1" width="6.5" height="6.5" rx="0.5" />
<rect x="1" y="8.5" width="6.5" height="6.5" rx="0.5" />
<rect x="8.5" y="8.5" width="6.5" height="6.5" rx="0.5" />
</svg>
</template>

View File

@ -0,0 +1,9 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="0.5" y="0.5" width="15" height="15" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.3" />
<rect x="3.5" y="3.5" width="3" height="3" rx="0.5" />
<rect x="9.5" y="3.5" width="3" height="3" rx="0.5" />
<rect x="3.5" y="9.5" width="3" height="3" rx="0.5" />
<rect x="9.5" y="9.5" width="3" height="3" rx="0.5" />
</svg>
</template>

View File

@ -2,3 +2,5 @@ export { default as BackgroundRepeat } from './Repeat.vue';
export { default as BackgroundRepeatX } from './RepeatX.vue';
export { default as BackgroundRepeatY } from './RepeatY.vue';
export { default as BackgroundNoRepeat } from './NoRepeat.vue';
export { default as BackgroundRepeatRound } from './Round.vue';
export { default as BackgroundRepeatSpace } from './Space.vue';

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M960 128a64 64 0 0 1 64 64v640a64 64 0 0 1-64 64H64a64 64 0 0 1-64-64V192a64 64 0 0 1 64-64h896z m0 64H64v640h896V192zM416 256v512H368V256h48zM656 256v512H608V256h48zM768 368v48H256V368h512zM768 608v48H256V608h512z"
></path>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M960 128a64 64 0 0 1 64 64v640a64 64 0 0 1-64 64H64a64 64 0 0 1-64-64V192a64 64 0 0 1 64-64h896z m0 64H64v640h896V192zM288 400v48H128V400h160zM288 576v48H128V576h160zM896 400v48H736V400h160zM896 576v48H736V576h160zM704 320v384H320V320h384zM656 368H368v288h288V368zM448 416v192H384V416h64zM544 416v192H480V416h64zM640 416v192H576V416h64z"
></path>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M960 128a64 64 0 0 1 64 64v640a64 64 0 0 1-64 64H64a64 64 0 0 1-64-64V192a64 64 0 0 1 64-64h896z m0 64H64v640h896V192zM288 400v48H128V400h160zM288 576v48H128V576h160zM896 400v48H736V400h160zM896 576v48H736V576h160zM704 320v384H320V320h384zM656 368H368v288h288V368zM496 384v112H384V384h112zM640 384v112H528V384h112zM496 528v112H384V528h112zM640 528v112H528V528h112z"
></path>
</svg>
</template>

View File

@ -1,5 +1,8 @@
export { default as DisplayBlock } from './Block.vue';
export { default as DisplayFlex } from './Flex.vue';
export { default as DisplayGrid } from './Grid.vue';
export { default as DisplayInline } from './Inline.vue';
export { default as DisplayInlineBlock } from './InlineBlock.vue';
export { default as DisplayInlineFlex } from './InlineFlex.vue';
export { default as DisplayInlineGrid } from './InlineGrid.vue';
export { default as DisplayNone } from './None.vue';

View File

@ -0,0 +1,11 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="3" y="4.5" width="1.5" height="7" rx="0.25" fill="none" stroke="currentColor" />
<rect x="7.5" y="4.5" width="1.5" height="7" rx="0.25" fill="none" stroke="currentColor" />
<rect x="12" y="4.5" width="1.5" height="7" rx="0.25" fill="none" stroke="currentColor" />
<rect x="1.25" y="2" width="0.5" height="12" rx="0.25" fill-opacity="0.3" />
<rect x="6" y="2" width="0.5" height="12" rx="0.25" fill-opacity="0.3" />
<rect x="10.5" y="2" width="0.5" height="12" rx="0.25" fill-opacity="0.3" />
<rect x="15" y="2" width="0.5" height="12" rx="0.25" fill-opacity="0.3" />
</svg>
</template>

View File

@ -3,3 +3,4 @@ export { default as JustifyContentFlexEnd } from './FlexEnd.vue';
export { default as JustifyContentFlexStart } from './FlexStart.vue';
export { default as JustifyContentSpaceAround } from './SpaceAround.vue';
export { default as JustifyContentSpaceBetween } from './SpaceBetween.vue';
export { default as JustifyContentSpaceEvenly } from './SpaceEvenly.vue';

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M128 96h64v192H128zM224 160h544v64H224zM768 96L896 192 768 288zM256 384h640v64H256zM128 512h768v64H128zM384 640h512v64H384zM192 768h704v64H192z"
></path>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M128 128h768v85.333333H128z m0 170.666667h768v85.333333H128z m0 170.666666h768v85.333334H128z m0 170.666667h768v85.333333H128z m0 170.666667h768v85.333333H128z"
></path>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M64 64h896v48H64z m0 848h896v48H64z m0-800h48v800H64z m848 0h48v800h-48zM192 208h416v64H192z m416-48L768 240 608 320zM464 368h96v96h-96z m-64 96h224l-112 128zM192 608h640v56H192z m0 112h512v56H192z m0 112h576v56H192z"
></path>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path
d="M128 96h64v192H128zM224 160h544v64H224zM768 96L896 192 768 288zM128 384h640v64H128zM128 512h768v64H128zM128 640h512v64H128zM128 768h704v64H128z"
></path>
</svg>
</template>

View File

@ -1,3 +1,7 @@
export { default as AlignLeft } from './Left.vue';
export { default as AlignCenter } from './Center.vue';
export { default as AlignRight } from './Right.vue';
export { default as AlignEnd } from './End.vue';
export { default as AlignJustify } from './Justify.vue';
export { default as AlignMatchParent } from './MatchParent.vue';
export { default as AlignStart } from './Start.vue';

View File

@ -103,14 +103,14 @@ describe('StyleSetter/Layout.vue', () => {
});
});
test('display 选项含 inline/flex/block/inline-block/none', () => {
test('display 选项含常用布局模式', () => {
const wrapper = mount(Layout, {
props: { values: { position: 'static', display: 'flex' } } as any,
});
const configs = wrapper.findAllComponents({ name: 'FakeMContainer' }).map((c) => c.props('config') as any);
const displayItem = configs.find((it: any) => it.name === 'display');
const values = displayItem.options.map((o: any) => o.value);
expect(values).toEqual(['inline', 'flex', 'block', 'inline-block', 'none']);
expect(values).toEqual(['inline', 'flex', 'block', 'inline-block', 'none', 'inline-flex', 'grid', 'inline-grid']);
});
test('lastValues/isCompare 透传到 MContainer 与 Box', () => {

View File

@ -0,0 +1,255 @@
/*
* 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 { describe, expect, test, vi } from 'vitest';
import {
borderRadiusConfig,
createBackgroundConfig,
createBorderDirectionConfig,
createFontConfig,
createLayoutConfig,
createPositionConfig,
createStyleSetterConfig,
transformConfig,
} from '@editor/fields/StyleSetter/configs';
type Item = Record<string, any>;
const find = (config: Item[], name: string): Item => {
const item = config.find((it) => it.name === name);
if (!item) throw new Error(`missing config: ${name}`);
return item;
};
const values = (options: Item[] = []) => options.map((option) => option.value);
describe('StyleSetter configs CSS 关键字', () => {
const validateDataSourceField = vi.fn(() => undefined);
const layout = createLayoutConfig('default') as Item[];
const background = createBackgroundConfig() as Item[];
const font = createFontConfig(validateDataSourceField as any) as Item[];
test('textAlign 覆盖 CSS 对齐关键字', () => {
expect(values(find(font, 'textAlign').options)).toEqual([
'left',
'center',
'right',
'start',
'end',
'justify',
'match-parent',
]);
// 只保留图标,避免字数不同的文案撑出不等宽按钮、换行后边框错位;中文说明留在 tooltip
const { options } = find(font, 'textAlign');
expect(options.every((option: Item) => Boolean(option.icon) && !option.text && Boolean(option.tooltip))).toBe(true);
});
test('display 覆盖常用布局模式且每项都有图标', () => {
expect(find(layout, 'display').options.every((option: Item) => Boolean(option.icon))).toBe(true);
expect(values(find(layout, 'display').options)).toEqual([
'inline',
'flex',
'block',
'inline-block',
'none',
'inline-flex',
'grid',
'inline-grid',
]);
});
test('新增的对齐 / 重复选项都使用图标按钮', () => {
const fields: [Item[], string][] = [
[layout, 'justifyContent'],
[layout, 'alignItems'],
[background, 'backgroundRepeat'],
[font, 'textAlign'],
];
fields.forEach(([config, name]) => {
expect(find(config, name).options.every((option: Item) => Boolean(option.icon))).toBe(true);
});
});
test('justifyContent 补 space-evenly,alignItems 补合法对齐值', () => {
expect(values(find(layout, 'justifyContent').options)).toEqual([
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly',
]);
expect(values(find(layout, 'alignItems').options)).toEqual([
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'stretch',
'baseline',
'normal',
'start',
'end',
]);
});
test('flexDirection / flexWrap 保持完整', () => {
expect(values(find(layout, 'flexDirection').options)).toEqual(['row', 'row-reverse', 'column', 'column-reverse']);
expect(values(find(layout, 'flexWrap').options)).toEqual(['nowrap', 'wrap', 'wrap-reverse']);
});
test('backgroundRepeat 补 space / round', () => {
expect(values(find(background, 'backgroundRepeat').options)).toEqual([
'no-repeat',
'repeat-x',
'repeat-y',
'repeat',
'space',
'round',
]);
expect(values(find(background, 'backgroundSize').options)).toEqual(['auto', 'contain', 'cover']);
});
test('fontWeight 覆盖 100-900 与相对关键字', () => {
expect(values(find(font, 'fontWeight').fieldConfig.options)).toEqual([
'normal',
'bold',
'bolder',
'lighter',
...Array(9)
.fill(1)
.map((_x, i) => `${i + 1}00`),
]);
});
test('borderStyle 覆盖全部 CSS 线型', () => {
const border = createBorderDirectionConfig('') as Item;
const styleItem = border.items.find((it: Item) => it.name === 'borderStyle');
expect(values(styleItem.fieldConfig.options)).toEqual([
'none',
'hidden',
'dotted',
'dashed',
'solid',
'double',
'groove',
'ridge',
'inset',
'outset',
]);
});
test('overflow / position 的关键字保持完整', () => {
const overflow = layout.find((it) => it.type === 'row' && it.items?.[0]?.name === 'overflow');
expect(values(overflow.items[0].fieldConfig.options)).toEqual([
'visible',
'hidden',
'clip',
'scroll',
'auto',
'overlay',
'initial',
]);
const position = createPositionConfig({ position: 'static' }) as Item[];
expect(values(find(position, 'position').fieldConfig.options)).toEqual([
'static',
'relative',
'absolute',
'fixed',
'sticky',
]);
});
test('flex 相关字段仅在 display 为 flex 时展示', () => {
['flexDirection', 'justifyContent', 'alignItems', 'flexWrap'].forEach((name) => {
expect(find(layout, name).display(null, { model: { display: 'flex' } })).toBe(true);
expect(find(layout, name).display(null, { model: { display: 'block' } })).toBe(false);
});
});
test('position 为 static 时隐藏方位字段', () => {
const position = createPositionConfig({ position: 'static' }) as Item[];
expect(position.filter((it) => it.type === 'row').every((it) => it.display() === false)).toBe(true);
});
test('backgroundSize 校验覆盖空值、关键字、多值与非法值', () => {
const { validator } = find(background, 'backgroundSize').rules[1];
const check = (value: any) => {
const callback = vi.fn();
validator({ value, callback }, {}, null);
return callback;
};
expect(check('')).toHaveBeenCalledWith();
expect(check(null)).toHaveBeenCalledWith();
expect(check('cover')).toHaveBeenCalledWith();
expect(check('inherit')).toHaveBeenCalledWith();
expect(check('100px 50%')).toHaveBeenCalledWith();
expect(check('cover contain')).toHaveBeenCalledWith('cover/contain 不能与其他值组合');
expect(check('10px 20px 30px')).toHaveBeenCalledWith('backgroundSize 最多支持两个值');
expect(check('foo')).toHaveBeenCalledWith('backgroundSize 值不合法');
});
test('fontWeight 校验委托给 validateDataSourceField 并校验普通值', () => {
const validate = vi.fn((value, _ctx, opts) => opts.validatePlainValue(value));
const { validator } = find(createFontConfig(validate as any) as Item[], 'fontWeight').rules[1];
const ctx = { config: {}, model: {}, prop: '' };
const check = (value: any) => {
const callback = vi.fn();
validator({ value, callback }, ctx, null);
return callback;
};
expect(check('')).toHaveBeenCalledWith();
expect(check('700')).toHaveBeenCalledWith(undefined);
expect(check(700)).toHaveBeenCalledWith(undefined);
expect(check({})).toHaveBeenCalledWith('字重应为字符串或数字');
expect(validate).toHaveBeenCalledTimes(3);
});
test('fontWeight 校验支持异步校验结果', async () => {
const validate = vi.fn().mockResolvedValueOnce(undefined).mockRejectedValueOnce('数据源错误');
const { validator } = find(createFontConfig(validate as any) as Item[], 'fontWeight').rules[1];
const ctx = { config: {}, model: {}, prop: '' };
const resolved = vi.fn();
validator({ value: '700', callback: resolved }, ctx, null);
await vi.waitFor(() => expect(resolved).toHaveBeenCalledWith(undefined));
const rejected = vi.fn();
validator({ value: '700', callback: rejected }, ctx, null);
await vi.waitFor(() => expect(rejected).toHaveBeenCalledWith('数据源错误'));
});
test('合成配置按面板顺序包含各字段', () => {
const config = createStyleSetterConfig({}, 'default', validateDataSourceField as any) as Item[];
const names = config.flatMap((it) => (it.name ? [it.name] : (it.items || []).map((sub: Item) => sub.name)));
expect(names).toContain('display');
expect(names).toContain('opacity');
expect(names).toContain('position');
expect(names).toContain('backgroundRepeat');
expect(names).toContain('fontWeight');
expect(names).toContain('textAlign');
expect(borderRadiusConfig.name).toBe('borderRadius');
expect(transformConfig.items.map((it: Item) => it.name)).toEqual(['rotate', 'scale']);
});
});

View File

@ -6,6 +6,11 @@
import { describe, expect, test } from 'vitest';
import { mount } from '@vue/test-utils';
import AiBaseline from '@editor/fields/StyleSetter/icons/align-items/Baseline.vue';
import AiEnd from '@editor/fields/StyleSetter/icons/align-items/End.vue';
import AiNormal from '@editor/fields/StyleSetter/icons/align-items/Normal.vue';
import AiStart from '@editor/fields/StyleSetter/icons/align-items/Start.vue';
import AiStretch from '@editor/fields/StyleSetter/icons/align-items/Stretch.vue';
import BgPosLeftBottom from '@editor/fields/StyleSetter/icons/background-position/LeftBottom.vue';
import BgPosLeftCenter from '@editor/fields/StyleSetter/icons/background-position/LeftCenter.vue';
import BgPosLeftTop from '@editor/fields/StyleSetter/icons/background-position/LeftTop.vue';
@ -13,10 +18,15 @@ import NoRepeat from '@editor/fields/StyleSetter/icons/background-repeat/NoRepea
import Repeat from '@editor/fields/StyleSetter/icons/background-repeat/Repeat.vue';
import RepeatX from '@editor/fields/StyleSetter/icons/background-repeat/RepeatX.vue';
import RepeatY from '@editor/fields/StyleSetter/icons/background-repeat/RepeatY.vue';
import BgRound from '@editor/fields/StyleSetter/icons/background-repeat/Round.vue';
import BgSpace from '@editor/fields/StyleSetter/icons/background-repeat/Space.vue';
import DisplayBlock from '@editor/fields/StyleSetter/icons/display/Block.vue';
import DisplayFlex from '@editor/fields/StyleSetter/icons/display/Flex.vue';
import DisplayGrid from '@editor/fields/StyleSetter/icons/display/Grid.vue';
import DisplayInline from '@editor/fields/StyleSetter/icons/display/Inline.vue';
import DisplayInlineBlock from '@editor/fields/StyleSetter/icons/display/InlineBlock.vue';
import DisplayInlineFlex from '@editor/fields/StyleSetter/icons/display/InlineFlex.vue';
import DisplayInlineGrid from '@editor/fields/StyleSetter/icons/display/InlineGrid.vue';
import DisplayNone from '@editor/fields/StyleSetter/icons/display/None.vue';
import FdColumn from '@editor/fields/StyleSetter/icons/flex-direction/Column.vue';
import FdColumnReverse from '@editor/fields/StyleSetter/icons/flex-direction/ColumnReverse.vue';
@ -27,12 +37,22 @@ import JcFlexEnd from '@editor/fields/StyleSetter/icons/justify-content/FlexEnd.
import JcFlexStart from '@editor/fields/StyleSetter/icons/justify-content/FlexStart.vue';
import JcSpaceAround from '@editor/fields/StyleSetter/icons/justify-content/SpaceAround.vue';
import JcSpaceBetween from '@editor/fields/StyleSetter/icons/justify-content/SpaceBetween.vue';
import JcSpaceEvenly from '@editor/fields/StyleSetter/icons/justify-content/SpaceEvenly.vue';
import TaCenter from '@editor/fields/StyleSetter/icons/text-align/Center.vue';
import TaEnd from '@editor/fields/StyleSetter/icons/text-align/End.vue';
import TaJustify from '@editor/fields/StyleSetter/icons/text-align/Justify.vue';
import TaLeft from '@editor/fields/StyleSetter/icons/text-align/Left.vue';
import TaMatchParent from '@editor/fields/StyleSetter/icons/text-align/MatchParent.vue';
import TaRight from '@editor/fields/StyleSetter/icons/text-align/Right.vue';
import TaStart from '@editor/fields/StyleSetter/icons/text-align/Start.vue';
describe('StyleSetter icons', () => {
const icons = [
['AiBaseline', AiBaseline],
['AiEnd', AiEnd],
['AiNormal', AiNormal],
['AiStart', AiStart],
['AiStretch', AiStretch],
['BgPosLeftBottom', BgPosLeftBottom],
['BgPosLeftCenter', BgPosLeftCenter],
['BgPosLeftTop', BgPosLeftTop],
@ -40,10 +60,15 @@ describe('StyleSetter icons', () => {
['Repeat', Repeat],
['RepeatX', RepeatX],
['RepeatY', RepeatY],
['BgRound', BgRound],
['BgSpace', BgSpace],
['DisplayBlock', DisplayBlock],
['DisplayFlex', DisplayFlex],
['DisplayGrid', DisplayGrid],
['DisplayInline', DisplayInline],
['DisplayInlineBlock', DisplayInlineBlock],
['DisplayInlineFlex', DisplayInlineFlex],
['DisplayInlineGrid', DisplayInlineGrid],
['DisplayNone', DisplayNone],
['FdColumn', FdColumn],
['FdColumnReverse', FdColumnReverse],
@ -54,9 +79,14 @@ describe('StyleSetter icons', () => {
['JcFlexStart', JcFlexStart],
['JcSpaceAround', JcSpaceAround],
['JcSpaceBetween', JcSpaceBetween],
['JcSpaceEvenly', JcSpaceEvenly],
['TaCenter', TaCenter],
['TaEnd', TaEnd],
['TaJustify', TaJustify],
['TaLeft', TaLeft],
['TaMatchParent', TaMatchParent],
['TaRight', TaRight],
['TaStart', TaStart],
];
test.each(icons)('%s 渲染 svg', (_name, comp) => {