| @@ -17,6 +17,9 @@ const config: StorybookConfig = { | |||||
| options: {}, | options: {}, | ||||
| }, | }, | ||||
| staticDirs: ['../public'], | staticDirs: ['../public'], | ||||
| docs: { | |||||
| defaultName: 'Documentation', | |||||
| }, | |||||
| webpackFinal: async (config) => { | webpackFinal: async (config) => { | ||||
| if (config.resolve) { | if (config.resolve) { | ||||
| config.resolve.alias = { | config.resolve.alias = { | ||||
| @@ -14,3 +14,6 @@ export const request = (url: string, options: any) => { | |||||
| }) | }) | ||||
| .then((res) => res.json()); | .then((res) => res.json()); | ||||
| }; | }; | ||||
| export { useNavigate, useParams, useSearchParams } from 'react-router-dom'; | |||||
| export const history = window.history; | |||||
| @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; | |||||
| import '../public/fonts/font.css'; | import '../public/fonts/font.css'; | ||||
| import { getAccessToken } from './access'; | import { getAccessToken } from './access'; | ||||
| import './dayjsConfig'; | import './dayjsConfig'; | ||||
| import './global.less'; | |||||
| import { removeAllPageCacheState } from './hooks/pageCacheState'; | import { removeAllPageCacheState } from './hooks/pageCacheState'; | ||||
| import { | import { | ||||
| getRemoteMenu, | getRemoteMenu, | ||||
| @@ -13,8 +13,10 @@ import './index.less'; | |||||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | ||||
| type CodeSelectProps = ParameterInputProps; | |||||
| /** 代码配置选择表单组件 */ | /** 代码配置选择表单组件 */ | ||||
| function CodeSelect({ value, onChange, disabled, ...rest }: ParameterInputProps) { | |||||
| function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | |||||
| const selectResource = () => { | const selectResource = () => { | ||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| onOk: (res) => { | onOk: (res) => { | ||||
| @@ -21,7 +21,7 @@ export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||||
| onOk?: (params: CodeConfigData | undefined) => void; | onOk?: (params: CodeConfigData | undefined) => void; | ||||
| } | } | ||||
| /** 代码配置选择弹窗 */ | |||||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | |||||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | ||||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | const [dataList, setDataList] = useState<CodeConfigData[]>([]); | ||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| @@ -14,7 +14,7 @@ export interface KFModalProps extends ModalProps { | |||||
| image?: string; | image?: string; | ||||
| } | } | ||||
| /** 自定义 Modal */ | |||||
| /** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ | |||||
| function KFModal({ | function KFModal({ | ||||
| title, | title, | ||||
| image, | image, | ||||
| @@ -7,12 +7,17 @@ | |||||
| import { Spin, SpinProps } from 'antd'; | import { Spin, SpinProps } from 'antd'; | ||||
| import './index.less'; | import './index.less'; | ||||
| interface KFSpinProps extends SpinProps { | |||||
| /** 加载文本 */ | |||||
| label: string; | |||||
| } | |||||
| /** 自定义 Spin */ | /** 自定义 Spin */ | ||||
| function KFSpin(props: SpinProps) { | |||||
| function KFSpin({ label = '加载中', ...rest }: KFSpinProps) { | |||||
| return ( | return ( | ||||
| <div className={'kf-spin'}> | <div className={'kf-spin'}> | ||||
| <Spin {...props} /> | |||||
| <div className={'kf-spin__label'}>加载中</div> | |||||
| <Spin {...rest} /> | |||||
| <div className={'kf-spin__label'}>{label}</div> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -20,10 +20,10 @@ import './index.less'; | |||||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | ||||
| export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | ||||
| type ResourceSelectProps = { | |||||
| interface ResourceSelectProps extends ParameterInputProps { | |||||
| /** 类型,数据集、模型、镜像 */ | /** 类型,数据集、模型、镜像 */ | ||||
| type: ResourceSelectorType; | type: ResourceSelectorType; | ||||
| } & ParameterInputProps; | |||||
| } | |||||
| // 获取选择数据集、模型、镜像后面按钮 icon | // 获取选择数据集、模型、镜像后面按钮 icon | ||||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | const getSelectBtnIcon = (type: ResourceSelectorType) => { | ||||
| @@ -69,7 +69,7 @@ const getIdAndVersion = (versionKey: string) => { | |||||
| }; | }; | ||||
| }; | }; | ||||
| /** 选择 数据集、模型、镜像 弹框 */ | |||||
| /** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ | |||||
| function ResourceSelectorModal({ | function ResourceSelectorModal({ | ||||
| type, | type, | ||||
| defaultExpandedKeys = [], | defaultExpandedKeys = [], | ||||
| @@ -26,6 +26,7 @@ export default meta; | |||||
| type Story = StoryObj<typeof meta>; | type Story = StoryObj<typeof meta>; | ||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | ||||
| /** 一行两列 */ | |||||
| export const Primary: Story = { | export const Primary: Story = { | ||||
| args: { | args: { | ||||
| datas: [ | datas: [ | ||||
| @@ -86,6 +87,16 @@ export const Primary: Story = { | |||||
| ), | ), | ||||
| }, | }, | ||||
| ], | ], | ||||
| labelWidth: 100, | |||||
| labelWidth: 80, | |||||
| labelAlign: 'justify', | |||||
| }, | |||||
| }; | |||||
| /** 一行三列 */ | |||||
| export const ThreeColumn: Story = { | |||||
| args: { | |||||
| ...Primary.args, | |||||
| labelAlign: 'start', | |||||
| threeColumns: true, | |||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -27,6 +27,6 @@ type Story = StoryObj<typeof meta>; | |||||
| export const Primary: Story = { | export const Primary: Story = { | ||||
| args: { | args: { | ||||
| ...BasicInfoStories.Primary.args, | ...BasicInfoStories.Primary.args, | ||||
| labelWidth: 70, | |||||
| labelWidth: 100, | |||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -48,12 +48,12 @@ export const Primary: Story = { | |||||
| function onClick() { | function onClick() { | ||||
| updateArgs({ open: true }); | updateArgs({ open: true }); | ||||
| } | } | ||||
| function onModalOk(res: any) { | |||||
| function handleOk(res: any) { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onOk?.(res); | onOk?.(res); | ||||
| } | } | ||||
| function onModalCancel() { | |||||
| function handleCancel() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onCancel?.(); | onCancel?.(); | ||||
| } | } | ||||
| @@ -63,15 +63,16 @@ export const Primary: Story = { | |||||
| <Button type="primary" onClick={onClick}> | <Button type="primary" onClick={onClick}> | ||||
| 选择代码配置 | 选择代码配置 | ||||
| </Button> | </Button> | ||||
| <CodeSelectorModal {...args} open={open} onOk={onModalOk} onCancel={onModalCancel} /> | |||||
| <CodeSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }, | }, | ||||
| }; | }; | ||||
| export const OpenInFunction: Story = { | |||||
| /** 通过 `openAntdModal` 函数打开 */ | |||||
| export const OpenByFunction: Story = { | |||||
| render: function Render(args) { | render: function Render(args) { | ||||
| const handleOnChange = () => { | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| onOk: (res) => { | onOk: (res) => { | ||||
| const { onOk } = args; | const { onOk } = args; | ||||
| @@ -81,7 +82,7 @@ export const OpenInFunction: Story = { | |||||
| }); | }); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Button type="primary" onClick={handleOnChange}> | |||||
| <Button type="primary" onClick={handleClick}> | |||||
| 以函数的方式打开 | 以函数的方式打开 | ||||
| </Button> | </Button> | ||||
| ); | ); | ||||
| @@ -1,5 +1,6 @@ | |||||
| import FullScreenFrame from '@/components/FullScreenFrame'; | import FullScreenFrame from '@/components/FullScreenFrame'; | ||||
| import type { Meta, StoryObj } from '@storybook/react'; | import type { Meta, StoryObj } from '@storybook/react'; | ||||
| import { fn } from '@storybook/test'; | |||||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | ||||
| const meta = { | const meta = { | ||||
| @@ -16,7 +17,7 @@ const meta = { | |||||
| // backgroundColor: { control: 'color' }, | // backgroundColor: { control: 'color' }, | ||||
| }, | }, | ||||
| // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | ||||
| // args: { onClick: fn() }, | |||||
| args: { onLoad: fn(), onError: fn() }, | |||||
| } satisfies Meta<typeof FullScreenFrame>; | } satisfies Meta<typeof FullScreenFrame>; | ||||
| export default meta; | export default meta; | ||||
| @@ -50,12 +50,12 @@ export const Primary: Story = { | |||||
| function onClick() { | function onClick() { | ||||
| updateArgs({ open: true }); | updateArgs({ open: true }); | ||||
| } | } | ||||
| function onModalOk() { | |||||
| function handleOk() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onOk?.(); | onOk?.(); | ||||
| } | } | ||||
| function onModalCancel() { | |||||
| function handleCancel() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onCancel?.(); | onCancel?.(); | ||||
| } | } | ||||
| @@ -65,15 +65,16 @@ export const Primary: Story = { | |||||
| <Button type="primary" onClick={onClick}> | <Button type="primary" onClick={onClick}> | ||||
| 打开 KFModal | 打开 KFModal | ||||
| </Button> | </Button> | ||||
| <KFModal {...args} open={open} onOk={onModalOk} onCancel={onModalCancel} /> | |||||
| <KFModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }, | }, | ||||
| }; | }; | ||||
| export const OpenInFunction: Story = { | |||||
| /** 通过 `openAntdModal` 函数打开 */ | |||||
| export const OpenByFunction: Story = { | |||||
| render: function Render() { | render: function Render() { | ||||
| const handleOnChange = () => { | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(KFModal, { | const { close } = openAntdModal(KFModal, { | ||||
| title: '创建实验', | title: '创建实验', | ||||
| image: CreateExperiment, | image: CreateExperiment, | ||||
| @@ -84,7 +85,7 @@ export const OpenInFunction: Story = { | |||||
| }); | }); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Button type="primary" onClick={handleOnChange}> | |||||
| <Button type="primary" onClick={handleClick}> | |||||
| 以函数的方式打开 | 以函数的方式打开 | ||||
| </Button> | </Button> | ||||
| ); | ); | ||||
| @@ -39,12 +39,12 @@ export const Primary: Story = { | |||||
| function onClick() { | function onClick() { | ||||
| updateArgs({ open: true }); | updateArgs({ open: true }); | ||||
| } | } | ||||
| function onModalOk(value: string) { | |||||
| function handleOk(value: string) { | |||||
| updateArgs({ selectedIcon: value, open: false }); | updateArgs({ selectedIcon: value, open: false }); | ||||
| onOk?.(value); | onOk?.(value); | ||||
| } | } | ||||
| function onModalCancel() { | |||||
| function handleCancel() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onCancel?.(); | onCancel?.(); | ||||
| } | } | ||||
| @@ -58,8 +58,8 @@ export const Primary: Story = { | |||||
| {...args} | {...args} | ||||
| open={open} | open={open} | ||||
| selectedIcon={selectedIcon} | selectedIcon={selectedIcon} | ||||
| onOk={onModalOk} | |||||
| onCancel={onModalCancel} | |||||
| onOk={handleOk} | |||||
| onCancel={handleCancel} | |||||
| /> | /> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -0,0 +1,23 @@ | |||||
| import { Meta, Canvas } from '@storybook/blocks'; | |||||
| import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" | |||||
| <Meta of={ResourceSelectorModalStories} name="Usage" /> | |||||
| # Usage | |||||
| 推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 | |||||
| ```ts | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||||
| type: ResourceSelectorType.Dataset, | |||||
| onOk: (res) => { | |||||
| // 处理逻辑 | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| ``` | |||||
| <Canvas of={ResourceSelectorModalStories.OpenByFunction} /> | |||||
| @@ -70,12 +70,12 @@ export const Dataset: Story = { | |||||
| function onClick() { | function onClick() { | ||||
| updateArgs({ open: true }); | updateArgs({ open: true }); | ||||
| } | } | ||||
| function onModalOk(res: any) { | |||||
| function handleOk(res: any) { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onOk?.(res); | onOk?.(res); | ||||
| } | } | ||||
| function onModalCancel() { | |||||
| function handleCancel() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onCancel?.(); | onCancel?.(); | ||||
| } | } | ||||
| @@ -85,7 +85,7 @@ export const Dataset: Story = { | |||||
| <Button type="primary" onClick={onClick}> | <Button type="primary" onClick={onClick}> | ||||
| 选择数据集 | 选择数据集 | ||||
| </Button> | </Button> | ||||
| <ResourceSelectorModal {...args} open={open} onOk={onModalOk} onCancel={onModalCancel} /> | |||||
| <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }, | }, | ||||
| @@ -116,12 +116,12 @@ export const Model: Story = { | |||||
| function onClick() { | function onClick() { | ||||
| updateArgs({ open: true }); | updateArgs({ open: true }); | ||||
| } | } | ||||
| function onModalOk(res: any) { | |||||
| function handleOk(res: any) { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onOk?.(res); | onOk?.(res); | ||||
| } | } | ||||
| function onModalCancel() { | |||||
| function handleCancel() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onCancel?.(); | onCancel?.(); | ||||
| } | } | ||||
| @@ -131,7 +131,7 @@ export const Model: Story = { | |||||
| <Button type="primary" onClick={onClick}> | <Button type="primary" onClick={onClick}> | ||||
| 选择模型 | 选择模型 | ||||
| </Button> | </Button> | ||||
| <ResourceSelectorModal {...args} open={open} onOk={onModalOk} onCancel={onModalCancel} /> | |||||
| <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }, | }, | ||||
| @@ -159,12 +159,12 @@ export const Mirror: Story = { | |||||
| function onClick() { | function onClick() { | ||||
| updateArgs({ open: true }); | updateArgs({ open: true }); | ||||
| } | } | ||||
| function onModalOk(res: any) { | |||||
| function handleOk(res: any) { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onOk?.(res); | onOk?.(res); | ||||
| } | } | ||||
| function onModalCancel() { | |||||
| function handleCancel() { | |||||
| updateArgs({ open: false }); | updateArgs({ open: false }); | ||||
| onCancel?.(); | onCancel?.(); | ||||
| } | } | ||||
| @@ -174,13 +174,14 @@ export const Mirror: Story = { | |||||
| <Button type="primary" onClick={onClick}> | <Button type="primary" onClick={onClick}> | ||||
| 选择镜像 | 选择镜像 | ||||
| </Button> | </Button> | ||||
| <ResourceSelectorModal {...args} open={open} onOk={onModalOk} onCancel={onModalCancel} /> | |||||
| <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }, | }, | ||||
| }; | }; | ||||
| export const OpenInFunction: Story = { | |||||
| /** 通过 `openAntdModal` 函数打开 */ | |||||
| export const OpenByFunction: Story = { | |||||
| args: { | args: { | ||||
| type: ResourceSelectorType.Mirror, | type: ResourceSelectorType.Mirror, | ||||
| }, | }, | ||||
| @@ -197,7 +198,7 @@ export const OpenInFunction: Story = { | |||||
| }, | }, | ||||
| }, | }, | ||||
| render: function Render(args) { | render: function Render(args) { | ||||
| const handleOnChange = () => { | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(ResourceSelectorModal, { | const { close } = openAntdModal(ResourceSelectorModal, { | ||||
| type: args.type, | type: args.type, | ||||
| onOk: (res) => { | onOk: (res) => { | ||||
| @@ -208,7 +209,7 @@ export const OpenInFunction: Story = { | |||||
| }); | }); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Button type="primary" onClick={handleOnChange}> | |||||
| <Button type="primary" onClick={handleClick}> | |||||
| 以函数的方式打开 | 以函数的方式打开 | ||||
| </Button> | </Button> | ||||
| ); | ); | ||||
| @@ -0,0 +1,199 @@ | |||||
| import { Meta, Controls } from '@storybook/blocks'; | |||||
| <Meta title="Documentation/Less" /> | |||||
| # Less 规范 | |||||
| ## Theme | |||||
| ### 自定义主题 | |||||
| `src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。在开发过程中使用这个文件的定义的变量、函数以及混合,通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。 | |||||
| 颜色变量还可以在 `js/ts/jsx/tsx` 里使用 | |||||
| ```js | |||||
| import themes from "@/styles/theme.less" | |||||
| const primaryColor = themes['primaryColor']; // #1664ff | |||||
| ``` | |||||
| ### Ant Design 主题覆盖 | |||||
| Ant Design 可以[定制主题](https://ant-design.antgroup.com/docs/react/customize-theme-cn),Ant Design 是通过 [ConfigProvider](https://ant-design.antgroup.com/components/config-provider-cn) 组件进行主题定制,而 UmiJS 可以在[配置文件](https://umijs.org/docs/max/antd#%E6%9E%84%E5%BB%BA%E6%97%B6%E9%85%8D%E7%BD%AE)或者 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置。我选择在 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置,因为这里可以使用主题颜色变量。 | |||||
| ```tsx | |||||
| // 主题修改 | |||||
| export const antd: RuntimeAntdConfig = (memo) => { | |||||
| memo.theme ??= {}; | |||||
| memo.theme.token = { | |||||
| colorPrimary: themes['primaryColor'], | |||||
| colorSuccess: themes['successColor'], | |||||
| colorError: themes['errorColor'], | |||||
| colorWarning: themes['warningColor'], | |||||
| colorLink: themes['primaryColor'], | |||||
| colorText: themes['textColor'], | |||||
| controlHeightLG: 46, | |||||
| }; | |||||
| memo.theme.components ??= {}; | |||||
| memo.theme.components.Tabs = {}; | |||||
| memo.theme.components.Button = { | |||||
| defaultBg: 'rgba(22, 100, 255, 0.06)', | |||||
| defaultBorderColor: 'rgba(22, 100, 255, 0.11)', | |||||
| defaultColor: themes['textColor'], | |||||
| defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | |||||
| defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | |||||
| defaultHoverColor: '#3F7FFF', | |||||
| defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | |||||
| defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | |||||
| defaultActiveColor: themes['primaryColor'], | |||||
| contentFontSize: parseInt(themes['fontSize']), | |||||
| }; | |||||
| memo.theme.components.Input = { | |||||
| inputFontSize: parseInt(themes['fontSizeInput']), | |||||
| inputFontSizeLG: parseInt(themes['fontSizeInputLg']), | |||||
| paddingBlockLG: 10, | |||||
| }; | |||||
| memo.theme.components.Select = { | |||||
| singleItemHeightLG: 46, | |||||
| optionSelectedColor: themes['primaryColor'], | |||||
| }; | |||||
| memo.theme.components.Table = { | |||||
| headerBg: 'rgba(242, 244, 247, 0.36)', | |||||
| headerBorderRadius: 4, | |||||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||||
| }; | |||||
| memo.theme.components.Tabs = { | |||||
| titleFontSize: 16, | |||||
| }; | |||||
| memo.theme.components.Form = { | |||||
| labelColor: 'rgba(29, 29, 32, 0.8);', | |||||
| }; | |||||
| memo.theme.components.Breadcrumb = { | |||||
| iconFontSize: parseInt(themes['fontSize']), | |||||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||||
| }; | |||||
| memo.theme.cssVar = true; | |||||
| memo.appConfig = { | |||||
| message: { | |||||
| // 配置 message 最大显示数,超过限制时,最早的消息会被自动关闭 | |||||
| maxCount: 3, | |||||
| }, | |||||
| }; | |||||
| return memo; | |||||
| }; | |||||
| ``` | |||||
| 覆盖 Ant Design 的默认样式,优先选择这种方式,如果没有相应的变量,才覆盖 Ant Design 的样式,在 `src/overrides.less` 文件里覆盖,请查看 UmiJS 关于[`global.less`](https://umijs.org/docs/guides/directory-structure#globalcsslesssassscss) 与 [`overrides.less`](https://umijs.org/docs/guides/directory-structure#overridescsslesssassscss) 的说明。 | |||||
| ## BEM | |||||
| 类名遵循 [BEM - Block, Element, Modifier](https://getbem.com/) 规范 | |||||
| ### Block | |||||
| 有意义的独立实体,Block 的类名由小写字母、数字和横线组成,比如 `model`、`form`、`paramneter-input` | |||||
| ### Element | |||||
| 块的一部分,Element 的类名由 `Block 的类名` + `双下划线(__)` + `Element 的名称` 组成,比如 `model__title`、`form__input`、`paramneter-input__content` | |||||
| ### Modifier | |||||
| 块或元素的变种,Modifier 的类名由 `Block 的类名` 或者 `Element 的类名` + `双横线(--)` + `Modifier 的名称` 组成,比如 `button--active`、`form--large` | |||||
| 举个 🌰 | |||||
| ```tsx | |||||
| // @/components/CodeConfigItem/index.tsx | |||||
| import classNames from 'classnames'; | |||||
| import styles from './index.less'; | |||||
| function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||||
| return ( | |||||
| <div className={styles['code-config-item']}> | |||||
| <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | |||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__name']} | |||||
| ellipsis={{ tooltip: item.code_repo_name }} | |||||
| > | |||||
| {item.code_repo_name} | |||||
| </Typography.Paragraph> | |||||
| <div | |||||
| className={classNames( | |||||
| styles['code-config-item__tag'], | |||||
| item.code_repo_vis === AvailableRange.Public | |||||
| ? styles['code-config-item__tag--public'] | |||||
| : styles['code-config-item__tag--private'], | |||||
| )} | |||||
| > | |||||
| {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} | |||||
| </div> | |||||
| </Flex> | |||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__url']} | |||||
| ellipsis={{ rows: 2, tooltip: item.git_url }} | |||||
| > | |||||
| {item.git_url} | |||||
| </Typography.Paragraph> | |||||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| ``` | |||||
| ### 一些建议 | |||||
| 如果你陷入嵌套地狱,比如 | |||||
| ```tsx | |||||
| function Component() { | |||||
| return ( | |||||
| <div className="component"> | |||||
| <div className="component__element1"> | |||||
| <div className="component__element1__element2"> | |||||
| <div className="component__element1__element2__element3"> | |||||
| <div className="component__element1__element2__element3__element4"> | |||||
| // 等等 | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| ``` | |||||
| 说明你需要拆分组件了 | |||||
| ```tsx | |||||
| function Component1() { | |||||
| return ( | |||||
| <div className="component1"> | |||||
| <div className="component1__element2"> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| function Component() { | |||||
| return ( | |||||
| <div className="component"> | |||||
| <div className="component__element1"> | |||||
| <Component1></Component1> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| ``` | |||||
| 既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 | |||||