From 0e8b676fe7e4aacbce8036db70cf8f4ddb96e22e Mon Sep 17 00:00:00 2001 From: roymondchen Date: Thu, 12 Nov 2026 20:12:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E8=A1=A5=E5=85=A8=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E9=9D=A2=E6=9D=BF=20CSS=20=E6=9E=9A=E4=B8=BE=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E5=B9=B6=E8=A1=A5=E9=BD=90=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - textAlign/display/justifyContent/alignItems/backgroundRepeat/fontWeight/borderStyle 按 CSS 规范补齐关键字 - 新增 15 个图标组件,新增项统一为图标按钮;textAlign 去掉不等宽文案,避免换行后边框错位 - 修正 opacity 标签由「透明度(%)」改为「透明度(0-1)」 - 新增 configs.spec.ts 并更新 Layout.spec.ts / icons.spec.ts --- .../editor/src/fields/StyleSetter/configs.ts | 77 +++++- .../icons/align-items/Baseline.vue | 8 + .../StyleSetter/icons/align-items/End.vue | 8 + .../StyleSetter/icons/align-items/Normal.vue | 6 + .../StyleSetter/icons/align-items/Start.vue | 8 + .../StyleSetter/icons/align-items/Stretch.vue | 7 + .../StyleSetter/icons/align-items/index.ts | 5 + .../icons/background-repeat/Round.vue | 8 + .../icons/background-repeat/Space.vue | 9 + .../icons/background-repeat/index.ts | 2 + .../fields/StyleSetter/icons/display/Grid.vue | 7 + .../StyleSetter/icons/display/InlineFlex.vue | 7 + .../StyleSetter/icons/display/InlineGrid.vue | 7 + .../fields/StyleSetter/icons/display/index.ts | 3 + .../icons/justify-content/SpaceEvenly.vue | 11 + .../icons/justify-content/index.ts | 1 + .../StyleSetter/icons/text-align/End.vue | 7 + .../StyleSetter/icons/text-align/Justify.vue | 7 + .../icons/text-align/MatchParent.vue | 7 + .../StyleSetter/icons/text-align/Start.vue | 7 + .../StyleSetter/icons/text-align/index.ts | 4 + .../unit/fields/StyleSetter/Layout.spec.ts | 4 +- .../unit/fields/StyleSetter/configs.spec.ts | 255 ++++++++++++++++++ .../unit/fields/StyleSetter/icons.spec.ts | 30 +++ 24 files changed, 479 insertions(+), 16 deletions(-) create mode 100644 packages/editor/src/fields/StyleSetter/icons/align-items/Baseline.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/align-items/End.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/align-items/Normal.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/align-items/Start.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/align-items/Stretch.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/background-repeat/Round.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/background-repeat/Space.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/display/Grid.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/display/InlineFlex.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/display/InlineGrid.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/justify-content/SpaceEvenly.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/text-align/End.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/text-align/Justify.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/text-align/MatchParent.vue create mode 100644 packages/editor/src/fields/StyleSetter/icons/text-align/Start.vue create mode 100644 packages/editor/tests/unit/fields/StyleSetter/configs.spec.ts diff --git a/packages/editor/src/fields/StyleSetter/configs.ts b/packages/editor/src/fields/StyleSetter/configs.ts index 69f43086..23fb529e 100644 --- a/packages/editor/src/fields/StyleSetter/configs.ts +++ b/packages/editor/src/fields/StyleSetter/configs.ts @@ -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 }) => 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 }) => 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, + }), + ), }, }, ], diff --git a/packages/editor/src/fields/StyleSetter/icons/align-items/Baseline.vue b/packages/editor/src/fields/StyleSetter/icons/align-items/Baseline.vue new file mode 100644 index 00000000..e7047fa8 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/align-items/Baseline.vue @@ -0,0 +1,8 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/align-items/End.vue b/packages/editor/src/fields/StyleSetter/icons/align-items/End.vue new file mode 100644 index 00000000..9b599105 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/align-items/End.vue @@ -0,0 +1,8 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/align-items/Normal.vue b/packages/editor/src/fields/StyleSetter/icons/align-items/Normal.vue new file mode 100644 index 00000000..25899a7c --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/align-items/Normal.vue @@ -0,0 +1,6 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/align-items/Start.vue b/packages/editor/src/fields/StyleSetter/icons/align-items/Start.vue new file mode 100644 index 00000000..fe8acab7 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/align-items/Start.vue @@ -0,0 +1,8 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/align-items/Stretch.vue b/packages/editor/src/fields/StyleSetter/icons/align-items/Stretch.vue new file mode 100644 index 00000000..bd3b86a0 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/align-items/Stretch.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/align-items/index.ts b/packages/editor/src/fields/StyleSetter/icons/align-items/index.ts index 73bc8a2c..e5f0890e 100644 --- a/packages/editor/src/fields/StyleSetter/icons/align-items/index.ts +++ b/packages/editor/src/fields/StyleSetter/icons/align-items/index.ts @@ -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'; diff --git a/packages/editor/src/fields/StyleSetter/icons/background-repeat/Round.vue b/packages/editor/src/fields/StyleSetter/icons/background-repeat/Round.vue new file mode 100644 index 00000000..94196480 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/background-repeat/Round.vue @@ -0,0 +1,8 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/background-repeat/Space.vue b/packages/editor/src/fields/StyleSetter/icons/background-repeat/Space.vue new file mode 100644 index 00000000..de1f8f36 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/background-repeat/Space.vue @@ -0,0 +1,9 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/background-repeat/index.ts b/packages/editor/src/fields/StyleSetter/icons/background-repeat/index.ts index 9f1712d4..94402de3 100644 --- a/packages/editor/src/fields/StyleSetter/icons/background-repeat/index.ts +++ b/packages/editor/src/fields/StyleSetter/icons/background-repeat/index.ts @@ -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'; diff --git a/packages/editor/src/fields/StyleSetter/icons/display/Grid.vue b/packages/editor/src/fields/StyleSetter/icons/display/Grid.vue new file mode 100644 index 00000000..f319a75a --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/display/Grid.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/display/InlineFlex.vue b/packages/editor/src/fields/StyleSetter/icons/display/InlineFlex.vue new file mode 100644 index 00000000..7519100b --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/display/InlineFlex.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/display/InlineGrid.vue b/packages/editor/src/fields/StyleSetter/icons/display/InlineGrid.vue new file mode 100644 index 00000000..02c11df9 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/display/InlineGrid.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/display/index.ts b/packages/editor/src/fields/StyleSetter/icons/display/index.ts index 566ad4a5..27382b2a 100644 --- a/packages/editor/src/fields/StyleSetter/icons/display/index.ts +++ b/packages/editor/src/fields/StyleSetter/icons/display/index.ts @@ -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'; diff --git a/packages/editor/src/fields/StyleSetter/icons/justify-content/SpaceEvenly.vue b/packages/editor/src/fields/StyleSetter/icons/justify-content/SpaceEvenly.vue new file mode 100644 index 00000000..ca78302b --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/justify-content/SpaceEvenly.vue @@ -0,0 +1,11 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/justify-content/index.ts b/packages/editor/src/fields/StyleSetter/icons/justify-content/index.ts index fbc5431b..73fbb551 100644 --- a/packages/editor/src/fields/StyleSetter/icons/justify-content/index.ts +++ b/packages/editor/src/fields/StyleSetter/icons/justify-content/index.ts @@ -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'; diff --git a/packages/editor/src/fields/StyleSetter/icons/text-align/End.vue b/packages/editor/src/fields/StyleSetter/icons/text-align/End.vue new file mode 100644 index 00000000..ed68a5e4 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/text-align/End.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/text-align/Justify.vue b/packages/editor/src/fields/StyleSetter/icons/text-align/Justify.vue new file mode 100644 index 00000000..f8e2f978 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/text-align/Justify.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/text-align/MatchParent.vue b/packages/editor/src/fields/StyleSetter/icons/text-align/MatchParent.vue new file mode 100644 index 00000000..8c7f8ba9 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/text-align/MatchParent.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/text-align/Start.vue b/packages/editor/src/fields/StyleSetter/icons/text-align/Start.vue new file mode 100644 index 00000000..0303b649 --- /dev/null +++ b/packages/editor/src/fields/StyleSetter/icons/text-align/Start.vue @@ -0,0 +1,7 @@ + diff --git a/packages/editor/src/fields/StyleSetter/icons/text-align/index.ts b/packages/editor/src/fields/StyleSetter/icons/text-align/index.ts index 92af3aa4..d1b053ab 100644 --- a/packages/editor/src/fields/StyleSetter/icons/text-align/index.ts +++ b/packages/editor/src/fields/StyleSetter/icons/text-align/index.ts @@ -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'; diff --git a/packages/editor/tests/unit/fields/StyleSetter/Layout.spec.ts b/packages/editor/tests/unit/fields/StyleSetter/Layout.spec.ts index 9ebed40e..527da02e 100644 --- a/packages/editor/tests/unit/fields/StyleSetter/Layout.spec.ts +++ b/packages/editor/tests/unit/fields/StyleSetter/Layout.spec.ts @@ -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', () => { diff --git a/packages/editor/tests/unit/fields/StyleSetter/configs.spec.ts b/packages/editor/tests/unit/fields/StyleSetter/configs.spec.ts new file mode 100644 index 00000000..8ba84b91 --- /dev/null +++ b/packages/editor/tests/unit/fields/StyleSetter/configs.spec.ts @@ -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; + +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']); + }); +}); diff --git a/packages/editor/tests/unit/fields/StyleSetter/icons.spec.ts b/packages/editor/tests/unit/fields/StyleSetter/icons.spec.ts index 29685989..26db1f08 100644 --- a/packages/editor/tests/unit/fields/StyleSetter/icons.spec.ts +++ b/packages/editor/tests/unit/fields/StyleSetter/icons.spec.ts @@ -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) => {