├── .editorconfig ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .husky ├── commit-msg ├── common.sh └── pre-commit ├── .prettierignore ├── .prettierrc ├── .stylelintignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── index.html ├── mock ├── _createProductionServer.ts ├── _util.ts ├── demo │ └── hero │ │ ├── _heroList.json │ │ └── index.ts ├── log │ ├── _reqLog.data.ts │ └── index.ts └── user │ └── notice.ts ├── package.json ├── prettier.config.js ├── public ├── favicon.ico ├── iconfont.js ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── api │ ├── account │ │ ├── index.ts │ │ └── model.d.ts │ ├── demos │ │ └── hero.ts │ ├── login │ │ ├── index.ts │ │ └── model.d.ts │ ├── notice │ │ ├── enum.ts │ │ ├── index.ts │ │ └── model.d.ts │ ├── system │ │ ├── dept │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ ├── log │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ ├── menu │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ ├── online │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ ├── role │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ ├── serve │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ ├── task │ │ │ ├── index.ts │ │ │ └── model.d.ts │ │ └── user │ │ │ ├── index.ts │ │ │ └── model.d.ts │ └── typings.d.ts ├── assets │ ├── caret-down.svg │ ├── caret-up.svg │ ├── header │ │ ├── avatar.jpg │ │ ├── en_US.svg │ │ ├── language.svg │ │ ├── notice.svg │ │ └── zh_CN.svg │ ├── login-bg.svg │ ├── logo │ │ ├── antd.svg │ │ └── react.svg │ └── menu │ │ ├── account.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── guide.svg │ │ └── permission.svg ├── components │ ├── iconfont │ │ ├── icon-font.tsx │ │ └── index.ts │ ├── icons-select │ │ ├── icons.json │ │ ├── index.less │ │ └── index.tsx │ ├── index.ts │ └── tabsViewLayout │ │ ├── index.less │ │ └── index.tsx ├── core │ ├── permission │ │ ├── index.ts │ │ ├── modules │ │ │ ├── index.ts │ │ │ ├── netdisk │ │ │ │ ├── index.ts │ │ │ │ └── manage.ts │ │ │ ├── sys │ │ │ │ ├── dept.ts │ │ │ │ ├── index.ts │ │ │ │ ├── log.ts │ │ │ │ ├── menu.ts │ │ │ │ ├── online.ts │ │ │ │ ├── role.ts │ │ │ │ ├── serve.ts │ │ │ │ ├── task.ts │ │ │ │ └── user.ts │ │ │ └── types.ts │ │ └── utils.ts │ └── socket │ │ ├── event-type.ts │ │ ├── socket-io.ts │ │ └── useSocket.ts ├── enums │ ├── cacheEnum.ts │ ├── httpEnum.ts │ └── roleEnum.ts ├── hooks │ ├── useAsyncEffect.ts │ └── usePrevious.ts ├── layout │ ├── customIcon.tsx │ ├── header.tsx │ ├── index.less │ ├── index.tsx │ ├── menu.tsx │ ├── notice.tsx │ ├── suspendFallbackLoading.tsx │ └── tagView │ │ ├── index.tsx │ │ └── tagViewAction.tsx ├── locales │ ├── en-US │ │ ├── account │ │ │ └── index.ts │ │ ├── dashboard │ │ │ └── index.ts │ │ ├── documentation │ │ │ └── index.ts │ │ ├── global │ │ │ └── tips.ts │ │ ├── guide │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── permission │ │ │ └── role.ts │ │ └── user │ │ │ ├── avatorDropMenu.ts │ │ │ ├── tagsViewDropMenu.ts │ │ │ └── title.ts │ ├── index.tsx │ └── zh-CN │ │ ├── account │ │ └── index.ts │ │ ├── dashboard │ │ └── index.ts │ │ ├── documentation │ │ └── index.ts │ │ ├── global │ │ └── tips.ts │ │ ├── guide │ │ └── index.ts │ │ ├── index.ts │ │ ├── permission │ │ └── role.ts │ │ └── user │ │ ├── avatorDropMenu.ts │ │ ├── tagsViewDropMenu.ts │ │ └── title.ts ├── main.tsx ├── routes │ ├── config.tsx │ ├── generator-router.tsx │ ├── index.tsx │ ├── modules │ │ ├── index.ts │ │ └── system.ts │ ├── pravateRoute.tsx │ ├── routeView.tsx │ └── types.ts ├── stores │ ├── index.ts │ ├── tags-view.ts │ ├── user.ts │ └── ws.ts ├── styles │ ├── antd.reset.less │ ├── index.less │ └── main.less ├── utils │ ├── Storage.ts │ ├── index.ts │ ├── is │ │ └── index.ts │ ├── request.ts │ ├── use-states.ts │ └── validate.ts └── views │ ├── dashboard │ ├── index.less │ ├── index.tsx │ ├── overview.tsx │ ├── salePercent.tsx │ └── timeLine.tsx │ ├── doucumentation │ └── index.tsx │ ├── error │ └── 404.tsx │ ├── login │ ├── index.less │ └── index.tsx │ └── system │ ├── monitor │ ├── login-log │ │ └── index.tsx │ ├── online │ │ └── index.tsx │ ├── req-log │ │ └── index.tsx │ └── serve │ │ └── index.tsx │ ├── permission │ ├── menu │ │ ├── components │ │ │ └── OperateFormModal.tsx │ │ └── index.tsx │ ├── role │ │ ├── components │ │ │ └── OperateFormModal.tsx │ │ └── index.tsx │ └── user │ │ ├── columns.tsx │ │ ├── components │ │ ├── OperateDeptFormModal.tsx │ │ ├── OperateUserFormModal.tsx │ │ ├── deptTreePanel.module.less │ │ └── deptTreePanel.tsx │ │ └── index.tsx │ └── schedule │ ├── index.tsx │ ├── log │ └── index.tsx │ └── task │ └── index.tsx ├── stylelint.config.js ├── tsconfig.json ├── types ├── env.d.ts ├── global.d.ts └── react-app-env.d.ts ├── vite.config.ts ├── windi.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # 🎨 editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 只在开发模式中被载入 2 | 3 | # 网站前缀 4 | VITE_BASE_URL = / 5 | 6 | # base api 7 | VITE_BASE_API = '/api/admin/' 8 | VITE_BASE_SOCKET_PATH = '/ws-api' 9 | VITE_BASE_SOCKET_NSP = '/admin' 10 | 11 | # mock api 12 | VITE_MOCK_API = '/mock-api/' 13 | 14 | VITE_DROP_CONSOLE = false 15 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 只在生产模式中被载入 2 | 3 | 4 | # 网站前缀 5 | VITE_BASE_URL = /react-antd-admin/ 6 | # VITE_BASE_URL = / 7 | 8 | 9 | # base api 10 | VITE_BASE_API = 'http://175.24.200.3:7001/admin/' 11 | VITE_BASE_SOCKET_PATH = '/ws-api' 12 | VITE_BASE_SOCKET_NSP = 'ws://175.24.200.3:7002/admin' 13 | 14 | # mock api 15 | VITE_MOCK_API = '/mock-api/' 16 | 17 | VITE_DROP_CONSOLE = true 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | *.sh 3 | node_modules 4 | *.md 5 | *.woff 6 | *.ttf 7 | .vscode 8 | .idea 9 | dist 10 | /public 11 | /docs 12 | .husky 13 | .local 14 | /bin 15 | Dockerfile 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ['@typescript-eslint', 'simple-import-sort', 'prettier'], 4 | parser: '@typescript-eslint/parser', 5 | parserOptions: { 6 | ecmaVersion: 11, 7 | ecmaFeatures: { 8 | jsx: true 9 | }, 10 | sourceType: 'module' 11 | }, 12 | globals: { 13 | API: true, 14 | React: true 15 | }, 16 | settings: { 17 | react: { 18 | version: 'detect' 19 | } 20 | }, 21 | env: { 22 | browser: true, 23 | amd: true, 24 | node: true 25 | }, 26 | extends: [ 27 | 'eslint:recommended', 28 | 'plugin:react/recommended', 29 | 'plugin:@typescript-eslint/recommended', 30 | 'prettier', 31 | 'plugin:prettier/recommended' // Make sure this is always the last element in the array. 32 | ], 33 | rules: { 34 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }], 35 | 'react/react-in-jsx-scope': 'off', 36 | 'react/prop-types': 'off', 37 | '@typescript-eslint/explicit-function-return-type': 'off', 38 | 'simple-import-sort/imports': 'error', 39 | 'simple-import-sort/exports': 'error', 40 | '@typescript-eslint/ban-ts-ignore': 'off', 41 | '@typescript-eslint/no-explicit-any': 'off', 42 | '@typescript-eslint/no-var-requires': 'off', 43 | '@typescript-eslint/no-empty-function': 'off', 44 | 'no-use-before-define': 'off', 45 | '@typescript-eslint/no-use-before-define': 'off', 46 | '@typescript-eslint/ban-ts-comment': 'off', 47 | '@typescript-eslint/ban-types': 'off', 48 | '@typescript-eslint/no-non-null-assertion': 'off', 49 | '@typescript-eslint/explicit-module-boundary-types': 'off', 50 | '@typescript-eslint/no-unused-vars': [ 51 | 'error', 52 | { 53 | argsIgnorePattern: '^_', 54 | varsIgnorePattern: '^_' 55 | } 56 | ], 57 | 'no-unused-vars': [ 58 | 'error', 59 | { 60 | argsIgnorePattern: '^_', 61 | varsIgnorePattern: '^_' 62 | } 63 | ], 64 | 'space-before-function-paren': 'off' 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: syncToGitee 2 | env: 3 | # 7 GiB by default on GitHub, setting to 6 GiB 4 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources 5 | NODE_OPTIONS: --max-old-space-size=6144 6 | on: 7 | push: 8 | branches: [main] 9 | jobs: 10 | repo-sync: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js v14.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '14.x' 19 | 20 | - name: Install 21 | run: yarn install --frozen-lockfile 22 | 23 | - name: Build 24 | run: yarn build 25 | 26 | - name: Deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | publish_dir: ./dist 30 | personal_token: ${{ secrets.PERSONAL_TOKEN }} 31 | commit_message: Update ghPages 32 | force_orphan: true 33 | 34 | - name: Mirror the Github organization repos to Gitee. 35 | uses: Yikun/hub-mirror-action@master 36 | with: 37 | src: 'github/buqiyuan' 38 | dst: 'gitee/buqiyuan' 39 | dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} 40 | dst_token: ${{ secrets.GITEE_TOKEN }} 41 | static_list: 'react-antd-admin' 42 | force_update: true 43 | debug: true 44 | 45 | - name: Build Gitee Pages 46 | uses: yanglbme/gitee-pages-action@main 47 | with: 48 | # 注意替换为你的 Gitee 用户名 49 | gitee-username: buqiyuan 50 | # 注意在 Settings->Secrets 配置 GITEE_PASSWORD 51 | gitee-password: ${{ secrets.GITEE_PASSWORD }} 52 | # 注意替换为你的 Gitee 仓库,仓库名严格区分大小写,请准确填写,否则会出错 53 | gitee-repo: buqiyuan/react-antd-admin 54 | # 是否强制使用 HTTPS 55 | https: false 56 | # 要部署的分支,默认是 master,若是其他分支,则需要指定(指定的分支必须存在) 57 | branch: gh-pages 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | .npmrc 5 | .cache 6 | 7 | tests/server/static 8 | tests/server/static/upload 9 | 10 | .local 11 | # local env files 12 | .env.local 13 | .env.*.local 14 | .eslintcache 15 | 16 | # Log files 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | pnpm-debug.log* 21 | 22 | # Editor directories and files 23 | .idea 24 | # .vscode 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | npx --no-install commitlint --edit "$1" 7 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command_exists () { 3 | command -v "$1" >/dev/null 2>&1 4 | } 5 | 6 | # Workaround for Windows 10, Git Bash and Yarn 7 | if command_exists winpty && test -t 1; then 8 | exec < /dev/tty 9 | fi 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | [ -n "$CI" ] && exit 0 6 | 7 | # Format and submit code according to lintstagedrc.js configuration 8 | npm run lint:lint-staged 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | .local 3 | .output.js 4 | /node_modules/** 5 | 6 | **/*.svg 7 | **/*.sh 8 | 9 | /public/* 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "volar.tsPlugin": true, 4 | "volar.tsPluginStatus": false, 5 | "npm.packageManager": "yarn", 6 | "editor.tabSize": 2, 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "files.eol": "\n", 9 | "search.exclude": { 10 | "**/node_modules": true, 11 | "**/*.log": true, 12 | "**/*.log*": true, 13 | "**/bower_components": true, 14 | "**/dist": true, 15 | "**/elehukouben": true, 16 | "**/.git": true, 17 | "**/.gitignore": true, 18 | "**/.svn": true, 19 | "**/.DS_Store": true, 20 | "**/.idea": true, 21 | "**/.vscode": false, 22 | "**/yarn.lock": true, 23 | "**/tmp": true, 24 | "out": true, 25 | "dist": true, 26 | "node_modules": true, 27 | "CHANGELOG.md": true, 28 | "examples": true, 29 | "res": true, 30 | "screenshots": true, 31 | "yarn-error.log": true, 32 | "**/.yarn": true 33 | }, 34 | "files.exclude": { 35 | "**/.cache": true, 36 | "**/.editorconfig": true, 37 | "**/.eslintcache": true, 38 | "**/bower_components": true, 39 | "**/.idea": true, 40 | "**/tmp": true, 41 | "**/.git": true, 42 | "**/.svn": true, 43 | "**/.hg": true, 44 | "**/CVS": true, 45 | "**/.DS_Store": true 46 | }, 47 | "files.watcherExclude": { 48 | "**/.git/objects/**": true, 49 | "**/.git/subtree-cache/**": true, 50 | "**/.vscode/**": true, 51 | "**/node_modules/**": true, 52 | "**/tmp/**": true, 53 | "**/bower_components/**": true, 54 | "**/dist/**": true, 55 | "**/yarn.lock": true 56 | }, 57 | "stylelint.enable": true, 58 | "stylelint.packageManager": "yarn", 59 | "path-intellisense.mappings": { 60 | "@/": "${workspaceRoot}/src" 61 | }, 62 | "[javascriptreact]": { 63 | "editor.defaultFormatter": "esbenp.prettier-vscode" 64 | }, 65 | "[typescript]": { 66 | "editor.defaultFormatter": "esbenp.prettier-vscode" 67 | }, 68 | "[typescriptreact]": { 69 | "editor.defaultFormatter": "esbenp.prettier-vscode" 70 | }, 71 | "[html]": { 72 | "editor.defaultFormatter": "esbenp.prettier-vscode" 73 | }, 74 | "[css]": { 75 | "editor.defaultFormatter": "esbenp.prettier-vscode" 76 | }, 77 | "[less]": { 78 | "editor.defaultFormatter": "esbenp.prettier-vscode" 79 | }, 80 | "[scss]": { 81 | "editor.defaultFormatter": "esbenp.prettier-vscode" 82 | }, 83 | "[markdown]": { 84 | "editor.defaultFormatter": "esbenp.prettier-vscode" 85 | }, 86 | // onSave 87 | "editor.formatOnSave": true, // Format automatically every time you save 88 | // eslint 89 | "eslint.alwaysShowStatus": true, // Always in VSCode Show ESLint The state of 90 | "eslint.quiet": true, // Ignore warning Error of 91 | "editor.codeActionsOnSave": { // Save with ESLint Fix repairable errors 92 | "source.fixAll": true, 93 | "source.fixAll.eslint": true 94 | }, 95 | "[vue]": { 96 | "editor.codeActionsOnSave": { 97 | "source.fixAll.eslint": true 98 | } 99 | }, 100 | "i18n-ally.localesPaths": [ 101 | "src/locales" 102 | ], 103 | "i18n-ally.keystyle": "nested", 104 | } 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 buqiyuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### REACT ANTD ADMIN 2 | 3 | ## 技术栈 4 | 5 | - 编程语言:[TypeScript 4.x](https://www.typescriptlang.org/zh/) + [JavaScript](https://www.javascript.com/) 6 | - 构建工具:[Vite 2.x](https://www.webpackjs.com/) 7 | - 前端框架:[React 18.2.0](https://reactjs.org/) 8 | - 路由工具:[React-router-dom 6.x](https://github.com/remix-run/react-router#readme) 9 | - 状态管理:[Redux/toolkit 1.8.5](https://github.com/ReduxKit/ReduxKit/) 10 | - PC 端 UI 框架:[Ant Design](https://ant.design/components/overview-cn/) 11 | - CSS 预编译:[Stylus](https://stylus-lang.com/) / [Sass](https://sass.bootcss.com/documentation) / [Less](http://lesscss.cn/) 12 | - HTTP 工具:[Axios](https://axios-http.com/) 13 | - Git Hook 工具:[husky](https://typicode.github.io/husky/#/) + [lint-staged](https://github.com/okonet/lint-staged) 14 | - 代码规范:[EditorConfig](http://editorconfig.org) + [Prettier](https://prettier.io/) + [ESLint](https://eslint.org/) + [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript#translation) 15 | - 提交规范:[Commitizen](http://commitizen.github.io/cz-cli/) + [Commitlint](https://commitlint.js.org/#/) 16 | - 单元测试:- 17 | - 自动部署:[GitHub Actions](https://docs.github.com/cn/actions/learn-github-actions) 18 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignores: [commit => commit.includes('init')], 3 | extends: ['@commitlint/config-conventional'], 4 | rules: { 5 | 'body-leading-blank': [2, 'always'], 6 | 'footer-leading-blank': [1, 'always'], 7 | 'header-max-length': [2, 'always', 108], 8 | 'subject-empty': [2, 'never'], 9 | 'type-empty': [2, 'never'], 10 | 'type-enum': [ 11 | 2, 12 | 'always', 13 | [ 14 | 'feat', 15 | 'fix', 16 | 'perf', 17 | 'style', 18 | 'docs', 19 | 'test', 20 | 'refactor', 21 | 'build', 22 | 'ci', 23 | 'chore', 24 | 'revert', 25 | 'wip', 26 | 'workflow', 27 | 'types', 28 | 'release' 29 | ] 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | react-antd-admin 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /mock/_createProductionServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'; 2 | 3 | const modules = import.meta.globEager('./**/*.ts'); 4 | 5 | const mockModules: any[] = []; 6 | Object.keys(modules).forEach(key => { 7 | if (key.includes('/_')) { 8 | return; 9 | } 10 | mockModules.push(...modules[key].default); 11 | }); 12 | 13 | /** 14 | * Used in a production environment. Need to manually import all modules 15 | */ 16 | export function setupProdMockServer() { 17 | console.log('mockModules', mockModules); 18 | 19 | createProdMockServer(mockModules); 20 | } 21 | -------------------------------------------------------------------------------- /mock/_util.ts: -------------------------------------------------------------------------------- 1 | // Interface data format used to return a unified format 2 | 3 | export function resultSuccess(data: T, { message = 'ok' } = {}) { 4 | return { 5 | code: 200, 6 | data, 7 | message, 8 | type: 'success' 9 | }; 10 | } 11 | 12 | export function resultPageSuccess(page: number, pageSize: number, list: T[], { message = 'ok' } = {}) { 13 | const pageData = pagination(page, pageSize, list); 14 | 15 | return { 16 | ...resultSuccess({ 17 | list: pageData, 18 | pagination: { 19 | page: ~~page, 20 | size: ~~pageSize, 21 | total: list.length 22 | } 23 | }), 24 | message 25 | }; 26 | } 27 | 28 | export function resultError(message = 'Request failed', { code = -1, result = null } = {}) { 29 | return { 30 | code, 31 | result, 32 | message, 33 | type: 'error' 34 | }; 35 | } 36 | 37 | export function pagination(page: number, pageSize: number, array: T[]): T[] { 38 | const offset = (page - 1) * Number(pageSize); 39 | const ret = 40 | offset + Number(pageSize) >= array.length 41 | ? array.slice(offset, array.length) 42 | : array.slice(offset, offset + Number(pageSize)); 43 | return ret; 44 | } 45 | 46 | export interface requestParams { 47 | method: string; 48 | body: any; 49 | headers?: { authorization?: string }; 50 | query: any; 51 | } 52 | 53 | /** 54 | * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改 55 | * 56 | */ 57 | export function getRequestToken({ headers }: requestParams): string | undefined { 58 | return headers?.authorization; 59 | } 60 | -------------------------------------------------------------------------------- /mock/demo/hero/index.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | 3 | import { resultPageSuccess } from '../../_util'; 4 | import heroListJson from './_heroList.json'; 5 | 6 | export default [ 7 | { 8 | url: '/mock-api/demos/hero/list', 9 | timeout: 1000, 10 | method: 'get', 11 | response: ({ query }) => { 12 | const { page = 1, limit = 10 } = query; 13 | const filterResult = heroListJson.filter(n => n.cname.includes(query.cname || '')); 14 | return resultPageSuccess(page, limit, filterResult); 15 | } 16 | } 17 | ] as MockMethod[]; 18 | -------------------------------------------------------------------------------- /mock/log/index.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | 3 | import { resultPageSuccess } from '../_util'; 4 | import data from './_reqLog.data'; 5 | 6 | export default [ 7 | { 8 | url: '/mock-api/sys/log/req/page', 9 | timeout: 1000, 10 | method: 'get', 11 | response: ({ query }) => { 12 | const { page = 1, limit = 10 } = query; 13 | return resultPageSuccess(page, limit, data); 14 | } 15 | } 16 | ] as MockMethod[]; 17 | -------------------------------------------------------------------------------- /mock/user/notice.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock'; 2 | 3 | import { resultSuccess } from '../_util'; 4 | 5 | const mockNoticeList: API.Notice<'all'>[] = [ 6 | { 7 | id: '000000001', 8 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 9 | title: '你收到了 14 份新周报', 10 | datetime: '2017-08-09', 11 | type: 'notification' 12 | }, 13 | { 14 | id: '000000002', 15 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', 16 | title: '你推荐的 曲妮妮 已通过第三轮面试', 17 | datetime: '2017-08-08', 18 | type: 'notification' 19 | }, 20 | { 21 | id: '000000003', 22 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', 23 | title: '这种模板可以区分多种通知类型', 24 | datetime: '2017-08-07', 25 | read: true, 26 | type: 'notification' 27 | }, 28 | { 29 | id: '000000004', 30 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', 31 | title: '左侧图标用于区分不同的类型', 32 | datetime: '2017-08-07', 33 | type: 'notification' 34 | }, 35 | { 36 | id: '000000005', 37 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', 38 | title: '内容不要超过两行字,超出时自动截断', 39 | datetime: '2017-08-07', 40 | type: 'notification' 41 | }, 42 | { 43 | id: '000000006', 44 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 45 | title: '曲丽丽 评论了你', 46 | description: '描述信息描述信息描述信息', 47 | datetime: '2017-08-07', 48 | type: 'message', 49 | clickClose: true 50 | }, 51 | { 52 | id: '000000007', 53 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 54 | title: '朱偏右 回复了你', 55 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 56 | datetime: '2017-08-07', 57 | type: 'message', 58 | clickClose: true 59 | }, 60 | { 61 | id: '000000008', 62 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', 63 | title: '标题', 64 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', 65 | datetime: '2017-08-07', 66 | type: 'message', 67 | clickClose: true 68 | }, 69 | { 70 | id: '000000009', 71 | title: '任务名称', 72 | description: '任务需要在 2017-01-12 20:00 前启动', 73 | extra: '未开始', 74 | status: 'todo', 75 | type: 'event' 76 | }, 77 | { 78 | id: '000000010', 79 | title: '第三方紧急代码变更', 80 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 81 | extra: '马上到期', 82 | status: 'urgent', 83 | type: 'event' 84 | }, 85 | { 86 | id: '000000011', 87 | title: '信息安全考试', 88 | description: '指派竹尔于 2017-01-09 前完成更新并发布', 89 | extra: '已耗时 8 天', 90 | status: 'doing', 91 | type: 'event' 92 | }, 93 | { 94 | id: '000000012', 95 | title: 'ABCD 版本发布', 96 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', 97 | extra: '进行中', 98 | status: 'processing', 99 | type: 'event' 100 | } 101 | ]; 102 | 103 | export default [ 104 | { 105 | url: '/mock-api/user/notice', 106 | timeout: 1000, 107 | method: 'get', 108 | response: () => { 109 | return resultSuccess(mockNoticeList); 110 | } 111 | } 112 | ] as MockMethod[]; 113 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | semi: true, 4 | vueIndentScriptAndStyle: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | proseWrap: 'never', 8 | htmlWhitespaceSensitivity: 'strict', 9 | endOfLine: 'auto' 10 | }; 11 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buqiyuan/react-antd-admin/3d0d7e05ff31055c32c9b8fe67c16cd763931779/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | react-antd-admin 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buqiyuan/react-antd-admin/3d0d7e05ff31055c32c9b8fe67c16cd763931779/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buqiyuan/react-antd-admin/3d0d7e05ff31055c32c9b8fe67c16cd763931779/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import 'moment/locale/zh-cn'; 2 | 3 | import { ConfigProvider } from 'antd'; 4 | import enUS from 'antd/es/locale/en_US'; 5 | import zhCN from 'antd/es/locale/zh_CN'; 6 | import { observer } from 'mobx-react-lite'; 7 | import moment from 'moment'; 8 | import React, { useEffect } from 'react'; 9 | import { IntlProvider } from 'react-intl'; 10 | import { HashRouter } from 'react-router-dom'; 11 | 12 | import { userStore } from '@/stores/user'; 13 | 14 | import { localeConfig } from './locales'; 15 | import RenderRouter from './routes'; 16 | 17 | const App: React.FC = () => { 18 | const { locale } = userStore; 19 | 20 | // set the locale for the user 21 | // more languages options can be added here 22 | useEffect(() => { 23 | if (locale === 'en_US') { 24 | moment.locale('en'); 25 | } else if (locale === 'zh_CN') { 26 | moment.locale('zh-cn'); 27 | } 28 | }, [locale]); 29 | 30 | /** 31 | * handler function that passes locale 32 | * information to ConfigProvider for 33 | * setting language across text components 34 | */ 35 | const getAntdLocale = () => { 36 | if (locale === 'en_US') { 37 | return enUS; 38 | } else if (locale === 'zh_CN') { 39 | return zhCN; 40 | } 41 | }; 42 | 43 | return ( 44 | // 45 | 46 | 47 | 48 | {/* */} 49 | 50 | {/* */} 51 | 52 | 53 | 54 | // 55 | ); 56 | }; 57 | 58 | export default observer(App); 59 | -------------------------------------------------------------------------------- /src/api/account/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponse, request } from '@/utils/request'; 2 | 3 | export function updateAccountInfo(data: any) { 4 | return request>({ 5 | url: 'account/update', 6 | method: 'post', 7 | data 8 | }); 9 | } 10 | 11 | export function updatePassword(data: any) { 12 | return request({ 13 | url: 'account/password', 14 | method: 'post', 15 | data 16 | }); 17 | } 18 | 19 | export function getInfo() { 20 | return request({ 21 | url: 'account/info', 22 | method: 'get' 23 | }); 24 | } 25 | 26 | export function permmenu() { 27 | return request({ 28 | url: 'account/permmenu', 29 | method: 'get' 30 | }); 31 | } 32 | 33 | export function logout() { 34 | return request({ 35 | url: 'account/logout', 36 | method: 'post' 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/api/account/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | type Menu = { 3 | createTime: Date; 4 | updateTime: Date; 5 | id: number; 6 | parentId: number; 7 | name: string; 8 | router: string; 9 | perms: string; 10 | type: number; 11 | icon: string; 12 | orderNum: number; 13 | viewPath: string; 14 | keepalive: boolean; 15 | isShow: boolean; 16 | }; 17 | 18 | type PermMenu = { 19 | menus: Menu[]; 20 | perms: string[]; 21 | }; 22 | 23 | type AdminUserInfo = { 24 | createTime: Date; 25 | updateTime: Date; 26 | id: number; 27 | departmentId: number; 28 | name: string; 29 | username: string; 30 | password: string; 31 | psalt: string; 32 | nickName: string; 33 | headImg: string; 34 | loginIp: string; 35 | email: string; 36 | phone: string; 37 | remark: string; 38 | status: number; 39 | roles: number[]; 40 | departmentName: string; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/api/demos/hero.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponse, request } from '@/utils/request'; 2 | 3 | export function getHeroList(query: API.PageParams) { 4 | return request>( 5 | { 6 | url: '/demos/hero/list', 7 | method: 'get', 8 | params: query 9 | }, 10 | { 11 | isMock: true, 12 | isGetDataDirectly: false 13 | } 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/api/login/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponse, request } from '@/utils/request'; 2 | 3 | /** 4 | * @description 登录 5 | * @param {LoginParams} data 6 | * @returns 7 | */ 8 | export function login(data: API.LoginParams) { 9 | return request>( 10 | { 11 | url: 'login', 12 | method: 'post', 13 | data 14 | }, 15 | { 16 | isGetDataDirectly: false 17 | } 18 | ); 19 | } 20 | /** 21 | * @description 获取验证码 22 | */ 23 | export function getImageCaptcha(params?: API.CaptchaParams) { 24 | return request({ 25 | url: 'captcha/img', 26 | method: 'get', 27 | params 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/api/login/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | /** 登录参数 */ 3 | type LoginParams = { 4 | captchaId: string; 5 | password: string; 6 | username: string; 7 | verifyCode: string; 8 | }; 9 | 10 | /** 登录成功结果 */ 11 | type LoginResult = { 12 | token: string; 13 | }; 14 | 15 | /** 获取验证码参数 */ 16 | type CaptchaParams = { 17 | width?: number; 18 | height?: number; 19 | }; 20 | 21 | /** 获取验证码结果 */ 22 | type CaptchaResult = { 23 | img: string; 24 | id: string; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/api/notice/enum.ts: -------------------------------------------------------------------------------- 1 | export enum EventStatus { 2 | todo = 'rgba(255,255,255,0.65)', 3 | urgent = '#f5222d', 4 | doing = '#faad14', 5 | processing = '#1890ff' 6 | } 7 | -------------------------------------------------------------------------------- /src/api/notice/index.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils/request'; 2 | 3 | export function getNoticeList() { 4 | return request[]>( 5 | { 6 | url: '/user/notice', 7 | method: 'get' 8 | }, 9 | { 10 | isMock: true 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/api/notice/model.d.ts: -------------------------------------------------------------------------------- 1 | import { EventStatus } from './enum'; 2 | 3 | declare namespace API { 4 | interface Base { 5 | type: 'message' | 'notification' | 'event'; 6 | id: string; 7 | title: string; 8 | } 9 | 10 | interface Notification extends Base { 11 | type: 'notification'; 12 | read?: boolean; 13 | avatar: string; 14 | datetime: string; 15 | } 16 | 17 | interface Message extends Base { 18 | type: 'message'; 19 | read?: boolean; 20 | avatar: string; 21 | datetime: string; 22 | description: string; 23 | clickClose: boolean; 24 | } 25 | 26 | interface Event extends Base { 27 | type: 'event'; 28 | description: string; 29 | extra: string; 30 | status: keyof typeof EventStatus; 31 | } 32 | 33 | type Notices = Notification | Message | Event; 34 | 35 | type Notice = T extends 'all' ? Notices : Extract; 36 | } 37 | 38 | // type MinusKeys = Pick> 39 | 40 | // type Defined = T extends undefined ? never : T 41 | 42 | // type MergedProperties = { [K in keyof T & keyof U]: undefined extends T[K] ? Defined : T[K] } 43 | 44 | // type Merge = MinusKeys & MinusKeys & MergedProperties 45 | -------------------------------------------------------------------------------- /src/api/system/dept/index.ts: -------------------------------------------------------------------------------- 1 | // import type { BaseResponse } from '@/utils/request'; 2 | import Api from '@/core/permission/modules/sys/dept'; 3 | import { request } from '@/utils/request'; 4 | 5 | /** 6 | * @description 获取部门列表 7 | * @returns 8 | */ 9 | export function getDeptList() { 10 | return request({ 11 | url: Api.list, 12 | method: 'get' 13 | }); 14 | } 15 | 16 | /** 17 | * @description 部门移动排序 18 | * @param {API.MovedDeptsParams} data 19 | * @returns 20 | */ 21 | export function moveDeptList(data: API.MovedDeptsParams) { 22 | return request({ 23 | url: Api.move, 24 | method: 'post', 25 | data 26 | }); 27 | } 28 | 29 | /** 30 | * @description 删除部门 31 | * @param {API.DelDeptParams} data 32 | * @returns 33 | */ 34 | export function deleteDept(data: API.DelDeptParams) { 35 | return request( 36 | { 37 | url: 'sys/dept/delete', 38 | method: 'post', 39 | data 40 | }, 41 | { 42 | successMsg: '删除成功' 43 | } 44 | ); 45 | } 46 | 47 | /** 48 | * @description 更新某个部门 49 | * @param {API.UpdateDeptParams} data 参数 50 | * @returns 51 | */ 52 | export function updateDept(data: API.UpdateDeptParams) { 53 | return request({ 54 | url: Api.update, 55 | method: 'post', 56 | data 57 | }); 58 | } 59 | 60 | /** 61 | * @description 创建部门 62 | * @param {API.CreateDeptParams} data 参数 63 | * @returns 64 | */ 65 | export function createDept(data: API.CreateDeptParams) { 66 | return request({ 67 | url: Api.add, 68 | method: 'post', 69 | data 70 | }); 71 | } 72 | /** 73 | * @description 查询单个部门信息 74 | * @param query 75 | * @returns 76 | */ 77 | export function getDeptInfo(query: { departmentId: string | number }) { 78 | return request({ 79 | url: Api.info, 80 | method: 'get', 81 | params: query 82 | }); 83 | } 84 | 85 | /** 86 | * @description 管理员部门转移 87 | * @param data 88 | * @returns 89 | */ 90 | export function transferDept(data: API.TransferDeptParams) { 91 | return request({ 92 | url: Api.transfer, 93 | method: 'post', 94 | data 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/api/system/dept/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | /** 获取系统部门返回结果 */ 3 | type SysDeptListResult = { 4 | createTime: string; 5 | updateTime: string; 6 | id: number; 7 | parentId: number; 8 | name: string; 9 | orderNum: number; 10 | keyPath?: number[]; 11 | }; 12 | /** 部门 */ 13 | type MovedDeptItem = { 14 | id: number; 15 | parentId: number; 16 | }; 17 | 18 | /** 要排序的部门 */ 19 | type MovedDeptsParams = { 20 | depts: MovedDeptItem[]; 21 | }; 22 | 23 | /** 删除部门的参数 */ 24 | type DelDeptParams = { 25 | departmentId: number | string; 26 | }; 27 | 28 | /** 更新某个部门需要传的参数 */ 29 | type UpdateDeptParams = { 30 | name: string; 31 | parentId: number | string; 32 | orderNum: number; 33 | id: number | string; 34 | }; 35 | 36 | /** 创建部门参数 */ 37 | type CreateDeptParams = { 38 | name: string; 39 | parentId: number; 40 | orderNum: number; 41 | }; 42 | 43 | /** 管理员部门转移 */ 44 | type TransferDeptParams = { 45 | userIds: number[]; 46 | departmentId: number; 47 | }; 48 | 49 | /** 部门详情 */ 50 | type GetDeptInfoResult = { 51 | department: { 52 | createTime: string; 53 | updateTime: string; 54 | id: number; 55 | parentId: number; 56 | name: 'string'; 57 | orderNum: number; 58 | }; 59 | parentDepartment: { 60 | createTime: string; 61 | updateTime: string; 62 | id: number; 63 | parentId: number; 64 | name: 'string'; 65 | orderNum: number; 66 | }; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/api/system/log/index.ts: -------------------------------------------------------------------------------- 1 | import LogApi from '@/core/permission/modules/sys/log'; 2 | import { request } from '@/utils/request'; 3 | 4 | export function getReqLogList(query: API.PageParams) { 5 | return request( 6 | { 7 | url: LogApi.req, 8 | method: 'get', 9 | params: query 10 | }, 11 | { 12 | isMock: true 13 | } 14 | ); 15 | } 16 | 17 | export function getLoginLogList(query: API.PageParams) { 18 | return request>({ 19 | url: LogApi.login, 20 | method: 'get', 21 | params: query 22 | }); 23 | } 24 | 25 | export function getTaskLogList(query: API.PageParams) { 26 | return request>({ 27 | url: LogApi.task, 28 | method: 'get', 29 | params: query 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/api/system/log/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | /** 登录日志项结果 */ 3 | type LoginLogListItemResult = { 4 | id: number; 5 | ip: string; 6 | os: string; 7 | browser: string; 8 | time: string; 9 | username: string; 10 | }; 11 | /** 登录日志结果 */ 12 | type LoginLogListResult = LoginLogListItemResult[]; 13 | 14 | /** 请求日志项结果 */ 15 | type ReqLogListItemResult = { 16 | createTime: string; 17 | updateTime: string; 18 | id: number; 19 | ip: string; 20 | userId: number; 21 | params: string; 22 | action: string; 23 | method: string; 24 | status: number; 25 | consumeTime: number; 26 | }; 27 | /** 请求日志结果 */ 28 | type ReqLogListResult = ReqLogListItemResult[]; 29 | 30 | /** 任务日志项结果 */ 31 | type TaskLogListItemResult = { 32 | id: number; 33 | taskId: number; 34 | name: string; 35 | createdAt: string; 36 | consumeTime: number; 37 | detail: string; 38 | status: number; 39 | }; 40 | /** 任务日志结果 */ 41 | type TaskLogListResult = TaskLogListItemResult[]; 42 | } 43 | -------------------------------------------------------------------------------- /src/api/system/menu/index.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/core/permission/modules/sys/menu'; 2 | import { request } from '@/utils/request'; 3 | 4 | export function getMenuList() { 5 | return request({ 6 | url: Api.list, 7 | method: 'get' 8 | }); 9 | } 10 | 11 | export function getMenuInfo(query: { menuId: number }) { 12 | return request({ 13 | url: Api.info, 14 | method: 'get', 15 | params: query 16 | }); 17 | } 18 | 19 | export function createMenu(data: API.MenuAddParams) { 20 | return request( 21 | { 22 | url: Api.add, 23 | method: 'post', 24 | data 25 | }, 26 | { 27 | successMsg: '创建成功' 28 | } 29 | ); 30 | } 31 | 32 | export function updateMenu(data: API.MenuUpdateParams) { 33 | return request( 34 | { 35 | url: Api.update, 36 | method: 'post', 37 | data 38 | }, 39 | { 40 | successMsg: '更新成功' 41 | } 42 | ); 43 | } 44 | 45 | export function deleteMenu(data: { menuId: number }) { 46 | return request( 47 | { 48 | url: Api.delete, 49 | method: 'post', 50 | data 51 | }, 52 | { 53 | successMsg: '删除成功' 54 | } 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/api/system/menu/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | type MenuListResultItem = { 3 | createTime: string; 4 | updatedAt: string; 5 | id: number; 6 | parentId: number; 7 | name: string; 8 | router: string; 9 | perms: string; 10 | type: number; 11 | icon: string; 12 | orderNum: number; 13 | viewPath: string; 14 | keepalive: boolean; 15 | isShow: boolean; 16 | keyPath?: number[]; 17 | }; 18 | 19 | /** 获取菜单列表参数 */ 20 | type MenuListResult = MenuListResultItem[]; 21 | 22 | /** 新增菜单参数 */ 23 | type MenuAddParams = { 24 | type: number; 25 | parentId: number; 26 | name: string; 27 | orderNum: number; 28 | router: string; 29 | isShow: boolean; 30 | keepalive: boolean; 31 | icon: string; 32 | perms: string; 33 | viewPath: string; 34 | }; 35 | 36 | /** 更新某项菜单参数 */ 37 | type MenuUpdateParams = MenuAddParams & { 38 | menuId: number; 39 | }; 40 | 41 | /** 获取菜单详情结果 */ 42 | type MenuInfoResult = { 43 | menu: { 44 | createTime: string; 45 | updateTime: string; 46 | id: number; 47 | parentId: number; 48 | name: string; 49 | router: string; 50 | perms: string; 51 | type: number; 52 | icon: string; 53 | orderNum: number; 54 | viewPath: string; 55 | keepalive: boolean; 56 | isShow: boolean; 57 | }; 58 | parentMenu: { 59 | createTime: string; 60 | updateTime: string; 61 | id: number; 62 | parentId: number; 63 | name: string; 64 | router: string; 65 | perms: string; 66 | type: number; 67 | icon: string; 68 | orderNum: number; 69 | viewPath: string; 70 | keepalive: boolean; 71 | isShow: boolean; 72 | }; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/api/system/online/index.ts: -------------------------------------------------------------------------------- 1 | import OnlineApi from '@/core/permission/modules/sys/online'; 2 | import { request } from '@/utils/request'; 3 | 4 | export function getOnlineList() { 5 | return request({ 6 | url: OnlineApi.list, 7 | method: 'get' 8 | }); 9 | } 10 | 11 | export function kickUser(data: { id: number }) { 12 | return request({ 13 | url: OnlineApi.kick, 14 | method: 'post', 15 | data 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/api/system/online/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | /** 在线用户列表项 */ 3 | type OnlineUserListItem = { 4 | id: number; 5 | ip: string; 6 | username: string; 7 | isCurrent: true; 8 | time: string; 9 | os: string; 10 | browser: string; 11 | disable: boolean; 12 | }; 13 | /** 在线用户列表 */ 14 | type OnlineUserListResult = OnlineUserListItem[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/api/system/role/index.ts: -------------------------------------------------------------------------------- 1 | // import type { BaseResponse } from '@/utils/request'; 2 | import Api from '@/core/permission/modules/sys/role'; 3 | import { request } from '@/utils/request'; 4 | 5 | export function getRoleInfo(query: { roleId: number }) { 6 | return request({ 7 | url: Api.info, 8 | method: 'get', 9 | params: query 10 | }); 11 | } 12 | 13 | export function getRoleList(data?: API.PageParams) { 14 | return request({ 15 | url: Api.list, 16 | method: 'get', 17 | data 18 | }); 19 | } 20 | 21 | export function getRoleListByPage(query: API.PageParams) { 22 | return request({ 23 | url: Api.page, 24 | method: 'get', 25 | params: query 26 | }); 27 | } 28 | 29 | export function createRole(data: API.CreateRoleParams) { 30 | return request( 31 | { 32 | url: Api.add, 33 | method: 'post', 34 | data 35 | }, 36 | { 37 | successMsg: '创建角色成功' 38 | } 39 | ); 40 | } 41 | 42 | export function updateRole(data: API.UpdateRoleParams) { 43 | return request( 44 | { 45 | url: Api.update, 46 | method: 'post', 47 | data 48 | }, 49 | { 50 | successMsg: '更新角色成功' 51 | } 52 | ); 53 | } 54 | 55 | export function deleteRole(data: { roleIds: number[] }) { 56 | return request( 57 | { 58 | url: Api.delete, 59 | method: 'post', 60 | data 61 | }, 62 | { 63 | successMsg: '删除角色成功' 64 | } 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/api/system/role/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | /** 新增角色 */ 3 | type CreateRoleParams = { 4 | name: string; 5 | label: string; 6 | remark: string; 7 | menus: Key[]; 8 | depts: number[]; 9 | }; 10 | /** 更新角色 */ 11 | type UpdateRoleParams = CreateRoleParams & { 12 | roleId: number; 13 | }; 14 | 15 | /** 角色列表项 */ 16 | type RoleListResultItem = { 17 | createdAt: string; 18 | updatedAt: string; 19 | id: number; 20 | userId: string; 21 | name: string; 22 | label: string; 23 | remark: string; 24 | }; 25 | 26 | /** 角色列表 */ 27 | type RoleListResult = RoleListResultItem[]; 28 | 29 | /** 角色详情 */ 30 | type RoleInfoResult = { 31 | roleInfo: { 32 | createTime: string; 33 | updateTime: string; 34 | id: number; 35 | userId: string; 36 | name: string; 37 | label: string; 38 | remark: string; 39 | }; 40 | menus: { 41 | createTime: string; 42 | updateTime: string; 43 | id: number; 44 | roleId: number; 45 | menuId: number; 46 | }[]; 47 | depts: { 48 | createTime: string; 49 | updateTime: string; 50 | id: number; 51 | roleId: number; 52 | departmentId: number; 53 | }[]; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/api/system/serve/index.ts: -------------------------------------------------------------------------------- 1 | import ServeApi from '@/core/permission/modules/sys/serve'; 2 | import { request } from '@/utils/request'; 3 | 4 | export function getServeStat() { 5 | return request({ 6 | url: ServeApi.stat, 7 | method: 'get' 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/api/system/serve/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | export interface Runtime { 3 | os: string; 4 | arch: string; 5 | nodeVersion: string; 6 | npmVersion: string; 7 | } 8 | 9 | export interface CoresLoad { 10 | rawLoad: number; 11 | rawLoadIdle: number; 12 | } 13 | 14 | export interface Cpu { 15 | manufacturer: string; 16 | brand: string; 17 | physicalCores: number; 18 | model: string; 19 | speed: number; 20 | rawCurrentLoad: number; 21 | rawCurrentLoadIdle: number; 22 | coresLoad: CoresLoad[]; 23 | } 24 | 25 | export interface Disk { 26 | size: number; 27 | used: number; 28 | available: number; 29 | } 30 | 31 | export interface Memory { 32 | total: number; 33 | available: number; 34 | } 35 | 36 | export interface SysServeStat { 37 | runtime: Runtime; 38 | cpu: Cpu; 39 | disk: Disk; 40 | memory: Memory; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/api/system/task/index.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/core/permission/modules/sys/task'; 2 | import { request } from '@/utils/request'; 3 | 4 | type CommonParams = { 5 | id: number; 6 | }; 7 | 8 | export function getSysTaskList(params?: API.PageParams) { 9 | return request>({ 10 | url: Api.page, 11 | method: 'get', 12 | params 13 | }); 14 | } 15 | 16 | export function getSysTaskInfo(params: CommonParams) { 17 | return request({ 18 | url: Api.info, 19 | method: 'get', 20 | params 21 | }); 22 | } 23 | 24 | export function sysTaskAdd(data?: API.PageParams) { 25 | return request( 26 | { 27 | url: Api.add, 28 | method: 'post', 29 | data 30 | }, 31 | { 32 | successMsg: '添加成功' 33 | } 34 | ); 35 | } 36 | 37 | export function sysTaskDelete(data?: API.PageParams) { 38 | return request({ 39 | url: Api.delete, 40 | method: 'post', 41 | data 42 | }); 43 | } 44 | 45 | export function sysTaskUpdate(data?: API.PageParams) { 46 | return request( 47 | { 48 | url: Api.update, 49 | method: 'post', 50 | data 51 | }, 52 | { 53 | successMsg: '修改成功' 54 | } 55 | ); 56 | } 57 | 58 | export function sysTaskOnce(data: CommonParams) { 59 | return request({ 60 | url: Api.once, 61 | method: 'post', 62 | data 63 | }); 64 | } 65 | 66 | export function sysTaskStart(data: CommonParams) { 67 | return request({ 68 | url: Api.start, 69 | method: 'post', 70 | data 71 | }); 72 | } 73 | 74 | export function sysTaskStop(data: CommonParams) { 75 | return request({ 76 | url: Api.stop, 77 | method: 'post', 78 | data 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /src/api/system/task/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | /** 任务列表项 */ 3 | export type SysTaskListItem = { 4 | createdAt: string; 5 | updatedAt: string; 6 | id: number; 7 | name: string; 8 | service: string; 9 | type: number; 10 | status: number; 11 | startTime: string; 12 | endTime: string; 13 | limit: number; 14 | cron: string; 15 | every: number; 16 | data: string; 17 | jobOpts: string; 18 | remark: string; 19 | }; 20 | /** 添加任务参数 */ 21 | export type SysTaskAddParams = { 22 | name: string; 23 | service: string; 24 | type: number; 25 | status: number; 26 | startTime: string; 27 | endTime: string; 28 | limit: number; 29 | cron: string; 30 | every: number; 31 | data: string; 32 | remark: string; 33 | }; 34 | 35 | /** 更新任务参数 */ 36 | export type SysTaskUpdateParams = SysTaskAddParams & { 37 | id: number; 38 | }; 39 | /** 获取任务详情返回结果 */ 40 | export type SysTaskInfoResult = { 41 | createdAt: string; 42 | updatedAt: string; 43 | id: number; 44 | name: string; 45 | service: string; 46 | type: number; 47 | status: number; 48 | startTime: string; 49 | endTime: string; 50 | limit: number; 51 | cron: string; 52 | every: number; 53 | data: string; 54 | jobOpts: string; 55 | remark: string; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/api/system/user/index.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/core/permission/modules/sys/user'; 2 | import { request } from '@/utils/request'; 3 | 4 | export function getUserListPage(data: API.PageParams<{ departmentIds: number[] }>) { 5 | return request>({ 6 | url: Api.page, 7 | method: 'post', 8 | data 9 | }); 10 | } 11 | 12 | export function createUser(data: API.CreateUserParams) { 13 | return request( 14 | { 15 | url: Api.add, 16 | method: 'post', 17 | data 18 | }, 19 | { 20 | successMsg: '创建用户成功' 21 | } 22 | ); 23 | } 24 | 25 | export function getUserInfo(query: { userId: number }) { 26 | return request({ 27 | url: Api.info, 28 | method: 'get', 29 | params: query 30 | }); 31 | } 32 | 33 | export function updateUser(data: API.UpdateAdminInfoParams) { 34 | return request( 35 | { 36 | url: Api.update, 37 | method: 'post', 38 | data 39 | }, 40 | { 41 | successMsg: '修改用户成功' 42 | } 43 | ); 44 | } 45 | 46 | export function updateUserPassword(data: API.UpdateAdminUserPassword) { 47 | return request( 48 | { 49 | url: Api.password, 50 | method: 'post', 51 | data 52 | }, 53 | { 54 | successMsg: '操作成功' 55 | } 56 | ); 57 | } 58 | 59 | export function deleteUsers(data: { userIds: number[] }) { 60 | return request({ 61 | url: Api.delete, 62 | method: 'post', 63 | data 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/api/system/user/model.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | type UserListPageResultItem = { 3 | createTime: string; 4 | departmentId: number; 5 | email: string; 6 | headImg: string; 7 | id: number; 8 | name: string; 9 | nickName: string; 10 | phone: string; 11 | remark: string; 12 | status: number; 13 | updateTime: string; 14 | username: string; 15 | departmentName: string; 16 | roleNames: string[]; 17 | keyPath?: number[]; 18 | }; 19 | 20 | /** 获取用户列表结果 */ 21 | type UserListPageResult = UserListPageResultItem[]; 22 | 23 | /** 创建用户参数 */ 24 | type CreateUserParams = { 25 | departmentId: number; 26 | name: string; 27 | username: string; 28 | roles: number[]; 29 | nickName: string; 30 | email: string; 31 | phone: string; 32 | remark: string; 33 | status: number; 34 | }; 35 | 36 | /** 管理员用户详情 */ 37 | type AdminUserInfo = { 38 | createTime: string; 39 | updateTime: string; 40 | id: number; 41 | departmentId: number; 42 | name: string; 43 | username: string; 44 | password: string; 45 | psalt: string; 46 | nickName: string; 47 | headImg: string; 48 | email: string; 49 | phone: string; 50 | remark: string; 51 | status: number; 52 | roles: string[]; 53 | departmentName: string; 54 | }; 55 | 56 | /** 更新管理员用户参数 */ 57 | type UpdateAdminInfoParams = { 58 | departmentId: number; 59 | name: string; 60 | username: string; 61 | roles: number[]; 62 | nickName: string; 63 | email: string; 64 | phone: string; 65 | remark: string; 66 | status: number; 67 | id: number; 68 | }; 69 | 70 | /** 更新管理员密码 */ 71 | type UpdateAdminUserPassword = { 72 | userId: number; 73 | password: string; 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/api/typings.d.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | 4 | declare namespace API { 5 | /** 全局通过表格查询返回结果 */ 6 | type TableListResult = { 7 | list: T; 8 | pagination?: PaginationResult; 9 | }; 10 | 11 | /** 全局通用表格分页返回数据结构 */ 12 | type PaginationResult = { 13 | page: number; 14 | size: number; 15 | total: number; 16 | }; 17 | 18 | /** 全局通用表格分页请求参数 */ 19 | type PageParams = { 20 | limit?: number; 21 | page?: number; 22 | } & { 23 | [P in keyof T]?: T[P]; 24 | }; 25 | 26 | type ErrorResponse = { 27 | /** 业务约定的错误码 */ 28 | errorCode: string; 29 | /** 业务上的错误信息 */ 30 | errorMessage?: string; 31 | /** 业务上的请求是否成功 */ 32 | success?: boolean; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/caret-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/caret-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/header/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buqiyuan/react-antd-admin/3d0d7e05ff31055c32c9b8fe67c16cd763931779/src/assets/header/avatar.jpg -------------------------------------------------------------------------------- /src/assets/header/en_US.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/header/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/header/notice.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/header/zh_CN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/menu/account.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/menu/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/menu/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/menu/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/menu/permission.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/iconfont/icon-font.tsx: -------------------------------------------------------------------------------- 1 | import { createFromIconfontCN } from '@ant-design/icons'; 2 | import { omit } from 'lodash'; 3 | import { useMemo } from 'react'; 4 | 5 | import { isString } from '@/utils/is'; 6 | 7 | let scriptUrls = [`${import.meta.env.BASE_URL}iconfont.js`]; 8 | 9 | let MyIconFont = createFromIconfontCN({ 10 | // scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', 11 | // scriptUrl: '//at.alicdn.com/t/font_2184398_zflo1kjcemp.js', 12 | // iconfont字体图标本地化,详见:/public/iconfont.js 13 | scriptUrl: scriptUrls 14 | }); 15 | 16 | export type IconFontProps = { 17 | type: string; 18 | prefix?: string; 19 | color?: string; 20 | size?: number | string; 21 | scriptUrl?: string | string[]; 22 | }; 23 | 24 | export const IconFont = (props: IconFontProps) => { 25 | const { type, prefix = 'icon-', color = 'unset', size = 14, scriptUrl } = props; 26 | 27 | // 如果外部传进来字体图标路径,则覆盖默认的 28 | if (scriptUrl) { 29 | scriptUrls = [...new Set(scriptUrls.concat(scriptUrl))]; 30 | MyIconFont = createFromIconfontCN({ 31 | scriptUrl: scriptUrls 32 | }); 33 | } 34 | 35 | const wrapStyleRef = useMemo(() => { 36 | const fs = isString(size) ? parseFloat(size) : size; 37 | return { 38 | color, 39 | fontSize: `${fs}px` 40 | }; 41 | }, [size]); 42 | 43 | return type ? ( 44 | 49 | ) : null; 50 | }; 51 | 52 | export default IconFont; 53 | -------------------------------------------------------------------------------- /src/components/iconfont/index.ts: -------------------------------------------------------------------------------- 1 | import IconFont from './icon-font'; 2 | export { IconFont }; 3 | -------------------------------------------------------------------------------- /src/components/icons-select/index.less: -------------------------------------------------------------------------------- 1 | .icon-select { 2 | .select-box { 3 | @apply grid grid-cols-9 h-300px overflow-auto; 4 | 5 | &-item { 6 | @apply flex m-2px p-6px; 7 | border: 1px solid #e5e7eb; 8 | 9 | &:hover, 10 | &.active { 11 | @apply border-blue-600; 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/icons-select/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import { Input, Popover } from 'antd'; 4 | import { type FC, useEffect, useState } from 'react'; 5 | 6 | import { IconFont } from '@/components/iconfont'; 7 | 8 | import icons from './icons.json'; 9 | 10 | const { glyphs } = icons; 11 | 12 | interface IconSelectProps { 13 | value?: string; 14 | placeholder?: string; 15 | onChange?: (value: string) => void; 16 | } 17 | 18 | export const IconSelect: FC = props => { 19 | const { value, placeholder = '请选择', onChange } = props; 20 | 21 | const [modelValue, setModelValue] = useState(value); 22 | 23 | const selectIcon = (iconItem: typeof glyphs[number]) => { 24 | setModelValue(iconItem.font_class); 25 | onChange?.(iconItem.font_class); 26 | }; 27 | 28 | useEffect(() => { 29 | setModelValue(value); 30 | }, [value]); 31 | 32 | return ( 33 | 38 | {glyphs.map(iconItem => ( 39 |
selectIcon(iconItem)} 47 | > 48 | 49 |
50 | ))} 51 | 52 | } 53 | trigger="focus" 54 | > 55 | : null} 59 | > 60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * example 3 | * path -> ./modules/user 4 | * Button 5 | * path -> ./modules/sys/user 6 | * Button 7 | */ 8 | import type { DataNode } from 'rc-cascader/lib/interface'; 9 | 10 | interface Permissions { 11 | [key: string]: { 12 | [key: string]: string; 13 | }; 14 | } 15 | 16 | const modulesPermissionFiles = import.meta.globEager('./**/*.ts'); 17 | 18 | /** 19 | * @description 权限列表 20 | */ 21 | export const permissions: Permissions = Object.keys(modulesPermissionFiles).reduce((modules, modulePath) => { 22 | // set './app.js' => 'app' 23 | // set './sys/app.js' => 'sysApp' 24 | const moduleName = modulePath 25 | .replace(/^\.\/(.*)\.\w+$/, '$1') 26 | .replace(/[-_/][a-z]/gi, s => s.substring(1).toUpperCase()); 27 | const value = modulesPermissionFiles[modulePath].default; 28 | 29 | // pass sys/user/add => sys:user:add 30 | const permissionModule = Object.keys(value).reduce((obj, key) => { 31 | obj[key] = value[key].replace(/\//g, ':'); 32 | return obj; 33 | }, {}); 34 | 35 | modules[moduleName] = permissionModule; 36 | return modules; 37 | }, {}); 38 | 39 | /** 所有的权限码 */ 40 | export const permissionValues = Object.keys(permissions).flatMap(k => Object.values(permissions[k])); 41 | 42 | /** 43 | * @description 将权限列表转成级联选择器要求的数据格式 44 | */ 45 | export const formarPermsToCascader = () => { 46 | return Object.keys(permissions).reduce((prev, moduleKey) => { 47 | const module = permissions[moduleKey]; 48 | Object.keys(module).forEach(key => { 49 | module[key].split(':').reduce((p, k) => { 50 | const index = p.findIndex(item => item?.value === k); 51 | if (Number.isInteger(index) && index !== -1) { 52 | return p[index].children!; 53 | } else { 54 | const item: DataNode = { 55 | label: k, 56 | value: k, 57 | children: [] 58 | }; 59 | p.push(item); 60 | return item.children!; 61 | } 62 | }, prev); 63 | }); 64 | return prev; 65 | }, []); 66 | }; 67 | 68 | // 挂载所有权限列表到实例上 69 | // !Vue.prototype.$permission && (Vue.prototype.$permission = modules) 70 | 71 | // // auth 72 | // !Vue.prototype.$auth && Object.defineProperties(Vue.prototype, { 73 | // $auth: { 74 | // get() { 75 | // const _this = this 76 | // return (perm) => { 77 | // const [pm, action] = perm.split('.') 78 | // const permissionList = _this.$store.getters.perms 79 | // if (_this.$permission[pm] && _this.$permission[pm][action]) { 80 | // return permissionList.indexOf(_this.$permission[pm][action]) > -1 81 | // } 82 | // return false 83 | // } 84 | // } 85 | // } 86 | // }) 87 | export default permissions; 88 | -------------------------------------------------------------------------------- /src/components/tabsViewLayout/index.less: -------------------------------------------------------------------------------- 1 | .separator { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | display: flex; 6 | width: 14px; 7 | height: 100%; 8 | cursor: col-resize; 9 | background-color: white; 10 | box-shadow: -4px -2px 4px -5px rgba(0, 0, 0, 0.35), 4px 3px 4px -5px rgba(0, 0, 0, 0.35); 11 | align-items: center; 12 | justify-content: center; 13 | 14 | i { 15 | width: 1px; 16 | height: 14px; 17 | margin: 0 1px; 18 | background-color: #e9e9e9; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/tabsViewLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import { Card, Layout } from 'antd'; 4 | import { throttle } from 'lodash'; 5 | import type React from 'react'; 6 | import type { FC } from 'react'; 7 | import { useCallback } from 'react'; 8 | 9 | const { Sider, Content } = Layout; 10 | 11 | interface TabsViewLayoutProps { 12 | /** 默认插槽 */ 13 | children?: React.ReactNode; 14 | /** 侧边栏 */ 15 | assideRender?: React.ReactNode; 16 | /** 侧边栏宽度 */ 17 | assideWidth?: number; 18 | /** 拖拽侧边栏回调函数 */ 19 | onSeparatorDrag?: (width: number) => void; 20 | } 21 | 22 | export const TabsViewLayout: FC = ({ 23 | children, 24 | assideRender, 25 | assideWidth = 280, 26 | onSeparatorDrag 27 | }) => { 28 | let startX: number; 29 | 30 | /** 31 | * @description 正在拖拽 32 | */ 33 | const onDrag = throttle((e: MouseEvent) => { 34 | requestAnimationFrame(() => { 35 | const width = assideWidth + e.clientX - startX; 36 | onSeparatorDrag?.(Math.max(width, 10)); 37 | }); 38 | }, 20); 39 | 40 | /** 41 | * @description 拖拽结束 42 | */ 43 | const onDragEnd = useCallback(() => { 44 | document.documentElement.style.userSelect = 'unset'; 45 | document.documentElement.removeEventListener('mousemove', onDrag); 46 | document.documentElement.removeEventListener('mouseup', onDragEnd); 47 | }, [onDrag]); 48 | 49 | /** 50 | * @description 鼠标按下样式 51 | */ 52 | const onDragStart = (e: React.MouseEvent) => { 53 | startX = e.clientX; 54 | document.documentElement.style.userSelect = 'none'; 55 | document.documentElement.addEventListener('mousemove', onDrag); 56 | document.documentElement.addEventListener('mouseup', onDragEnd); 57 | }; 58 | 59 | return ( 60 | 61 | 62 | {assideRender ? ( 63 | 64 | {assideRender} 65 |
66 | 67 | 68 |
69 |
70 | ) : null} 71 | 72 | {children} 73 | 74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/core/permission/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * example 3 | * path -> ./modules/user 4 | * Button 5 | * path -> ./modules/sys/user 6 | * Button 7 | */ 8 | // import type { DataNode } from 'rc-tree-select/lib/interface' 9 | 10 | import type { DataNode } from 'rc-cascader/lib/interface'; 11 | 12 | import { userStore } from '@/stores/user'; 13 | 14 | import { permissions, permissionValues } from './modules/'; 15 | import type { PermissionType } from './modules/types'; 16 | 17 | /** 18 | * @description 将权限列表转成级联选择器要求的数据格式 19 | */ 20 | export const formarPermsToCascader = () => { 21 | return Object.keys(permissions).reduce((prev, moduleKey) => { 22 | const module = permissions[moduleKey]; 23 | Object.keys(module).forEach(key => { 24 | module[key].split(':').reduce((p, k, currentIndex, arr) => { 25 | const value = arr.slice(0, currentIndex + 1).join(':'); 26 | const index = p.findIndex(item => item?.value === value); 27 | if (Number.isInteger(index) && index !== -1) { 28 | return p[index].children; 29 | } else { 30 | const item: DataNode = { 31 | // key: k, 32 | title: k, 33 | label: k, 34 | value: value, 35 | children: [] 36 | }; 37 | p.push(item); 38 | return item.children!; 39 | } 40 | }, prev); 41 | }); 42 | return prev; 43 | }, []); 44 | }; 45 | 46 | /** 47 | * 验证权限 48 | * @param {PermissionType} perm 权限码 49 | * @returns {boolean} true | false 50 | */ 51 | export const verifyAuth = (perm: PermissionType) => { 52 | const permCode = perm.split('/').join(':'); 53 | const permissionList = userStore.perms; 54 | 55 | return permissionList.some(n => n === permCode); 56 | }; 57 | 58 | export { 59 | permissions, 60 | permissionValues 61 | // install(app) { 62 | // app.config.globalProperties.$auth = verifyAuth; 63 | // } 64 | }; 65 | -------------------------------------------------------------------------------- /src/core/permission/modules/index.ts: -------------------------------------------------------------------------------- 1 | interface Permissions { 2 | [key: string]: { 3 | [key: string]: string; 4 | }; 5 | } 6 | 7 | const modulesPermissionFiles = import.meta.globEager('./**/*.ts'); 8 | /** 9 | * 根据接口路径生成接口权限码, eg: sys/user/add => sys:user:add 10 | * @param str 接口路径 11 | * @returns {string} 12 | */ 13 | export const generatePermCode = (str: string) => str.replace(/\//g, ':'); 14 | 15 | const filterDirs = ['/index.ts', './types.ts']; 16 | 17 | /** 18 | * @description 权限列表 19 | */ 20 | export const permissions: Permissions = Object.keys(modulesPermissionFiles).reduce((modules, modulePath) => { 21 | if (filterDirs.some(n => modulePath.includes(n))) return modules; 22 | // set './app.js' => 'app' 23 | // set './sys/app.js' => 'sysApp' 24 | const moduleName = modulePath 25 | .replace(/^\.\/(.*)\.\w+$/, '$1') 26 | .replace(/[-_/][a-z]/gi, s => s.substring(1).toUpperCase()); 27 | const value = modulesPermissionFiles[modulePath].default; 28 | 29 | // pass sys/user/add => sys:user:add 30 | const permissionModule = Object.keys(value).reduce((obj, key) => { 31 | obj[key] = generatePermCode(value[key]); 32 | return obj; 33 | }, {}); 34 | 35 | modules[moduleName] = permissionModule; 36 | // console.log('permissions modules', modules); 37 | return modules; 38 | }, {}); 39 | 40 | /** 所有的权限码 */ 41 | export const permissionValues = Object.keys(permissions).flatMap(k => Object.values(permissions[k])); 42 | 43 | console.log('permissions', permissions); 44 | -------------------------------------------------------------------------------- /src/core/permission/modules/netdisk/index.ts: -------------------------------------------------------------------------------- 1 | import type { NetdiskMangePerms } from './manage'; 2 | 3 | export type NetdiskPermissionType = NetdiskMangePerms; 4 | -------------------------------------------------------------------------------- /src/core/permission/modules/netdisk/manage.ts: -------------------------------------------------------------------------------- 1 | export const netdiskMange = { 2 | list: 'netdisk/manage/list', 3 | mkdir: 'netdisk/manage/mkdir', 4 | token: 'netdisk/manage/token', 5 | rename: 'netdisk/manage/rename', 6 | download: 'netdisk/manage/download', 7 | delete: 'netdisk/manage/delete', 8 | check: 'netdisk/manage/check', 9 | info: 'netdisk/manage/info', 10 | mark: 'netdisk/manage/mark', 11 | cut: 'netdisk/manage/cut', 12 | copy: 'netdisk/manage/copy' 13 | } as const; 14 | 15 | export const values = Object.values(netdiskMange); 16 | 17 | export type NetdiskMangePerms = typeof values[number]; 18 | 19 | export default netdiskMange; 20 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/dept.ts: -------------------------------------------------------------------------------- 1 | export const sysDept = { 2 | /** 获取部门列表 */ 3 | list: 'sys/dept/list', 4 | /** 移动部门 */ 5 | move: 'sys/dept/move', 6 | /** 更新部门 */ 7 | update: 'sys/dept/update', 8 | delete: 'sys/dept/delete', 9 | add: 'sys/dept/add', 10 | info: 'sys/dept/info', 11 | transfer: 'sys/dept/transfer' 12 | } as const; 13 | 14 | export const values = Object.values(sysDept); 15 | 16 | export type SysDeptPerms = typeof values[number]; 17 | 18 | export default sysDept; 19 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/index.ts: -------------------------------------------------------------------------------- 1 | import type { SysDeptPerms } from './dept'; 2 | import type { SysLogPerms } from './log'; 3 | import type { SysMenuPerms } from './menu'; 4 | import type { SysOnlinePerms } from './online'; 5 | import type { SysRolePerms } from './role'; 6 | import type { SysServePerms } from './serve'; 7 | import type { SysTaskPerms } from './task'; 8 | import type { SysUserPerms } from './user'; 9 | 10 | export type SysPermissionType = 11 | | SysLogPerms 12 | | SysDeptPerms 13 | | SysMenuPerms 14 | | SysOnlinePerms 15 | | SysRolePerms 16 | | SysTaskPerms 17 | | SysServePerms 18 | | SysUserPerms; 19 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/log.ts: -------------------------------------------------------------------------------- 1 | export const sysLog = { 2 | req: 'sys/log/req/page', 3 | login: 'sys/log/login/page', 4 | task: 'sys/log/task/page' 5 | } as const; 6 | 7 | export const values = Object.values(sysLog); 8 | 9 | export type SysLogPerms = typeof values[number]; 10 | 11 | export default sysLog; 12 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/menu.ts: -------------------------------------------------------------------------------- 1 | export const sysMenu = { 2 | list: 'sys/menu/list', 3 | add: 'sys/menu/add', 4 | update: 'sys/menu/update', 5 | info: 'sys/menu/info', 6 | delete: 'sys/menu/delete' 7 | } as const; 8 | 9 | export const deptValues = Object.values(sysMenu); 10 | 11 | export type SysMenuPerms = typeof deptValues[number]; 12 | 13 | export default sysMenu; 14 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/online.ts: -------------------------------------------------------------------------------- 1 | export const sysOnline = { 2 | list: 'sys/online/list', 3 | kick: 'sys/online/kick' 4 | } as const; 5 | 6 | export const values = Object.values(sysOnline); 7 | 8 | export type SysOnlinePerms = typeof values[number]; 9 | 10 | export default sysOnline; 11 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/role.ts: -------------------------------------------------------------------------------- 1 | export const sysRole = { 2 | list: 'sys/role/list', 3 | page: 'sys/role/page', 4 | add: 'sys/role/add', 5 | update: 'sys/role/update', 6 | delete: 'sys/role/delete', 7 | info: 'sys/role/info' 8 | } as const; 9 | 10 | export const values = Object.values(sysRole); 11 | 12 | export type SysRolePerms = typeof values[number]; 13 | 14 | export default sysRole; 15 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/serve.ts: -------------------------------------------------------------------------------- 1 | export const sysServe = { 2 | stat: 'sys/serve/stat' 3 | } as const; 4 | 5 | export const values = Object.values(sysServe); 6 | 7 | export type SysServePerms = typeof values[number]; 8 | 9 | export default sysServe; 10 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/task.ts: -------------------------------------------------------------------------------- 1 | export const sysTask = { 2 | page: 'sys/task/page', 3 | add: 'sys/task/add', 4 | update: 'sys/task/update', 5 | delete: 'sys/task/delete', 6 | once: 'sys/task/once', 7 | start: 'sys/task/start', 8 | stop: 'sys/task/stop', 9 | info: 'sys/task/info' 10 | } as const; 11 | 12 | export const values = Object.values(sysTask); 13 | 14 | export type SysTaskPerms = typeof values[number]; 15 | 16 | export default sysTask; 17 | -------------------------------------------------------------------------------- /src/core/permission/modules/sys/user.ts: -------------------------------------------------------------------------------- 1 | export const sysUser = { 2 | add: 'sys/user/add', 3 | page: 'sys/user/page', 4 | info: 'sys/user/info', 5 | update: 'sys/user/update', 6 | delete: 'sys/user/delete', 7 | password: 'sys/user/password' 8 | } as const; 9 | 10 | export const values = Object.values(sysUser); 11 | 12 | export type SysUserPerms = typeof values[number]; 13 | 14 | export default sysUser; 15 | -------------------------------------------------------------------------------- /src/core/permission/modules/types.ts: -------------------------------------------------------------------------------- 1 | import type { NetdiskPermissionType } from './netdisk'; 2 | import type { SysPermissionType } from './sys'; 3 | 4 | export type PermissionType = SysPermissionType | NetdiskPermissionType; 5 | -------------------------------------------------------------------------------- /src/core/permission/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DataNode } from 'rc-tree/lib/interface'; 2 | 3 | export interface TreeDataItem extends DataNode { 4 | children: any; 5 | } 6 | 7 | /** 8 | * 渲染部门至树形控件 9 | * @param {Array} depts 所有部门 10 | * @param {Number | null} parentId 父级部门ID 11 | * @param {number[]|string[]} keyPath ID路径 12 | */ 13 | export const formatDept2Tree = ( 14 | depts: API.SysDeptListResult[], 15 | parentId: number | null = null, 16 | keyPath: (string | number)[] = [] 17 | ): TreeDataItem[] => { 18 | return depts 19 | .filter(item => item.parentId === parentId) 20 | .map(item => { 21 | const _keyPath = keyPath.concat(parentId || []); 22 | const arr = formatDept2Tree(depts, item.id, _keyPath); 23 | return Object.assign(item, { 24 | keyPath: _keyPath, 25 | title: item.name, 26 | key: item.id, 27 | value: item.id, 28 | formData: item, 29 | children: arr.length ? arr : null 30 | }); 31 | }); 32 | }; 33 | 34 | /** 35 | * 渲染菜单至树形控件 36 | * @param {Array} menus 所有菜单 37 | * @param {Number | null} parentId 父级菜单ID 38 | * @param {number[]|string[]} keyPath ID路径 39 | */ 40 | export const formatMenu2Tree = ( 41 | menus: API.MenuListResult, 42 | parentId: number | null = null, 43 | keyPath: (string | number)[] = [] 44 | ): TreeDataItem[] => { 45 | return menus 46 | .filter(item => item.parentId === parentId) 47 | .map(item => { 48 | const _keyPath = keyPath.concat(parentId || []); 49 | const arr = formatMenu2Tree(menus, item.id, _keyPath); 50 | return Object.assign(item, { 51 | keyPath: _keyPath, 52 | title: item.name, 53 | key: item.id, 54 | value: item.id, 55 | formData: item, 56 | children: arr.length ? arr : null 57 | }); 58 | }); 59 | }; 60 | 61 | /** 62 | * 在树中根据ID找child 63 | * @param {string|number} id 64 | * @param {any[]} treeData 树形数据 65 | * @param {string} keyName 指定ID的属性名,默认是id 66 | * @param {string} children 指定children的属性名,默认是children 67 | */ 68 | export const findChildById = (id, treeData: T[] = [], keyName = 'id', children = 'children') => { 69 | return treeData.reduce((prev, curr) => { 70 | if (curr[keyName] === id) { 71 | return curr; 72 | } 73 | if (prev) { 74 | return prev; 75 | } 76 | if (curr[children]?.length) { 77 | return findChildById(id, curr[children], keyName, children); 78 | } 79 | }, undefined); 80 | }; 81 | -------------------------------------------------------------------------------- /src/core/socket/event-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket事件名定义 3 | */ 4 | 5 | // 强制踢下线 6 | export const EVENT_KICK = 'kick'; 7 | -------------------------------------------------------------------------------- /src/core/socket/useSocket.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, onMounted, watch } from 'vue'; 2 | 3 | import { useWsStore } from '@/store/modules/ws'; 4 | 5 | export const useSocket = (socketHooks = {}) => { 6 | const socketClient = useWsStore().client; 7 | 8 | // cache wrapper func 9 | const socketMap = new Map(); 10 | 11 | const registerSocketEvent = () => { 12 | Object.keys(socketHooks).forEach(e => { 13 | if (socketClient) { 14 | // bind this 15 | const wrapFunc = socketHooks[e]; 16 | socketMap.set(e, wrapFunc); 17 | socketClient.subscribe(e, wrapFunc); 18 | } 19 | }); 20 | }; 21 | const unregisterSocketEvent = () => { 22 | Object.keys(socketHooks).forEach(e => { 23 | // 增加判断避免被移除掉所有事件 24 | if (socketClient && socketMap.has(e)) { 25 | socketClient.unsubscribe(e, socketMap.get(e)); 26 | } 27 | }); 28 | }; 29 | watch(() => socketClient, registerSocketEvent); 30 | onMounted(registerSocketEvent); 31 | onBeforeUnmount(unregisterSocketEvent); 32 | }; 33 | -------------------------------------------------------------------------------- /src/enums/cacheEnum.ts: -------------------------------------------------------------------------------- 1 | // 用户token 2 | export const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; 3 | 4 | // 用户信息 5 | export const USER_INFO_KEY = 'USER__INFO__'; 6 | 7 | // role info key 8 | export const ROLES_KEY = 'ROLES__KEY__'; 9 | 10 | // locale 11 | export const LOCALE = 'LOCALE'; 12 | 13 | export const IS_LOCKSCREEN = 'IS_LOCKSCREEN'; // 是否锁屏 14 | export const TABS_ROUTES = 'TABS_ROUTES'; // 标签页 15 | -------------------------------------------------------------------------------- /src/enums/httpEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 请求结果集 3 | */ 4 | export enum ResultEnum { 5 | SUCCESS = 0, 6 | ERROR = -1, 7 | TIMEOUT = 10042, 8 | TYPE = 'success' 9 | } 10 | 11 | /** 12 | * @description: 请求方法 13 | */ 14 | export enum RequestEnum { 15 | GET = 'GET', 16 | POST = 'POST', 17 | PATCH = 'PATCH', 18 | PUT = 'PUT', 19 | DELETE = 'DELETE' 20 | } 21 | 22 | /** 23 | * @description: 常用的contentTyp类型 24 | */ 25 | export enum ContentTypeEnum { 26 | // json 27 | JSON = 'application/json;charset=UTF-8', 28 | // json 29 | TEXT = 'text/plain;charset=UTF-8', 30 | // form-data 一般配合qs 31 | FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', 32 | // form-data 上传 33 | FORM_DATA = 'multipart/form-data;charset=UTF-8' 34 | } 35 | -------------------------------------------------------------------------------- /src/enums/roleEnum.ts: -------------------------------------------------------------------------------- 1 | export enum RoleEnum { 2 | // 管理员 3 | ADMIN = 'admin', 4 | 5 | // 普通用户 6 | NORMAL = 'normal' 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/useAsyncEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | type Callback = () => Promise; 4 | 5 | type Deps = readonly any[]; 6 | 7 | /** 8 | * hook that wraps a callback function inside 9 | * useEffect hook, triggered everytime dependencies change 10 | * @param callback callback 11 | * @param deps dependences 12 | */ 13 | export default function useAsyncEffect(callback: Callback, deps: Deps = []) { 14 | useEffect(() => { 15 | callback().catch(e => console.log('useAsyncEffect error:', e)); 16 | }, deps); 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * @param value 5 | * @returns previous value stored in ref object 6 | */ 7 | export default function usePrevious(value: T) { 8 | const ref = useRef(value); 9 | useEffect(() => { 10 | ref.current = value; 11 | }); 12 | return ref.current; 13 | } 14 | -------------------------------------------------------------------------------- /src/layout/customIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { ReactComponent as AccountSvg } from '@/assets/menu/account.svg'; 4 | import { ReactComponent as DashboardSvg } from '@/assets/menu/dashboard.svg'; 5 | import { ReactComponent as DocumentationSvg } from '@/assets/menu/documentation.svg'; 6 | import { ReactComponent as GuideSvg } from '@/assets/menu/guide.svg'; 7 | import { ReactComponent as PermissionSvg } from '@/assets/menu/permission.svg'; 8 | 9 | interface CustomIconProps { 10 | type: string; 11 | } 12 | 13 | export const CustomIcon: FC = props => { 14 | const { type } = props; 15 | let com = ; 16 | if (type === 'guide') { 17 | com = ; 18 | } else if (type === 'permission') { 19 | com = ; 20 | } else if (type === 'dashboard') { 21 | com = ; 22 | } else if (type === 'account') { 23 | com = ; 24 | } else if (type === 'documentation') { 25 | com = ; 26 | } else { 27 | com = ; 28 | } 29 | return {com}; 30 | }; 31 | -------------------------------------------------------------------------------- /src/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import { LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons'; 2 | import { Avatar, Dropdown, Layout, Menu } from 'antd'; 3 | import { observer } from 'mobx-react-lite'; 4 | import { FC } from 'react'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | import UserAvatar from '@/assets/header/avatar.jpg'; 8 | import { ReactComponent as EnUsSvg } from '@/assets/header/en_US.svg'; 9 | import { ReactComponent as LanguageSvg } from '@/assets/header/language.svg'; 10 | import { ReactComponent as ZhCnSvg } from '@/assets/header/zh_CN.svg'; 11 | import AntdSvg from '@/assets/logo/antd.svg'; 12 | import ReactSvg from '@/assets/logo/react.svg'; 13 | import { useLocale } from '@/locales'; 14 | import { userStore } from '@/stores/user'; 15 | 16 | import HeaderNoticeComponent from './notice'; 17 | 18 | const { Header } = Layout; 19 | 20 | interface HeaderProps { 21 | collapsed: boolean; 22 | toggle: () => void; 23 | } 24 | 25 | type Action = 'userInfo' | 'userSetting' | 'logout'; 26 | 27 | const HeaderComponent: FC = ({ collapsed, toggle }) => { 28 | const { name, locale } = userStore; 29 | const navigate = useNavigate(); 30 | const { formatMessage } = useLocale(); 31 | 32 | const onActionClick = async (action: Action) => { 33 | switch (action) { 34 | case 'userInfo': 35 | return; 36 | case 'userSetting': 37 | return; 38 | case 'logout': 39 | // eslint-disable-next-line no-case-declarations 40 | const res = Boolean(await userStore.logout()); 41 | res && navigate('/login'); 42 | return; 43 | } 44 | }; 45 | 46 | const selectLocale = ({ key }: { key: any }) => { 47 | userStore.setLocale(key); 48 | }; 49 | const menu = ( 50 | 51 | 52 | 53 | {formatMessage({ id: 'header.avator.account' })} 54 | 55 | 56 | onActionClick('logout')}> 57 | 58 | {formatMessage({ id: 'header.avator.logout' })} 59 | 60 | 61 | ); 62 | return ( 63 |
64 |
65 | 66 | 67 |
68 |
69 |
70 | {collapsed ? : } 71 |
72 |
73 | 74 | 78 | 79 | 简体中文 80 | 81 | 82 | English 83 | 84 | 85 | } 86 | > 87 | 88 | 89 | 90 | 91 |
92 | 93 | 94 | 95 | 96 | 97 | {name} 98 |
99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | export default observer(HeaderComponent); 106 | -------------------------------------------------------------------------------- /src/layout/index.less: -------------------------------------------------------------------------------- 1 | .layout-page { 2 | height: 100%; 3 | &-header { 4 | padding: 0; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | z-index: 9; 9 | background-color: #fff !important; 10 | box-shadow: 0 4px 10px #dddddd; 11 | &-main { 12 | padding: 0 15px; 13 | flex: 1; 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | } 18 | .logo { 19 | height: 64px; 20 | width: 200px; 21 | box-sizing: border-box; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | z-index: 9; 26 | img { 27 | width: 30px; 28 | height: 30px; 29 | } 30 | } 31 | } 32 | &-sider { 33 | background-color: #fff !important; 34 | box-sizing: border-box; 35 | border-right: 1px solid #f0f0f0; 36 | margin-bottom: 10px; 37 | } 38 | &-content { 39 | display: flex; 40 | flex-direction: column; 41 | flex: 1; 42 | > :nth-child(1) .ant-tabs-bar { 43 | padding: 6px 0 0; 44 | background: #fff; 45 | } 46 | 47 | > :nth-child(2) { 48 | flex: auto; 49 | overflow: hidden; 50 | padding: 6px; 51 | box-sizing: border-box; 52 | .innerText { 53 | background-color: #fff; 54 | padding: 24px; 55 | border-radius: 2px; 56 | display: block; 57 | line-height: 32px; 58 | font-size: 16px; 59 | } 60 | } 61 | } 62 | &-footer { 63 | background-color: #ffffff !important; 64 | text-align: center; 65 | padding: 14px 20px; 66 | font-size: 12px; 67 | } 68 | .actions { 69 | height: 100%; 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | > * { 74 | margin-left: 30px; 75 | height: 100%; 76 | display: flex; 77 | align-items: center; 78 | .notice { 79 | display: block; 80 | display: flex; 81 | justify-content: center; 82 | align-items: center; 83 | width: 22px; 84 | height: 22px; 85 | cursor: pointer; 86 | } 87 | } 88 | } 89 | .user-action { 90 | cursor: pointer; 91 | } 92 | .user-avator { 93 | margin-right: 8px; 94 | width: 40px; 95 | height: 40px; 96 | } 97 | } 98 | 99 | .layout-page-sider-menu { 100 | border-right: none !important; 101 | } 102 | .ant-menu-inline-collapsed { 103 | width: 79px !important; 104 | } 105 | 106 | .notice-description { 107 | font-size: 12px; 108 | &-datetime { 109 | margin-top: 4px; 110 | line-height: 1.5; 111 | } 112 | } 113 | 114 | .notice-title { 115 | display: flex; 116 | justify-content: space-between; 117 | } 118 | 119 | .tagsView-extra { 120 | height: 100%; 121 | width: 50px; 122 | cursor: pointer; 123 | display: block; 124 | line-height: 40px; 125 | text-align: center; 126 | } 127 | 128 | .themeSwitch { 129 | position: fixed; 130 | right: 32px; 131 | bottom: 102px; 132 | cursor: pointer; 133 | > span { 134 | display: block; 135 | text-align: center; 136 | background: #fff; 137 | border-radius: 50%; 138 | box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); 139 | width: 44px; 140 | height: 44px; 141 | line-height: 44px; 142 | font-size: 22px; 143 | z-index: 10001; 144 | } 145 | } 146 | 147 | .theme-color-content { 148 | display: flex; 149 | .theme-color-block { 150 | width: 20px; 151 | height: 20px; 152 | margin-right: 8px; 153 | color: #fff; 154 | font-weight: 700; 155 | text-align: center; 156 | border-radius: 2px; 157 | cursor: pointer; 158 | border-radius: 2px; 159 | &:last-child { 160 | margin-right: 0; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import { Layout } from 'antd'; 4 | import { FC, useEffect, useState } from 'react'; 5 | import { useLocation, useNavigate } from 'react-router'; 6 | 7 | import { RouteView } from '@/routes/routeView'; 8 | import { userStore } from '@/stores/user'; 9 | 10 | import HeaderComponent from './header'; 11 | import MenuComponent from './menu'; 12 | import TagsView from './tagView'; 13 | 14 | const { Sider, Content } = Layout; 15 | 16 | const LayoutPage: FC = () => { 17 | const [collapsed, setCollapsed] = useState(false); 18 | const location = useLocation(); 19 | const navigate = useNavigate(); 20 | 21 | useEffect(() => { 22 | if (location.pathname === '/') { 23 | navigate('/dashboard'); 24 | } 25 | }, [navigate, location]); 26 | 27 | const toggle = () => { 28 | setCollapsed(!collapsed); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default LayoutPage; 48 | -------------------------------------------------------------------------------- /src/layout/menu.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from 'antd'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { FC, useEffect, useState } from 'react'; 4 | import { useLocation, useNavigate } from 'react-router-dom'; 5 | 6 | import { IconFont } from '@/components/iconfont'; 7 | import type { MenuList } from '@/routes/types'; 8 | import { tagsViewStore } from '@/stores/tags-view'; 9 | import { userStore } from '@/stores/user'; 10 | import { isExternal } from '@/utils/validate'; 11 | 12 | const { SubMenu, Item } = Menu; 13 | 14 | interface MenuProps { 15 | menuList: MenuList; 16 | prefix?: string; 17 | } 18 | 19 | const MenuComponent: FC = ({ menuList }) => { 20 | const [openKeys, setOpenkeys] = useState([]); 21 | const [selectedKeys, setSelectedKeys] = useState([]); 22 | 23 | const { collapsed, device, locale, routeList } = userStore; 24 | const navigate = useNavigate(); 25 | const { pathname } = useLocation(); 26 | 27 | const getTitie = (menu: MenuList[0]) => { 28 | return ( 29 | 30 | {menu.meta?.icon && } 31 | {menu.meta?.title?.[locale]} 32 | 33 | ); 34 | }; 35 | 36 | const onMenuClick = (menu: MenuList[0]) => { 37 | const fullPath = menu.key || menu.path; 38 | if (fullPath === pathname) return; 39 | const { key, meta } = menu; 40 | if (device !== 'DESKTOP') { 41 | userStore.collapsed = true; 42 | } 43 | if (isExternal(menu.path)) { 44 | return window.open(menu.path); 45 | } 46 | tagsViewStore.addTag({ 47 | id: key, 48 | title: meta?.title || '', 49 | path: fullPath, 50 | closable: true 51 | }); 52 | setSelectedKeys([fullPath]); 53 | navigate(fullPath, { state: { fullPath } }); 54 | }; 55 | 56 | useEffect(() => { 57 | const currentRoute = routeList.find(m => m.key === pathname); 58 | console.log('currentRoute', currentRoute); 59 | setSelectedKeys([pathname]); 60 | setOpenkeys(collapsed ? [] : currentRoute?.keyPath || [pathname]); 61 | }, [collapsed, pathname, routeList]); 62 | 63 | const onOpenChange = (keys: string[]) => { 64 | setOpenkeys(keys); 65 | }; 66 | 67 | const getMenus = (menuList: MenuList) => { 68 | return menuList 69 | ?.filter(item => !item.meta?.hidden) 70 | ?.map(menu => { 71 | return menu.children ? ( 72 | 73 | {getMenus(menu.children)} 74 | 75 | ) : ( 76 | onMenuClick(menu)}> 77 | {getTitie(menu)} 78 | 79 | ); 80 | }); 81 | }; 82 | 83 | return ( 84 | 92 | {getMenus(menuList)} 93 | 94 | ); 95 | }; 96 | 97 | export default observer(MenuComponent); 98 | -------------------------------------------------------------------------------- /src/layout/suspendFallbackLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Spin } from 'antd'; 2 | import { FC } from 'react'; 3 | 4 | interface FallbackMessageProps { 5 | message: string; 6 | description?: string; 7 | } 8 | 9 | export const SuspendFallbackLoading: FC = ({ message, description }) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default SuspendFallbackLoading; 18 | -------------------------------------------------------------------------------- /src/layout/tagView/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from 'antd'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { FC, useCallback, useEffect } from 'react'; 4 | import { useLocation, useNavigate } from 'react-router-dom'; 5 | 6 | import { tagsViewStore } from '@/stores/tags-view'; 7 | import { userStore } from '@/stores/user'; 8 | 9 | import TagsViewAction from './tagViewAction'; 10 | 11 | const { TabPane } = Tabs; 12 | 13 | const TagsView: FC = () => { 14 | const { tags, activeTagId } = tagsViewStore; 15 | const { locale, routeList } = userStore; 16 | const navigate = useNavigate(); 17 | const location = useLocation(); 18 | 19 | // onClick tag 20 | const onChange = (key: string) => { 21 | const tag = tags.find(tag => tag.id === key); 22 | if (tag) { 23 | setCurrentTag(tag.id); 24 | navigate(tag.path); 25 | } 26 | }; 27 | 28 | // onRemove tag 29 | const onClose = (targetKey: string) => { 30 | tagsViewStore.removeTag(targetKey); 31 | }; 32 | 33 | const setCurrentTag = useCallback( 34 | (id?: string) => { 35 | const tag = tags.find(item => { 36 | if (id) { 37 | return item.id === id; 38 | } else { 39 | return item.path === location.pathname; 40 | } 41 | }); 42 | 43 | if (tag) { 44 | tagsViewStore.setActiveTag(tag.id); 45 | } 46 | }, 47 | [location.pathname, tags] 48 | ); 49 | 50 | useEffect(() => { 51 | if (routeList.length) { 52 | const menu = routeList.find(m => m.key === location.pathname); 53 | if (menu) { 54 | // Initializes dashboard page. 55 | const dashboard = routeList[0]; 56 | tagsViewStore.addTag({ 57 | path: dashboard.key, 58 | title: dashboard.meta?.title || '', 59 | id: dashboard.key, 60 | closable: false 61 | }); 62 | // Initializes the tag generated for the current page 63 | // Duplicate tag will be ignored in redux. 64 | tagsViewStore.addTag({ 65 | path: menu.key, 66 | title: menu.meta?.title || '', 67 | id: menu.key, 68 | closable: true 69 | }); 70 | } 71 | } 72 | }, [location.pathname, routeList]); 73 | 74 | //fix: remove tab route back auto 75 | useEffect(() => { 76 | if (tags && activeTagId) { 77 | const target = tags.find(e => e.id === activeTagId); 78 | if (target) { 79 | navigate(target.path); 80 | } else { 81 | setCurrentTag(tags[1].id); 82 | } 83 | } 84 | }, [tags, activeTagId, navigate, setCurrentTag]); 85 | 86 | return ( 87 |
88 | action === 'remove' && onClose(targetKey as string)} 95 | tabBarExtraContent={} 96 | > 97 | {tags.map(tag => ( 98 | 102 | {tag.title[locale]} 103 | 104 | } 105 | closable={tag.closable} 106 | /> 107 | ))} 108 | 109 |
110 | ); 111 | }; 112 | 113 | export default observer(TagsView); 114 | -------------------------------------------------------------------------------- /src/layout/tagView/tagViewAction.tsx: -------------------------------------------------------------------------------- 1 | import { SettingOutlined } from '@ant-design/icons'; 2 | import { Dropdown, Menu } from 'antd'; 3 | import type { DropDownProps } from 'antd/lib/dropdown/dropdown'; 4 | import { observer } from 'mobx-react-lite'; 5 | import { FC } from 'react'; 6 | 7 | import { LocaleFormatter } from '@/locales'; 8 | import { tagsViewStore } from '@/stores/tags-view'; 9 | 10 | interface TagsViewActionProps extends Partial { 11 | activeTagId: string; 12 | } 13 | 14 | const TagsViewAction: FC = props => { 15 | const { activeTagId } = props; 16 | return ( 17 | 21 | tagsViewStore.removeTag(activeTagId)}> 22 | 23 | 24 | tagsViewStore.removeOtherTag()}> 25 | 26 | 27 | tagsViewStore.removeAllTag()}> 28 | 29 | 30 | 31 | tagsViewStore.removeAllTag()}> 32 | 33 | 34 | 35 | } 36 | > 37 | {props.children ?? } 38 | 39 | ); 40 | }; 41 | 42 | export default observer(TagsViewAction); 43 | -------------------------------------------------------------------------------- /src/locales/en-US/account/index.ts: -------------------------------------------------------------------------------- 1 | export const enUS_account = { 2 | 'app.settings.menuMap.basic': 'Basic Settings', 3 | 'app.settings.menuMap.security': 'Security Settings', 4 | 'app.settings.menuMap.binding': 'Account Binding', 5 | 'app.settings.menuMap.notification': 'New Message Notification', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Change avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Please input your email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'app.settings.basic.profile': 'Personal profile', 13 | 'app.settings.basic.profile-message': 'Please input your personal profile!', 14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'app.settings.basic.country': 'Country/Region', 16 | 'app.settings.basic.country-message': 'Please input your country!', 17 | 'app.settings.basic.geographic': 'Province or city', 18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'app.settings.basic.address': 'Street Address', 20 | 'app.settings.basic.address-message': 'Please input your address!', 21 | 'app.settings.basic.phone': 'Phone Number', 22 | 'app.settings.basic.phone-message': 'Please input your phone!', 23 | 'app.settings.basic.update': 'Update Information', 24 | 'app.settings.security.strong': 'Strong', 25 | 'app.settings.security.medium': 'Medium', 26 | 'app.settings.security.weak': 'Weak', 27 | 'app.settings.security.password': 'Account Password', 28 | 'app.settings.security.password-description': 'Current password strength', 29 | 'app.settings.security.phone': 'Security Phone', 30 | 'app.settings.security.phone-description': 'Bound phone', 31 | 'app.settings.security.question': 'Security Question', 32 | 'app.settings.security.question-description': 33 | 'The security question is not set, and the security policy can effectively protect the account security', 34 | 'app.settings.security.email': 'Backup Email', 35 | 'app.settings.security.email-description': 'Bound Email', 36 | 'app.settings.security.mfa': 'MFA Device', 37 | 'app.settings.security.mfa-description': 'Unbound MFA device, after binding, can be confirmed twice', 38 | 'app.settings.security.modify': 'Modify', 39 | 'app.settings.security.set': 'Set', 40 | 'app.settings.security.bind': 'Bind', 41 | 'app.settings.binding.taobao': 'Binding Taobao', 42 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', 43 | 'app.settings.binding.alipay': 'Binding Alipay', 44 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', 45 | 'app.settings.binding.dingding': 'Binding DingTalk', 46 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', 47 | 'app.settings.binding.bind': 'Bind', 48 | 'app.settings.notification.password': 'Account Password', 49 | 'app.settings.notification.password-description': 50 | 'Messages from other users will be notified in the form of a station letter', 51 | 'app.settings.notification.messages': 'System Messages', 52 | 'app.settings.notification.messages-description': 'System messages will be notified in the form of a station letter', 53 | 'app.settings.notification.todo': 'To-do Notification', 54 | 'app.settings.notification.todo-description': 55 | 'The to-do list will be notified in the form of a letter from the station', 56 | 'app.settings.open': 'Open', 57 | 'app.settings.close': 'Close' 58 | }; 59 | -------------------------------------------------------------------------------- /src/locales/en-US/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export const enUS_dashboard = { 2 | 'app.dashboard.overview.totalSales': 'Total Sales', 3 | 'app.dashboard.overview.visits': 'Visits', 4 | 'app.dashboard.overview.payments': 'Payments', 5 | 'app.dashboard.overview.operationalEffect': 'Operational Effect', 6 | 'app.dashboard.overview.wowChange': 'WoW Change', 7 | 'app.dashboard.overview.dodChange': 'DoD Change', 8 | 'app.dashboard.overview.dailySales': 'Daily Sales', 9 | 'app.dashboard.overview.visits.dailyVisits': 'Daily Visits', 10 | 'app.dashboard.overview.conversionRate': 'Conversion Rate', 11 | 'app.dashboard.salePercent.proportionOfSales': 'The Proportion Of Sales', 12 | 'app.dashboard.salePercent.all': 'All', 13 | 'app.dashboard.salePercent.online': 'Online', 14 | 'app.dashboard.salePercent.offline': 'Offline', 15 | 'app.dashboard.timeline.traffic': 'Traffic', 16 | 'app.dashboard.timeline.payments': 'Payments' 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/en-US/documentation/index.ts: -------------------------------------------------------------------------------- 1 | export const en_US_documentation = { 2 | 'app.documentation.introduction.title': 'Introduction', 3 | 'app.documentation.introduction.description': ` 4 | react-antd-admin is an enterprise - level background management system template based on react and ant-design. 5 | Use the latest React Hooks API instead of the traditional class API, 6 | Typescript was also used to standardize code readability and maintainability, enhancing development efficiency, 7 | Use redux as the global state management library. 8 | This project allows you to quickly develop a new project template and remove some of the code according to your needs. 9 | If you don't have a need to use templates, 10 | This project will also be a good resource for learning react and typescript. 11 | In addition, if you think this project is worth optimizing or modifying, 12 | please feel free to ask, my contact information will be shown at the bottom of the article. 13 | `, 14 | 'app.documentation.catalogue.title': 'Catalogue', 15 | 'app.documentation.catalogue.description': 'Click the catalogue to quickly reach the specified content', 16 | 'app.documentation.catalogue.list.layout': 'Layout', 17 | 'app.documentation.catalogue.list.routes': 'Routes', 18 | 'app.documentation.catalogue.list.request': 'HTTP Request', 19 | 'app.documentation.catalogue.list.theme': 'Theme', 20 | 'app.documentation.catalogue.list.typescript': 'Typescript', 21 | 'app.documentation.catalogue.list.international': 'International' 22 | }; 23 | -------------------------------------------------------------------------------- /src/locales/en-US/global/tips.ts: -------------------------------------------------------------------------------- 1 | export const enUS_globalTips = { 2 | 'gloabal.tips.notfound': 'Sorry, the page you visited does not exist.', 3 | 'gloabal.tips.unauthorized': 'Sorry, you are not authorized to access this page.', 4 | 'gloabal.tips.loginResult': 'When you see this page, it means you are logged in.', 5 | 'gloabal.tips.goToLogin': 'Go To Login', 6 | 'gloabal.tips.username': 'Username', 7 | 'gloabal.tips.password': 'Password', 8 | 'gloabal.tips.login': 'Login', 9 | 'gloabal.tips.backHome': 'Back Home', 10 | 'gloabal.tips.operation': 'Operation', 11 | 'gloabal.tips.authorize': 'Authorize', 12 | 'gloabal.tips.delete': 'Delete', 13 | 'gloabal.tips.create': 'Create', 14 | 'gloabal.tips.modify': 'Modify', 15 | 'gloabal.tips.search': 'Search', 16 | 'gloabal.tips.reset': 'Reset', 17 | 'gloabal.tips.deleteConfirm': 'Do you Want to delete these items?' 18 | }; 19 | -------------------------------------------------------------------------------- /src/locales/en-US/guide/index.ts: -------------------------------------------------------------------------------- 1 | export const enUS_guide = { 2 | 'app.guide.guideIntro': `The guide page is useful for 3 | some people who entered the 4 | project for the first time. 5 | You can briefly introduce 6 | the features of the project. 7 | Demo is based on`, 8 | 'app.guide.showGuide': 'Show Guide', 9 | 'app.guide.driverjs.closeBtnText': 'Close', 10 | 'app.guide.driverjs.prevBtnText': 'Previous', 11 | 'app.guide.driverjs.nextBtnText': 'Next', 12 | 'app.guide.driverjs.doneBtnText': 'Done', 13 | 'app.guide.driverStep.sidebarTrigger.title': 'Sidebar Trigger', 14 | 'app.guide.driverStep.sidebarTrigger.description': 'Open and close the Sidebar', 15 | 'app.guide.driverStep.notices.title': 'Notices', 16 | 'app.guide.driverStep.notices.description': 'All notification messages were be displayed here', 17 | 'app.guide.driverStep.switchLanguages.title': 'Switch Languages', 18 | 'app.guide.driverStep.switchLanguages.description': 'You can click here to switch languages', 19 | 'app.guide.driverStep.pageTabs.title': 'Page Tabs', 20 | 'app.guide.driverStep.pageTabs.description': 'The history of the page you visited will be displayed here', 21 | 'app.guide.driverStep.pageTabsActions.title': 'Page Tabs Actions', 22 | 'app.guide.driverStep.pageTabsActions.description': 'Click here to do some quick operations to the Page Tabs', 23 | 'app.guide.driverStep.switchTheme.title': 'Switch Theme', 24 | 'app.guide.driverStep.switchTheme.description': 'Click here to switch system theme color' 25 | }; 26 | -------------------------------------------------------------------------------- /src/locales/en-US/index.ts: -------------------------------------------------------------------------------- 1 | import { enUS_account } from './account'; 2 | import { enUS_dashboard } from './dashboard'; 3 | import { en_US_documentation } from './documentation'; 4 | import { enUS_globalTips } from './global/tips'; 5 | import { enUS_guide } from './guide'; 6 | import { enUS_permissionRole } from './permission/role'; 7 | import { enUS_avatorDropMenu } from './user/avatorDropMenu'; 8 | import { enUS_tagsViewDropMenu } from './user/tagsViewDropMenu'; 9 | import { enUS_title } from './user/title'; 10 | 11 | const en_US = { 12 | ...enUS_account, 13 | ...enUS_avatorDropMenu, 14 | ...enUS_tagsViewDropMenu, 15 | ...enUS_title, 16 | ...enUS_globalTips, 17 | ...enUS_permissionRole, 18 | ...enUS_dashboard, 19 | ...enUS_guide, 20 | ...en_US_documentation 21 | }; 22 | 23 | export default en_US; 24 | -------------------------------------------------------------------------------- /src/locales/en-US/permission/role.ts: -------------------------------------------------------------------------------- 1 | export const enUS_permissionRole = { 2 | 'app.permission.role.name': 'Role Name', 3 | 'app.permission.role.code': 'Role Code', 4 | 'app.permission.role.status': 'Status', 5 | 'app.permission.role.status.all': 'All', 6 | 'app.permission.role.status.enabled': 'Enabled', 7 | 'app.permission.role.status.disabled': 'Disabled', 8 | 'app.permission.role.nameRequired': 'Please input the role name', 9 | 'app.permission.role.codeRequired': 'Please input the role code', 10 | 'app.permission.role.statusRequired': 'Please select the enabled status' 11 | }; 12 | -------------------------------------------------------------------------------- /src/locales/en-US/user/avatorDropMenu.ts: -------------------------------------------------------------------------------- 1 | export const enUS_avatorDropMenu = { 2 | 'header.avator.account': 'Account', 3 | 'header.avator.logout': 'Logout', 4 | 'global.theme.switchTheme': 'Switch Theme', 5 | 'global.theme.switchingTheme': 'Switching Theme...', 6 | 'global.theme.switchThemeDone': 'Update theme successfully!', 7 | 'global.theme.switchThemeFail': 'Update theme fail' 8 | }; 9 | -------------------------------------------------------------------------------- /src/locales/en-US/user/tagsViewDropMenu.ts: -------------------------------------------------------------------------------- 1 | export const enUS_tagsViewDropMenu = { 2 | 'tagsView.operation.closeCurrent': 'Close Current', 3 | 'tagsView.operation.closeOther': 'Close Other', 4 | 'tagsView.operation.closeAll': 'Close All', 5 | 'tagsView.operation.dashboard': 'Dashboard' 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/en-US/user/title.ts: -------------------------------------------------------------------------------- 1 | export const enUS_title = { 2 | 'title.login': 'Login', 3 | 'title.dashboard': 'Dashboard', 4 | 'title.documentation': 'Documentation', 5 | 'title.guide': 'Guide', 6 | 'title.permission.route': 'Route Permission', 7 | 'title.permission.button': 'Button Permission', 8 | 'title.permission.config': 'Permission Config', 9 | 'title.account': 'Account', 10 | 'title.notFount': '404' 11 | }; 12 | -------------------------------------------------------------------------------- /src/locales/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { FormattedMessage, MessageDescriptor, useIntl } from 'react-intl'; 3 | 4 | import en_US from './en-US'; 5 | import zh_CN from './zh-CN'; 6 | 7 | export const localeConfig = { 8 | zh_CN: zh_CN, 9 | en_US: en_US 10 | }; 11 | 12 | type Id = keyof typeof en_US; 13 | 14 | interface Props extends MessageDescriptor { 15 | id: Id; 16 | } 17 | 18 | export const LocaleFormatter: FC = ({ ...props }) => { 19 | const notChildProps = { ...props, children: undefined }; 20 | return ; 21 | }; 22 | 23 | type FormatMessageProps = (descriptor: Props) => string; 24 | 25 | export const useLocale = () => { 26 | const { formatMessage: _formatMessage, ...rest } = useIntl(); 27 | const formatMessage: FormatMessageProps = _formatMessage; 28 | return { 29 | ...rest, 30 | formatMessage 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/locales/zh-CN/account/index.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_account = { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关' 55 | }; 56 | -------------------------------------------------------------------------------- /src/locales/zh-CN/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_dashboard = { 2 | 'app.dashboard.overview.totalSales': '总销售额', 3 | 'app.dashboard.overview.visits': '访问量', 4 | 'app.dashboard.overview.payments': '支付笔数', 5 | 'app.dashboard.overview.operationalEffect': '运营活动效果', 6 | 'app.dashboard.overview.wowChange': '周同比', 7 | 'app.dashboard.overview.dodChange': '日同比', 8 | 'app.dashboard.overview.dailySales': '日销售额', 9 | 'app.dashboard.overview.visits.dailyVisits': '日访问量', 10 | 'app.dashboard.overview.conversionRate': '转化率', 11 | 'app.dashboard.salePercent.proportionOfSales': '销售额类别占比', 12 | 'app.dashboard.salePercent.all': '全部', 13 | 'app.dashboard.salePercent.online': '线上', 14 | 'app.dashboard.salePercent.offline': '线下', 15 | 'app.dashboard.timeline.traffic': '客流量', 16 | 'app.dashboard.timeline.payments': '支付笔数' 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN/documentation/index.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_documentation = { 2 | 'app.documentation.introduction.title': '介绍', 3 | 'app.documentation.introduction.description': ` 4 | react-antd-admin是一个基于react和ant-design开发的企业级中后台管理系统模板。 5 | 使用了最新的React Hooks API代替了传统的class API, 6 | 并且使用了typescript来规范代码的可读性和维护性,增强开发效率, 7 | 使用redux作为全局的状态管理库。 8 | 此项目可以你的新项目模板快速开发,根据自己的需求删除掉部分代码。如果你没有使用模板的需求, 9 | 此项目也会是一个学习react和typescript的好的资料。 10 | 此外,如果你觉得此项目有值得优化或修改的地方,也欢迎提出,我的联系方式将会显示在文章底部。 11 | `, 12 | 'app.documentation.catalogue.title': '目录', 13 | 'app.documentation.catalogue.description': '点击目录到达指定内容', 14 | 'app.documentation.catalogue.list.layout': '布局', 15 | 'app.documentation.catalogue.list.routes': '路由', 16 | 'app.documentation.catalogue.list.request': '网络请求', 17 | 'app.documentation.catalogue.list.theme': '主题', 18 | 'app.documentation.catalogue.list.typescript': 'Typescript', 19 | 'app.documentation.catalogue.list.international': '国际化' 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/zh-CN/global/tips.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_globalTips = { 2 | 'gloabal.tips.notfound': '对不起,您访问的页面不存在。', 3 | 'gloabal.tips.unauthorized': '对不起,您没有权限访问此页。', 4 | 'gloabal.tips.loginResult': '看到此页面代表您已登录。', 5 | 'gloabal.tips.goToLogin': '去登录', 6 | 'gloabal.tips.login': '登录', 7 | 'gloabal.tips.username': '用户名', 8 | 'gloabal.tips.password': '密码', 9 | 'gloabal.tips.rememberUser': '记住用户', 10 | 'gloabal.tips.backHome': '返回首页', 11 | 'gloabal.tips.operation': '操作', 12 | 'gloabal.tips.authorize': '授权', 13 | 'gloabal.tips.delete': '删除', 14 | 'gloabal.tips.create': '新建', 15 | 'gloabal.tips.modify': '修改', 16 | 'gloabal.tips.search': '搜索', 17 | 'gloabal.tips.reset': '重置', 18 | 'gloabal.tips.deleteConfirm': '确定要删除此条数据吗?' 19 | }; 20 | -------------------------------------------------------------------------------- /src/locales/zh-CN/guide/index.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_guide = { 2 | 'app.guide.guideIntro': `引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于`, 3 | 'app.guide.showGuide': '打开引导', 4 | 'app.guide.driverjs.closeBtnText': '关闭', 5 | 'app.guide.driverjs.prevBtnText': '上一步', 6 | 'app.guide.driverjs.nextBtnText': '下一步', 7 | 'app.guide.driverjs.doneBtnText': '完成', 8 | 'app.guide.driverStep.sidebarTrigger.title': 'Siderbar开关', 9 | 'app.guide.driverStep.sidebarTrigger.description': '打开和关闭Siderbar', 10 | 'app.guide.driverStep.notices.title': '通知中心', 11 | 'app.guide.driverStep.notices.description': '所有通知消息都会显示在这里', 12 | 'app.guide.driverStep.switchLanguages.title': '切换语言', 13 | 'app.guide.driverStep.switchLanguages.description': '你可以点击这里来切换语言', 14 | 'app.guide.driverStep.pageTabs.title': '页面标签', 15 | 'app.guide.driverStep.pageTabs.description': '你的浏览历史会在这里集中显示', 16 | 'app.guide.driverStep.pageTabsActions.title': '标签操作栏', 17 | 'app.guide.driverStep.pageTabsActions.description': '点击这里可以对页面标签做一些快捷操作', 18 | 'app.guide.driverStep.switchTheme.title': '切换主题', 19 | 'app.guide.driverStep.switchTheme.description': '点击这里切换系统主题颜色' 20 | }; 21 | -------------------------------------------------------------------------------- /src/locales/zh-CN/index.ts: -------------------------------------------------------------------------------- 1 | import { zhCN_account } from './account'; 2 | import { zhCN_dashboard } from './dashboard'; 3 | import { zhCN_documentation } from './documentation'; 4 | import { zhCN_globalTips } from './global/tips'; 5 | import { zhCN_guide } from './guide'; 6 | import { zhCN_permissionRole } from './permission/role'; 7 | import { zhCN_avatorDropMenu } from './user/avatorDropMenu'; 8 | import { zhCN_tagsViewDropMenu } from './user/tagsViewDropMenu'; 9 | import { zhCN_title } from './user/title'; 10 | 11 | const zh_CN = { 12 | ...zhCN_account, 13 | ...zhCN_avatorDropMenu, 14 | ...zhCN_tagsViewDropMenu, 15 | ...zhCN_title, 16 | ...zhCN_globalTips, 17 | ...zhCN_permissionRole, 18 | ...zhCN_dashboard, 19 | ...zhCN_guide, 20 | ...zhCN_documentation 21 | }; 22 | 23 | export default zh_CN; 24 | -------------------------------------------------------------------------------- /src/locales/zh-CN/permission/role.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_permissionRole = { 2 | 'app.permission.role.name': '角色名称', 3 | 'app.permission.role.code': '角色编码', 4 | 'app.permission.role.status': '状态', 5 | 'app.permission.role.status.all': '全部', 6 | 'app.permission.role.status.enabled': '启用', 7 | 'app.permission.role.status.disabled': '禁用', 8 | 'app.permission.role.nameRequired': '请输入角色名称', 9 | 'app.permission.role.codeRequired': '请输入角色编码', 10 | 'app.permission.role.statusRequired': '请选择角色启用状态' 11 | }; 12 | -------------------------------------------------------------------------------- /src/locales/zh-CN/user/avatorDropMenu.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_avatorDropMenu = { 2 | 'header.avator.account': '个人设置', 3 | 'header.avator.logout': '退出登录', 4 | 'global.theme.switchTheme': '切换主题', 5 | 'global.theme.switchingTheme': '切换主题中...', 6 | 'global.theme.switchThemeDone': '主题更新成功', 7 | 'global.theme.switchThemeFail': '主题更新失败' 8 | }; 9 | -------------------------------------------------------------------------------- /src/locales/zh-CN/user/tagsViewDropMenu.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_tagsViewDropMenu = { 2 | 'tagsView.operation.closeCurrent': '关闭当前', 3 | 'tagsView.operation.closeOther': '关闭其他', 4 | 'tagsView.operation.closeAll': '关闭全部', 5 | 'tagsView.operation.dashboard': '首页' 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN/user/title.ts: -------------------------------------------------------------------------------- 1 | export const zhCN_title = { 2 | 'title.login': '登录', 3 | 'title.dashboard': '首页', 4 | 'title.documentation': '文档', 5 | 'title.guide': '引导页', 6 | 'title.permission.route': '路由权限', 7 | 'title.permission.button': '按钮权限', 8 | 'title.permission.config': '权限配置', 9 | 'title.account': '个人设置', 10 | 'title.notFount': '404' 11 | }; 12 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import 'virtual:windi.css'; 2 | import 'virtual:windi-devtools'; 3 | import 'antd/dist/antd.less'; 4 | import 'antd/lib/style/index.css'; 5 | import './styles/index.less'; 6 | 7 | import ReactDOM from 'react-dom'; 8 | 9 | import App from './App'; 10 | // import {RootStoreContext,} from '@/stores' 11 | 12 | ReactDOM.render(, document.getElementById('root')); 13 | -------------------------------------------------------------------------------- /src/routes/config.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { useIntl } from 'react-intl'; 3 | import { RouteProps } from 'react-router'; 4 | 5 | import PrivateRoute from './pravateRoute'; 6 | 7 | export interface WrapperRouteProps extends RouteProps { 8 | /** document title locale id */ 9 | titleId: string; 10 | /** authorization? */ 11 | auth?: boolean; 12 | } 13 | 14 | const WrapperRouteComponent: FC = ({ titleId, auth, ...props }) => { 15 | const { formatMessage } = useIntl(); 16 | const WitchRoute = auth ? : props.element; 17 | if (titleId) { 18 | requestIdleCallback(() => { 19 | document.title = formatMessage({ 20 | id: titleId 21 | }); 22 | }); 23 | } 24 | return WitchRoute || null; 25 | }; 26 | 27 | export default WrapperRouteComponent; 28 | -------------------------------------------------------------------------------- /src/routes/generator-router.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from 'antd'; 2 | 3 | import { NotFound } from '@/routes'; 4 | import WrapperRouteComponent from '@/routes/config'; 5 | import { constantRouterComponents } from '@/routes/modules'; 6 | import RouteView from '@/routes/routeView'; 7 | import { isExternal } from '@/utils/validate'; 8 | 9 | import { MenuItem } from './types'; 10 | 11 | export function filterAsyncRoute(routes: API.Menu[], parentRoute: API.Menu | null, lastKeyPath: string[] = []) { 12 | return ( 13 | routes 14 | // eslint-disable-next-line 15 | .filter(item => item.type !== 2 && item.isShow && item.parentId == parentRoute?.id) 16 | .map(item => { 17 | const { router, viewPath, name, icon, keepalive } = item; 18 | let fullPath = ''; 19 | const pathPrefix = lastKeyPath.slice(-1)[0] || ''; 20 | if (/http(s)?:/.test(router)) { 21 | fullPath = router; 22 | } else { 23 | fullPath = router.startsWith('/') ? router : '/' + router; 24 | fullPath = router.startsWith(pathPrefix) ? fullPath : pathPrefix + fullPath; 25 | fullPath = [...new Set(fullPath.split('/'))].join('/'); 26 | } 27 | let realRoutePath = router; 28 | if (parentRoute) { 29 | if (fullPath.startsWith(parentRoute?.router)) { 30 | realRoutePath = fullPath.split(parentRoute.router)[1]; 31 | } else if (!isExternal(parentRoute.router) && !isExternal(router)) { 32 | realRoutePath = router; 33 | } 34 | } 35 | realRoutePath = realRoutePath.startsWith('/') ? realRoutePath.slice(1) : realRoutePath; 36 | const route: MenuItem = { 37 | path: realRoutePath, 38 | key: fullPath, 39 | keyPath: lastKeyPath.concat(fullPath), 40 | // name: toHump(viewPath), 41 | meta: { 42 | title: { 43 | zh_CN: name, 44 | en_US: name 45 | }, 46 | icon: icon, 47 | noCache: !keepalive 48 | } 49 | }; 50 | 51 | if (item.type === 0) { 52 | // 如果是目录 53 | const children = filterAsyncRoute(routes, item, route.keyPath); 54 | if (children?.length) { 55 | route.element = } auth={true} titleId="title.dashboard" />; 56 | route.children = children; 57 | } else { 58 | route.element = ( 59 | 64 | ); 65 | } 66 | return route; 67 | } else if (item.type === 1) { 68 | // 如果是页面 69 | const Component = constantRouterComponents[viewPath.replace('.vue', '')] || NotFound; 70 | route.element = } auth={true} titleId="title.dashboard" />; 71 | return route; 72 | } 73 | return undefined; 74 | }) 75 | .filter((item): item is MenuItem => !!item) 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { FC, lazy, Suspense, useEffect, useMemo } from 'react'; 4 | import type { RouteObject } from 'react-router'; 5 | import { useLocation, useNavigate, useRoutes } from 'react-router-dom'; 6 | 7 | import LayoutPage from '@/layout'; 8 | import { SuspendFallbackLoading } from '@/layout/suspendFallbackLoading'; 9 | import type { MenuList } from '@/routes/types'; 10 | import { userStore } from '@/stores/user'; 11 | 12 | import WrapperRouteComponent from './config'; 13 | 14 | export const NotFound = lazy(() => import('@/views/error/404')); 15 | const LoginPage = lazy(() => import('@/views/login')); 16 | const Dashboard = lazy(() => import('@/views/dashboard')); 17 | 18 | const defaultRouteList: RouteObject[] = [ 19 | { 20 | path: 'login', 21 | element: } titleId="title.login" /> 22 | }, 23 | { 24 | path: '/', 25 | element: } titleId="" />, 26 | children: [] 27 | } 28 | ]; 29 | 30 | /** 31 | * @description 默认的菜单项 32 | */ 33 | export const defaultMenuRoutes: MenuList = [ 34 | { 35 | path: '/dashboard', 36 | key: '/dashboard', 37 | element: } titleId="title.dashboard" />, 38 | meta: { 39 | title: { 40 | zh_CN: '首页', 41 | en_US: 'dashboard' 42 | } 43 | } 44 | } 45 | ]; 46 | 47 | const errorPages = [ 48 | { 49 | path: '*', 50 | element: } titleId="title.notFount" /> 51 | } 52 | ]; 53 | 54 | const DynamicRouter: FC = () => { 55 | const { token, menuList = [] } = userStore; 56 | const navigate = useNavigate(); 57 | const { pathname, state } = useLocation(); 58 | 59 | useEffect(() => { 60 | console.log('logged', !!token, state); 61 | if (!token && pathname !== '/login') { 62 | return navigate({ pathname: 'login' }, { replace: true, state: { from: pathname } }); 63 | } 64 | 65 | if (token) { 66 | !menuList.length && userStore.afterLogin(); 67 | if (pathname === '/login') { 68 | navigate({ pathname: '/' }, { replace: true }); 69 | } 70 | } 71 | }, [menuList, token, navigate, pathname, state]); 72 | 73 | const newRoutes = useMemo(() => { 74 | const routes = cloneDeep(defaultRouteList); 75 | const layoutRoute = routes.find(item => item.path === '/')?.children; 76 | layoutRoute?.push(...cloneDeep([...defaultMenuRoutes, ...menuList]), ...errorPages); 77 | return routes; 78 | }, [menuList]); 79 | 80 | return ; 81 | }; 82 | 83 | interface RenderRouterProps { 84 | routerList: RouteObject[]; 85 | } 86 | 87 | const RenderRouter: FC = ({ routerList }) => { 88 | console.log('routerList', routerList); 89 | const element = useRoutes(routerList); 90 | return }>{element}; 91 | }; 92 | 93 | export default observer(DynamicRouter); 94 | -------------------------------------------------------------------------------- /src/routes/modules/index.ts: -------------------------------------------------------------------------------- 1 | export const constantRouterComponents = {} as any; 2 | 3 | // auto load 4 | const modulesFiles = import.meta.globEager('./**/*.ts'); 5 | 6 | Object.keys(modulesFiles).forEach(path => { 7 | if (path.startsWith('./index.')) return; 8 | const value = modulesFiles[path].default; 9 | 10 | // mouted 11 | Object.keys(value).forEach(ele => { 12 | constantRouterComponents[ele] = value[ele]; 13 | }); 14 | }); 15 | 16 | console.log('constantRouterComponents', constantRouterComponents); 17 | -------------------------------------------------------------------------------- /src/routes/modules/system.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | /** 4 | * system module 5 | */ 6 | export default { 7 | 'views/system/permission/user': lazy(() => import('@/views/system/permission/user')), 8 | 'views/system/permission/menu': lazy(() => import('@/views/system/permission/menu')), 9 | 'views/system/permission/role': lazy(() => import('@/views/system/permission/role')), 10 | 'views/system/monitor/req-log': lazy(() => import('@/views/system/monitor/req-log')), 11 | 'views/system/monitor/online': lazy(() => import('@/views/system/monitor/online')), 12 | 'views/system/monitor/serve': lazy(() => import('@/views/system/monitor/serve')), 13 | 'views/system/monitor/login-log': lazy(() => import('@/views/system/monitor/login-log')), 14 | 'views/system/schedule/task': lazy(() => import('@/views/system/schedule/task')), 15 | 'views/system/schedule/log': lazy(() => import('@/views/system/schedule/log')) 16 | }; 17 | -------------------------------------------------------------------------------- /src/routes/pravateRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import { FC } from 'react'; 3 | import { useLocation } from 'react-router'; 4 | import { RouteProps, useNavigate } from 'react-router-dom'; 5 | 6 | import { useLocale } from '@/locales'; 7 | import { userStore } from '@/stores/user'; 8 | 9 | const PrivateRoute: FC = props => { 10 | const { token } = userStore; 11 | const navigate = useNavigate(); 12 | const { formatMessage } = useLocale(); 13 | const { pathname } = useLocation(); 14 | 15 | return token ? ( 16 | props.element || null 17 | ) : ( 18 | navigate({ pathname: 'login' }, { replace: true, state: { from: pathname } })} 26 | > 27 | {formatMessage({ id: 'gloabal.tips.goToLogin' })} 28 | 29 | } 30 | /> 31 | ); 32 | }; 33 | 34 | export default PrivateRoute; 35 | -------------------------------------------------------------------------------- /src/routes/routeView.tsx: -------------------------------------------------------------------------------- 1 | import { FC, Suspense } from 'react'; 2 | import { Outlet } from 'react-router'; 3 | 4 | import SuspendFallbackLoading from '@/layout/suspendFallbackLoading'; 5 | 6 | export const RouteView: FC = () => { 7 | return ( 8 | 14 | } 15 | > 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default RouteView; 22 | -------------------------------------------------------------------------------- /src/routes/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | // interface MenuItem { 3 | // /** menu item name */ 4 | // name: string; 5 | // /** menu labels */ 6 | // label: { 7 | // zh_CN: string; 8 | // en_US: string; 9 | // }; 10 | // /** 图标名称 11 | // * 12 | // * 子子菜单不需要图标 13 | // */ 14 | // icon?: string; 15 | // /** 菜单id */ 16 | // key: string; 17 | // /** 菜单路由 */ 18 | // path: string; 19 | // /** 子菜单 */ 20 | // children?: MenuItem[]; 21 | // } 22 | 23 | export type MenuChild = Omit; 24 | 25 | export type MenuList = MenuItem[]; 26 | export interface MenuItem { 27 | path: string; 28 | /** 菜单唯一的key */ 29 | key: string; 30 | name?: string; 31 | keyPath?: string[]; 32 | auth?: boolean; 33 | redirect?: string; 34 | element?: ReactNode; 35 | alwaysShow?: boolean; 36 | children?: MenuItem[]; 37 | meta?: { 38 | hidden?: boolean; 39 | title: { 40 | zh_CN: string; 41 | en_US: string; 42 | }; 43 | icon?: string; 44 | noCache?: boolean; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | import { tagsViewStore } from './tags-view'; 4 | import { userStore } from './user'; 5 | import { wsStore } from './ws'; 6 | 7 | export const stores = { userStore, wsStore, tagsViewStore }; 8 | 9 | export const RootStoreContext = createContext(stores); 10 | 11 | export const useStores = () => useContext(RootStoreContext); 12 | -------------------------------------------------------------------------------- /src/stores/tags-view.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | export type TagItem = { 4 | id: string; 5 | 6 | title: 7 | | { 8 | zh_CN: string; 9 | en_US: string; 10 | } 11 | | string; 12 | 13 | /** tag's route path */ 14 | path: string; 15 | 16 | /** can be closed ? */ 17 | closable: boolean; 18 | }; 19 | 20 | export const tagsViewStore = makeAutoObservable({ 21 | activeTagId: '' as TagItem['id'], 22 | tags: [] as TagItem[], 23 | setActiveTag(activeTagId: string) { 24 | this.activeTagId = activeTagId; 25 | }, 26 | addTag(tagItem: TagItem) { 27 | if (!this.tags.find(tag => tag.id === tagItem.id)) { 28 | this.tags.push(tagItem); 29 | } 30 | 31 | this.activeTagId = tagItem.id; 32 | }, 33 | removeTag(targetKey: string) { 34 | // dashboard cloud't be closed 35 | if (targetKey === this.tags[0].id) { 36 | return; 37 | } 38 | 39 | const activeTagId = this.activeTagId; 40 | const currentIndex = this.tags.findIndex(n => n.id === targetKey); 41 | const lastIndex = currentIndex - 1; 42 | this.tags.splice(currentIndex, 1); 43 | 44 | if (this.tags.length && activeTagId === targetKey) { 45 | if (lastIndex >= 0) { 46 | this.activeTagId = this.tags[lastIndex].id; 47 | } else { 48 | this.activeTagId = this.tags[0].id; 49 | } 50 | } 51 | }, 52 | removeAllTag() { 53 | this.activeTagId = this.tags[0].id; 54 | this.tags = [this.tags[0]]; 55 | }, 56 | removeOtherTag() { 57 | const activeTag = this.tags.find(tag => tag.id === this.activeTagId); 58 | const activeIsDashboard = activeTag!.id === this.tags[0].id; 59 | 60 | this.tags = activeIsDashboard ? [this.tags[0]] : [this.tags[0], activeTag!]; 61 | } 62 | }); 63 | 64 | export default tagsViewStore; 65 | -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable, runInAction } from 'mobx'; 2 | 3 | import { getInfo, logout, permmenu } from '@/api/account'; 4 | import { login } from '@/api/login'; 5 | import { ACCESS_TOKEN_KEY, LOCALE } from '@/enums/cacheEnum'; 6 | import { defaultMenuRoutes } from '@/routes'; 7 | import { filterAsyncRoute } from '@/routes/generator-router'; 8 | import type { MenuChild } from '@/routes/types'; 9 | import { wsStore } from '@/stores/ws'; 10 | import { flatArrayObject } from '@/utils'; 11 | import { Storage } from '@/utils/Storage'; 12 | 13 | const device = /(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent) ? 'MOBILE' : 'DESKTOP'; 14 | 15 | export const userStore = makeAutoObservable({ 16 | token: Storage.get(ACCESS_TOKEN_KEY, null), 17 | userInfo: {}, 18 | collapsed: device !== 'DESKTOP', 19 | device: device as 'MOBILE' | 'DESKTOP', 20 | locale: Storage.get(LOCALE, 'zh_CN'), 21 | // locale: (localStorage.getItem('locale')! || 'zh_CN') as Locale, 22 | name: 'amdin', 23 | avatar: '', 24 | perms: [] as string[], 25 | menus: [] as MenuChild[], 26 | routeList: [] as MenuChild[], 27 | menuList: [] as MenuChild[], 28 | // 清空token及用户信息 29 | resetToken() { 30 | this.avatar = this.token = this.name = ''; 31 | this.perms = []; 32 | this.menus = []; 33 | this.userInfo = {}; 34 | Storage.clear(); 35 | }, 36 | setLocale(locale) { 37 | this.locale = locale; 38 | Storage.set(LOCALE, locale); 39 | }, 40 | // 登录成功保存token 41 | setToken(token: string) { 42 | this.token = token ?? ''; 43 | const ex = 7 * 24 * 60 * 60 * 1000; 44 | Storage.set(ACCESS_TOKEN_KEY, this.token, ex); 45 | }, 46 | generateRoutes(menus: API.Menu[]) { 47 | // 后端路由json进行转换成真正的router map 48 | const routeList = filterAsyncRoute(menus, null); 49 | // 404 route must be end 50 | // routeList.push(NotFoundRouter) 51 | const defaultRoutes = flatArrayObject(defaultMenuRoutes, ['children']); 52 | const asyncRoutes = flatArrayObject(routeList, ['children']); 53 | // 获取数据后,将赋值操作放到 runInAction 中 54 | runInAction(() => { 55 | this.routeList = [...defaultRoutes, ...asyncRoutes]; 56 | this.menuList = [...defaultMenuRoutes, ...routeList]; 57 | }); 58 | }, 59 | // 登录 60 | async login(params: API.LoginParams) { 61 | try { 62 | const { data } = await login(params); 63 | this.setToken(data.token); 64 | return this.afterLogin(); 65 | } catch (error) { 66 | return Promise.reject(error); 67 | } 68 | }, 69 | async afterLogin() { 70 | try { 71 | const [userInfo, { perms, menus }] = await Promise.all([getInfo(), permmenu()]); 72 | runInAction(() => { 73 | this.perms = perms; 74 | this.name = userInfo.name; 75 | this.avatar = userInfo.headImg; 76 | this.userInfo = userInfo; 77 | }); 78 | 79 | this.generateRoutes(menus); 80 | // 生成路由 81 | // const routes = generatorDynamicRouter(menus); 82 | // this.menus = routes; 83 | wsStore.initSocket(); 84 | // router.push('/sys/permission/role') 85 | return { menus, perms, userInfo }; 86 | } catch (error) { 87 | console.log(error); 88 | // return this.logout(); 89 | } 90 | }, 91 | // 登出 92 | async logout() { 93 | await logout(); 94 | wsStore.closeSocket(); 95 | this.resetToken(); 96 | } 97 | }); 98 | 99 | export default userStore; 100 | -------------------------------------------------------------------------------- /src/stores/ws.ts: -------------------------------------------------------------------------------- 1 | import { Modal } from 'antd'; 2 | import { makeAutoObservable } from 'mobx'; 3 | 4 | import { EVENT_KICK } from '@/core/socket/event-type'; 5 | import type { SocketIOWrapperType, SocketStatusType } from '@/core/socket/socket-io'; 6 | import { SocketIOWrapper, SocketStatus } from '@/core/socket/socket-io'; 7 | import { userStore } from '@/stores/user'; 8 | 9 | export const wsStore = makeAutoObservable({ 10 | // socket wrapper 实例 11 | client: null as SocketIOWrapperType | null, 12 | // socket 连接状态 13 | status: SocketStatus.CLOSE as SocketStatusType, 14 | setClient(client: SocketIOWrapperType | null) { 15 | this.client = client as any; 16 | }, 17 | setStatus(status: SocketStatusType) { 18 | if (this.status === status) { 19 | return; 20 | } 21 | this.status = status; 22 | }, 23 | // 初始化Socket 24 | initSocket() { 25 | // check is init 26 | if (this.client?.isConnected?.()) { 27 | return; 28 | } 29 | const ws = new SocketIOWrapper(); 30 | ws.subscribe(EVENT_KICK, async data => { 31 | // reset token 32 | userStore.resetToken(); 33 | Modal.warning({ 34 | title: '警告', 35 | content: `您已被管理员${data.operater}踢下线!`, 36 | centered: true, 37 | okText: '重新登录', 38 | onOk() { 39 | // 刷新页面 40 | window.location.reload(); 41 | } 42 | }); 43 | }); 44 | this.setClient(ws); 45 | }, 46 | 47 | // 关闭Socket连接 48 | closeSocket() { 49 | this.client?.close?.(); 50 | this.setClient(null); 51 | } 52 | }); 53 | 54 | export default wsStore; 55 | -------------------------------------------------------------------------------- /src/styles/antd.reset.less: -------------------------------------------------------------------------------- 1 | // body { 2 | // .ant-message { 3 | // z-index: 999999; 4 | // } 5 | 6 | // span.anticon:not(.app-iconify) { 7 | // vertical-align: 0.125em !important; 8 | // } 9 | // } 10 | 11 | // .ant-image-preview-root img { 12 | // display: unset; 13 | // } 14 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './main.less'; 2 | 3 | @import './antd.reset.less'; 4 | -------------------------------------------------------------------------------- /src/styles/main.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root, 4 | #root > * { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | font-size: 14px; 10 | color: #333 !important; 11 | } 12 | iframe { 13 | display: none; 14 | } 15 | 16 | a { 17 | text-decoration: underline; 18 | cursor: pointer; 19 | } 20 | 21 | #searchForm > .ant-row > .ant-row.ant-form-item { 22 | width: 100%; 23 | text-align: right; 24 | padding-right: 12px; 25 | .ant-col { 26 | flex: 1; 27 | max-width: 100%; 28 | width: 100%; 29 | .ant-btn:first-child { 30 | margin-right: 10px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/Storage.ts: -------------------------------------------------------------------------------- 1 | // 默认缓存期限为7天 2 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; 3 | 4 | /** 5 | * 创建本地缓存对象 6 | * @param {string=} prefixKey - 7 | * @param {Object} [storage=localStorage] - sessionStorage | localStorage 8 | */ 9 | export const createStorage = ({ prefixKey = '', storage = localStorage } = {}) => { 10 | /** 11 | * 本地缓存类 12 | * @class Storage 13 | */ 14 | const Storage = class { 15 | private storage = storage; 16 | private prefixKey?: string = prefixKey; 17 | 18 | private getKey(key: string) { 19 | return `${this.prefixKey}${key}`.toUpperCase(); 20 | } 21 | 22 | /** 23 | * @description 设置缓存 24 | * @param {string} key 缓存键 25 | * @param {*} value 缓存值 26 | * @param expire 27 | */ 28 | set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) { 29 | const stringData = JSON.stringify({ 30 | value, 31 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null 32 | }); 33 | this.storage.setItem(this.getKey(key), stringData); 34 | } 35 | 36 | /** 37 | * 读取缓存 38 | * @param {string} key 缓存键 39 | * @param {*=} def 默认值 40 | */ 41 | get(key: string, def: any = null): T { 42 | const item = this.storage.getItem(this.getKey(key)); 43 | if (item) { 44 | try { 45 | const data = JSON.parse(item); 46 | const { value, expire } = data; 47 | // 在有效期内直接返回 48 | if (expire === null || expire >= Date.now()) { 49 | return value; 50 | } 51 | this.remove(this.getKey(key)); 52 | } catch (e) { 53 | return def; 54 | } 55 | } 56 | return def; 57 | } 58 | 59 | /** 60 | * 从缓存删除某项 61 | * @param {string} key 62 | */ 63 | remove(key: string) { 64 | console.log(key, '搜索'); 65 | this.storage.removeItem(this.getKey(key)); 66 | } 67 | 68 | /** 69 | * 清空所有缓存 70 | * @memberOf Cache 71 | */ 72 | clear(): void { 73 | this.storage.clear(); 74 | } 75 | 76 | /** 77 | * 设置cookie 78 | * @param {string} name cookie 名称 79 | * @param {*} value cookie 值 80 | * @param {number=} expire 过期时间 81 | * 如果过期时间为设置,默认关闭浏览器自动删除 82 | * @example 83 | */ 84 | setCookie(name: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) { 85 | document.cookie = `${this.getKey(name)}=${value}; Max-Age=${expire}`; 86 | } 87 | 88 | /** 89 | * 根据名字获取cookie值 90 | * @param name 91 | */ 92 | getCookie(name: string): string { 93 | const cookieArr = document.cookie.split('; '); 94 | for (let i = 0, length = cookieArr.length; i < length; i++) { 95 | const kv = cookieArr[i].split('='); 96 | if (kv[0] === this.getKey(name)) { 97 | return kv[1]; 98 | } 99 | } 100 | return ''; 101 | } 102 | 103 | /** 104 | * 根据名字删除指定的cookie 105 | * @param {string} key 106 | */ 107 | removeCookie(key: string) { 108 | this.setCookie(key, 1, -1); 109 | } 110 | 111 | /** 112 | * 清空cookie,使所有cookie失效 113 | */ 114 | clearCookie(): void { 115 | const keys = document.cookie.match(/[^ =;]+(?==)/g); 116 | if (keys) { 117 | for (let i = keys.length; i--; ) { 118 | document.cookie = keys[i] + '=0;expire=' + new Date(0).toUTCString(); 119 | } 120 | } 121 | } 122 | }; 123 | return new Storage(); 124 | }; 125 | 126 | export const Storage = createStorage(); 127 | 128 | export default Storage; 129 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * / _ - 转换成驼峰并将pages替换成空字符串 3 | * @param {*} name name 4 | */ 5 | export function toHump(name: string) { 6 | return name 7 | .replace(/[-/_](\w)/g, function (all, letter) { 8 | return letter.toUpperCase(); 9 | }) 10 | .replace('views', ''); 11 | } 12 | 13 | /** 14 | * 将数组对象磨平 15 | * @param menuList 16 | * @param arr 17 | * @returns 18 | */ 19 | export const flatArrayObject = (list: any[], keys: string[], arr: T[] = []): T[] => { 20 | list.forEach(item => { 21 | keys.forEach(key => { 22 | if (Array.isArray(item[key])) { 23 | flatArrayObject(item[key], keys, arr); 24 | } else { 25 | arr.push({ ...item }); 26 | } 27 | }); 28 | }); 29 | return arr; 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | export function is(val: unknown, type: string) { 4 | return toString.call(val) === `[object ${type}]`; 5 | } 6 | 7 | export function isDef(val?: T): val is T { 8 | return typeof val !== 'undefined'; 9 | } 10 | 11 | export function isUnDef(val?: T): val is T { 12 | return !isDef(val); 13 | } 14 | 15 | export function isObject(val: any): val is Record { 16 | return val !== null && is(val, 'Object'); 17 | } 18 | 19 | export function isEmpty(val: T): val is T { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0; 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0; 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function isDate(val: unknown): val is Date { 36 | return is(val, 'Date'); 37 | } 38 | 39 | export function isNull(val: unknown): val is null { 40 | return val === null; 41 | } 42 | 43 | export function isNullAndUnDef(val: unknown): val is null | undefined { 44 | return isUnDef(val) && isNull(val); 45 | } 46 | 47 | export function isNullOrUnDef(val: unknown): val is null | undefined { 48 | return isUnDef(val) || isNull(val); 49 | } 50 | 51 | export function isNumber(val: unknown): val is number { 52 | return is(val, 'Number'); 53 | } 54 | 55 | export function isPromise(val: unknown): val is Promise { 56 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); 57 | } 58 | 59 | export function isString(val: unknown): val is string { 60 | return is(val, 'String'); 61 | } 62 | 63 | export function isFunction(val: unknown): val is Function { 64 | return typeof val === 'function'; 65 | } 66 | 67 | export function isBoolean(val: unknown): val is boolean { 68 | return is(val, 'Boolean'); 69 | } 70 | 71 | export function isRegExp(val: unknown): val is RegExp { 72 | return is(val, 'RegExp'); 73 | } 74 | 75 | export function isArray(val: any): val is Array { 76 | return val && Array.isArray(val); 77 | } 78 | 79 | export function isWindow(val: any): val is Window { 80 | return typeof window !== 'undefined' && is(val, 'Window'); 81 | } 82 | 83 | export function isElement(val: unknown): val is Element { 84 | return isObject(val) && !!val.tagName; 85 | } 86 | 87 | export function isMap(val: unknown): val is Map { 88 | return is(val, 'Map'); 89 | } 90 | 91 | export const isServer = typeof window === 'undefined'; 92 | 93 | export const isClient = !isServer; 94 | 95 | export function isUrl(path: string): boolean { 96 | const reg = 97 | /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 98 | return reg.test(path); 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { message as $message } from 'antd'; 2 | import axios, { AxiosRequestConfig } from 'axios'; 3 | 4 | import { ACCESS_TOKEN_KEY } from '@/enums/cacheEnum'; 5 | import { userStore } from '@/stores/user'; 6 | import { Storage } from '@/utils/Storage'; 7 | // import {ExclamationCircleOutlined} from '@ant-design/icons' 8 | 9 | export interface RequestOptions { 10 | /** 当前接口权限, 不需要鉴权的接口请忽略, 格式:sys:user:add */ 11 | permCode?: string; 12 | /** 是否直接获取data,而忽略message等 */ 13 | isGetDataDirectly?: boolean; 14 | /** 请求成功是提示信息 */ 15 | successMsg?: string; 16 | /** 请求失败是提示信息 */ 17 | errorMsg?: string; 18 | /** 是否mock数据请求 */ 19 | isMock?: boolean; 20 | } 21 | 22 | const UNKNOWN_ERROR = '未知错误,请重试'; 23 | 24 | /** 真实请求的路径前缀 */ 25 | const baseApiUrl = import.meta.env.VITE_BASE_API; 26 | /** mock请求路径前缀 */ 27 | const baseMockUrl = import.meta.env.VITE_MOCK_API; 28 | 29 | const service = axios.create({ 30 | // baseURL: baseApiUrl, 31 | timeout: 6000 32 | }); 33 | 34 | service.interceptors.request.use( 35 | config => { 36 | const token = Storage.get(ACCESS_TOKEN_KEY); 37 | if (token && config.headers) { 38 | // 请求头token信息,请根据实际情况进行修改 39 | config.headers['Authorization'] = token; 40 | } 41 | return config; 42 | }, 43 | error => { 44 | Promise.reject(error); 45 | } 46 | ); 47 | 48 | service.interceptors.response.use( 49 | response => { 50 | const res = response.data; 51 | 52 | // if the custom code is not 200, it is judged as an error. 53 | if (res.code !== 200) { 54 | $message.error(res.message || UNKNOWN_ERROR); 55 | 56 | // Illegal token 57 | if (res.code === 11001 || res.code === 11002) { 58 | window.localStorage.clear(); 59 | window.location.reload(); 60 | // to re-login 61 | // Modal.confirm({ 62 | // title: '警告', 63 | // content: res.message || '账号异常,您可以取消停留在该页上,或重新登录', 64 | // okText: '重新登录', 65 | // cancelText: '取消', 66 | // onOk: () => { 67 | // localStorage.clear(); 68 | // window.location.reload(); 69 | // } 70 | // }); 71 | } 72 | 73 | // throw other 74 | const error = new Error(res.message || UNKNOWN_ERROR) as Error & { code: any }; 75 | error.code = res.code; 76 | return Promise.reject(error); 77 | } else { 78 | return res; 79 | } 80 | }, 81 | error => { 82 | // 处理 422 或者 500 的错误异常提示 83 | const errMsg = error?.response?.data?.message ?? UNKNOWN_ERROR; 84 | $message.error(errMsg); 85 | error.message = errMsg; 86 | return Promise.reject(error); 87 | } 88 | ); 89 | 90 | export type Response = { 91 | code: number; 92 | message: string; 93 | data: T; 94 | }; 95 | 96 | export type BaseResponse = Promise>; 97 | 98 | /** 99 | * 100 | * @param method - request methods 101 | * @param url - request url 102 | * @param data - request data or params 103 | */ 104 | export const request = async (config: AxiosRequestConfig, options: RequestOptions = {}): Promise => { 105 | try { 106 | const { successMsg, errorMsg, permCode, isMock, isGetDataDirectly = true } = options; 107 | // 如果当前是需要鉴权的接口 并且没有权限的话 则终止请求发起 108 | if (permCode && !userStore.perms.includes(permCode)) { 109 | return $message.error('你没有访问该接口的权限,请联系管理员!'); 110 | } 111 | const fullUrl = `${(isMock ? baseMockUrl : baseApiUrl) + config.url}`; 112 | config.url = fullUrl.replace(/(?(initialState: T): [T, (state: Partial) => void] { 4 | const [state, setState] = useState(initialState); 5 | const setMergedState = (newState: Partial) => setState(prevState => Object.assign({}, prevState, newState)); 6 | return [state, setMergedState]; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @returns {Boolean} 4 | */ 5 | export function isExternal(path: string) { 6 | return /^(https?:|mailto:|tel:)/.test(path); 7 | } 8 | -------------------------------------------------------------------------------- /src/views/dashboard/index.less: -------------------------------------------------------------------------------- 1 | .overview { 2 | .ant-card-body { 3 | padding: 20px 24px 8px !important; 4 | } 5 | > * { 6 | box-sizing: border-box; 7 | } 8 | &-header { 9 | position: relative; 10 | width: 100%; 11 | overflow: hidden; 12 | &-meta { 13 | height: 22px; 14 | font-size: 14px; 15 | line-height: 22px; 16 | } 17 | &-count { 18 | height: 38px; 19 | margin-top: 4px; 20 | margin-bottom: 0; 21 | overflow: hidden; 22 | font-size: 30px; 23 | line-height: 38px; 24 | white-space: nowrap; 25 | text-overflow: ellipsis; 26 | word-break: break-all; 27 | } 28 | &-action { 29 | position: absolute; 30 | top: 4px; 31 | right: 0; 32 | line-height: 1; 33 | cursor: pointer; 34 | } 35 | } 36 | 37 | &-body { 38 | height: 46px; 39 | margin-bottom: 12px; 40 | position: relative; 41 | } 42 | 43 | &-footer { 44 | margin-top: 8px; 45 | padding-top: 9px; 46 | border-top: 1px solid #292a2d; 47 | } 48 | } 49 | 50 | .trend { 51 | position: absolute; 52 | bottom: 0; 53 | left: 0; 54 | width: 100%; 55 | &-item { 56 | display: inline-block; 57 | font-size: 14px; 58 | line-height: 22px; 59 | &:first-child { 60 | margin-right: 16px; 61 | } 62 | svg { 63 | vertical-align: middle; 64 | } 65 | > * { 66 | margin-right: 8px; 67 | &:nth-child(2) { 68 | color: rgba(0, 0, 0, 0.85); 69 | } 70 | } 71 | } 72 | } 73 | 74 | .field { 75 | font-size: 14px; 76 | line-height: 22px; 77 | // &-label { 78 | // } 79 | &-number { 80 | margin-left: 8px; 81 | color: rgba(0, 0, 0, 0.85); 82 | } 83 | } 84 | 85 | .salePercent { 86 | .ant-list-item { 87 | padding: 8px 12px !important; 88 | > * { 89 | margin: 0 4px; 90 | } 91 | } 92 | } 93 | 94 | .customTooltip { 95 | transition: visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1) 0s, left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, 96 | top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s; 97 | background-color: rgba(256, 2560, 256, 0.9); 98 | box-shadow: rgb(174, 174, 174) 0px 0px 10px; 99 | border-radius: 3px; 100 | color: rgb(87, 87, 87); 101 | font-size: 12px; 102 | line-height: 20px; 103 | padding: 10px 10px 6px; 104 | &-titile { 105 | margin-bottom: 4px; 106 | } 107 | &-content { 108 | margin: 0px; 109 | list-style-type: none; 110 | padding: 0px; 111 | > li { 112 | margin-bottom: 4px; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/views/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import { FC, useEffect, useState } from 'react'; 4 | 5 | import Overview from './overview'; 6 | import SalePercent from './salePercent'; 7 | import TimeLine from './timeLine'; 8 | 9 | const DashBoardPage: FC = () => { 10 | const [loading, setLoading] = useState(true); 11 | 12 | // mock timer to mimic dashboard data loading 13 | useEffect(() => { 14 | const timer = setTimeout(() => { 15 | setLoading(undefined as any); 16 | }, 2000); 17 | return () => { 18 | clearTimeout(timer); 19 | }; 20 | }, []); 21 | return ( 22 |
23 | 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default DashBoardPage; 31 | -------------------------------------------------------------------------------- /src/views/dashboard/timeLine.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Card } from 'antd'; 2 | import moment from 'moment'; 3 | import { FC } from 'react'; 4 | import { Brush, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; 5 | 6 | import { LocaleFormatter } from '@/locales'; 7 | 8 | const data = new Array(20).fill(null).map((_, index) => ({ 9 | name: moment() 10 | .add(index * 30, 'minute') 11 | .format('HH:mm'), 12 | traffic: Math.floor(Math.random() * 120 + 1), 13 | payments: Math.floor(Math.random() * 120 + 1) 14 | })); 15 | 16 | const CustomTooltip: FC = ({ active, payload, label }) => { 17 | if (active) { 18 | const { value: value1, stroke: stroke1 } = payload[0]; 19 | const { value: value2, stroke: stroke2 } = payload[1]; 20 | return ( 21 |
22 | {label} 23 |
    24 |
  • 25 | 26 | {value1} 27 |
  • 28 |
  • 29 | 30 | {value2} 31 |
  • 32 |
33 |
34 | ); 35 | } 36 | return null; 37 | }; 38 | 39 | const TimeLine: FC<{ loading: boolean }> = ({ loading }) => { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | } /> 48 | 49 | 50 | 51 | } 55 | /> 56 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default TimeLine; 63 | -------------------------------------------------------------------------------- /src/views/doucumentation/index.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd'; 2 | import { FC } from 'react'; 3 | 4 | import { LocaleFormatter } from '@/locales'; 5 | 6 | const { Title, Paragraph } = Typography; 7 | 8 | const div =
2333
; 9 | 10 | const DocumentationPage: FC = () => { 11 | return ( 12 |
13 | 14 | 15 | <LocaleFormatter id="app.documentation.introduction.title" /> 16 | 17 | 18 | 19 | 20 | 21 | <LocaleFormatter id="app.documentation.catalogue.title" /> 22 | 23 | 24 | 25 | 26 | 27 | 59 | 60 | 61 | <LocaleFormatter id="app.documentation.catalogue.list.layout" /> 62 | 63 | {div} 64 | 65 | <LocaleFormatter id="app.documentation.catalogue.list.routes" /> 66 | 67 | {div} 68 | 69 | <LocaleFormatter id="app.documentation.catalogue.list.request" /> 70 | 71 | {div} 72 | 73 | <LocaleFormatter id="app.documentation.catalogue.list.theme" /> 74 | 75 | {div} 76 | 77 | <LocaleFormatter id="app.documentation.catalogue.list.typescript" /> 78 | 79 | {div} 80 | 81 | <LocaleFormatter id="app.documentation.catalogue.list.international" /> 82 | 83 | {div} 84 | 85 |
86 | ); 87 | }; 88 | 89 | export default DocumentationPage; 90 | -------------------------------------------------------------------------------- /src/views/error/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import { useLocale } from '@/locales'; 5 | 6 | const NotFoundPage: React.FC<{}> = () => { 7 | const navigate = useNavigate(); 8 | const { formatMessage } = useLocale(); 9 | return ( 10 | navigate('/')}> 16 | {formatMessage({ id: 'gloabal.tips.backHome' })} 17 | 18 | } 19 | > 20 | ); 21 | }; 22 | 23 | export default NotFoundPage; 24 | -------------------------------------------------------------------------------- /src/views/login/index.less: -------------------------------------------------------------------------------- 1 | .login-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | background: url(../../assets/login-bg.svg); 6 | &-form { 7 | width: 300px; 8 | h2 { 9 | text-align: center; 10 | } 11 | &_button { 12 | width: 100%; 13 | } 14 | &_capatcha { 15 | position: absolute; 16 | top: 0; 17 | right: 0; 18 | height: 100%; 19 | width: 80px; 20 | cursor: pointer; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/login/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | import { LockOutlined, SafetyOutlined, UserOutlined } from '@ant-design/icons'; 4 | import { Button, Form, Input } from 'antd'; 5 | import type { Location } from 'history'; 6 | import { FC, useEffect, useState } from 'react'; 7 | import { useLocation, useNavigate } from 'react-router-dom'; 8 | 9 | import { getImageCaptcha } from '@/api/login'; 10 | import { userStore } from '@/stores/user'; 11 | 12 | const initialValues: API.LoginParams = { 13 | username: 'rootadmin', 14 | password: '123456', 15 | captchaId: '', 16 | verifyCode: '' 17 | // remember: true 18 | }; 19 | 20 | const LoginForm: FC = () => { 21 | const navigate = useNavigate(); 22 | const location = useLocation() as Location<{ from: string }>; 23 | const [capatcha, setCapatcha] = useState({ 24 | id: '', 25 | img: '' 26 | }); 27 | 28 | const getCapatcha = async (e?: React.MouseEvent) => { 29 | e?.preventDefault(); 30 | const data = await getImageCaptcha(); 31 | setCapatcha(data); 32 | }; 33 | 34 | useEffect(() => { 35 | getCapatcha(); 36 | }, []); 37 | 38 | /** 39 | * 表单验证成功回调 40 | * @param form 41 | */ 42 | const onFinished = async (form: API.LoginParams) => { 43 | console.log('LoginParams', form); 44 | form.captchaId = capatcha.id; 45 | const res = await userStore.login(form); 46 | console.log('登录结果:', res); 47 | if (Object.is(res, false)) { 48 | return getCapatcha(); 49 | } 50 | const search = new URLSearchParams(location.search); 51 | console.log('location.search', location.search); 52 | 53 | const from = location.state?.from || search.get('from') || { pathname: '/dashboard' }; 54 | navigate(from, { replace: true }); 55 | }; 56 | 57 | return ( 58 |
59 | onFinish={onFinished} className="login-page-form" initialValues={initialValues}> 60 |

REACT ANTD ADMIN

61 | 62 | } size="large" /> 63 | 64 | 65 | } size="large" /> 66 | 67 | 68 | } 71 | size="large" 72 | maxLength={4} 73 | suffix={验证码} 74 | /> 75 | 76 | 77 | 80 | 81 | 82 |
83 | ); 84 | }; 85 | 86 | export default LoginForm; 87 | -------------------------------------------------------------------------------- /src/views/system/monitor/login-log/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionType, ProColumns } from '@ant-design/pro-table'; 2 | import ProTable from '@ant-design/pro-table'; 3 | import { useRef } from 'react'; 4 | 5 | import { getLoginLogList } from '@/api/system/log'; 6 | 7 | type TableListItem = API.LoginLogListItemResult; 8 | 9 | export default function SystemMonitorLoginLog() { 10 | const actionRef = useRef(); 11 | 12 | const columns: ProColumns[] = [ 13 | { 14 | title: '用户名', 15 | dataIndex: 'username', 16 | width: 280, 17 | align: 'center' 18 | }, 19 | { 20 | title: '登录IP', 21 | dataIndex: 'ip', 22 | width: 150, 23 | align: 'center' 24 | }, 25 | { 26 | title: '登录时间', 27 | dataIndex: 'time', 28 | align: 'center' 29 | }, 30 | { 31 | title: '操作系统', 32 | dataIndex: 'os', 33 | align: 'center' 34 | }, 35 | { 36 | title: '浏览器', 37 | dataIndex: 'browser', 38 | align: 'center' 39 | } 40 | ]; 41 | 42 | return ( 43 | 44 | actionRef={actionRef} 45 | columns={columns} 46 | request={async params => { 47 | // 表单搜索项会从 params 传入,传递给后端接口。 48 | const { list, pagination } = await getLoginLogList({ limit: params.pageSize, page: params.current }); 49 | return { 50 | data: list, 51 | success: true, 52 | total: pagination?.total 53 | }; 54 | }} 55 | sticky={true} 56 | rowKey="id" 57 | scroll={{ x: 1300, y: 500 }} 58 | pagination={{ 59 | showQuickJumper: true 60 | }} 61 | search={false} 62 | dateFormatter="string" 63 | /> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/views/system/monitor/online/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionType, ProColumns } from '@ant-design/pro-table'; 2 | import ProTable from '@ant-design/pro-table'; 3 | import { Tag } from 'antd'; 4 | import { useRef } from 'react'; 5 | 6 | import { getOnlineList } from '@/api/system/online'; 7 | 8 | type TableListItem = API.OnlineUserListItem; 9 | 10 | export default function SystemMonitorOnline() { 11 | const actionRef = useRef(); 12 | 13 | const columns: ProColumns[] = [ 14 | { 15 | title: '#', 16 | dataIndex: 'id', 17 | width: 80, 18 | align: 'center' 19 | }, 20 | { 21 | title: '用户名', 22 | dataIndex: 'username', 23 | width: 280, 24 | align: 'center', 25 | render: (_, { username, isCurrent }) => ( 26 | <> 27 | {username} 28 | {isCurrent ? 当前 : null} 29 | 30 | ) 31 | }, 32 | { 33 | title: '登录IP', 34 | dataIndex: 'ip', 35 | align: 'center' 36 | }, 37 | { 38 | title: '登录时间', 39 | dataIndex: 'time', 40 | align: 'center' 41 | }, 42 | { 43 | title: '操作系统', 44 | dataIndex: 'os', 45 | align: 'center' 46 | }, 47 | { 48 | title: '浏览器', 49 | dataIndex: 'browser', 50 | align: 'center' 51 | } 52 | ]; 53 | 54 | return ( 55 | 56 | actionRef={actionRef} 57 | columns={columns} 58 | request={async _ => { 59 | // 表单搜索项会从 params 传入,传递给后端接口。 60 | const list = await getOnlineList(); 61 | return { 62 | data: list, 63 | success: true 64 | }; 65 | }} 66 | sticky={true} 67 | rowKey="id" 68 | scroll={{ x: 1300, y: 500 }} 69 | pagination={{ 70 | showQuickJumper: true 71 | }} 72 | search={false} 73 | dateFormatter="string" 74 | /> 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/views/system/monitor/req-log/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionType, ProColumns } from '@ant-design/pro-table'; 2 | import ProTable from '@ant-design/pro-table'; 3 | import { Tag } from 'antd'; 4 | import { useRef } from 'react'; 5 | 6 | import { getReqLogList } from '@/api/system/log'; 7 | 8 | type TableListItem = API.ReqLogListItemResult; 9 | 10 | const getStatusType = (status: number) => { 11 | if (status >= 200 && status < 300) { 12 | return 'success'; 13 | } else if (status >= 300 && status < 400) { 14 | return 'default'; 15 | } else if (status >= 400 && status < 500) { 16 | return 'warning'; 17 | } else if (status >= 500) { 18 | return 'error'; 19 | } else { 20 | return 'default'; 21 | } 22 | }; 23 | 24 | const getConsumeTimeType = (time: number) => { 25 | if (time <= 20) { 26 | return 'success'; 27 | } else if (time <= 40) { 28 | return 'warning'; 29 | } else { 30 | return 'error'; 31 | } 32 | }; 33 | 34 | export default function SystemMonitorReqLog() { 35 | const actionRef = useRef(); 36 | 37 | const columns: ProColumns[] = [ 38 | { 39 | title: '请求IP', 40 | dataIndex: 'ip', 41 | width: 150, 42 | align: 'center' 43 | }, 44 | { 45 | title: '操作人ID', 46 | dataIndex: 'userId', 47 | align: 'center', 48 | width: 100 49 | }, 50 | { 51 | title: '请求方式', 52 | dataIndex: 'method', 53 | align: 'center', 54 | render: (_, { method }) => {method} 55 | }, 56 | { 57 | title: '请求参数', 58 | dataIndex: 'params', 59 | align: 'center', 60 | ellipsis: true, 61 | width: 150 62 | }, 63 | { 64 | title: '请求地址', 65 | dataIndex: 'action', 66 | align: 'center' 67 | }, 68 | { 69 | title: '响应状态', 70 | dataIndex: 'status', 71 | align: 'center', 72 | width: 120, 73 | render: (_, { status }) => {status} 74 | }, 75 | { 76 | title: '耗时', 77 | dataIndex: 'consumeTime', 78 | align: 'center', 79 | width: 120, 80 | render: (_, { consumeTime }) => {consumeTime + 'ms'} 81 | }, 82 | { 83 | title: '操作时间', 84 | dataIndex: 'createTime', 85 | align: 'center', 86 | width: 220 87 | } 88 | ]; 89 | 90 | return ( 91 | 92 | actionRef={actionRef} 93 | columns={columns} 94 | request={async params => { 95 | // 表单搜索项会从 params 传入,传递给后端接口。 96 | const { list, pagination } = await getReqLogList({ limit: params.pageSize, page: params.current }); 97 | return { 98 | data: list, 99 | success: true, 100 | total: pagination?.total 101 | }; 102 | }} 103 | sticky={true} 104 | rowKey="id" 105 | scroll={{ x: 1300, y: 500 }} 106 | pagination={{ 107 | showQuickJumper: true 108 | }} 109 | search={false} 110 | dateFormatter="string" 111 | /> 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/views/system/monitor/serve/index.tsx: -------------------------------------------------------------------------------- 1 | export const Serve = () =>
服务监控
; 2 | 3 | export default Serve; 4 | -------------------------------------------------------------------------------- /src/views/system/permission/role/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionType, ProColumns } from '@ant-design/pro-table'; 2 | import ProTable from '@ant-design/pro-table'; 3 | import { Button, Popconfirm } from 'antd'; 4 | import { useRef, useState } from 'react'; 5 | 6 | import { getRoleList } from '@/api/system/role'; 7 | import { deleteRole } from '@/api/system/role'; 8 | 9 | import { OperateFormModal } from './components/OperateFormModal'; 10 | 11 | type TableListItem = API.RoleListResultItem; 12 | 13 | export const SystemPermissionRole = () => { 14 | const actionRef = useRef(); 15 | 16 | const [modalVisible, setModalVisible] = useState(false); 17 | const [currentRow, setCurrentRow] = useState(undefined); 18 | 19 | const refreshTable = () => { 20 | actionRef?.current?.reload(); 21 | }; 22 | 23 | const handleModalOnCancel = () => { 24 | setCurrentRow(undefined); 25 | setModalVisible(false); 26 | }; 27 | 28 | const handleModalOnSuccess = () => { 29 | handleModalOnCancel(); 30 | refreshTable(); 31 | }; 32 | 33 | /** 34 | * @description 表格删除行 35 | */ 36 | const delRowConfirm = (record: API.RoleListResultItem) => { 37 | deleteRole({ roleIds: [record.id] }).finally(actionRef?.current?.reload); 38 | }; 39 | 40 | const columns: ProColumns[] = [ 41 | { 42 | title: '#', 43 | width: 60, 44 | dataIndex: 'id' 45 | }, 46 | { 47 | title: '名称', 48 | dataIndex: 'name', 49 | width: 200, 50 | align: 'center' 51 | }, 52 | { 53 | title: '标识', 54 | dataIndex: 'label', 55 | width: 200, 56 | align: 'center' 57 | }, 58 | { 59 | title: '备注', 60 | dataIndex: 'remark', 61 | align: 'center', 62 | width: 180, 63 | ellipsis: true 64 | }, 65 | { 66 | title: '创建时间', 67 | dataIndex: 'createdAt', 68 | width: 180, 69 | align: 'center' 70 | }, 71 | { 72 | title: '更新时间', 73 | dataIndex: 'updatedAt', 74 | width: 180, 75 | align: 'center' 76 | }, 77 | { 78 | title: '操作', 79 | width: 120, 80 | key: 'option', 81 | valueType: 'option', 82 | fixed: 'right', 83 | align: 'center', 84 | render: (_, record) => [ 85 | , 95 | delRowConfirm(record)}> 96 | 删除 97 | 98 | ] 99 | } 100 | ]; 101 | 102 | return ( 103 | <> 104 | 105 | actionRef={actionRef} 106 | columns={columns} 107 | rowKey="id" 108 | scroll={{ x: 1300 }} 109 | pagination={{ 110 | showQuickJumper: true 111 | }} 112 | toolbar={{ 113 | title: '角色管理' 114 | }} 115 | search={false} 116 | dateFormatter="string" 117 | headerTitle={ 118 | 127 | } 128 | request={async params => { 129 | // 表单搜索项会从 params 传入,传递给后端接口。 130 | const list = await getRoleList({ limit: params.pageSize, page: params.current }); 131 | return { 132 | data: list, 133 | success: true 134 | }; 135 | }} 136 | /> 137 | 143 | 144 | ); 145 | }; 146 | 147 | export default SystemPermissionRole; 148 | -------------------------------------------------------------------------------- /src/views/system/permission/user/columns.tsx: -------------------------------------------------------------------------------- 1 | import type { ProColumns } from '@ant-design/pro-table'; 2 | import { Avatar, Space, Tag } from 'antd'; 3 | 4 | export type TableListItem = API.UserListPageResultItem; 5 | export type ColumnItem = ProColumns; 6 | 7 | export const baseColumns: ColumnItem[] = [ 8 | { 9 | title: '头像', 10 | width: 80, 11 | dataIndex: 'headImg', 12 | render: (_, record) => 13 | }, 14 | { 15 | title: '姓名', 16 | width: 120, 17 | dataIndex: 'name', 18 | align: 'center' 19 | }, 20 | { 21 | title: '用户名', 22 | width: 120, 23 | align: 'center', 24 | dataIndex: 'username' 25 | }, 26 | { 27 | title: '所在部门', 28 | dataIndex: 'departmentName', 29 | align: 'center', 30 | width: 180 31 | }, 32 | { 33 | title: '所属角色', 34 | dataIndex: 'roleNames', 35 | align: 'center', 36 | width: 220, 37 | render: (_, record) => ( 38 | 39 | {record.roleNames.map(item => ( 40 | 41 | {item} 42 | 43 | ))} 44 | 45 | ) 46 | }, 47 | { 48 | title: '呢称', 49 | width: 120, 50 | align: 'center', 51 | dataIndex: 'nickName' 52 | }, 53 | { 54 | title: '邮箱', 55 | width: 120, 56 | align: 'center', 57 | dataIndex: 'email' 58 | }, 59 | { 60 | title: '手机', 61 | width: 120, 62 | align: 'center', 63 | dataIndex: 'phone' 64 | }, 65 | { 66 | title: '备注', 67 | width: 120, 68 | align: 'center', 69 | dataIndex: 'remark' 70 | }, 71 | { 72 | title: '状态', 73 | dataIndex: 'status', 74 | width: 100, 75 | render: (_, record) => { 76 | const isEnable = record.status === 1; 77 | return {isEnable ? '启用' : '禁用'}; 78 | } 79 | } 80 | ]; 81 | -------------------------------------------------------------------------------- /src/views/system/permission/user/components/OperateDeptFormModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalForm, ProFormDigit, ProFormText } from '@ant-design/pro-form'; 2 | import { Form, FormInstance, message, TreeSelect } from 'antd'; 3 | import { DataNode } from 'antd/lib/tree'; 4 | import React, { useEffect, useRef } from 'react'; 5 | 6 | import { createDept, getDeptInfo, updateDept } from '@/api/system/dept'; 7 | 8 | type FormDetail = API.CreateDeptParams & API.UpdateDeptParams; 9 | 10 | interface OperateUserFormModalProps { 11 | visible: boolean; 12 | deptTree: DataNode[]; 13 | currentDept?: DataNode | undefined; 14 | onCancel: () => void; 15 | onSuccess: () => void; 16 | } 17 | 18 | export const OperateDeptFormModal: React.FC = props => { 19 | const { currentDept, deptTree, onCancel, onSuccess, visible } = props; 20 | const formRef = useRef>(); 21 | 22 | useEffect(() => { 23 | const fetchDeptInfo = async () => { 24 | if (currentDept?.key && Number.isInteger(currentDept.key)) { 25 | const { department } = await getDeptInfo({ departmentId: currentDept.key }); 26 | formRef.current?.setFieldsValue({ 27 | parentId: department.parentId ?? -1, 28 | name: department.name, 29 | orderNum: department.orderNum 30 | }); 31 | } 32 | }; 33 | if (visible) { 34 | fetchDeptInfo(); 35 | } 36 | }, [currentDept, visible]); 37 | 38 | return ( 39 | 40 | formRef={formRef} 41 | title={`${currentDept?.key ? '编辑' : '新增'}部门`} 42 | visible={visible} 43 | modalProps={{ onCancel }} 44 | onFinish={async values => { 45 | if (currentDept?.key && Number.isInteger(currentDept.key)) { 46 | const params = { 47 | ...values, 48 | id: currentDept?.key 49 | }; 50 | const data = await updateDept(params); 51 | console.log(data, values); 52 | message.success('修改部门成功'); 53 | } else { 54 | const data = await createDept(values); 55 | console.log(data, values); 56 | message.success('新增部门成功'); 57 | } 58 | formRef.current?.resetFields(); 59 | onSuccess(); 60 | return true; 61 | }} 62 | > 63 | 69 | 70 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default OperateDeptFormModal; 84 | -------------------------------------------------------------------------------- /src/views/system/permission/user/components/deptTreePanel.module.less: -------------------------------------------------------------------------------- 1 | .dept-tree-panel-container { 2 | .header { 3 | display: flex; 4 | justify-content: space-between; 5 | .title { 6 | font-size: 14px; 7 | font-weight: 500; 8 | } 9 | .operate-wrapper { 10 | display: flex; 11 | .tool-item { 12 | padding: 2px 4px; 13 | cursor: pointer; 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/views/system/schedule/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import { useLocale } from '@/locales'; 5 | 6 | const NotFoundPage: React.FC<{}> = () => { 7 | const navigate = useNavigate(); 8 | const { formatMessage } = useLocale(); 9 | return ( 10 | navigate('/')}> 15 | 任务调度页面 16 | 17 | } 18 | > 19 | ); 20 | }; 21 | 22 | export default NotFoundPage; 23 | -------------------------------------------------------------------------------- /src/views/system/schedule/log/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionType, ProColumns } from '@ant-design/pro-table'; 2 | import ProTable from '@ant-design/pro-table'; 3 | import { Tag } from 'antd'; 4 | import { useRef } from 'react'; 5 | 6 | import { getTaskLogList } from '@/api/system/log'; 7 | 8 | type TableListItem = API.TaskLogListItemResult; 9 | 10 | const getStatusType = (status: number) => { 11 | switch (status) { 12 | case 0: 13 | return 'danger'; 14 | case 1: 15 | return 'success'; 16 | } 17 | }; 18 | 19 | const getStatusTip = (status: number) => { 20 | switch (status) { 21 | case 0: 22 | return '失败'; 23 | case 1: 24 | return '成功'; 25 | } 26 | }; 27 | 28 | const getConsumeTimeType = (time: number) => { 29 | if (time <= 20) { 30 | return 'success'; 31 | } else if (time <= 40) { 32 | return 'warning'; 33 | } else { 34 | return 'error'; 35 | } 36 | }; 37 | 38 | export const SystemScheduleTaskLog = () => { 39 | const actionRef = useRef(); 40 | 41 | const columns: ProColumns[] = [ 42 | { 43 | title: '#', 44 | dataIndex: 'id', 45 | width: 80, 46 | align: 'center' 47 | }, 48 | { 49 | title: '任务编号', 50 | dataIndex: 'taskId', 51 | align: 'center', 52 | width: 100 53 | }, 54 | { 55 | title: '任务名称', 56 | dataIndex: 'name', 57 | align: 'center', 58 | ellipsis: true, 59 | width: 200 60 | }, 61 | { 62 | title: '异常信息', 63 | dataIndex: 'detail', 64 | align: 'center', 65 | width: 150 66 | }, 67 | { 68 | title: '耗时', 69 | dataIndex: 'consumeTime', 70 | align: 'center', 71 | width: 120, 72 | render: (_, { consumeTime }) => {consumeTime + 'ms'} 73 | }, 74 | { 75 | title: '状态', 76 | dataIndex: 'status', 77 | align: 'center', 78 | width: 100, 79 | render: (_, { status }) => {getStatusTip(status)} 80 | }, 81 | { 82 | title: '执行时间', 83 | dataIndex: 'createdAt', 84 | align: 'center', 85 | width: 220 86 | } 87 | ]; 88 | 89 | return ( 90 | 91 | actionRef={actionRef} 92 | columns={columns} 93 | request={async params => { 94 | // 表单搜索项会从 params 传入,传递给后端接口。 95 | const { list, pagination } = await getTaskLogList({ limit: params.pageSize, page: params.current }); 96 | return { 97 | data: list, 98 | success: true, 99 | total: pagination?.total 100 | }; 101 | }} 102 | sticky={true} 103 | rowKey="id" 104 | scroll={{ x: 1300, y: 500 }} 105 | pagination={{ 106 | showQuickJumper: true 107 | }} 108 | search={false} 109 | dateFormatter="string" 110 | /> 111 | ); 112 | }; 113 | 114 | export default SystemScheduleTaskLog; 115 | -------------------------------------------------------------------------------- /src/views/system/schedule/task/index.tsx: -------------------------------------------------------------------------------- 1 | export const Task = () =>
任务调度
; 2 | 3 | export default Task; 4 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ['stylelint-order'], 4 | customSyntax: 'postcss-html', 5 | extends: ['stylelint-config-standard', 'stylelint-config-prettier'], 6 | rules: { 7 | 'selector-class-pattern': null, 8 | 'selector-pseudo-class-no-unknown': [ 9 | true, 10 | { 11 | ignorePseudoClasses: ['global'] 12 | } 13 | ], 14 | 'selector-pseudo-element-no-unknown': [ 15 | true, 16 | { 17 | ignorePseudoElements: ['v-deep'] 18 | } 19 | ], 20 | 'at-rule-no-unknown': [ 21 | true, 22 | { 23 | ignoreAtRules: [ 24 | 'tailwind', 25 | 'apply', 26 | 'variants', 27 | 'responsive', 28 | 'screen', 29 | 'function', 30 | 'if', 31 | 'each', 32 | 'include', 33 | 'mixin' 34 | ] 35 | } 36 | ], 37 | 'no-empty-source': null, 38 | 'named-grid-areas-no-invalid': null, 39 | 'unicode-bom': 'never', 40 | 'no-descending-specificity': null, 41 | 'font-family-no-missing-generic-family-keyword': null, 42 | 'declaration-colon-space-after': 'always-single-line', 43 | 'declaration-colon-space-before': 'never', 44 | // 'declaration-block-trailing-semicolon': 'always', 45 | 'rule-empty-line-before': [ 46 | 'always', 47 | { 48 | ignore: ['after-comment', 'first-nested'] 49 | } 50 | ], 51 | 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }], 52 | 'order/order': [ 53 | [ 54 | 'dollar-variables', 55 | 'custom-properties', 56 | 'at-rules', 57 | 'declarations', 58 | { 59 | type: 'at-rule', 60 | name: 'supports' 61 | }, 62 | { 63 | type: 'at-rule', 64 | name: 'media' 65 | }, 66 | 'rules' 67 | ], 68 | { severity: 'warning' } 69 | ] 70 | }, 71 | ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'], 72 | overrides: [ 73 | { 74 | files: ['*.vue', '**/*.vue', '*.html', '**/*.html'], 75 | extends: ['stylelint-config-recommended', 'stylelint-config-html'], 76 | rules: { 77 | 'keyframes-name-pattern': null, 78 | 'selector-pseudo-class-no-unknown': [ 79 | true, 80 | { 81 | ignorePseudoClasses: ['deep', 'global'] 82 | } 83 | ], 84 | 'selector-pseudo-element-no-unknown': [ 85 | true, 86 | { 87 | ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'] 88 | } 89 | ] 90 | } 91 | } 92 | ] 93 | }; 94 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noLib": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strictFunctionTypes": false, 11 | "jsx": "preserve", 12 | "baseUrl": ".", 13 | "allowJs": true, 14 | "sourceMap": true, 15 | "esModuleInterop": true, 16 | "resolveJsonModule": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "experimentalDecorators": true, 20 | "lib": [ 21 | "dom", 22 | "esnext" 23 | ], 24 | "noImplicitAny": false, 25 | "skipLibCheck": true, 26 | "removeComments": true, 27 | "typeRoots": [ 28 | "./node_modules/@types/", 29 | "./types", 30 | ], 31 | "paths": { 32 | "@/*": [ 33 | "src/*" 34 | ] 35 | }, 36 | }, 37 | "include": [ 38 | "src/**/*.ts", 39 | "src/**/*.d.ts", 40 | "src/**/*.tsx", 41 | "types/**/*.d.ts", 42 | "types/**/*.ts", 43 | "types/**/*.ts", 44 | "types/**/*.d.ts", 45 | "mock/**/*.ts", 46 | "vite.config.ts" 47 | ], 48 | "exclude": [ 49 | "node_modules", 50 | "dist", 51 | "**/*.js" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | /** 网站标题 */ 5 | readonly VITE_APP_TITLE: string; 6 | /** 网站部署的目录 */ 7 | readonly VITE_BASE_URL: string; 8 | /** API 接口路径 */ 9 | readonly VITE_BASE_API: string; 10 | /** socket 请求路径前缀 */ 11 | readonly VITE_BASE_SOCKET_PATH: string; 12 | /** socket 命名空间 */ 13 | readonly VITE_BASE_SOCKET_NSP: string; 14 | /** mock API 路径 */ 15 | readonly VITE_MOCK_API: string; 16 | // 更多环境变量... 17 | } 18 | 19 | interface ImportMeta { 20 | readonly env: ImportMetaEnv; 21 | } 22 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | const __APP_INFO__: { 3 | pkg: { 4 | name: string; 5 | version: string; 6 | dependencies: Recordable; 7 | devDependencies: Recordable; 8 | }; 9 | lastBuildTime: string; 10 | }; 11 | // declare interface Window { 12 | // // Global vue app instance 13 | // __APP__: App; 14 | // } 15 | 16 | export type Writable = { 17 | -readonly [P in keyof T]: T[P]; 18 | }; 19 | 20 | declare type Key = number | string; 21 | declare type Nullable = T | null; 22 | declare type NonNullable = T extends null | undefined ? never : T; 23 | declare type Recordable = Record; 24 | declare type ReadonlyRecordable = { 25 | readonly [key: string]: T; 26 | }; 27 | declare type Indexable = { 28 | [key: string]: T; 29 | }; 30 | declare type DeepPartial = { 31 | [P in keyof T]?: DeepPartial; 32 | }; 33 | 34 | declare type TimeoutHandle = ReturnType; 35 | declare type IntervalHandle = ReturnType; 36 | 37 | declare interface ChangeEvent extends Event { 38 | target: HTMLInputElement; 39 | } 40 | 41 | declare interface WheelEvent { 42 | path?: EventTarget[]; 43 | } 44 | 45 | declare function parseInt(s: string | number, radix?: number): number; 46 | 47 | declare function parseFloat(string: string | number): number; 48 | } 49 | 50 | export {}; 51 | -------------------------------------------------------------------------------- /types/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | declare module '*.svg' { 4 | import * as React from 'react'; 5 | export const ReactComponent: React.FunctionComponent & { title?: string }>; 6 | const src: string; 7 | export default src; 8 | } 9 | 10 | declare module '@emotion/core/jsx-runtime'; 11 | declare interface Window { 12 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: Function; 13 | } 14 | 15 | declare interface ObjectConstructor { 16 | keys(o: T): (keyof T)[]; 17 | } 18 | 19 | declare module '*.module.css' { 20 | const classes: { [key: string]: string }; 21 | export default classes; 22 | } 23 | 24 | declare module '*.module.scss' { 25 | const classes: { [key: string]: string }; 26 | export default classes; 27 | } 28 | 29 | declare module '*.module.sass' { 30 | const classes: { [key: string]: string }; 31 | export default classes; 32 | } 33 | 34 | declare module '*.module.less' { 35 | const classes: { [key: string]: string }; 36 | export default classes; 37 | } 38 | 39 | declare module '*.module.styl' { 40 | const classes: { [key: string]: string }; 41 | export default classes; 42 | } 43 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import legacy from '@vitejs/plugin-legacy'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | import { type ConfigEnv, type UserConfig, loadEnv } from 'vite'; 5 | import vitePluginImp from 'vite-plugin-imp'; 6 | import { viteMockServe } from 'vite-plugin-mock'; 7 | import svgrPlugin from 'vite-plugin-svgr'; 8 | import WindiCSS from 'vite-plugin-windicss'; 9 | 10 | const CWD = process.cwd(); 11 | 12 | // https://vitejs.dev/config/ 13 | export default ({ command, mode }: ConfigEnv): UserConfig => { 14 | // 环境变量 15 | const { VITE_BASE_URL, VITE_DROP_CONSOLE } = loadEnv(mode, CWD); 16 | 17 | const isBuild = command === 'build'; 18 | console.log('当前执行环境:', isBuild); 19 | return { 20 | base: VITE_BASE_URL, 21 | resolve: { 22 | alias: [ 23 | { 24 | find: '@', 25 | replacement: path.resolve(__dirname, 'src') 26 | }, 27 | { 28 | find: '~antd', 29 | replacement: path.resolve(__dirname, 'node_modules/antd') 30 | } 31 | ] 32 | }, 33 | server: { 34 | port: 8889, 35 | proxy: { 36 | '/api': { 37 | target: `http://175.24.200.3:7001`, 38 | // changeOrigin: true, 39 | rewrite: path => path.replace(/^\/api/, '') 40 | }, 41 | '/ws-api': { 42 | target: 'ws://175.24.200.3:7002', 43 | changeOrigin: true, //是否允许跨域 44 | ws: true 45 | } 46 | } 47 | }, 48 | css: { 49 | preprocessorOptions: { 50 | less: { 51 | javascriptEnabled: true, 52 | modifyVars: { '@primary-color': '#13c2c2' } 53 | } 54 | // .... 55 | } 56 | }, 57 | plugins: [ 58 | WindiCSS(), 59 | legacy({ 60 | targets: ['ie >= 11'], 61 | additionalLegacyPolyfills: ['regenerator-runtime/runtime'] 62 | }), 63 | react({ 64 | jsxImportSource: '@emotion/react', 65 | babel: { 66 | plugins: ['@emotion/babel-plugin'] 67 | } 68 | }), 69 | viteMockServe({ 70 | ignore: /^_/, 71 | mockPath: 'mock', 72 | localEnabled: !isBuild, 73 | prodEnabled: isBuild, 74 | logger: true, 75 | injectCode: ` 76 | import { setupProdMockServer } from '../mock/_createProductionServer'; 77 | 78 | setupProdMockServer(); 79 | ` 80 | }), 81 | vitePluginImp({ 82 | libList: [ 83 | // { 84 | // libName: 'antd', 85 | // style: name => `antd/es/${name}/style/index.css`, 86 | // }, 87 | { 88 | libName: 'lodash', 89 | libDirectory: '', 90 | camel2DashComponentName: false, 91 | style: () => { 92 | return false; 93 | } 94 | } 95 | ] 96 | }), 97 | svgrPlugin({ 98 | svgrOptions: { 99 | icon: true 100 | // ...svgr options (https://react-svgr.com/docs/options/) 101 | } 102 | }) 103 | ], 104 | build: { 105 | terserOptions: { 106 | compress: { 107 | keep_infinity: true, 108 | drop_console: !!VITE_DROP_CONSOLE 109 | } 110 | } 111 | } 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /windi.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-check - enable TS check for js file 2 | import { defineConfig } from 'vite-plugin-windicss'; 3 | 4 | export default defineConfig({ 5 | darkMode: 'class', // or 'media' 6 | theme: { 7 | extend: { 8 | screens: { 9 | sm: '640px', 10 | md: '768px', 11 | lg: '1024px', 12 | xl: '1280px', 13 | '2xl': '1536px' 14 | } 15 | } 16 | } 17 | }); 18 | --------------------------------------------------------------------------------