├── .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 | 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 |
63 |
64 | {children} 65 |
66 | {footer || } 67 | 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 | --------------------------------------------------------------------------------