├── .dumirc.ts
├── .editorconfig
├── .fatherrc.ts
├── .github
└── workflows
│ └── npm-publish-github-packages.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .umirc.ts
├── README.md
├── docs
└── index.md
├── package.json
├── src
├── assets
│ ├── images
│ │ ├── minus.svg
│ │ └── plus.svg
│ └── styles
│ │ └── variable.less
├── components
│ ├── Card
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── Cron
│ │ ├── CustomCron
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ ├── index.tsx
│ │ └── utils.ts
│ ├── Empty
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── ExportBtn
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ ├── index.tsx
│ │ └── useDownload.ts
│ ├── FormFooterAction
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── FormLayout
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── IconFont
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── IconText
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── SearchInput
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── Skeleton
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── Subtitle
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ ├── TooltipText
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ └── Tree
│ │ ├── index.less
│ │ ├── index.md
│ │ ├── index.test.tsx
│ │ └── index.tsx
└── index.ts
├── tsconfig.json
└── typings.d.ts
/.dumirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | apiParser: {},
5 | resolve: {
6 | // 配置入口文件路径,API 解析将从这里开始
7 | entryFile: './src/index.tsx',
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.fatherrc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'father';
2 |
3 | export default defineConfig({
4 | // more father 4 config: https://github.com/umijs/father-next/blob/master/docs/config.md
5 | esm: {},
6 | extraBabelPlugins: [
7 | [
8 | 'babel-plugin-import',
9 | {
10 | libraryName: 'antd',
11 | libraryDirectory: 'es',
12 | style: true,
13 | },
14 | ],
15 | ],
16 | autoprefixer: {
17 | browsers: ['ie>9', 'Safari >= 6'],
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish-github-packages.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: build-and-publish
5 |
6 | on:
7 | push:
8 | branches:
9 | - master
10 | tags:
11 | - v*
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: actions/setup-node@v3
19 | with:
20 | node-version: 16
21 | - run: npm install
22 | - run: npm test
23 |
24 | publish-npm:
25 | needs: build
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v3
30 | - name: Node
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: 16
34 | registry-url: https://registry.npmjs.org
35 | - run: npm install
36 | - run: npm run build
37 | - run: npm publish
38 | env:
39 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /npm-debug.log*
6 | /yarn-error.log
7 | /yarn.lock
8 | /package-lock.json
9 |
10 | # production
11 | /es
12 | /docs-dist
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | /coverage
18 |
19 | # umi
20 | .umi
21 | .umi-production
22 | .umi-test
23 | .env.local
24 |
25 | # ide
26 | /.vscode
27 | /.idea
28 | .github/
29 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.svg
2 | **/*.ejs
3 | **/*.html
4 | package.json
5 | .umi
6 | .umi-production
7 | .umi-test
8 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require('@umijs/fabric').prettier;
2 |
--------------------------------------------------------------------------------
/.umirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | title: 'szdt',
5 | mode: 'site',
6 | favicon:
7 | 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
8 | logo: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png',
9 | outputPath: 'docs-dist',
10 | base: '/',
11 | publicPath: './',
12 | navs: [
13 | null,
14 | {
15 | title: 'AntDesign',
16 | path: 'https://4x.ant.design/components/overview-cn/',
17 | },
18 | {
19 | title: 'GitHub',
20 | path: 'https://github.com/mkgrow/szdt-admin-components',
21 | },
22 | ],
23 | lessLoader: {
24 | javascriptEnabled: true,
25 | },
26 | resolve: {
27 | includes: ['docs', 'src'],
28 | },
29 | // mfsu: {},
30 | dynamicImport: {},
31 | exportStatic: {},
32 | extraBabelPlugins: [
33 | [
34 | 'babel-plugin-import',
35 | {
36 | libraryName: 'antd',
37 | libraryDirectory: 'es',
38 | style: true,
39 | },
40 | ],
41 | ],
42 | history: {
43 | type: 'hash',
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## ✨ Online preview
2 |
3 | https://mkgrow.github.io/szdt-admin-components/#/
4 |
5 | ## 📦 Install
6 |
7 | ```bash
8 | npm install szdt-admin-components
9 | ```
10 |
11 | ```bash
12 | yarn add szdt-admin-components
13 | ```
14 |
15 | ## 🔨 Usage
16 |
17 | ```jsx
18 | import React from 'react';
19 | import { IconFont, IconText, Skeleton, TooltipText } from 'szdt-admin-components';
20 |
21 | const App = () => (
22 | <>
23 |
24 |
25 |
26 | content
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | ```
34 |
35 | ## ⌨️ Development
36 |
37 | clone locally:
38 |
39 | ```bash
40 | $ git clone git@github.com:mkgrow/szdt-admin-components.git
41 | $ cd szdt-admin-components
42 | $ npm install
43 | $ npm start
44 | ```
45 |
46 | Open your browser and visit http://localhost:8000
47 |
48 | ## 🤖 Command introduction
49 |
50 | | Name | Description | Remarks |
51 | | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
52 | | `npm install` | Install dependencies | - |
53 | | `npm start` | Project begining | Document usage [dumi](https://github.com/umijs/dumi), component development and documentation development together |
54 | | `npm test` | Component test | - |
55 | | `npm run test:coverage` | Code coverage review | - |
56 | | `npm run prettier` | Code prettier | - |
57 | | `npm run build` | Component packaging | Use [father](https://github.com/umijs/father) | |
58 | | `npm run docs:build` | Document packaging | - |
59 | | `npm run docs:deploy` | Document release | The default is to use GitHub Pages |
60 | | `npm run deploy` | Document package release | - |
61 |
62 | ## 🔗 Links
63 |
64 | - [Ant Design](https://ant.design/)
65 | - [Ant Design Icons](https://github.com/ant-design/ant-design-icons)
66 | - [Icon Font](https://www.iconfont.cn/)
67 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: szdt-admin-components
3 | order: 1
4 | hero:
5 | title: '数据管理系统组件库'
6 | desc: '项目中常用的一些组件封装'
7 | actions:
8 | - text: 开始使用
9 | link: /components
10 | footer: Copyright szdt © 2023
11 | ---
12 |
13 | ## ✨ Online preview
14 |
15 | https://mkgrow.github.io/szdt-admin-components/#/
16 |
17 | ## 📦 Install
18 |
19 | ```bash
20 | npm install szdt-admin-components
21 | ```
22 |
23 | ```bash
24 | yarn add szdt-admin-components
25 | ```
26 |
27 | ## 🔨 Usage
28 |
29 | ```jsx | pure
30 | import React from 'react';
31 | import { IconFont, IconText, Skeleton, TooltipText } from 'szdt-admin-components';
32 |
33 | export default () => (
34 | <>
35 |
36 | } text="测试" />
37 |
38 | content
39 |
40 |
41 |
42 |
43 | >
44 | );
45 | ```
46 |
47 | ## ⌨️ Development
48 |
49 | clone locally:
50 |
51 | ```bash
52 | $ git clone git@github.com:mkgrow/szdt-admin-components.git
53 | $ cd szdt-admin-components
54 | $ npm install
55 | $ npm start
56 | ```
57 |
58 | Open your browser and visit http://localhost:8000
59 |
60 | ## 📒 Catalog Introduction
61 |
62 | ```
63 | ├── docs Component documentation
64 | │ ├── index.md Home page
65 | │ └── **.** Site Directory Document
66 | ├── src Component home directory
67 | │ ├── index.ts Component registration
68 | │ └── Foo Component development
69 | ├── .eslintrc.js eslint config
70 | ├── .fatherrc.ts father config
71 | ├── .umirc.ts dumi config
72 | └── tsconfig.json typescript config
73 | ```
74 |
75 | The rest of the documents can be consulted by yourself.
76 |
77 | ## 🤖 Command introduction
78 |
79 | | Name | Description | Remarks |
80 | | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
81 | | `npm install` | Install dependencies | - |
82 | | `npm start` | Project begining | Document usage [dumi](https://github.com/umijs/dumi), component development and documentation development together |
83 | | `npm test` | Component test | - |
84 | | `npm run test:coverage` | Code coverage review | - |
85 | | `npm run prettier` | Code prettier | - |
86 | | `npm run build` | Component packaging | Use [father](https://github.com/umijs/father) | |
87 | | `npm run docs:build` | Document packaging | - |
88 | | `npm run docs:deploy` | Document release | The default is to use GitHub Pages |
89 | | `npm run deploy` | Document package release | - |
90 |
91 | ## 🔗 Links
92 |
93 | - [Ant Design](https://ant.design/)
94 | - [Ant Design Icons](https://github.com/ant-design/ant-design-icons)
95 | - [Icon Font](https://www.iconfont.cn/)
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "szdt-admin-components",
3 | "version": "1.0.57",
4 | "homepage": "https://mkgrow.github.io/szdt-admin-components/",
5 | "scripts": {
6 | "start": "dumi dev",
7 | "docs:build": "dumi build",
8 | "docs:deploy": "gh-pages -d docs-dist",
9 | "build": "father build",
10 | "deploy": "npm run docs:build && npm run docs:deploy",
11 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,json}\"",
12 | "test": "umi-test",
13 | "test:coverage": "umi-test --coverage",
14 | "lint-staged": "lint-staged",
15 | "release": "npm version patch --no-commit-hooks -m \"v%s\""
16 | },
17 | "files": [
18 | "dist"
19 | ],
20 | "main": "dist/esm/index.d.js",
21 | "module": "dist/esm/index.js",
22 | "typings": "dist/esm/index.d.ts",
23 | "gitHooks": {
24 | "pre-commit": "lint-staged"
25 | },
26 | "lint-staged": {
27 | "*.{js,jsx,less,json}": [
28 | "prettier --write"
29 | ],
30 | "*.ts?(x)": [
31 | "prettier --parser=typescript --write"
32 | ]
33 | },
34 | "dependencies": {
35 | "@ant-design/icons": "^4.5.0",
36 | "antd": "4.20.0",
37 | "babel-plugin-import": "^1.13.6",
38 | "cron-builder-ts": "^1.0.5",
39 | "cronstrue": "^2.22.0",
40 | "react": "^18.0.0",
41 | "umi-request": "^1.4.0"
42 | },
43 | "devDependencies": {
44 | "@testing-library/jest-dom": "^5.15.1",
45 | "@testing-library/react": "^13.0.0",
46 | "@types/jest": "^27.0.3",
47 | "@umijs/fabric": "^2.8.1",
48 | "@umijs/test": "^3.0.5",
49 | "@umijs/yorkie": "^2.0.5",
50 | "dumi": "^1.1.0",
51 | "father": "^4.0.0-rc.2",
52 | "gh-pages": "^3.0.0",
53 | "lint-staged": "^10.0.7",
54 | "prettier": "^2.2.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/assets/images/minus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/styles/variable.less:
--------------------------------------------------------------------------------
1 | /*
2 | * 注意:
3 | * 1. 优先使用 ant 定义变量,方便主题定制
4 | * 2. 请不要在此处覆盖 ant 变量;请在 @/theme.js覆盖
5 | * 3. 变量命名请统一继承 ant 风格
6 | *
7 | * [Ant default theme变量](https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less)
8 | */
9 |
10 | @import (reference) '~antd/es/style/themes/variable';
11 |
12 | @primary-light: ~'tint(@primary-color, 97%)';
13 |
14 | @margin-lg-reverse: -@margin-lg;
15 |
16 | @icon-size-lg: 18px;
17 | @icon-size-md: 14px;
18 | @icon-size-sm: 12px;
19 | @icon-size-xs: 10px;
20 |
21 | @font-size-xs: @font-size-sm - 2;
22 |
23 | @border-radius-md: @border-radius-base * 2;
24 | @border-radius-lg: @border-radius-base * 3;
25 |
26 | @border-width-md: @border-width-base * 2;
27 |
28 | @box-shadow-transition: box-shadow 0.3s, border-color 0.3s;
29 |
30 | @line: #ebf0ff;
31 | @deep-pink: #d1496f;
32 |
33 | @blue-bg: #3e7fcf;
34 | @blue: #3365f9;
35 | @light-blue: #1890ff;
36 | @baby-blue: #ebf0ff;
37 | @gray-bg: #fafafa;
38 | @base-bg: #f7f9f9;
39 |
40 | @dashboard-key-color: #4a4a4a;
41 | @dashboard-up-color: #d04858;
42 | @dashboard-down-color: #6edfb2;
43 | @tip-bg: #dce9fd;
44 |
45 | @setting-item-border: #e8e8e8;
46 | @setting-item-hover: #33a9e0;
47 | @setting-active: #35a5e4;
48 | @setting-active-bg: #f0fdff;
49 |
50 | @red: #cc3615;
51 | @orange: #ffa12a;
52 | @dark: #000;
53 | @navy: #f7f9f9;
54 | @gray: #f1f2f2;
55 | @light-gray: #dcdcdc;
56 | @effect-preview-btn-bg: #f8a02b;
57 | @light-green: #6fbd5e;
58 | @border-color: #f0f0f0;
59 | @header-text-color: #303030;
60 |
61 | @danger-red: #f05557;
62 | @text-gray: #646565;
63 |
64 | @value-color: #515250;
65 | @unused-color: #4db541;
66 |
--------------------------------------------------------------------------------
/src/components/Card/index.less:
--------------------------------------------------------------------------------
1 | @padding-left-right: 1.25rem;
2 | @padding-top-bottom: 1.13rem;
3 | @border-radius-base: 2px;
4 | @font-size-lg: 16px;
5 | @border-color: #f0f0f0;
6 | @box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.48), 0 6px 16px 0 rgba(0, 0, 0, 0.32),
7 | 0 9px 28px 8px rgba(0, 0, 0, 0.2);
8 |
9 | .cus-card {
10 | width: 100%;
11 | background-color: #fff;
12 | border-radius: @border-radius-base * 4;
13 |
14 | &[data-hover='true']:hover {
15 | box-shadow: @box-shadow-base;
16 | }
17 |
18 | &-bordered {
19 | border: 1px solid @border-color;
20 | }
21 |
22 | &-title {
23 | padding: @padding-left-right;
24 | color: rgba(0, 0, 0, 0.85);
25 | font-weight: 500;
26 | font-size: @font-size-lg;
27 | border-bottom: 1px solid @border-color;
28 | }
29 |
30 | &-body {
31 | min-height: 108px;
32 | padding: @padding-left-right;
33 | }
34 |
35 | &-footer {
36 | padding: @padding-top-bottom @padding-left-right;
37 | border-top: 1px solid @border-color;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Card/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | order: 1
7 | title: 基础
8 | ---
9 |
10 | ## Card 组件
11 |
12 | 卡片
13 |
14 | ```tsx
15 | import React from 'react';
16 | import { Card } from 'szdt-admin-components';
17 |
18 | export default () => (
19 | Footer}>
20 | Body
21 |
22 | );
23 | ```
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/Card/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import Card from './index';
5 |
6 | describe('', () => {
7 | it('render Card with dumi', () => {
8 | const msg = 'dumi';
9 |
10 | render();
11 | expect(screen.queryByText(msg)).toBeInTheDocument();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/Card/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './index.less';
4 |
5 | interface Props {
6 | /**
7 | * @description 是否显示
8 | * @default true
9 | * */
10 | show?: boolean;
11 | /**
12 | * @description 标题
13 | * */
14 | title?: string | React.ReactComponentElement;
15 | /**
16 | * @description 内容
17 | * */
18 | children?: string | React.ReactComponentElement | React.ReactNode;
19 | /**
20 | * @description 底部内容
21 | * */
22 | footer?: string | React.ReactComponentElement;
23 | /**
24 | * @description 样式
25 | * */
26 | className?: React.CSSProperties | string;
27 | /**
28 | * @description 样式
29 | * */
30 | style?: React.CSSProperties;
31 | /**
32 | * @description 是否显示鼠标悬浮效果
33 | * @default false
34 | * */
35 | activeHover?: boolean;
36 | }
37 |
38 | export default (props: Props): React.ReactComponentElement => {
39 | const { show = true, title, children, footer, className, activeHover = false } = props;
40 |
41 | return (
42 |
47 |
48 | {title}
49 |
50 |
51 | {children}
52 |
53 |
54 | {footer}
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/components/Cron/CustomCron/index.less:
--------------------------------------------------------------------------------
1 | @import '../../../assets/styles/variable';
2 |
3 | .cron-box {
4 | display: flex;
5 | align-items: center;
6 | margin-top: @margin-md;
7 |
8 | .cron-label {
9 | min-width: 40px;
10 | text-align: left;
11 | }
12 |
13 | .cron-select {
14 | min-width: 30%;
15 | max-width: 90%;
16 | margin-left: @margin-xs;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Cron/CustomCron/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useEffect, useState } from 'react';
2 | import { Select } from 'antd';
3 | import { CronBuilder } from 'cron-builder-ts';
4 | import {
5 | convertResult,
6 | dayOfTheMonthOption,
7 | dayOfTheWeekOption,
8 | defaultCron,
9 | hourOption,
10 | minuteOption,
11 | monthOption,
12 | } from '../utils';
13 | import './index.less';
14 |
15 | const cronExp = new CronBuilder();
16 | const EVERY = ['*'];
17 |
18 | const { Option } = Select;
19 |
20 | export default function CustomCron({ value, disabled, onChange }: any) {
21 | const [expression, setExpression] = useState({});
22 | // const [cronResult, setCronResult] = useState('');
23 |
24 | useEffect(() => {
25 | if (!value) return;
26 | const currentCron = value.split(' ');
27 | currentCron.shift();
28 | const [minutes, hours, dayOfMonth, month1, dayOfWeek] = currentCron;
29 |
30 | /* eslint-disable */
31 | function splitMultiple(value: any) {
32 | if (!value || value === '*' || value === '?') {
33 | return;
34 | }
35 |
36 | if (value.includes(',')) {
37 | return value.split(',');
38 | }
39 | return [value];
40 | }
41 |
42 | const expre: any = {
43 | minute: splitMultiple(minutes) || [],
44 | hour: splitMultiple(hours) || [],
45 | dayOfTheMonth: splitMultiple(dayOfMonth !== '0' ? dayOfMonth : '0'),
46 | dayOfTheWeek: splitMultiple(dayOfWeek) || [],
47 | month: splitMultiple(month1) || [],
48 | };
49 | setExpression(expre);
50 | // setCronResult(convertCron(value));
51 | }, [value]);
52 |
53 | function handleChange(obj: any) {
54 | const tmp = { ...expression, ...obj };
55 | setExpression(tmp);
56 | onChange(generateExpression(tmp));
57 | }
58 |
59 | function generateExpression(expression: any) {
60 | const {
61 | minute = [],
62 | hour = [],
63 | dayOfTheMonth = [],
64 | month = [],
65 | dayOfTheWeek = [],
66 | } = expression;
67 | const exp = cronExp.getAll();
68 | exp.minute = minute.length > 0 ? minute : EVERY;
69 | exp.hour = hour.length > 0 ? hour : EVERY;
70 | exp.dayOfTheMonth =
71 | dayOfTheMonth && dayOfTheMonth.length > 0 && dayOfTheMonth[0] !== '0' ? dayOfTheMonth : EVERY;
72 | exp.month = month.length > 0 ? month : EVERY;
73 | exp.dayOfTheWeek = dayOfTheWeek.length > 0 ? dayOfTheWeek : EVERY;
74 | cronExp.setAll(exp);
75 | const expressionResult = cronExp.build();
76 | const result = expressionResult === defaultCron ? '' : expressionResult;
77 | // setCronResult(result);
78 |
79 | return convertResult(result);
80 | }
81 |
82 | function renderSelect(label: any, placeholder: any, key: string, value: any, data: any[] = []) {
83 | return (
84 |
85 |
{label}:
86 |
87 |
101 |
102 |
103 | );
104 | }
105 |
106 | const { minute, hour, dayOfTheMonth, dayOfTheWeek, month }: any = expression;
107 |
108 | return (
109 |
110 | {renderSelect('月份', '每月', 'month', month, monthOption)}
111 | {renderSelect('星期', '每周', 'dayOfTheWeek', dayOfTheWeek, dayOfTheWeekOption)}
112 | {renderSelect('日', '每天', 'dayOfTheMonth', dayOfTheMonth, dayOfTheMonthOption)}
113 | {renderSelect('小时', '每小时', 'hour', hour, hourOption)}
114 | {renderSelect('分钟', '每分钟', 'minute', minute, minuteOption)}
115 |
116 | // {cronResult}
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/Cron/index.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/styles/variable';
2 |
3 | .cron {
4 | .select-right {
5 | margin-right: @margin-md;
6 | }
7 |
8 | .select-auto-width {
9 | width: auto;
10 | }
11 |
12 | .select-fixed-width {
13 | width: 200px;
14 | }
15 | }
16 |
17 | .custom {
18 | margin-top: @margin-md;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Cron/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 其他
7 | ---
8 |
9 | ## Cron 组件
10 |
11 | 根据选择的类型和时间生成cron表达式
12 |
13 | ```tsx
14 | import React, { useState } from 'react';
15 | import { Space } from 'antd';
16 | import { Cron } from 'szdt-admin-components';
17 |
18 | export default () => {
19 | const [value, setValue] = useState(null);
20 |
21 | return (
22 |
23 |
24 | {value}
25 |
26 | )
27 | };
28 | ```
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/Cron/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import Cron from './index';
5 |
6 | describe('', () => {
7 | it('render Cron with loading', () => {
8 | const onChange = jest.fn();
9 | render();
10 | expect(screen.getByPlaceholderText('请选择时间')).toBeInTheDocument();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/Cron/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useEffect, useState } from 'react';
2 | import { Select, TimePicker, ConfigProvider } from 'antd';
3 | import { dayOfTheMonthOption, dayOfTheWeekData } from './utils';
4 | import CustomCron from './CustomCron';
5 | import moment from 'moment';
6 | import zh_CN from 'antd/lib/locale-provider/zh_CN';
7 |
8 | import './index.less';
9 |
10 | const { Option } = Select;
11 | const format = 'HH:mm';
12 | const defaultCron = '0 * * * * ?';
13 | const space = ' ';
14 | const timeTypes = [
15 | { key: 'everyDay', label: '每天' },
16 | { key: 'everyWeek', label: '每周' },
17 | { key: 'everyMonth', label: '每月' },
18 | { key: 'customize', label: '周期' },
19 | ];
20 |
21 | interface Props {
22 | /**
23 | * @description 默认显示的cron
24 | * */
25 | value?: string;
26 | /**
27 | * @description 默认周期类型
28 | * @default everyDay
29 | * */
30 | defaultType?: 'everyDay' | 'everyWeek' | 'everyMonth' | 'customize';
31 | /**
32 | * @description 改变后回调
33 | * */
34 | onChange?: (cron?: string) => void;
35 | }
36 | const Cron: React.FC = ({ defaultType, value, onChange }) => {
37 | const [defaultTimeType, setDefaultTimeType] = useState(timeTypes[0].key);
38 | const [selectedValue, setSelectedValue] = useState<[]>([]);
39 | const [selectTime, setSelectTime] = useState(null);
40 | const [expression, setExpression] = useState(defaultCron);
41 |
42 | useEffect(() => {
43 | if (defaultType) {
44 | setDefaultTimeType(defaultType);
45 | }
46 | }, [defaultType]);
47 |
48 | /* eslint-disable */
49 | useEffect(() => {
50 | if (!value || value === defaultCron) return;
51 | const currentCron = value.split(' ');
52 | const [seconds, minutes, hours, dayOfMonth, , dayOfWeek] = currentCron;
53 | if (defaultTimeType !== 'customize') {
54 | let selectTimeType = '';
55 | let defaultSelectValue = [];
56 | if (dayOfWeek === '?' && dayOfMonth === '*' && hours !== '*' && minutes !== '*')
57 | selectTimeType = 'everyDay';
58 |
59 | if (
60 | dayOfWeek !== '?' &&
61 | (dayOfMonth === '*' || dayOfMonth === '?') &&
62 | minutes !== '*' &&
63 | hours !== '*'
64 | ) {
65 | selectTimeType = 'everyWeek';
66 | defaultSelectValue = dayOfWeek.split(',');
67 | }
68 | if (
69 | dayOfMonth !== '*' &&
70 | dayOfMonth !== '?' &&
71 | dayOfWeek === '?' &&
72 | minutes !== '*' &&
73 | hours !== '*' &&
74 | seconds
75 | ) {
76 | selectTimeType = 'everyMonth';
77 | defaultSelectValue = dayOfMonth.split(',');
78 | }
79 | setSelectTime(moment({ hours, minutes }));
80 | setExpression(value);
81 | setSelectedValue(defaultSelectValue);
82 | setDefaultTimeType(selectTimeType);
83 | }
84 | if (
85 | minutes.indexOf(',') !== -1 ||
86 | hours.indexOf(',') !== -1 ||
87 | (hours === '*' && minutes !== '*') ||
88 | (hours !== '*' && minutes === '*')
89 | ) {
90 | setDefaultTimeType('customize');
91 | }
92 | }, [value]);
93 |
94 | const handleTimeTypeChange = (selectValue: string) => {
95 | setDefaultTimeType(selectValue);
96 | setSelectTime(null);
97 | setSelectedValue([]);
98 | setExpression(defaultCron);
99 | };
100 |
101 | const handleSelectChange = (data: []) => {
102 | setSelectedValue(data);
103 | const selectValues = data.join(',');
104 | const currentCron = expression ? expression.split(' ') : [];
105 | const [seconds, minutes, hours, dayOfMonth, month1, dayOfWeek] = currentCron;
106 | let result = '';
107 | if (defaultTimeType === 'everyWeek') {
108 | result = seconds
109 | .concat(space)
110 | .concat(minutes)
111 | .concat(space)
112 | .concat(hours)
113 | .concat(space)
114 | .concat(dayOfMonth)
115 | .concat(space)
116 | .concat(month1)
117 | .concat(space)
118 | .concat(selectValues);
119 | }
120 | if (defaultTimeType === 'everyMonth') {
121 | result = seconds
122 | .concat(space)
123 | .concat(minutes)
124 | .concat(space)
125 | .concat(hours)
126 | .concat(space)
127 | .concat(data.length ? selectValues : '*')
128 | .concat(space)
129 | .concat(month1)
130 | .concat(space)
131 | .concat(dayOfWeek);
132 | }
133 | if (selectTime) onChange?.(result);
134 | setExpression(result);
135 | };
136 |
137 | const handleTimeChange = (time: moment.Moment | null) => {
138 | setSelectTime(time);
139 | // if (!time) return;
140 | const currentCron = expression ? expression.split(' ') : [];
141 | const [seconds, , , dayOfMonth, month1, dayOfWeek] = currentCron;
142 | const minutes = moment(time).minutes().toString();
143 | const hours = moment(time).hours().toString();
144 | let result = null;
145 | if (!Number.isNaN(Number(hours)) && !Number.isNaN(Number(minutes))) {
146 | const minutesAndHour = seconds
147 | .concat(space)
148 | .concat(minutes)
149 | .concat(space)
150 | .concat(hours)
151 | .concat(space);
152 | if (defaultTimeType === 'everyDay') result = minutesAndHour.concat('* * ?');
153 | if (defaultTimeType !== 'everyDay')
154 | result = minutesAndHour
155 | .concat(dayOfMonth)
156 | .concat(space)
157 | .concat(month1)
158 | .concat(space)
159 | .concat(dayOfWeek);
160 | }
161 | onChange?.(result);
162 | setExpression(result);
163 | };
164 |
165 | const RenderSelect = ({
166 | placeholder,
167 | data = [],
168 | }: {
169 | placeholder: string;
170 | data: { key: string; label: string }[];
171 | }) => {
172 | return (
173 |
174 |
191 | {defaultTimeType !== 'everyHour' ? (
192 |
193 |
199 |
200 | ) : null}
201 |
202 | );
203 | };
204 |
205 | return (
206 |
207 |
221 | {defaultTimeType === 'customize' && }
222 | {defaultTimeType === 'everyDay' && (
223 |
224 |
230 |
231 | )}
232 | {defaultTimeType === 'everyWeek' && (
233 |
234 | )}
235 | {defaultTimeType === 'everyMonth' && (
236 |
237 | )}
238 |
239 | );
240 | };
241 |
242 | export default Cron;
243 |
--------------------------------------------------------------------------------
/src/components/Cron/utils.ts:
--------------------------------------------------------------------------------
1 | import cronstrue from 'cronstrue/i18n';
2 |
3 | function getDayOfTheMonthOption() {
4 | const days = [];
5 | for (let i = 1; i < 32; i += 1) {
6 | days.push({ key: i.toString(), label: i.toString().concat('号') });
7 | }
8 | return days;
9 | }
10 |
11 | function getHourOption() {
12 | const hours = [];
13 | for (let i = 0; i < 24; i += 1) {
14 | hours.push({ key: i.toString(), label: i.toString() });
15 | }
16 | return hours;
17 | }
18 |
19 | function getMinuteOption() {
20 | const hours = [];
21 | for (let i = 0; i < 60; i += 1) {
22 | hours.push({ key: i.toString(), label: i.toString() });
23 | }
24 | return hours;
25 | }
26 |
27 | export const defaultCron = '* * * * *';
28 | export const everyDay = '0 0 0 * * ?';
29 | export const dayOfTheMonthOption = getDayOfTheMonthOption();
30 |
31 | /* eslint-disable */
32 | export function convertCron(exp: any) {
33 | if (!exp) return;
34 | if (exp === everyDay || exp === defaultCron) return '每天';
35 | const convertExpZH = cronstrue.toString(exp, { locale: 'zh_CN' });
36 | return convertExpZH.replace(/^每\D+,\s*/, '');
37 | }
38 |
39 | export const dayOfTheWeekData = [
40 | { key: 'MON', label: '星期一' },
41 | { key: 'TUE', label: '星期二' },
42 | { key: 'WED', label: '星期三' },
43 | { key: 'THU', label: '星期四' },
44 | { key: 'FRI', label: '星期五' },
45 | { key: 'SAT', label: '星期六' },
46 | { key: 'SUN', label: '星期天' },
47 | ];
48 |
49 | export const dayOfTheWeekOption = [
50 | { key: '1', label: '星期一' },
51 | { key: '2', label: '星期二' },
52 | { key: '3', label: '星期三' },
53 | { key: '4', label: '星期四' },
54 | { key: '5', label: '星期五' },
55 | { key: '6', label: '星期六' },
56 | { key: '7', label: '星期天' },
57 | ];
58 |
59 | export const monthOption = [
60 | { key: '1', label: '一月' },
61 | { key: '2', label: '二月' },
62 | { key: '3', label: '三月' },
63 | { key: '4', label: '四月' },
64 | { key: '5', label: '五月' },
65 | { key: '6', label: '六月' },
66 | { key: '7', label: '七月' },
67 | { key: '8', label: '八月' },
68 | { key: '9', label: '九月' },
69 | { key: '10', label: '十月' },
70 | { key: '11', label: '十一月' },
71 | { key: '12', label: '十二月' },
72 | ];
73 |
74 | export const hourOption: any = getHourOption();
75 | export const minuteOption: any = getMinuteOption();
76 |
77 | export function convertResult(expression: any) {
78 | const defaultSecond = '0 ';
79 | const defaultMinute = '0 * ';
80 | const defaultHour = '0 * * ';
81 | const questionMark = '?';
82 | const asterisk = '*';
83 | const space = ' ';
84 |
85 | if (!expression) return null;
86 | let result: any = '';
87 | const splitCron: any = expression.split(' ');
88 | const minute: any = splitCron[0];
89 | const hour: any = splitCron[1];
90 | const day: any = splitCron[2];
91 | const month: any = splitCron[3];
92 | const week: any = splitCron[4];
93 | const minuteSame: any = result.concat(defaultSecond).concat(minute).concat(space);
94 | const hourSame: any = result.concat(defaultMinute).concat(hour).concat(space);
95 | const daySame: any = result.concat(defaultHour).concat(day).concat(space);
96 | const weekSame: any = result.concat(defaultHour).concat(questionMark).concat(space);
97 |
98 | if (
99 | minute !== asterisk &&
100 | hour === asterisk &&
101 | day !== asterisk &&
102 | week === asterisk &&
103 | month === asterisk
104 | ) {
105 | result = minuteSame
106 | .concat(asterisk)
107 | .concat(space)
108 | .concat(day)
109 | .concat(space)
110 | .concat(asterisk)
111 | .concat(space)
112 | .concat(asterisk);
113 | }
114 |
115 | // cron error 不支持同时指定星期几和几月参数
116 | if (
117 | minute !== asterisk &&
118 | hour === asterisk &&
119 | day !== asterisk &&
120 | week !== asterisk &&
121 | month === asterisk
122 | ) {
123 | result = minuteSame
124 | .concat(asterisk)
125 | .concat(space)
126 | .concat(day)
127 | .concat(space)
128 | .concat(asterisk)
129 | .concat(space)
130 | .concat(week);
131 | }
132 |
133 | // cron error 不支持同时指定星期几和几月参数
134 | if (
135 | minute !== asterisk &&
136 | hour === asterisk &&
137 | day !== asterisk &&
138 | week !== asterisk &&
139 | month !== asterisk
140 | ) {
141 | result = minuteSame
142 | .concat(asterisk)
143 | .concat(space)
144 | .concat(day)
145 | .concat(space)
146 | .concat(month)
147 | .concat(space)
148 | .concat(week);
149 | }
150 |
151 | if (
152 | minute !== asterisk &&
153 | hour === asterisk &&
154 | day === asterisk &&
155 | week !== asterisk &&
156 | month !== asterisk
157 | ) {
158 | result = minuteSame
159 | .concat(asterisk)
160 | .concat(space)
161 | .concat(questionMark)
162 | .concat(space)
163 | .concat(month)
164 | .concat(space)
165 | .concat(week);
166 | }
167 |
168 | if (
169 | minute !== asterisk &&
170 | hour === asterisk &&
171 | day === asterisk &&
172 | week !== asterisk &&
173 | month === asterisk
174 | ) {
175 | result = minuteSame
176 | .concat(asterisk)
177 | .concat(space)
178 | .concat(questionMark)
179 | .concat(space)
180 | .concat(asterisk)
181 | .concat(space)
182 | .concat(week);
183 | }
184 |
185 | if (
186 | minute !== asterisk &&
187 | hour === asterisk &&
188 | day === asterisk &&
189 | week === asterisk &&
190 | month !== asterisk
191 | ) {
192 | result = minuteSame
193 | .concat(asterisk)
194 | .concat(space)
195 | .concat(asterisk)
196 | .concat(space)
197 | .concat(month)
198 | .concat(space)
199 | .concat(questionMark);
200 | }
201 |
202 | if (
203 | minute !== asterisk &&
204 | hour === asterisk &&
205 | day === asterisk &&
206 | week === asterisk &&
207 | month === asterisk
208 | ) {
209 | result = minuteSame
210 | .concat(asterisk)
211 | .concat(space)
212 | .concat(asterisk)
213 | .concat(space)
214 | .concat(asterisk)
215 | .concat(space)
216 | .concat(questionMark);
217 | }
218 |
219 | if (
220 | minute !== asterisk &&
221 | hour !== asterisk &&
222 | day === asterisk &&
223 | week === asterisk &&
224 | month === asterisk
225 | ) {
226 | result = minuteSame
227 | .concat(hour)
228 | .concat(space)
229 | .concat(asterisk)
230 | .concat(space)
231 | .concat(asterisk)
232 | .concat(space)
233 | .concat(questionMark);
234 | }
235 |
236 | if (
237 | minute !== asterisk &&
238 | hour !== asterisk &&
239 | day === asterisk &&
240 | week === asterisk &&
241 | month !== asterisk
242 | ) {
243 | result = minuteSame
244 | .concat(hour)
245 | .concat(space)
246 | .concat(asterisk)
247 | .concat(space)
248 | .concat(month)
249 | .concat(space)
250 | .concat(questionMark);
251 | }
252 |
253 | if (
254 | minute !== asterisk &&
255 | hour !== asterisk &&
256 | day === asterisk &&
257 | week !== asterisk &&
258 | month === asterisk
259 | ) {
260 | result = minuteSame
261 | .concat(hour)
262 | .concat(space)
263 | .concat(questionMark)
264 | .concat(space)
265 | .concat(asterisk)
266 | .concat(space)
267 | .concat(week);
268 | }
269 |
270 | if (
271 | minute !== asterisk &&
272 | hour !== asterisk &&
273 | day === asterisk &&
274 | week !== asterisk &&
275 | month !== asterisk
276 | ) {
277 | result = minuteSame
278 | .concat(hour)
279 | .concat(space)
280 | .concat(questionMark)
281 | .concat(space)
282 | .concat(month)
283 | .concat(space)
284 | .concat(week);
285 | }
286 |
287 | if (
288 | minute !== asterisk &&
289 | hour !== asterisk &&
290 | day !== asterisk &&
291 | week === asterisk &&
292 | month === asterisk
293 | ) {
294 | result = minuteSame
295 | .concat(hour)
296 | .concat(space)
297 | .concat(day)
298 | .concat(space)
299 | .concat(asterisk)
300 | .concat(space)
301 | .concat(questionMark);
302 | }
303 |
304 | if (
305 | minute !== asterisk &&
306 | hour !== asterisk &&
307 | day !== asterisk &&
308 | week === asterisk &&
309 | month !== asterisk
310 | ) {
311 | result = minuteSame
312 | .concat(hour)
313 | .concat(space)
314 | .concat(day)
315 | .concat(space)
316 | .concat(month)
317 | .concat(space)
318 | .concat(questionMark);
319 | }
320 |
321 | // cron error 不支持同时指定星期几和几月参数
322 | if (
323 | minute !== asterisk &&
324 | hour !== asterisk &&
325 | day !== asterisk &&
326 | week !== asterisk &&
327 | month === asterisk
328 | ) {
329 | result = minuteSame
330 | .concat(hour)
331 | .concat(space)
332 | .concat(day)
333 | .concat(space)
334 | .concat(asterisk)
335 | .concat(space)
336 | .concat(week);
337 | }
338 |
339 | // cron error 不支持同时指定星期几和几月参数
340 | if (
341 | minute !== asterisk &&
342 | hour !== asterisk &&
343 | day !== asterisk &&
344 | week !== asterisk &&
345 | month !== asterisk
346 | ) {
347 | result = minuteSame
348 | .concat(hour)
349 | .concat(space)
350 | .concat(day)
351 | .concat(space)
352 | .concat(month)
353 | .concat(space)
354 | .concat(week);
355 | }
356 |
357 | if (
358 | minute === asterisk &&
359 | hour !== asterisk &&
360 | day === asterisk &&
361 | week !== asterisk &&
362 | month !== asterisk
363 | ) {
364 | result = hourSame.concat(questionMark).concat(space).concat(month).concat(space).concat(week);
365 | }
366 |
367 | if (
368 | minute === asterisk &&
369 | hour !== asterisk &&
370 | day === asterisk &&
371 | week === asterisk &&
372 | month !== asterisk
373 | ) {
374 | result = hourSame
375 | .concat(questionMark)
376 | .concat(space)
377 | .concat(month)
378 | .concat(space)
379 | .concat(asterisk);
380 | }
381 |
382 | if (
383 | minute === asterisk &&
384 | hour !== asterisk &&
385 | day === asterisk &&
386 | week === asterisk &&
387 | month === asterisk
388 | ) {
389 | result = hourSame
390 | .concat(asterisk)
391 | .concat(space)
392 | .concat(asterisk)
393 | .concat(space)
394 | .concat(questionMark);
395 | }
396 |
397 | if (
398 | minute === asterisk &&
399 | hour !== asterisk &&
400 | day === asterisk &&
401 | week !== asterisk &&
402 | month === asterisk
403 | ) {
404 | result = hourSame
405 | .concat(questionMark)
406 | .concat(space)
407 | .concat(asterisk)
408 | .concat(space)
409 | .concat(week);
410 | }
411 |
412 | if (
413 | minute === asterisk &&
414 | hour !== asterisk &&
415 | day !== asterisk &&
416 | week === asterisk &&
417 | month !== asterisk
418 | ) {
419 | result = hourSame.concat(day).concat(space).concat(month).concat(space).concat(questionMark);
420 | }
421 |
422 | if (
423 | minute === asterisk &&
424 | hour !== asterisk &&
425 | day !== asterisk &&
426 | week === asterisk &&
427 | month === asterisk
428 | ) {
429 | result = hourSame.concat(day).concat(space).concat(asterisk).concat(space).concat(questionMark);
430 | }
431 |
432 | if (
433 | minute === asterisk &&
434 | hour !== asterisk &&
435 | day !== asterisk &&
436 | week !== asterisk &&
437 | month === asterisk
438 | ) {
439 | result = hourSame.concat(day).concat(space).concat(asterisk).concat(space).concat(week);
440 | }
441 |
442 | if (
443 | minute === asterisk &&
444 | hour !== asterisk &&
445 | day !== asterisk &&
446 | week !== asterisk &&
447 | month !== asterisk
448 | ) {
449 | result = hourSame.concat(day).concat(space).concat(month).concat(space).concat(week);
450 | }
451 |
452 | if (
453 | minute === asterisk &&
454 | hour === asterisk &&
455 | day !== asterisk &&
456 | week !== asterisk &&
457 | month !== asterisk
458 | ) {
459 | result = daySame.concat(month).concat(space).concat(week);
460 | }
461 |
462 | if (
463 | minute === asterisk &&
464 | hour === asterisk &&
465 | day !== asterisk &&
466 | week !== asterisk &&
467 | month === asterisk
468 | ) {
469 | result = daySame.concat(asterisk).concat(space).concat(week);
470 | }
471 |
472 | if (
473 | minute === asterisk &&
474 | hour === asterisk &&
475 | day !== asterisk &&
476 | week === asterisk &&
477 | month !== asterisk
478 | ) {
479 | result = daySame.concat(month).concat(space).concat(questionMark);
480 | }
481 |
482 | if (
483 | minute === asterisk &&
484 | hour === asterisk &&
485 | day !== asterisk &&
486 | week === asterisk &&
487 | month === asterisk
488 | ) {
489 | result = daySame.concat(asterisk).concat(space).concat(questionMark);
490 | }
491 |
492 | if (
493 | minute === asterisk &&
494 | hour === asterisk &&
495 | day === asterisk &&
496 | week !== asterisk &&
497 | month !== asterisk
498 | ) {
499 | result = weekSame.concat(month).concat(space).concat(week);
500 | }
501 |
502 | if (
503 | minute === asterisk &&
504 | hour === asterisk &&
505 | day === asterisk &&
506 | week !== asterisk &&
507 | month === asterisk
508 | ) {
509 | result = daySame.concat(questionMark).concat(space).concat(week);
510 | }
511 |
512 | if (
513 | minute === asterisk &&
514 | hour === asterisk &&
515 | day === asterisk &&
516 | week === asterisk &&
517 | month !== asterisk
518 | ) {
519 | result = weekSame.concat(month).concat(space).concat(asterisk);
520 | }
521 |
522 | return result;
523 | }
524 |
--------------------------------------------------------------------------------
/src/components/Empty/index.less:
--------------------------------------------------------------------------------
1 | .empty-content {
2 | border-radius: 6px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/Empty/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | order: 1
7 | title: 基础
8 | ---
9 |
10 | ## Empty 组件
11 |
12 | 空数据时显示
13 |
14 | ```tsx
15 | import React from 'react';
16 | import { Empty } from 'szdt-admin-components';
17 |
18 | export default () => ;
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Empty/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import Empty from './index';
5 |
6 | describe('', () => {
7 | it('render Empty with loading', () => {
8 | render();
9 | expect(screen.getByText('暂无数据')).toBeInTheDocument();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/Empty/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, Empty as AntEmpty } from 'antd';
3 | import cls from 'classnames';
4 |
5 | import './index.less';
6 |
7 | interface Props {
8 | /**
9 | * @description 空数据描述
10 | * */
11 | text?: React.ReactNode;
12 | /**
13 | * @description 样式
14 | * */
15 | className?: string;
16 | /**
17 | * @description 最外层卡片样式
18 | * */
19 | cardClassName?: string;
20 | /**
21 | * @description 空数据图片
22 | * */
23 | img?: React.ReactNode;
24 | /**
25 | * @description 是否显示
26 | * @default true
27 | * */
28 | show?: boolean;
29 | /**
30 | * @description 是否显示边框
31 | * @default true
32 | * */
33 | bordered?: boolean;
34 | /**
35 | * @description 空状态内容样式
36 | * */
37 | style?: React.CSSProperties;
38 | }
39 |
40 | export default function Empty(props: Props) {
41 | const {
42 | text = '暂无数据',
43 | className,
44 | cardClassName,
45 | img = AntEmpty.PRESENTED_IMAGE_SIMPLE,
46 | show = true,
47 | bordered = true,
48 | style,
49 | } = props;
50 |
51 | return show ? (
52 |
53 |
54 |
55 | ) : null;
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/ExportBtn/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 其他
7 | ---
8 |
9 | ## ExportBtn 组件
10 |
11 | 下载图片、Excel
12 |
13 | ```tsx
14 | import React from 'react';
15 | import { ExportBtn } from 'szdt-admin-components';
16 |
17 | export default () => (
18 | 下载图片
19 | );
20 | ```
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/ExportBtn/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { fireEvent, render, screen } from '@testing-library/react';
4 | import ExportBtn from './index';
5 |
6 | describe('', () => {
7 | it('render ExportBtn with loading', () => {
8 | render(
9 |
10 | 下载
11 | ,
12 | );
13 | expect(screen.getByRole('export')).toBeInTheDocument();
14 | });
15 |
16 | it('download image with click', () => {
17 | const onClick = jest.fn();
18 | render(
19 |
23 | 下载
24 | ,
25 | );
26 | const btn = screen.getByRole('export');
27 | fireEvent.click(btn);
28 | expect(onClick).toHaveBeenCalledTimes(1);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/components/ExportBtn/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { Button } from 'antd';
3 | import { UploadOutlined } from '@ant-design/icons';
4 | import useDownload from './useDownload';
5 | import { BaseButtonProps } from 'antd/lib/button/button';
6 |
7 | const ExportBtn: React.FC<
8 | {
9 | url?: string;
10 | } & BaseButtonProps
11 | > = (props) => {
12 | const {
13 | children,
14 | url = '/download',
15 | type,
16 | icon = ,
17 | disabled,
18 | onClick,
19 | style,
20 | className,
21 | size,
22 | ...options
23 | } = props;
24 | const { loading, download } = useDownload(url, options);
25 |
26 | const handleOnClick = useCallback(
27 | (e: any) => {
28 | download();
29 | onClick?.(e);
30 | },
31 | [download, onClick],
32 | );
33 |
34 | return (
35 |
48 | );
49 | };
50 |
51 | export default ExportBtn;
52 |
--------------------------------------------------------------------------------
/src/components/ExportBtn/useDownload.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 | import { message } from 'antd';
3 | import request from 'umi-request';
4 | import type { RequestOptionsInit } from 'umi-request';
5 |
6 | export function save(blob: Blob, filename: string) {
7 | const objectUrl = window.URL.createObjectURL(blob);
8 |
9 | const anchor = window.document.createElement('a');
10 | anchor.setAttribute('download', filename);
11 | anchor.setAttribute('href', objectUrl);
12 | anchor.click();
13 | anchor.remove();
14 |
15 | window.URL.revokeObjectURL(objectUrl);
16 | }
17 |
18 | function getFilename(filename?: string, headers?: Headers): string {
19 | const disposition = headers?.get?.('content-disposition');
20 | if (!disposition) {
21 | return filename || 'download';
22 | }
23 |
24 | const remoteFilename = decodeURIComponent(disposition.replace('attachment;filename=', ''));
25 |
26 | const [, ext] = remoteFilename.match(/.*(\.\w+)$/) || [];
27 |
28 | return filename ? `${filename}${ext}` : remoteFilename || 'download';
29 | }
30 |
31 | export interface DownloadRequestOptions extends RequestOptionsInit {
32 | filename?: string;
33 | onDownload?: () => void;
34 | onError?: (e: Error) => void;
35 | onFinished?: () => void;
36 | }
37 |
38 | export default function useDownload(url: string, options?: DownloadRequestOptions) {
39 | const [loading, setLoading] = useState(false);
40 |
41 | const download = useCallback(() => {
42 | setLoading(true);
43 |
44 | const { filename, onDownload, onError, onFinished, ...rest } = options || {};
45 | request(url, {
46 | ...rest,
47 | getResponse: true,
48 | responseType: 'blob',
49 | })
50 | .then(({ data, response }) => {
51 | save(data, getFilename(filename, response.headers));
52 |
53 | if (onDownload) {
54 | onDownload?.();
55 | } else {
56 | message.success('下载成功!', 2000);
57 | }
58 | })
59 | .catch((e: Error) => {
60 | // eslint-disable-next-line no-console
61 | console.error(e);
62 | if (onError) {
63 | onError(e);
64 | } else {
65 | message.error('下载失败!');
66 | }
67 | })
68 | .finally(() => {
69 | setLoading(false);
70 | onFinished?.();
71 | });
72 | }, [options, url]);
73 |
74 | return { loading, download };
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/FormFooterAction/index.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/styles/variable';
2 |
3 | .form-footer {
4 | display: flex;
5 | justify-content: flex-end;
6 | padding: @margin-md;
7 | background-color: @component-background;
8 |
9 | .ant-form-item {
10 | margin-bottom: 0;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/FormFooterAction/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | order: 3
7 | title: 布局
8 | ---
9 |
10 | ## FormFooterAction
11 | 表单底部操作按钮
12 |
13 | ```tsx
14 | import React from 'react';
15 | import { Button } from 'antd';
16 | import { FormFooterAction } from 'szdt-admin-components';
17 |
18 | export default () => 返回} />;
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/FormFooterAction/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import FormFooterAction from './index';
5 |
6 | describe('', () => {
7 | beforeAll(() => {
8 | Object.defineProperty(window, 'matchMedia', {
9 | writable: true,
10 | value: jest.fn().mockImplementation((query) => ({
11 | matches: false,
12 | media: query,
13 | onchange: null,
14 | addListener: jest.fn(),
15 | removeListener: jest.fn(),
16 | addEventListener: jest.fn(),
17 | removeEventListener: jest.fn(),
18 | dispatchEvent: jest.fn(),
19 | })),
20 | });
21 | });
22 |
23 | it('render FormFooterAction with dumi', () => {
24 | render();
25 | expect(screen.getByRole('form-footer')).toBeInTheDocument();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/FormFooterAction/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { Button, Form, Space } from 'antd';
3 |
4 | import './index.less';
5 |
6 | const { Item } = Form;
7 |
8 | interface Props {
9 | /**
10 | * @description 确认按钮是否可点
11 | * @default false
12 | * */
13 | disabled?: boolean;
14 | /**
15 | * @description 取消按钮文本
16 | * @default 取消
17 | * */
18 | onCancelText?: string;
19 | /**
20 | * @description 确认按钮文本
21 | * @default 确认
22 | * */
23 | onOkText?: string;
24 | /**
25 | * @description 取消按钮点击回调
26 | * */
27 | onCancel?: (v?: any) => void;
28 | /**
29 | * @description 确认按钮点击回调
30 | * */
31 | onOk?: (v?: any) => void;
32 | /**
33 | * @description 是否加载
34 | * @default false
35 | * */
36 | loading?: boolean;
37 | /**
38 | * @description 扩展按钮
39 | * @default null
40 | * */
41 | extraBtn?: React.ReactNode;
42 | /**
43 | * @description 其他参数
44 | * */
45 | [k: string]: any;
46 | }
47 | function FormFooterAction({
48 | disabled = false,
49 | onCancelText = '取消',
50 | onOkText = '确认',
51 | onCancel,
52 | onOk,
53 | loading,
54 | extraBtn,
55 | ...rest
56 | }: Props) {
57 | const onHandleCancel = useCallback(() => {
58 | if (onCancel) {
59 | onCancel();
60 | return;
61 | }
62 | window.history.back();
63 | }, [onCancel]);
64 |
65 | const onHandleOk = useCallback(
66 | (e) => {
67 | e.preventDefault();
68 | if (onOk) {
69 | onOk();
70 | }
71 | },
72 | [onOk],
73 | );
74 |
75 | return (
76 |
77 |
78 | - {extraBtn}
79 | -
80 |
81 |
82 |
91 |
92 |
93 |
94 |
95 | );
96 | }
97 |
98 | export default FormFooterAction;
99 |
--------------------------------------------------------------------------------
/src/components/FormLayout/index.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/styles/variable';
2 |
3 | .form-content {
4 | margin-bottom: @margin-lg;
5 | padding: @padding-lg 0;
6 | background: white;
7 |
8 | .ant-form-item:last-child {
9 | margin-bottom: 0;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/FormLayout/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | order: 3
7 | title: 布局
8 | ---
9 |
10 | ## FormLayout 布局
11 |
12 | ```tsx
13 | import React from 'react';
14 | import { Form, Input, Button } from 'antd';
15 | import { FormLayout } from 'szdt-admin-components';
16 | const { Item } = Form;
17 |
18 | export default () => {
19 | const [form] = Form.useForm();
20 | return (
21 | 返回}>
22 | -
32 |
33 |
34 | -
44 |
45 |
46 |
47 | )
48 | };
49 | ```
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/components/FormLayout/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Input } from 'antd';
3 | import '@testing-library/jest-dom';
4 | import { render, screen } from '@testing-library/react';
5 | import FormLayout from './index';
6 |
7 | describe('', () => {
8 | beforeAll(() => {
9 | Object.defineProperty(window, 'matchMedia', {
10 | writable: true,
11 | value: jest.fn().mockImplementation((query) => ({
12 | matches: false,
13 | media: query,
14 | onchange: null,
15 | addListener: jest.fn(),
16 | removeListener: jest.fn(),
17 | addEventListener: jest.fn(),
18 | removeEventListener: jest.fn(),
19 | dispatchEvent: jest.fn(),
20 | })),
21 | });
22 | });
23 |
24 | it('render FormLayout with dumi', () => {
25 | render(
26 |
27 |
37 |
38 |
39 | ,
40 | );
41 | expect(screen.getByRole('form-layout')).toBeInTheDocument();
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/components/FormLayout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form } from 'antd';
3 | import cls from 'classnames';
4 | import FormFooterAction from '../FormFooterAction';
5 | import type { FormContextProps } from 'antd/es/form/context';
6 | import type { ColProps } from 'antd/lib/grid';
7 |
8 | import './index.less';
9 |
10 | interface Props extends FormContextProps {
11 | /**
12 | * @description 表单内容
13 | * */
14 | children?: React.ReactNode;
15 | /**
16 | * @description 底部操作区
17 | * */
18 | footer?: React.ReactNode;
19 | /**
20 | * @description label 标签的文本换行方式
21 | * */
22 | labelCol?: ColProps;
23 | /**
24 | * @description 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol
25 | * */
26 | wrapCol?: ColProps;
27 | /**
28 | * @description 内容区域样式
29 | * */
30 | contentClass?: React.CSSProperties;
31 | /**
32 | * @description 内容style
33 | * */
34 | style?: object;
35 | /**
36 | * @description 底部操作区扩展
37 | * */
38 | extraBtn?: React.ReactNode;
39 | /**
40 | * @description 其他Form参数
41 | * */
42 | [k: string]: any;
43 | }
44 |
45 | function FormLayout(props: Props) {
46 | const { children, footer, labelCol, wrapCol, contentClass, style, extraBtn, ...rest } =
47 | props || [];
48 |
49 | const defaultLabelCol = {
50 | xs: { span: 2 },
51 | sm: { span: 3 },
52 | ...labelCol,
53 | };
54 |
55 | const defaultWrapCol = {
56 | xs: { span: 9 },
57 | sm: { span: 9 },
58 | ...wrapCol,
59 | };
60 |
61 | return (
62 |
68 | );
69 | }
70 |
71 | export default FormLayout;
72 |
--------------------------------------------------------------------------------
/src/components/IconFont/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | order: 1
7 | title: 基础
8 | ---
9 |
10 | ## IconFont 组件
11 |
12 | [图标](https://www.iconfont.cn/)
13 | 依赖阿里矢量图库中项目miracle_icon_fount
14 |
15 | ```tsx
16 | import React from 'react';
17 | import { Space } from 'antd';
18 | import { IconFont } from 'szdt-admin-components';
19 |
20 | export default () => (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | ```
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/IconFont/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import IconFont from './index';
5 |
6 | describe('', () => {
7 | it('render IconFont with dumi', () => {
8 | render();
9 | expect(screen.getByRole('icon-search')).toBeInTheDocument();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/IconFont/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createFromIconfontCN } from '@ant-design/icons';
3 |
4 | /**
5 | * 菜单图标,需要上传到https://www.iconfont.cn/
6 | */
7 | const Icon = createFromIconfontCN({
8 | scriptUrl: '//at.alicdn.com/t/c/font_3041065_869q5mjeen4.js',
9 | });
10 |
11 | interface Props {
12 | /**
13 | * @description 图标样式
14 | * */
15 | className?: React.CSSProperties;
16 | /**
17 | * @description IconFont中上传的图标名称
18 | * */
19 | type?: string;
20 | /**
21 | * @description 图标样式
22 | * */
23 | style?: object;
24 | /**
25 | * @description 图标大小
26 | * */
27 | size?: string;
28 | /**
29 | * @description 点击回调
30 | * */
31 | onClick?: () => void;
32 | }
33 |
34 | export default function IconFont(props: Props) {
35 | const { type, className, style, size = 'inherit', onClick } = props;
36 |
37 | return (
38 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/IconText/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 基础
7 | ---
8 |
9 | ## IconText 组件
10 |
11 | 图标文本
12 |
13 | ```tsx
14 | import React from 'react';
15 | import { TeamOutlined } from '@ant-design/icons';
16 | import { IconText } from 'szdt-admin-components';
17 |
18 | export default () => ;
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/IconText/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import IconText from './index';
5 | import { TeamOutlined } from '@ant-design/icons';
6 |
7 | describe('', () => {
8 | it('render IconFont with dumi', () => {
9 | render();
10 | expect(screen.getByText('测试')).toBeInTheDocument();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/IconText/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Space } from 'antd';
3 |
4 | interface Props {
5 | /**
6 | * @description 文案
7 | * */
8 | text?: string;
9 | /**
10 | * @description 图标
11 | * */
12 | icon?: any;
13 | /**
14 | * @description 点击回调
15 | * */
16 | onClick?: any;
17 | /**
18 | * @description 子元素
19 | * @default 显示text
20 | * */
21 | children?: string | React.ReactNode;
22 | [k: string]: any;
23 | }
24 | export default function Index(props: Props) {
25 | const { text, children, icon, ...rest } = props;
26 |
27 | return (
28 |
29 | {React.createElement(icon)}
30 | {text || children}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/SearchInput/index.less:
--------------------------------------------------------------------------------
1 | .input {
2 | :global {
3 | border: 1px solid rgba(0, 0, 0, 0.36);
4 | box-shadow: none;
5 |
6 | .ant-input {
7 | display: block;
8 | width: 200px;
9 | box-shadow: none;
10 | }
11 | }
12 | }
13 |
14 | .noInput {
15 | &:global(.ant-input-affix-wrapper) {
16 | background-color: transparent;
17 | }
18 |
19 | :global {
20 | .ant-input {
21 | width: 0;
22 | }
23 | }
24 | }
25 | .icon {
26 | color: #9d9e9c;
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/SearchInput/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | order: 3
7 | title: 输入
8 | ---
9 |
10 | ## SearchInput 组件
11 |
12 | 带搜索图标的输入框
13 |
14 | ```tsx
15 | import React from 'react';
16 | import { SearchInput } from 'szdt-admin-components';
17 |
18 | export default () => (
19 | console.log(v)} />
20 | );
21 | ```
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/SearchInput/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { fireEvent, render, screen } from '@testing-library/react';
4 | import SearchInput from './index';
5 |
6 | describe('', () => {
7 | it('render SearchInput with loading', () => {
8 | const onChange = jest.fn();
9 | render();
10 | expect(screen.getByRole('input')).toBeInTheDocument();
11 | });
12 |
13 | it('render and update SearchInput value', () => {
14 | const onChange = jest.fn();
15 | render();
16 | const input = screen.getByRole('input');
17 | fireEvent.change(input, { target: { value: '测试' } });
18 | expect(input.value).toEqual('测试');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/SearchInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Input } from 'antd';
3 | import './index.less';
4 | import IconFont from '../IconFont';
5 | import type { InputProps } from 'antd/es/input';
6 |
7 | const SearchInput = ({ placeholder = '请输入关键字', onChange, ...rest }: InputProps) => {
8 | const [isOnBlur, setIsOnBlur] = useState(false);
9 |
10 | const handleOnBlur = () => {
11 | setIsOnBlur(false);
12 | };
13 |
14 | const handleOnFocus = (e: any) => {
15 | const timer = setTimeout(() => {
16 | e?.target?.focus?.();
17 | clearTimeout(timer);
18 | }, 100);
19 | setIsOnBlur(true);
20 | };
21 |
22 | const handleOnChange = (e: any) => {
23 | onChange?.(e?.target?.value?.toUpperCase());
24 | };
25 |
26 | return (
27 | }
35 | {...rest}
36 | />
37 | );
38 | };
39 |
40 | export default SearchInput;
41 |
--------------------------------------------------------------------------------
/src/components/Skeleton/index.less:
--------------------------------------------------------------------------------
1 | .skeleton {
2 | display: flex;
3 | flex-wrap: wrap;
4 | width: 100%;
5 |
6 | &.xs div {
7 | width: calc(100% / 5 - 4 * 24px / 5);
8 | }
9 |
10 | &.sm div {
11 | width: calc(100% / 4 - 3 * 24px / 4);
12 | }
13 |
14 | &.md div {
15 | width: calc(100% / 3 - 2 * 24px / 3);
16 | }
17 |
18 | &.lg div {
19 | width: calc(100% / 2 - 24px / 2);
20 | }
21 | &.xl div {
22 | width: calc(100% - 24px);
23 | }
24 |
25 | div {
26 | width: calc(100% / 4 - 3 * 24px / 4);
27 | }
28 |
29 | @keyframes skeleton-loading {
30 | 0% {
31 | transform: translateX(-37.5%);
32 | }
33 | 100% {
34 | transform: translateX(37.5%);
35 | }
36 | }
37 |
38 | .child {
39 | position: relative;
40 | width: 100%;
41 | overflow: hidden;
42 | background: #fff;
43 |
44 | &::after {
45 | position: absolute;
46 | top: 0;
47 | right: -150%;
48 | bottom: 0;
49 | left: -150%;
50 | background: linear-gradient(
51 | 90deg,
52 | rgba(190, 190, 190, 0.2) 25%,
53 | rgba(129, 129, 129, 0.24) 37%,
54 | rgba(190, 190, 190, 0.2) 63%
55 | );
56 | animation: skeleton-loading 1.4s ease infinite;
57 | content: '';
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Skeleton/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 其他
7 | ---
8 |
9 | ## Skeleton 组件
10 |
11 | 卡片式骨架屏
12 |
13 | ```tsx
14 | import React from 'react';
15 | import { Skeleton } from 'szdt-admin-components';
16 |
17 | export default () => (
18 |
19 | content
20 |
21 | );
22 | ```
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/Skeleton/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import Skeleton from './index';
5 |
6 | describe('', () => {
7 | it('render Skeleton with loading', () => {
8 | render(
9 |
10 | content
11 | ,
12 | );
13 | expect(screen.getAllByRole('child')).toHaveLength(6);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cls from 'classnames';
3 |
4 | import './index.less';
5 |
6 | interface Props {
7 | /**
8 | * @description 一行显示的数量
9 | * @default 4
10 | * */
11 | count?: number;
12 | /**
13 | * @description 行数
14 | * @default 1
15 | * */
16 | row?: number;
17 | /**
18 | * @description 卡片高度
19 | * @default 100
20 | * */
21 | height?: any;
22 | /**
23 | * @description 卡片之间的间隔
24 | * @default 24px
25 | * */
26 | space?: number;
27 | /**
28 | * @description 控制是否显示骨架屏
29 | * @default false
30 | * */
31 | loading?: boolean;
32 | /**
33 | * @description 卡片样式
34 | * */
35 | childStyle?: React.CSSProperties;
36 | /**
37 | * @description 骨架屏样式
38 | * */
39 | className?: React.CSSProperties;
40 | /**
41 | * @description 加载万之后显示的内容
42 | * */
43 | children: React.ReactNode;
44 | }
45 |
46 | export default function Skeleton(props: Props) {
47 | const {
48 | count = 4,
49 | row = 1,
50 | space,
51 | children,
52 | loading,
53 | childStyle,
54 | height = 100,
55 | className,
56 | } = props;
57 | const totalCount = count * row;
58 | const list = Array.from({ length: totalCount ? totalCount : 4 }, () =>
59 | Math.round(Math.random() * 10),
60 | );
61 |
62 | if (loading) {
63 | return (
64 |
74 | {list.map((i: number, idx: number) => {
75 | return (
76 |
82 | );
83 | })}
84 |
85 | );
86 | }
87 | return <>{children}>;
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/Subtitle/index.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/styles/variable';
2 |
3 | .title {
4 | position: relative;
5 | display: flex;
6 | align-items: center;
7 | justify-content: space-between;
8 | margin: @margin-md 0;
9 | color: #3f3f3f;
10 | font-weight: 400;
11 | font-size: @font-size-lg;
12 | line-height: 1;
13 |
14 | .extra {
15 | font-size: @font-size-base;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Subtitle/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 基础
7 | ---
8 |
9 | ## Subtitle 组件
10 |
11 |
12 | ```tsx
13 | import React from 'react';
14 | import { Subtitle } from 'szdt-admin-components';
15 |
16 | export default () => (
17 | 更多} />
18 | );
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Subtitle/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import Subtitle from './index';
5 |
6 | describe('', () => {
7 | it('render Subtitle with loading', () => {
8 | render(更多} />);
9 | expect(screen.getByText('副标题')).toBeInTheDocument();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/Subtitle/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './index.less';
3 | import cls from 'classnames';
4 |
5 | interface Props {
6 | /**
7 | * @description 标题内容
8 | * */
9 | title: React.ReactNode | string;
10 | /**
11 | * @description 右侧扩展内容
12 | * @default null
13 | * */
14 | extra?: React.ReactNode;
15 | /**
16 | * @description 样式
17 | * */
18 | className?: React.CSSProperties;
19 | }
20 | export default function Subtitle({ title, extra, className }: Props) {
21 | return (
22 |
23 |
{title}
24 |
{extra}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/TooltipText/index.less:
--------------------------------------------------------------------------------
1 | .place-text {
2 | width: max-content;
3 | height: 0;
4 | overflow: hidden;
5 | }
6 |
7 | .text {
8 | overflow: hidden;
9 | white-space: nowrap;
10 | text-overflow: ellipsis;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/TooltipText/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 基础
7 | ---
8 |
9 | ## TooltipText 组件
10 |
11 | Tooltip 超出父元素宽度,鼠标悬浮显示弹窗
12 |
13 | ```tsx
14 | import React from 'react';
15 | import { TooltipText } from 'szdt-admin-components';
16 |
17 | export default () => (
18 |
19 |
20 |
21 | );
22 | ```
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/TooltipText/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import TooltipText from './index';
5 |
6 | describe('', () => {
7 | it('render Skeleton with loading', () => {
8 | render(
9 |
10 |
11 |
,
12 | );
13 | expect(screen.getAllByText('测试文本')).toHaveLength(2);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/TooltipText/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import { Tooltip } from 'antd';
3 | import cls from 'classnames';
4 |
5 | import './index.less';
6 |
7 | const TooltipText: React.FC<{
8 | /**
9 | * @description 文本内容
10 | * */
11 | text: React.ReactNode;
12 | /**
13 | * @description 弹窗内容
14 | * */
15 | title?: React.ReactNode;
16 | /**
17 | * @description 弹窗样式
18 | * */
19 | overlayStyle?: React.CSSProperties;
20 | /**
21 | * @description 文本样式
22 | * */
23 | textClassName?: React.CSSProperties;
24 | /**
25 | * @description 弹窗显示的行数
26 | * */
27 | rowNumber?: number;
28 | /**
29 | * @description 父节点两侧内边距
30 | * */
31 | betweenSpace?: number;
32 | }> = ({
33 | text,
34 | title: defaultTitle,
35 | overlayStyle,
36 | textClassName,
37 | rowNumber = 1,
38 | betweenSpace = 0,
39 | }) => {
40 | const [visible, setVisible] = useState(false);
41 | // 用来获取内容的宽度
42 | const ref = useRef();
43 | const title = {text}
;
44 | const parentWidth = ref?.current?.parentNode?.clientWidth
45 | ? ref?.current?.parentNode?.clientWidth
46 | : ref?.current?.parentNode?.parentNode?.clientWidth;
47 |
48 | return (
49 | (parentWidth - betweenSpace) * rowNumber}
51 | overlayStyle={overlayStyle || { maxWidth: 'max-content' }}
52 | title={defaultTitle || title}
53 | >
54 |
55 | {text}
56 |
57 | setVisible(true)}
60 | onMouseLeave={() => setVisible(false)}
61 | >
62 | {text}
63 |
64 |
65 | );
66 | };
67 |
68 | export default TooltipText;
69 |
--------------------------------------------------------------------------------
/src/components/Tree/index.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/styles/variable';
2 |
3 | .tree {
4 | padding: @padding-md @padding-lg;
5 | background-color: white;
6 | }
7 |
8 | .children {
9 | margin-left: @margin-md;
10 |
11 | &:first-child {
12 | margin-left: 0;
13 | }
14 |
15 | .child {
16 | display: flex;
17 | align-items: center;
18 | }
19 | }
20 |
21 | .icon {
22 | width: @icon-size-md;
23 |
24 | img {
25 | width: 100%;
26 | }
27 | }
28 |
29 | .text {
30 | margin-left: @margin-xss + 2;
31 | padding: @padding-xss + 2 0;
32 | color: #333;
33 | }
34 |
35 | .selected {
36 | color: @primary-color;
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Tree/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav:
3 | title: 组件
4 | path: /components
5 | group:
6 | title: 基础
7 | ---
8 |
9 | ## Tree 组件
10 |
11 | 树形组件,有子数据时并展开时现实'-'图标,收起时显示'+'图标。
12 | 可直接使用,也可以复制代码到项目中修改。
13 |
14 | ```tsx
15 | import React from 'react';
16 | import { Tree } from 'szdt-admin-components';
17 |
18 | const dataSource = [
19 | {
20 | value : '一级菜单-1'
21 | },
22 | {
23 | value : '一级菜单-2',
24 | className : 'multi-second-list sub-menu',
25 | children : [
26 | {
27 | value : '二级菜单-1'
28 | },
29 | {
30 | value : '二级菜单-2'
31 | },
32 | {
33 | value : '二级菜单-3'
34 | },
35 | {
36 | value : '二级菜单-4',
37 | className : 'multi-third-list sub-menu',
38 | children : [
39 | {
40 | value : '三级菜单-1'
41 | },
42 | {
43 | value : '三级菜单-2',
44 | },
45 | {
46 | value : '三级菜单-3'
47 | },
48 | {
49 | value : '三级菜单-4'
50 | },
51 | {
52 | value : '三级菜单-5',
53 | className : 'multi-third-list sub-menu',
54 | children : [
55 | {
56 | value : '四级菜单-1'
57 | },
58 | {
59 | value : '四级菜单-2',
60 | children : [
61 | {
62 | value : '五级菜单-1'
63 | },
64 | {
65 | value : '五级菜单-2',
66 | },
67 | {
68 | value : '五级菜单-3',
69 | children : [
70 | {
71 | value : '六级菜单-1'
72 | },
73 | {
74 | value : '六级菜单-2',
75 | children : [
76 | {
77 | value : '七级菜单-1'
78 | },
79 | {
80 | value : '七级菜单-2',
81 | },
82 | {
83 | value : '七级菜单-3'
84 | }
85 | ]
86 | }
87 | ]
88 | }
89 | ]
90 | },
91 | {
92 | value : '四级菜单-3'
93 | }
94 | ]
95 | }
96 | ]
97 | }
98 | ]
99 | }
100 | ];
101 |
102 | export default () => (
103 |
104 | );
105 | ```
106 |
107 |
108 |
--------------------------------------------------------------------------------
/src/components/Tree/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, screen } from '@testing-library/react';
4 | import Tree from './index';
5 |
6 | describe('', () => {
7 | it('render Tree with loading', () => {
8 | const dataSource = [
9 | {
10 | value: '一级菜单-1',
11 | },
12 | {
13 | value: '一级菜单-2',
14 | className: 'multi-second-list sub-menu',
15 | children: [
16 | {
17 | value: '二级菜单-1',
18 | },
19 | {
20 | value: '二级菜单-2',
21 | },
22 | {
23 | value: '二级菜单-3',
24 | },
25 | {
26 | value: '二级菜单-4',
27 | className: 'multi-third-list sub-menu',
28 | children: [
29 | {
30 | value: '三级菜单-1',
31 | },
32 | {
33 | value: '三级菜单-2',
34 | },
35 | {
36 | value: '三级菜单-3',
37 | },
38 | {
39 | value: '三级菜单-4',
40 | },
41 | {
42 | value: '三级菜单-5',
43 | className: 'multi-third-list sub-menu',
44 | children: [
45 | {
46 | value: '四级菜单-1',
47 | },
48 | {
49 | value: '四级菜单-2',
50 | children: [
51 | {
52 | value: '五级菜单-1',
53 | },
54 | {
55 | value: '五级菜单-2',
56 | },
57 | {
58 | value: '五级菜单-3',
59 | children: [
60 | {
61 | value: '六级菜单-1',
62 | },
63 | {
64 | value: '六级菜单-2',
65 | children: [
66 | {
67 | value: '七级菜单-1',
68 | },
69 | {
70 | value: '七级菜单-2',
71 | },
72 | {
73 | value: '七级菜单-3',
74 | },
75 | ],
76 | },
77 | ],
78 | },
79 | ],
80 | },
81 | {
82 | value: '四级菜单-3',
83 | },
84 | ],
85 | },
86 | ],
87 | },
88 | ],
89 | },
90 | ];
91 | render();
92 |
93 | expect(screen.getByRole('tree')).toBeInTheDocument();
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/src/components/Tree/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import cls from 'classnames';
3 | import plus from '../../assets/images/plus.svg';
4 | import minus from '../../assets/images/minus.svg';
5 |
6 | import './index.less';
7 |
8 | const Children = ({
9 | data = [],
10 | selectedMenu,
11 | onChange,
12 | icons = [],
13 | iconClassName = '',
14 | valueClassName = '',
15 | dropdownClassName = '',
16 | selectedClassName,
17 | }) => {
18 | const [parent, setParent] = useState();
19 |
20 | const handleParentClick = (it) => {
21 | if (it?.children) {
22 | setParent(parent?.value === it?.value ? {} : it);
23 | } else {
24 | onChange(it);
25 | }
26 | };
27 |
28 | return (
29 |
30 | {data?.map((it, index) => {
31 | const isParentSelected = parent?.value === it?.value;
32 | const isMenuSelected = selectedMenu?.value === it?.value;
33 | const hasChildren = it?.children?.length;
34 |
35 | return (
36 |
37 |
handleParentClick(it)}>
38 |
39 | {hasChildren && isParentSelected &&

}
40 | {hasChildren && !isParentSelected &&

}
41 |
42 |
47 | {it?.value}
48 |
49 |
50 | {hasChildren && isParentSelected && (
51 |
61 | )}
62 |
63 | );
64 | })}
65 |
66 | );
67 | };
68 |
69 | type DataType = {
70 | value: string;
71 | className?: React.CSSProperties;
72 | children?: Array;
73 | };
74 |
75 | interface Props {
76 | /**
77 | * @description 需要显示的数据。DataType包含value, className, children
78 | * */
79 | data: Array;
80 | /**
81 | * @description 展开收起时的图标
82 | * */
83 | icons: any[];
84 | /**
85 | * @description 选中之后回调函数
86 | * */
87 | onSelect?: (v: DataType) => void;
88 | /**
89 | * @description 最外层样式
90 | * */
91 | className?: React.CSSProperties;
92 | /**
93 | * @description 图标样式
94 | * */
95 | iconClassName?: React.CSSProperties;
96 | /**
97 | * @description 数据样式
98 | * */
99 | valueClassName?: React.CSSProperties;
100 | /**
101 | * @description 展开之后的数据样式
102 | * */
103 | dropdownClassName?: React.CSSProperties;
104 | /**
105 | * @description 选中之后数据样式
106 | * */
107 | selectedClassName?: React.CSSProperties;
108 | }
109 | const Tree = ({
110 | data = [],
111 | icons,
112 | onSelect,
113 | className = '',
114 | iconClassName = '',
115 | valueClassName = '',
116 | dropdownClassName = '',
117 | selectedClassName = '',
118 | }: Props) => {
119 | const [selectedMenu, setSelectedMenu] = useState();
120 |
121 | const handleChange = (v) => {
122 | setSelectedMenu(v);
123 | onSelect?.(v);
124 | };
125 |
126 | return (
127 |
128 |
138 |
139 | );
140 | };
141 |
142 | export default Tree;
143 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Card } from './components/Card';
2 | export { default as Skeleton } from './components/Skeleton';
3 | export { default as IconFont } from './components/IconFont';
4 | export { default as IconText } from './components/IconText';
5 | export { default as TooltipText } from './components/TooltipText';
6 | export { default as Empty } from './components/Empty';
7 | export { default as SearchInput } from './components/SearchInput';
8 | export { default as Subtitle } from './components/Subtitle';
9 | export { default as ExportBtn } from './components/ExportBtn';
10 | export { default as Cron } from './components/Cron';
11 | export { default as FormLayout } from './components/FormLayout';
12 | export { default as FormFooterAction } from './components/FormFooterAction';
13 | export { default as Tree } from './components/Tree';
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "jsx": "react",
7 | "esModuleInterop": true,
8 | "types": ["jest"],
9 | "strict": true,
10 | "skipLibCheck": true,
11 | "declaration": true,
12 | "outDir": "build/dist",
13 | "lib": ["esnext", "dom"],
14 | "sourceMap": true,
15 | "baseUrl": ".",
16 | "resolveJsonModule": true,
17 | "allowSyntheticDefaultImports": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "noImplicitReturns": true,
20 | "suppressImplicitAnyIndexErrors": true,
21 | "noUnusedLocals": true,
22 | "allowJs": true,
23 | "experimentalDecorators": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 |
--------------------------------------------------------------------------------