diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index d0c13b18..820a0eeb 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -17,6 +17,9 @@ const config: StorybookConfig = { options: {}, }, staticDirs: ['../public'], + docs: { + defaultName: 'Documentation', + }, webpackFinal: async (config) => { if (config.resolve) { config.resolve.alias = { diff --git a/react-ui/.storybook/mock/umijs.mock.tsx b/react-ui/.storybook/mock/umijs.mock.tsx index 4f25eeb3..ae8a7646 100644 --- a/react-ui/.storybook/mock/umijs.mock.tsx +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -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; diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index dcc4d247..65b4440a 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -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, diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 0dabae1d..242183e1 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -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) => { diff --git a/react-ui/src/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx index 6426e8c7..c983093e 100644 --- a/react-ui/src/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -21,7 +21,7 @@ export interface CodeSelectorModalProps extends Omit { onOk?: (params: CodeConfigData | undefined) => void; } -/** 代码配置选择弹窗 */ +/** 选择代码配置的弹窗,推荐使用函数的方式打开 */ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { const [dataList, setDataList] = useState([]); const [total, setTotal] = useState(0); diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index ec1b7717..8156d6a9 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -14,7 +14,7 @@ export interface KFModalProps extends ModalProps { image?: string; } -/** 自定义 Modal */ +/** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ function KFModal({ title, image, diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index 161775b9..a3a6c3e5 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -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 (
- -
加载中
+ +
{label}
); } diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index bc8d08cf..718d571a 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -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) => { diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index e65f1f02..24e97a4d 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -69,7 +69,7 @@ const getIdAndVersion = (versionKey: string) => { }; }; -/** 选择 数据集、模型、镜像 弹框 */ +/** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ function ResourceSelectorModal({ type, defaultExpandedKeys = [], diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index 6fece79a..80a0337c 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -26,6 +26,7 @@ export default meta; type Story = StoryObj; // 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, }, }; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index 3ed5f332..82581f6a 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -27,6 +27,6 @@ type Story = StoryObj; export const Primary: Story = { args: { ...BasicInfoStories.Primary.args, - labelWidth: 70, + labelWidth: 100, }, }; diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx index deab369c..59026ec6 100644 --- a/react-ui/src/stories/CodeSelectorModal.stories.tsx +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -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 = { - + ); }, }; -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 ( - ); diff --git a/react-ui/src/stories/FullScreenFrame.stories.tsx b/react-ui/src/stories/FullScreenFrame.stories.tsx index 6a2748c2..fa334ed1 100644 --- a/react-ui/src/stories/FullScreenFrame.stories.tsx +++ b/react-ui/src/stories/FullScreenFrame.stories.tsx @@ -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; export default meta; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index 208ced34..763aaf31 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -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 = { - + ); }, }; -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 ( - ); diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx index e2950549..f02fb77c 100644 --- a/react-ui/src/stories/MenuIconSelector.stories.tsx +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -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} /> ); diff --git a/react-ui/src/stories/ResourceSelectorModal.mdx b/react-ui/src/stories/ResourceSelectorModal.mdx new file mode 100644 index 00000000..28702123 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.mdx @@ -0,0 +1,23 @@ +import { Meta, Canvas } from '@storybook/blocks'; +import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" + + + +# Usage + +推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 + +```ts +const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: ResourceSelectorType.Dataset, + onOk: (res) => { + // 处理逻辑 + close(); + }, +}); +``` + + + + diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx index e2e0ebd2..9e72efd2 100644 --- a/react-ui/src/stories/ResourceSelectorModal.stories.tsx +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -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 = { - + ); }, @@ -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 = { - + ); }, @@ -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 = { - + ); }, }; -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 ( - ); diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx new file mode 100644 index 00000000..21543267 --- /dev/null +++ b/react-ui/src/stories/docs/Less.mdx @@ -0,0 +1,199 @@ +import { Meta, Controls } from '@storybook/blocks'; + + + +# 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 ( +
+ + + {item.code_repo_name} + +
+ {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} +
+
+ + {item.git_url} + +
{item.git_branch}
+
+ ); +} + +``` + +### 一些建议 + +如果你陷入嵌套地狱,比如 + +```tsx +function Component() { + return ( +
+
+
+
+
+ // 等等 +
+
+
+
+
+ ) +} +``` + +说明你需要拆分组件了 + +```tsx +function Component1() { + return ( +
+
+
+
+ ) +} + +function Component() { + return ( +
+
+ +
+
+ ) +} +``` + +既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 + + +