Browse Source

feat: storybook add less 规范

pull/170/head
cp3hnu 11 months ago
parent
commit
cdfe23ef08
18 changed files with 290 additions and 41 deletions
  1. +3
    -0
      react-ui/.storybook/main.ts
  2. +3
    -0
      react-ui/.storybook/mock/umijs.mock.tsx
  3. +0
    -1
      react-ui/src/app.tsx
  4. +3
    -1
      react-ui/src/components/CodeSelect/index.tsx
  5. +1
    -1
      react-ui/src/components/CodeSelectorModal/index.tsx
  6. +1
    -1
      react-ui/src/components/KFModal/index.tsx
  7. +8
    -3
      react-ui/src/components/KFSpin/index.tsx
  8. +2
    -2
      react-ui/src/components/ResourceSelect/index.tsx
  9. +1
    -1
      react-ui/src/components/ResourceSelectorModal/index.tsx
  10. +12
    -1
      react-ui/src/stories/BasicInfo.stories.tsx
  11. +1
    -1
      react-ui/src/stories/BasicTableInfo.stories.tsx
  12. +7
    -6
      react-ui/src/stories/CodeSelectorModal.stories.tsx
  13. +2
    -1
      react-ui/src/stories/FullScreenFrame.stories.tsx
  14. +7
    -6
      react-ui/src/stories/KFModal.stories.tsx
  15. +4
    -4
      react-ui/src/stories/MenuIconSelector.stories.tsx
  16. +23
    -0
      react-ui/src/stories/ResourceSelectorModal.mdx
  17. +13
    -12
      react-ui/src/stories/ResourceSelectorModal.stories.tsx
  18. +199
    -0
      react-ui/src/stories/docs/Less.mdx

+ 3
- 0
react-ui/.storybook/main.ts View File

@@ -17,6 +17,9 @@ const config: StorybookConfig = {
options: {},
},
staticDirs: ['../public'],
docs: {
defaultName: 'Documentation',
},
webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {


+ 3
- 0
react-ui/.storybook/mock/umijs.mock.tsx View File

@@ -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;

+ 0
- 1
react-ui/src/app.tsx View File

@@ -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,


+ 3
- 1
react-ui/src/components/CodeSelect/index.tsx View File

@@ -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) => {


+ 1
- 1
react-ui/src/components/CodeSelectorModal/index.tsx View File

@@ -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);


+ 1
- 1
react-ui/src/components/KFModal/index.tsx View File

@@ -14,7 +14,7 @@ export interface KFModalProps extends ModalProps {
image?: string;
}

/** 自定义 Modal */
/** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */
function KFModal({
title,
image,


+ 8
- 3
react-ui/src/components/KFSpin/index.tsx View File

@@ -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>
);
}


+ 2
- 2
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -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) => {


+ 1
- 1
react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -69,7 +69,7 @@ const getIdAndVersion = (versionKey: string) => {
};
};

/** 选择 数据集、模型、镜像 弹框 */
/** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */
function ResourceSelectorModal({
type,
defaultExpandedKeys = [],


+ 12
- 1
react-ui/src/stories/BasicInfo.stories.tsx View File

@@ -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,
},
};

+ 1
- 1
react-ui/src/stories/BasicTableInfo.stories.tsx View File

@@ -27,6 +27,6 @@ type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
...BasicInfoStories.Primary.args,
labelWidth: 70,
labelWidth: 100,
},
};

+ 7
- 6
react-ui/src/stories/CodeSelectorModal.stories.tsx View File

@@ -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>
);


+ 2
- 1
react-ui/src/stories/FullScreenFrame.stories.tsx View File

@@ -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;


+ 7
- 6
react-ui/src/stories/KFModal.stories.tsx View File

@@ -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>
);


+ 4
- 4
react-ui/src/stories/MenuIconSelector.stories.tsx View File

@@ -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}
/>
</>
);


+ 23
- 0
react-ui/src/stories/ResourceSelectorModal.mdx View File

@@ -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} />

+ 13
- 12
react-ui/src/stories/ResourceSelectorModal.stories.tsx View File

@@ -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>
);


+ 199
- 0
react-ui/src/stories/docs/Less.mdx View File

@@ -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的嵌套,使代码逻辑更加清晰,易于理解与维护




Loading…
Cancel
Save