├── .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 |
61 | );
62 | return (
63 |
64 |
65 |

66 |

67 |
68 |
69 |
70 |
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 |
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