| @@ -17,6 +17,9 @@ const config: StorybookConfig = { | |||
| options: {}, | |||
| }, | |||
| staticDirs: ['../public'], | |||
| docs: { | |||
| defaultName: 'Documentation', | |||
| }, | |||
| webpackFinal: async (config) => { | |||
| if (config.resolve) { | |||
| config.resolve.alias = { | |||
| @@ -14,3 +14,6 @@ export const request = (url: string, options: any) => { | |||
| }) | |||
| .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 { getAccessToken } from './access'; | |||
| import './dayjsConfig'; | |||
| import './global.less'; | |||
| import { removeAllPageCacheState } from './hooks/pageCacheState'; | |||
| import { | |||
| getRemoteMenu, | |||
| @@ -13,8 +13,10 @@ import './index.less'; | |||
| 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 { close } = openAntdModal(CodeSelectorModal, { | |||
| onOk: (res) => { | |||
| @@ -21,7 +21,7 @@ export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| onOk?: (params: CodeConfigData | undefined) => void; | |||
| } | |||
| /** 代码配置选择弹窗 */ | |||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | |||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| @@ -14,7 +14,7 @@ export interface KFModalProps extends ModalProps { | |||
| image?: string; | |||
| } | |||
| /** 自定义 Modal */ | |||
| /** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ | |||
| function KFModal({ | |||
| title, | |||
| image, | |||
| @@ -7,12 +7,17 @@ | |||
| import { Spin, SpinProps } from 'antd'; | |||
| import './index.less'; | |||
| interface KFSpinProps extends SpinProps { | |||
| /** 加载文本 */ | |||
| label: string; | |||
| } | |||
| /** 自定义 Spin */ | |||
| function KFSpin(props: SpinProps) { | |||
| function KFSpin({ label = '加载中', ...rest }: KFSpinProps) { | |||
| return ( | |||
| <div className={'kf-spin'}> | |||
| <Spin {...props} /> | |||
| <div className={'kf-spin__label'}>加载中</div> | |||
| <Spin {...rest} /> | |||
| <div className={'kf-spin__label'}>{label}</div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -20,10 +20,10 @@ import './index.less'; | |||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | |||
| export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | |||
| type ResourceSelectProps = { | |||
| interface ResourceSelectProps extends ParameterInputProps { | |||
| /** 类型,数据集、模型、镜像 */ | |||
| type: ResourceSelectorType; | |||
| } & ParameterInputProps; | |||
| } | |||
| // 获取选择数据集、模型、镜像后面按钮 icon | |||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | |||
| @@ -69,7 +69,7 @@ const getIdAndVersion = (versionKey: string) => { | |||
| }; | |||
| }; | |||
| /** 选择 数据集、模型、镜像 弹框 */ | |||
| /** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ | |||
| function ResourceSelectorModal({ | |||
| type, | |||
| defaultExpandedKeys = [], | |||
| @@ -26,6 +26,7 @@ export default meta; | |||
| type Story = StoryObj<typeof meta>; | |||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||
| /** 一行两列 */ | |||
| export const Primary: Story = { | |||
| args: { | |||
| 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 = { | |||
| args: { | |||
| ...BasicInfoStories.Primary.args, | |||
| labelWidth: 70, | |||
| labelWidth: 100, | |||
| }, | |||
| }; | |||
| @@ -48,12 +48,12 @@ export const Primary: Story = { | |||
| function onClick() { | |||
| updateArgs({ open: true }); | |||
| } | |||
| function onModalOk(res: any) { | |||
| function handleOk(res: any) { | |||
| updateArgs({ open: false }); | |||
| onOk?.(res); | |||
| } | |||
| function onModalCancel() { | |||
| function handleCancel() { | |||
| updateArgs({ open: false }); | |||
| onCancel?.(); | |||
| } | |||
| @@ -63,15 +63,16 @@ export const Primary: Story = { | |||
| <Button type="primary" onClick={onClick}> | |||
| 选择代码配置 | |||
| </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) { | |||
| const handleOnChange = () => { | |||
| const handleClick = () => { | |||
| const { close } = openAntdModal(CodeSelectorModal, { | |||
| onOk: (res) => { | |||
| const { onOk } = args; | |||
| @@ -81,7 +82,7 @@ export const OpenInFunction: Story = { | |||
| }); | |||
| }; | |||
| return ( | |||
| <Button type="primary" onClick={handleOnChange}> | |||
| <Button type="primary" onClick={handleClick}> | |||
| 以函数的方式打开 | |||
| </Button> | |||
| ); | |||
| @@ -1,5 +1,6 @@ | |||
| import FullScreenFrame from '@/components/FullScreenFrame'; | |||
| 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 | |||
| const meta = { | |||
| @@ -16,7 +17,7 @@ const meta = { | |||
| // 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 | |||
| // args: { onClick: fn() }, | |||
| args: { onLoad: fn(), onError: fn() }, | |||
| } satisfies Meta<typeof FullScreenFrame>; | |||
| export default meta; | |||
| @@ -50,12 +50,12 @@ export const Primary: Story = { | |||
| function onClick() { | |||
| updateArgs({ open: true }); | |||
| } | |||
| function onModalOk() { | |||
| function handleOk() { | |||
| updateArgs({ open: false }); | |||
| onOk?.(); | |||
| } | |||
| function onModalCancel() { | |||
| function handleCancel() { | |||
| updateArgs({ open: false }); | |||
| onCancel?.(); | |||
| } | |||
| @@ -65,15 +65,16 @@ export const Primary: Story = { | |||
| <Button type="primary" onClick={onClick}> | |||
| 打开 KFModal | |||
| </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() { | |||
| const handleOnChange = () => { | |||
| const handleClick = () => { | |||
| const { close } = openAntdModal(KFModal, { | |||
| title: '创建实验', | |||
| image: CreateExperiment, | |||
| @@ -84,7 +85,7 @@ export const OpenInFunction: Story = { | |||
| }); | |||
| }; | |||
| return ( | |||
| <Button type="primary" onClick={handleOnChange}> | |||
| <Button type="primary" onClick={handleClick}> | |||
| 以函数的方式打开 | |||
| </Button> | |||
| ); | |||
| @@ -39,12 +39,12 @@ export const Primary: Story = { | |||
| function onClick() { | |||
| updateArgs({ open: true }); | |||
| } | |||
| function onModalOk(value: string) { | |||
| function handleOk(value: string) { | |||
| updateArgs({ selectedIcon: value, open: false }); | |||
| onOk?.(value); | |||
| } | |||
| function onModalCancel() { | |||
| function handleCancel() { | |||
| updateArgs({ open: false }); | |||
| onCancel?.(); | |||
| } | |||
| @@ -58,8 +58,8 @@ export const Primary: Story = { | |||
| {...args} | |||
| open={open} | |||
| 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() { | |||
| updateArgs({ open: true }); | |||
| } | |||
| function onModalOk(res: any) { | |||
| function handleOk(res: any) { | |||
| updateArgs({ open: false }); | |||
| onOk?.(res); | |||
| } | |||
| function onModalCancel() { | |||
| function handleCancel() { | |||
| updateArgs({ open: false }); | |||
| onCancel?.(); | |||
| } | |||
| @@ -85,7 +85,7 @@ export const Dataset: Story = { | |||
| <Button type="primary" onClick={onClick}> | |||
| 选择数据集 | |||
| </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() { | |||
| updateArgs({ open: true }); | |||
| } | |||
| function onModalOk(res: any) { | |||
| function handleOk(res: any) { | |||
| updateArgs({ open: false }); | |||
| onOk?.(res); | |||
| } | |||
| function onModalCancel() { | |||
| function handleCancel() { | |||
| updateArgs({ open: false }); | |||
| onCancel?.(); | |||
| } | |||
| @@ -131,7 +131,7 @@ export const Model: Story = { | |||
| <Button type="primary" onClick={onClick}> | |||
| 选择模型 | |||
| </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() { | |||
| updateArgs({ open: true }); | |||
| } | |||
| function onModalOk(res: any) { | |||
| function handleOk(res: any) { | |||
| updateArgs({ open: false }); | |||
| onOk?.(res); | |||
| } | |||
| function onModalCancel() { | |||
| function handleCancel() { | |||
| updateArgs({ open: false }); | |||
| onCancel?.(); | |||
| } | |||
| @@ -174,13 +174,14 @@ export const Mirror: Story = { | |||
| <Button type="primary" onClick={onClick}> | |||
| 选择镜像 | |||
| </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: { | |||
| type: ResourceSelectorType.Mirror, | |||
| }, | |||
| @@ -197,7 +198,7 @@ export const OpenInFunction: Story = { | |||
| }, | |||
| }, | |||
| render: function Render(args) { | |||
| const handleOnChange = () => { | |||
| const handleClick = () => { | |||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||
| type: args.type, | |||
| onOk: (res) => { | |||
| @@ -208,7 +209,7 @@ export const OpenInFunction: Story = { | |||
| }); | |||
| }; | |||
| return ( | |||
| <Button type="primary" onClick={handleOnChange}> | |||
| <Button type="primary" onClick={handleClick}> | |||
| 以函数的方式打开 | |||
| </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的嵌套,使代码逻辑更加清晰,易于理解与维护 | |||