From 8433875ad8013d2dbefb3367b366ebe5f73e2b94 Mon Sep 17 00:00:00 2001
From: ZhouWei <1244620067@qq.com>
Date: Tue, 12 Aug 2025 10:00:55 +0800
Subject: [PATCH 1/2] refactor(flex): use SFC
---
apps/playground/src/pages/flex/basic.vue | 93 ++++++++++++++++++
packages/ui/src/components/flex/Flex.vue | 42 ++++++++
packages/ui/src/components/flex/index.ts | 14 +++
packages/ui/src/components/flex/meta.ts | 20 ++++
.../ui/src/components/flex/style/index.css | 95 +++++++++++++++++++
packages/ui/src/components/flex/utils.ts | 70 ++++++++++++++
packages/ui/src/components/index.ts | 1 +
packages/ui/src/utils/classNames.ts | 27 ++++++
packages/ui/src/utils/gapSize.ts | 13 +++
packages/ui/src/utils/util.ts | 4 +
10 files changed, 379 insertions(+)
create mode 100644 apps/playground/src/pages/flex/basic.vue
create mode 100644 packages/ui/src/components/flex/Flex.vue
create mode 100644 packages/ui/src/components/flex/index.ts
create mode 100644 packages/ui/src/components/flex/meta.ts
create mode 100644 packages/ui/src/components/flex/style/index.css
create mode 100644 packages/ui/src/components/flex/utils.ts
create mode 100644 packages/ui/src/utils/classNames.ts
create mode 100644 packages/ui/src/utils/gapSize.ts
create mode 100644 packages/ui/src/utils/util.ts
diff --git a/apps/playground/src/pages/flex/basic.vue b/apps/playground/src/pages/flex/basic.vue
new file mode 100644
index 000000000..32c06fccb
--- /dev/null
+++ b/apps/playground/src/pages/flex/basic.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+ Primary
+ Primary
+ Primary
+ Primary
+
+
+
+
+
+ Primary
+ Default
+ Dashed
+ Link
+
+
+
+
+
+ Button
+
+
+
+
+
diff --git a/packages/ui/src/components/flex/Flex.vue b/packages/ui/src/components/flex/Flex.vue
new file mode 100644
index 000000000..4aeb22e50
--- /dev/null
+++ b/packages/ui/src/components/flex/Flex.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/flex/index.ts b/packages/ui/src/components/flex/index.ts
new file mode 100644
index 000000000..c6b83d076
--- /dev/null
+++ b/packages/ui/src/components/flex/index.ts
@@ -0,0 +1,14 @@
+import { App, Plugin } from 'vue'
+import Flex from './Flex.vue'
+import './style/index.css'
+
+
+export { default as Flex } from './Flex.vue'
+export * from './meta'
+
+Flex.install = function (app: App) {
+ app.component('AFlex', Flex)
+ return app
+}
+
+export default Flex as typeof Flex & Plugin
diff --git a/packages/ui/src/components/flex/meta.ts b/packages/ui/src/components/flex/meta.ts
new file mode 100644
index 000000000..dc7d8ed54
--- /dev/null
+++ b/packages/ui/src/components/flex/meta.ts
@@ -0,0 +1,20 @@
+import { CSSProperties } from "vue"
+
+type SizeType = 'small' | 'middle' | 'large' | undefined
+
+export type FlexProps = {
+ prefixCls?: string
+ rootClassName?: string
+ vertical?: boolean
+ wrap?: CSSProperties['flexWrap'] | boolean
+ justify?: CSSProperties['justifyContent']
+ align?: CSSProperties['alignItems']
+ flex?: CSSProperties['flex']
+ gap?: CSSProperties['gap'] | SizeType
+ componentTag?: any
+}
+
+export const flexDefaultProps = {
+ prefixCls: 'ant-flex',
+ componentTag: 'div',
+} as const
diff --git a/packages/ui/src/components/flex/style/index.css b/packages/ui/src/components/flex/style/index.css
new file mode 100644
index 000000000..b7bcf7ac6
--- /dev/null
+++ b/packages/ui/src/components/flex/style/index.css
@@ -0,0 +1,95 @@
+@reference '../../../style/tailwind.css';
+
+.ant-flex {
+ @apply flex;
+ @apply m-0;
+ @apply p-0;
+ &:where(.ant-flex-vertical) {
+ @apply flex-col;
+ }
+ &:where(.ant-flex-rtl) {
+ @apply flex-row-reverse;
+ }
+ /* gap */
+ &:where(.ant-flex-gap-small) {
+ @apply gap-[8px];
+ }
+ &:where(.ant-flex-gap-middle) {
+ @apply gap-[16px];
+ }
+ &:where(.ant-flex-gap-large) {
+ @apply gap-[32px];
+ }
+ /* wrap */
+ &:where(.ant-flex-wrap-wrap) {
+ @apply flex-wrap;
+ }
+ &:where(.ant-flex-wrap-nowrap) {
+ @apply flex-nowrap;
+ }
+ &:where(.ant-flex-wrap-wrap-reverse) {
+ @apply flex-wrap-reverse;
+ }
+ /* align */
+ &:where(.ant-flex-align-center) {
+ @apply items-center;
+ }
+ &:where(.ant-flex-align-start) {
+ @apply items-start;
+ }
+ &:where(.ant-flex-align-end) {
+ @apply items-end;
+ }
+ &:where(.ant-flex-align-flex-start) {
+ @apply items-start;
+ }
+ &:where(.ant-flex-align-flex-end) {
+ @apply items-end;
+ }
+ &:where(.ant-flex-align-self-start) {
+ @apply self-start;
+ }
+ &:where(.ant-flex-align-self-end) {
+ @apply self-end;
+ }
+ &:where(.ant-flex-align-baseline) {
+ @apply items-baseline;
+ }
+ &:where(.ant-flex-align-normal) {
+ @apply content-normal;
+ }
+ &:where(.ant-flex-align-stretch) {
+ @apply items-stretch;
+ }
+ /* justify */
+ &:where(.ant-flex-justify-flex-start) {
+ @apply justify-start;
+ }
+ &:where(.ant-flex-justify-flex-end) {
+ @apply justify-end;
+ }
+ &:where(.ant-flex-justify-start) {
+ @apply justify-start;
+ }
+ &:where(.ant-flex-justify-end) {
+ @apply justify-end;
+ }
+ &:where(.ant-flex-justify-center) {
+ @apply justify-center;
+ }
+ &:where(.ant-flex-justify-space-between) {
+ @apply justify-between;
+ }
+ &:where(.ant-flex-justify-space-around) {
+ @apply justify-around;
+ }
+ &:where(.ant-flex-justify-space-evenly) {
+ @apply justify-evenly;
+ }
+ &:where(.ant-flex-justify-stretch) {
+ @apply justify-stretch;
+ }
+ &:where(.ant-flex-justify-normal) {
+ @apply justify-normal;
+ }
+}
diff --git a/packages/ui/src/components/flex/utils.ts b/packages/ui/src/components/flex/utils.ts
new file mode 100644
index 000000000..d3de11011
--- /dev/null
+++ b/packages/ui/src/components/flex/utils.ts
@@ -0,0 +1,70 @@
+import classNames from '../../utils/classNames'
+
+import type { FlexProps } from './meta'
+
+export const flexWrapValues = ['wrap', 'nowrap', 'wrap-reverse'] as const
+
+export const justifyContentValues = [
+ 'flex-start',
+ 'flex-end',
+ 'start',
+ 'end',
+ 'center',
+ 'space-between',
+ 'space-around',
+ 'space-evenly',
+ 'stretch',
+ 'normal',
+ 'left',
+ 'right',
+] as const
+
+export const alignItemsValues = [
+ 'center',
+ 'start',
+ 'end',
+ 'flex-start',
+ 'flex-end',
+ 'self-start',
+ 'self-end',
+ 'baseline',
+ 'normal',
+ 'stretch',
+] as const
+
+const genClsWrap = (prefixCls: string, props: FlexProps) => {
+ const wrapCls: Record = {}
+ flexWrapValues.forEach(cssKey => {
+ // Handle both boolean attribute (wrap="wrap") and string value (wrap="wrap")
+ const isMatch = props.wrap === true && cssKey === 'wrap' || props.wrap === cssKey
+ wrapCls[`${prefixCls}-wrap-${cssKey}`] = isMatch
+ })
+ return wrapCls
+}
+
+const genClsAlign = (prefixCls: string, props: FlexProps) => {
+ const alignCls: Record = {}
+ alignItemsValues.forEach(cssKey => {
+ alignCls[`${prefixCls}-align-${cssKey}`] = props.align === cssKey
+ })
+ alignCls[`${prefixCls}-align-stretch`] = !props.align && !!props.vertical
+ return alignCls
+}
+
+const genClsJustify = (prefixCls: string, props: FlexProps) => {
+ const justifyCls: Record = {}
+ justifyContentValues.forEach(cssKey => {
+ justifyCls[`${prefixCls}-justify-${cssKey}`] = props.justify === cssKey
+ })
+ return justifyCls
+}
+
+function createFlexClassNames(prefixCls: string, props: FlexProps) {
+ return classNames({
+ ...genClsWrap(prefixCls, props),
+ ...genClsAlign(prefixCls, props),
+ ...genClsJustify(prefixCls, props),
+ })
+}
+
+export default createFlexClassNames
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 6bb401da3..a2778c066 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -2,3 +2,4 @@ export { default as Button } from './button'
export { default as Input } from './input'
export { default as Theme } from './theme'
export { default as Affix } from './affix'
+export { default as Flex } from './flex'
diff --git a/packages/ui/src/utils/classNames.ts b/packages/ui/src/utils/classNames.ts
new file mode 100644
index 000000000..4ccf120db
--- /dev/null
+++ b/packages/ui/src/utils/classNames.ts
@@ -0,0 +1,27 @@
+import { isArray, isString, isObject } from './util'
+function classNames(...args: any[]) {
+ const classes = []
+ for (let i = 0; i < args.length; i++) {
+ const value = args[i]
+ if (!value) continue
+ if (isString(value)) {
+ classes.push(value)
+ } else if (isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ const inner = classNames(value[i])
+ if (inner) {
+ classes.push(inner)
+ }
+ }
+ } else if (isObject(value)) {
+ for (const name in value) {
+ if (value[name]) {
+ classes.push(name)
+ }
+ }
+ }
+ }
+ return classes.join(' ')
+}
+
+export default classNames
diff --git a/packages/ui/src/utils/gapSize.ts b/packages/ui/src/utils/gapSize.ts
new file mode 100644
index 000000000..c9a40af95
--- /dev/null
+++ b/packages/ui/src/utils/gapSize.ts
@@ -0,0 +1,13 @@
+export type SizeType = 'small' | 'middle' | 'large' | undefined
+
+export function isPresetSize(size?: SizeType | string | number): size is SizeType {
+ return ['small', 'middle', 'large'].includes(size as string)
+}
+
+export function isValidGapNumber(size?: SizeType | string | number): size is number {
+ if (!size) {
+ // The case of size = 0 is deliberately excluded here, because the default value of the gap attribute in CSS is 0, so if the user passes 0 in, we can directly ignore it.
+ return false
+ }
+ return typeof size === 'number' && !Number.isNaN(size)
+}
diff --git a/packages/ui/src/utils/util.ts b/packages/ui/src/utils/util.ts
new file mode 100644
index 000000000..ba253242f
--- /dev/null
+++ b/packages/ui/src/utils/util.ts
@@ -0,0 +1,4 @@
+export const isArray = Array.isArray
+export const isString = val => typeof val === 'string'
+export const isSymbol = val => typeof val === 'symbol'
+export const isObject = val => val !== null && typeof val === 'object'
From d879392106d6b50b782c09c3f8b3cf227cacd433 Mon Sep 17 00:00:00 2001
From: ZhouWei <1244620067@qq.com>
Date: Wed, 13 Aug 2025 11:43:24 +0800
Subject: [PATCH 2/2] test: add unit tests
---
.../__snapshots__/index.test.ts.snap | 7 +
.../components/flex/__tests__/index.test.ts | 149 ++++++++++++++++++
2 files changed, 156 insertions(+)
create mode 100644 packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap
create mode 100644 packages/ui/src/components/flex/__tests__/index.test.ts
diff --git a/packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap b/packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap
new file mode 100644
index 000000000..048662e1a
--- /dev/null
+++ b/packages/ui/src/components/flex/__tests__/__snapshots__/index.test.ts.snap
@@ -0,0 +1,7 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Flex > should render correctly 1`] = `
+""
+`;
diff --git a/packages/ui/src/components/flex/__tests__/index.test.ts b/packages/ui/src/components/flex/__tests__/index.test.ts
new file mode 100644
index 000000000..83d7686f2
--- /dev/null
+++ b/packages/ui/src/components/flex/__tests__/index.test.ts
@@ -0,0 +1,149 @@
+import { describe, expect, it, vi } from 'vitest'
+import { Flex } from '@ant-design-vue/ui'
+import { mount } from '@vue/test-utils'
+
+describe('Flex', () => {
+ it('should render correctly', () => {
+ const wrapper = mount(Flex, {
+ slots: {
+ default: `test
`,
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('Flex', () => {
+ const wrapper = mount(Flex, {
+ props: {
+ justify: 'center'
+ },
+ slots: {
+ default: `test
`
+ },
+ });
+
+ const wrapper3 = mount(Flex, {
+ props: {
+ flex: '0 1 auto',
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+
+ expect(wrapper.classes('ant-flex')).toBeTruthy();
+ expect(wrapper.find('.ant-flex-justify-center')).toBeTruthy();
+ expect(wrapper3.classes('ant-flex')).toBeTruthy();
+ expect(wrapper3.element.style.flex).toBe('0 1 auto');
+ });
+
+ describe('Props: gap', () => {
+ it('support string', () => {
+ const wrapper = mount(Flex, {
+ props: {
+ gap: 'inherit',
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+ expect(wrapper.classes('ant-flex')).toBeTruthy();
+ expect(wrapper.element.style.gap).toBe('inherit');
+ });
+
+ it('support number', () => {
+ const wrapper = mount(Flex, {
+ props: {
+ gap: '100',
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+ expect(wrapper.classes('ant-flex')).toBeTruthy();
+ expect(wrapper.element.style.gap).toBe('100px');
+ });
+
+ it('support preset size', () => {
+ const wrapper = mount(Flex, {
+ props: {
+ gap: 'small',
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+
+ expect(wrapper.classes('ant-flex')).toBeTruthy();
+ expect(wrapper.classes('ant-flex-gap-small')).toBeTruthy();
+ });
+ });
+
+ it('Component work', () => {
+ const wrapper = mount(Flex, {
+ slots: {
+ default: `test
`
+ },
+ });
+
+ const wrapper2 = mount(Flex, {
+ props: {
+ componentTag: 'span'
+ },
+ slots: {
+ default: `test
`
+ },
+ });
+
+ expect(wrapper.find('.ant-flex').element.tagName).toBe('DIV');
+ expect(wrapper2.find('.ant-flex').element.tagName).toBe('SPAN');
+ });
+
+ it('when vertical=true should stretch work', () => {
+ const wrapper = mount(Flex, {
+ props: {
+ vertical: true
+ },
+ slots: {
+ default: `test
`
+ },
+ });
+
+ const wrapper2 = mount(Flex, {
+ props: {
+ vertical: true,
+ align: 'center',
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+
+ expect(wrapper.find('.ant-flex-align-stretch')).toBeTruthy();
+ expect(wrapper2.find('.ant-flex-align-center')).toBeTruthy();
+ });
+
+ it('wrap prop shouled support boolean', () => {
+ const wrapper = mount(Flex, {
+ props: {
+ wrap: 'wrap',
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+
+ const wrapper2 = mount(Flex, {
+ props: {
+ wrap: true,
+ },
+ slots: {
+ default: `test
`,
+ },
+ });
+
+ expect(wrapper.classes('ant-flex-wrap-wrap')).toBeTruthy();
+ expect(wrapper2.classes('ant-flex-wrap-wrap')).toBeTruthy();
+ })
+})