├── .editorconfig ├── .env ├── .env.production ├── .gitconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── rfc.code-snippets ├── rfce.code-snippets └── settings.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── commitlint.config.ts ├── docs ├── .vitepress │ └── config │ │ ├── index.ts │ │ ├── shared.ts │ │ └── zh.ts ├── index.md ├── package.json ├── public │ ├── ahooks-logo.svg │ ├── antd-logo.svg │ ├── guide │ │ ├── analyzer-stats.jpg │ │ ├── app-loading.png │ │ ├── app-loading2.png │ │ ├── i18n-translation.png │ │ ├── ky-vs-fetch.jpeg │ │ ├── lokalise.i18n-ally-plugin.png │ │ ├── lokalise.i18n-ally.png │ │ ├── preferences.png │ │ └── version-monitor.png │ ├── logo.png │ ├── logo.svg │ ├── shared-og.png │ ├── sponsor.png │ └── vite-logo.svg ├── tailwind.config.mjs └── zh │ ├── guide │ ├── advanced │ │ ├── access.md │ │ ├── loading.md │ │ ├── locale.md │ │ ├── monitoring-updates.md │ │ ├── theme.md │ │ └── upgrading.md │ ├── fundamentals │ │ ├── build.md │ │ ├── directory-structure.md │ │ ├── icon.md │ │ ├── mock.md │ │ ├── request.md │ │ ├── routing.md │ │ ├── settings.md │ │ └── style.md │ └── introduction │ │ ├── index.md │ │ ├── quick-start.md │ │ └── why.md │ └── sponsor │ └── index.md ├── eslint.config.js ├── fake ├── README.md ├── async-routes.fake.ts ├── auth.fake.ts ├── constants.ts ├── home.fake.ts ├── notification.fake.ts ├── personal-center.fake.ts ├── system.fake.ts ├── user.fake.ts └── utils.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── api │ ├── README.md │ ├── home.ts │ ├── notifications │ │ └── index.ts │ ├── system │ │ ├── index.ts │ │ ├── menu │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── role │ │ │ ├── index.ts │ │ │ └── types.ts │ └── user │ │ ├── index.ts │ │ └── types.ts ├── app.tsx ├── assets │ ├── README.md │ └── svg │ │ ├── banner.svg │ │ ├── hero.svg │ │ ├── logo.svg │ │ └── undraw-bug-fixing.svg ├── components │ ├── access-control │ │ └── index.ts │ ├── antd-app │ │ ├── constants.ts │ │ ├── index.tsx │ │ ├── setup-antd-theme.ts │ │ └── utils.ts │ ├── basic-button │ │ └── index.tsx │ ├── basic-content │ │ └── index.tsx │ ├── basic-form │ │ ├── form-items │ │ │ ├── form-avatar-item.tsx │ │ │ ├── form-tree-item.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── basic-table │ │ ├── constants.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── use-table-scroll │ │ │ │ └── index.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── fullscreen-button │ │ └── index.tsx │ ├── global-spin │ │ └── index.tsx │ ├── iframe │ │ └── index.tsx │ ├── index.ts │ ├── jss-theme-provider │ │ └── index.tsx │ ├── page-error │ │ └── index.tsx │ ├── scrollbar │ │ └── index.tsx │ └── tanstack-query │ │ └── index.tsx ├── constants │ ├── config.ts │ ├── index.ts │ ├── options.ts │ ├── regular-expressions.ts │ └── rules.ts ├── hooks │ ├── index.ts │ ├── use-access │ │ ├── constants.ts │ │ └── index.ts │ ├── use-css-var │ │ └── index.ts │ ├── use-current-route │ │ └── index.ts │ ├── use-device-type │ │ └── index.ts │ ├── use-language │ │ └── index.ts │ ├── use-layout-menu │ │ └── index.tsx │ ├── use-layout-style │ │ └── index.ts │ ├── use-preferences │ │ └── index.ts │ └── use-scroll-to-hash │ │ └── index.ts ├── icons │ ├── index.tsx │ └── svg │ │ ├── embedded.svg │ │ ├── external.svg │ │ ├── follow-system.svg │ │ ├── fullscreen-exit.svg │ │ ├── fullscreen.svg │ │ ├── layout-center.svg │ │ ├── layout-left.svg │ │ ├── layout-right.svg │ │ ├── mail-check.svg │ │ ├── mixed-navigation.svg │ │ ├── moon.svg │ │ ├── outside-page.svg │ │ ├── profile-card.svg │ │ ├── react-logo.svg │ │ ├── server-error.svg │ │ ├── side-navigation.svg │ │ ├── sun.svg │ │ ├── top-navigation.svg │ │ ├── two-column-navigation.svg │ │ ├── user-circle.svg │ │ └── user-settings.svg ├── index.tsx ├── layout │ ├── constants.ts │ ├── container-layout │ │ └── index.tsx │ ├── hooks │ │ ├── index.ts │ │ └── use-layout.ts │ ├── index.ts │ ├── layout-content │ │ └── index.tsx │ ├── layout-footer │ │ └── index.tsx │ ├── layout-header │ │ ├── components │ │ │ ├── fullscreen-button.tsx │ │ │ ├── language-button.tsx │ │ │ ├── theme-button.tsx │ │ │ └── user-menu.tsx │ │ └── index.tsx │ ├── layout-menu │ │ ├── index.tsx │ │ ├── types.ts │ │ ├── use-menu.ts │ │ └── utils.ts │ ├── layout-mixed-sidebar │ │ ├── first-column-menu.tsx │ │ └── index.tsx │ ├── layout-mobile-menu │ │ └── index.tsx │ ├── layout-root │ │ └── index.tsx │ ├── layout-sidebar │ │ └── index.tsx │ ├── layout-tabbar │ │ ├── components │ │ │ ├── draggable-tab-bar.tsx │ │ │ ├── tab-maximize.tsx │ │ │ └── tab-options.tsx │ │ ├── hooks │ │ │ └── use-dropdown-menu.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── parent-layout │ │ └── index.tsx │ └── widgets │ │ ├── breadcrumb-views │ │ └── index.tsx │ │ ├── global-search │ │ ├── components │ │ │ ├── search-footer.tsx │ │ │ └── search-panel.tsx │ │ └── index.tsx │ │ ├── index.ts │ │ ├── logo │ │ └── index.tsx │ │ ├── notification │ │ ├── index.tsx │ │ ├── notification-container.tsx │ │ └── types.ts │ │ ├── preferences │ │ ├── blocks │ │ │ ├── animation │ │ │ │ └── index.tsx │ │ │ ├── footer │ │ │ │ └── index.tsx │ │ │ ├── general │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ │ ├── index.ts │ │ │ ├── layout │ │ │ │ ├── constants.ts │ │ │ │ └── index.tsx │ │ │ ├── sidebar │ │ │ │ └── index.tsx │ │ │ ├── tabbar │ │ │ │ └── index.tsx │ │ │ └── theme │ │ │ │ ├── builtin.tsx │ │ │ │ ├── index.ts │ │ │ │ └── theme.tsx │ │ ├── index.tsx │ │ ├── number-input-spinner.tsx │ │ ├── select-item.tsx │ │ ├── switch-item.tsx │ │ └── text-input.tsx │ │ ├── sider-trigger │ │ └── index.tsx │ │ └── version-monitor │ │ └── index.tsx ├── locales │ ├── README.md │ ├── en-US │ │ ├── about.json │ │ ├── access.json │ │ ├── authority.json │ │ ├── common.json │ │ ├── exception.json │ │ ├── form.json │ │ ├── home.json │ │ ├── personal-center.json │ │ ├── preferences.json │ │ ├── system.json │ │ └── widgets.json │ ├── helper.ts │ ├── index.ts │ ├── t.tsx │ └── zh-CN │ │ ├── about.json │ │ ├── access.json │ │ ├── authority.json │ │ ├── common.json │ │ ├── exception.json │ │ ├── form.json │ │ ├── home.json │ │ ├── personal-center.json │ │ ├── preferences.json │ │ ├── system.json │ │ └── widgets.json ├── pages │ ├── about │ │ ├── constants.ts │ │ └── index.tsx │ ├── access │ │ ├── access-mode │ │ │ └── index.tsx │ │ ├── admin-visible │ │ │ └── index.tsx │ │ ├── button-control │ │ │ └── index.tsx │ │ ├── common-visible │ │ │ └── index.tsx │ │ └── page-control │ │ │ └── index.tsx │ ├── exception │ │ ├── 403 │ │ │ └── index.tsx │ │ ├── 404 │ │ │ └── index.tsx │ │ ├── 500 │ │ │ └── index.tsx │ │ └── unknown-component │ │ │ └── index.tsx │ ├── home │ │ ├── components │ │ │ ├── bar-chart.tsx │ │ │ ├── card-list.tsx │ │ │ ├── line-chart.tsx │ │ │ └── pie-chart.tsx │ │ └── index.tsx │ ├── login │ │ ├── components │ │ │ ├── code-login.tsx │ │ │ ├── forgot-password.tsx │ │ │ ├── index.ts │ │ │ ├── password-login.tsx │ │ │ └── register-password.tsx │ │ ├── form-mode-context.ts │ │ └── index.tsx │ ├── personal-center │ │ ├── my-profile │ │ │ └── index.tsx │ │ └── settings │ │ │ └── index.tsx │ ├── privacy-policy │ │ └── index.tsx │ ├── route-nest │ │ ├── menu1 │ │ │ ├── menu1-1 │ │ │ │ └── index.tsx │ │ │ └── menu1-2 │ │ │ │ └── index.tsx │ │ └── menu2 │ │ │ └── index.tsx │ ├── system │ │ ├── dept │ │ │ └── index.tsx │ │ ├── menu │ │ │ ├── components │ │ │ │ └── detail.tsx │ │ │ ├── constants.tsx │ │ │ ├── index.tsx │ │ │ └── tree-menu.tsx │ │ ├── role │ │ │ ├── components │ │ │ │ └── detail.tsx │ │ │ ├── constants.tsx │ │ │ └── index.tsx │ │ └── user │ │ │ └── index.tsx │ └── terms-of-service │ │ └── index.tsx ├── plugins │ ├── README.md │ ├── hide-loading.ts │ ├── index.ts │ ├── loading.ts │ └── loading2.ts ├── router │ ├── README.md │ ├── constants.ts │ ├── extra-info │ │ ├── index.ts │ │ ├── order.ts │ │ └── route-path.ts │ ├── guard │ │ ├── auth-guard.tsx │ │ ├── index.ts │ │ └── utils.ts │ ├── index.ts │ ├── routes │ │ ├── config.ts │ │ ├── core │ │ │ ├── auth.ts │ │ │ ├── exception.ts │ │ │ ├── fallback.ts │ │ │ └── index.ts │ │ ├── external │ │ │ ├── privacy-policy.ts │ │ │ └── terms-of-service.ts │ │ ├── index.ts │ │ └── modules │ │ │ ├── about.ts │ │ │ ├── access.ts │ │ │ ├── home.ts │ │ │ ├── outside.ts │ │ │ ├── personal-center.ts │ │ │ ├── routeNest.ts │ │ │ └── system.ts │ ├── types.ts │ └── utils │ │ ├── add-route-id-by-path.ts │ │ ├── ascending.ts │ │ ├── flatten-routes.ts │ │ ├── generate-menu-items-from-routes.ts │ │ ├── generate-routes-from-backend.ts │ │ ├── generate-routes-from-frontend.ts │ │ ├── index.ts │ │ ├── merge-route-modules.tsx │ │ └── remove-trailing-slash.ts ├── setupTests.ts ├── store │ ├── access.ts │ ├── auth.ts │ ├── global.ts │ ├── index.ts │ ├── preferences │ │ ├── index.ts │ │ └── types.ts │ ├── tabs.ts │ └── user.ts ├── styles │ ├── animation.css │ ├── base.css │ ├── global.css │ ├── index.css │ ├── keep-alive.css │ └── theme │ │ └── antd │ │ ├── antd-theme.ts │ │ └── css-variables.ts ├── types │ ├── global.d.ts │ ├── index.d.ts │ ├── ky-extensions.d.ts │ ├── router.d.ts │ └── vite-env.d.ts └── utils │ ├── cn │ └── index.ts │ ├── dom │ └── index.ts │ ├── get-all-expanded-keys │ └── index.ts │ ├── index.ts │ ├── is-dark-theme │ └── index.ts │ ├── is-light-theme │ └── index.ts │ ├── is-mac-os │ └── index.ts │ ├── is-windows-os │ └── index.ts │ ├── is │ └── index.ts │ ├── progress │ └── index.tsx │ ├── remember-route │ └── index.ts │ ├── request │ ├── constants.ts │ ├── error-response.ts │ ├── global-progress.ts │ ├── go-login.ts │ ├── index.ts │ └── refresh.ts │ ├── search-route │ └── index.ts │ ├── static-antd │ └── index.ts │ ├── to-capitalize-case │ └── index.ts │ ├── toggle-html-class │ └── index.ts │ └── tree │ └── index.ts ├── tailwind.config.ts ├── taze.config.js ├── tests ├── Demo.tsx └── demo.test.tsx ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_style = space -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 后端 API 前缀 2 | VITE_API_BASE_URL = "/api" 3 | 4 | # 登录之后默认调转的路由 5 | VITE_BASE_HOME_PATH = "/home" 6 | 7 | # 网站标题 8 | VITE_GLOB_APP_TITLE = "React Antd Admin" 9 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # React Router Mode 2 | VITE_ROUTER_MODE = hash 3 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [log] 2 | # https://x.com/wyh236/status/1930535877035360291 3 | # Use ISO format for dates in logs 4 | # date = iso 5 | 6 | # https://x.com/yisibl/status/1930595306842026239 7 | # Custom date format: YYYY-MM-DD HH:MM:SS 8 | date = format:%Y-%m-%d %H:%M:%S 9 | 10 | [core] 11 | # Disable case-insensitive file/directory handling 12 | ignorecase = false 13 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build-and-deploy: 16 | concurrency: ci-${{ github.ref }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 🛎️ 20 | uses: actions/checkout@v4 21 | 22 | - uses: pnpm/action-setup@v2 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | cache: pnpm 27 | 28 | - name: Install and Build 🔧 29 | run: | 30 | pnpm install -r 31 | pnpm run build && pnpm --filter docs run docs:build 32 | 33 | - name: Deploy 🚀 34 | uses: JamesIves/github-pages-deploy-action@v4 35 | with: 36 | folder: build 37 | clean: true 38 | 39 | - name: Deploy Docs 🚀 40 | uses: JamesIves/github-pages-deploy-action@v4 41 | with: 42 | clean: true 43 | folder: docs/.vitepress/dist 44 | target-folder: docs 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # WebStorm 12 | .idea 13 | 14 | # production 15 | /build 16 | /dist 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # package lock 30 | package-lock.json 31 | yarn.lock 32 | # pnpm-lock.yaml 33 | 34 | # vitepress 35 | **/.vitepress/dist 36 | **/.vitepress/cache 37 | 38 | Other 39 | .eslintcache 40 | analyzer 41 | TODO.md 42 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # package-lock=false 2 | registry=https://registry.npmmirror.com 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "davidanson.vscode-markdownlint", 5 | "dbaeumer.vscode-eslint", 6 | "dsznajder.es7-react-js-snippets", 7 | // "vscode-icons-team.vscode-icons" 8 | "file-icons.file-icons", 9 | "lokalise.i18n-ally", 10 | "mikestead.dotenv", 11 | "naumovs.color-highlight", 12 | "planbcoding.vscode-react-refactor", 13 | "redhat.vscode-yaml", 14 | "streetsidesoftware.code-spell-checker", 15 | "jock.svg" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/rfc.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "React Functional Component": { 3 | "prefix": "rfc", 4 | "body": [ 5 | "export default function $1() {", 6 | " return (", 7 | " <>", 8 | " $2", 9 | " ", 10 | " );", 11 | "};", 12 | ], 13 | "description": "React Functional Component", 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/rfce.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "React Functional Component with ES6 export": { 3 | "prefix": "rfce", 4 | "body": [ 5 | "function $1() {", 6 | " return (", 7 | " <>", 8 | " $2", 9 | " ", 10 | " );", 11 | "}", 12 | "", 13 | "export default $1;", 14 | ], 15 | "description": "React Functional Component (ES6 export)", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "antd", 4 | "ahooks", 5 | "Cascader", 6 | "clsx", 7 | "lokalise", 8 | "Popconfirm", 9 | "reduxjs", 10 | "sider", 11 | "svgr", 12 | "vitepress", 13 | "vite", 14 | "taze", 15 | "zustand" 16 | ], 17 | // Disable the default formatter, use eslint instead 18 | "prettier.enable": false, 19 | // Auto fix 20 | "editor.codeActionsOnSave": { 21 | "source.fixAll.eslint": "explicit", 22 | "source.organizeImports": "never" 23 | }, 24 | "editor.formatOnSave": true, 25 | "editor.renderWhitespace": "all", 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "jsonc", 37 | "yaml" 38 | ], 39 | "i18n-ally.localesPaths": "src/locales", 40 | // https://github.com/lokalise/i18n-ally/wiki/Path-Matcher 41 | "i18n-ally.namespace": true, 42 | "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", 43 | "i18n-ally.sourceLanguage": "zh-CN", 44 | "i18n-ally.displayLanguage": "zh-CN", 45 | "i18n-ally.enabledFrameworks": [ 46 | "react-i18next" 47 | ], 48 | "i18n-ally.keystyle": "nested", 49 | "svg.preview.background": "editor", 50 | // https://github.com/r5n-dev/vscode-react-javascript-snippets/issues/244 51 | // "reactSnippets.settings.prettierEnabled": true, 52 | // "reactSnippets.settings.importReactOnTop": false, 53 | // https://x.com/rxliuli/status/1930431929691844752 54 | "gitlens.defaultDateFormat": "YYYY-MM-DD HH:mm:ss", 55 | // https://x.com/Condor2Hero/status/1930816870501990831 56 | "git-graph.date.format": "ISO Date & Time" 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CondorHero 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 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "body-leading-blank": [2, "always"], 5 | "footer-leading-blank": [1, "always"], 6 | "header-max-length": [2, "always", 108], 7 | "subject-empty": [2, "never"], 8 | "type-empty": [2, "never"], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "feat", 14 | "fix", 15 | "perf", 16 | "style", 17 | "docs", 18 | "test", 19 | "refactor", 20 | "build", 21 | "ci", 22 | "chore", 23 | "revert", 24 | "wip", 25 | "workflow", 26 | "types", 27 | "release", 28 | ], 29 | ], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /docs/.vitepress/config/index.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import { shared } from "./shared"; 3 | import { zh } from "./zh"; 4 | 5 | export default defineConfig({ 6 | ...shared, 7 | locales: { 8 | root: { label: "简体中文", ...zh }, 9 | // zh: { label: "简体中文", ...zh }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /docs/.vitepress/config/shared.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import { search as zhSearch } from "./zh"; 3 | 4 | export const shared = defineConfig({ 5 | title: "React Antd Admin", 6 | 7 | base: "/react-antd-admin/docs/", 8 | 9 | // rewrites: { 10 | // "zh/:rest*": ":rest*", 11 | // }, 12 | 13 | lastUpdated: true, 14 | cleanUrls: true, 15 | metaChunk: true, 16 | 17 | markdown: { 18 | // math: true, 19 | codeTransformers: [ 20 | // We use `[!!code` in demo to prevent transformation, here we revert it back. 21 | { 22 | postprocess(code) { 23 | return code.replace(/\[!!code/g, "[!code"); 24 | }, 25 | }, 26 | ], 27 | }, 28 | 29 | sitemap: { 30 | hostname: "https://condorheroblog.github.io/react-antd-admin", 31 | transformItems(items) { 32 | return items.filter(item => !item.url.includes("migration")); 33 | }, 34 | }, 35 | 36 | /* prettier-ignore */ 37 | head: [ 38 | ["link", { rel: "icon", type: "image/svg+xml", href: "/logo.svg" }], 39 | ["link", { rel: "icon", type: "image/png", href: "/logo.png" }], 40 | ["meta", { name: "theme-color", content: "#5f67ee" }], 41 | ["meta", { property: "og:type", content: "website" }], 42 | ["meta", { property: "og:locale", content: "en" }], 43 | ["meta", { property: "og:title", content: "React Antd Admin | 企业级管理系统框架" }], 44 | ["meta", { property: "og:site_name", content: "React Antd Admin" }], 45 | ["meta", { property: "og:image", content: "https://condorheroblog.github.io/react-antd-admin/docs/shared-og.png" }], 46 | ["meta", { property: "og:url", content: "https://condorheroblog.github.io/react-antd-admin/" }], 47 | ], 48 | 49 | themeConfig: { 50 | outline: "deep", 51 | logo: { src: "/logo.svg", width: 24, height: 24 }, 52 | 53 | socialLinks: [ 54 | { icon: "github", link: "https://github.com/condorheroblog/react-antd-admin" }, 55 | ], 56 | 57 | search: { 58 | provider: "local", 59 | options: { 60 | locales: { 61 | ...zhSearch, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "docs:dev": "vitepress dev", 8 | "docs:build": "vitepress build", 9 | "docs:preview": "vitepress preview" 10 | }, 11 | "devDependencies": { 12 | "vitepress": "^1.6.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/public/guide/analyzer-stats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/analyzer-stats.jpg -------------------------------------------------------------------------------- /docs/public/guide/app-loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/app-loading.png -------------------------------------------------------------------------------- /docs/public/guide/app-loading2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/app-loading2.png -------------------------------------------------------------------------------- /docs/public/guide/i18n-translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/i18n-translation.png -------------------------------------------------------------------------------- /docs/public/guide/ky-vs-fetch.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/ky-vs-fetch.jpeg -------------------------------------------------------------------------------- /docs/public/guide/lokalise.i18n-ally-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/lokalise.i18n-ally-plugin.png -------------------------------------------------------------------------------- /docs/public/guide/lokalise.i18n-ally.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/lokalise.i18n-ally.png -------------------------------------------------------------------------------- /docs/public/guide/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/preferences.png -------------------------------------------------------------------------------- /docs/public/guide/version-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/guide/version-monitor.png -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/shared-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/shared-og.png -------------------------------------------------------------------------------- /docs/public/sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/docs/public/sponsor.png -------------------------------------------------------------------------------- /docs/public/vite-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | export default { 4 | content: [ 5 | "*.{html,js,md}", 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced/loading.md: -------------------------------------------------------------------------------- 1 | # App Loading {#loading} 2 | 3 | 应用初次启动时或者应用刷新时,出现的加载效果。 4 | 5 | ![app-loading](/public/guide/app-loading.png) 6 | 7 | > 代码在 [app-loading](https://github.com/condorheroblog/react-antd-admin/tree/main/src/plugins/loading.ts) 8 | 9 | ::: info 特此声明 10 | 11 | loading 效果的代码属于 pure-admin 12 | 点击查看:https://github.com/pure-admin/vue-pure-admin/blob/cd21f1e050011d8f761094bf8a1e110fb8a33959/index.html#L20-L81 13 | 14 | ::: 15 | 16 | ## 为什么? 17 | 18 | 单页面应用,用户初次进入或者刷新应用,一是会下载大量代码进行页面渲染,二是页面展示前需要时间发送接口请求用户详情和动态路由,这个过程需要用户等待,为了避免用户看到白屏或者短暂的黑屏(暗黑主题),我们使用 loading 效果避免这个问题。 19 | 20 | ## 原理 21 | 22 | 进入应用调用 setupLoading 显示 App Loading,请求完用户详情接口调用 hideLoading 函数关闭 Loading。 23 | 24 | ## 关闭 Loading 效果 25 | 26 | 在 `src/index.tsx` 文件中,注释或者移除掉 setupLoading 函数。 27 | 28 | ## Loading2 29 | 30 | 在 [/src/plugins/loading.ts](https://github.com/condorheroblog/react-antd-admin/tree/main/src/plugins/loading2.ts) 中提供了另一种 loading 效果。 31 | 32 | ![app-loading2](/public/guide/app-loading2.png) 33 | 34 | 如果要使用这个 loading 效果,批量替换代码中的 setupLoading 函数为 setupLoading2 即可。 35 | 36 | ## 自定义 loading 效果 37 | 38 | 编辑文件 `src/plugins/loading.ts`,修改代码即可。 39 | 40 | 下面几个网站提供了 loading 效果,可以自行选择。 41 | 42 | ::: tip 推荐 43 | 44 | - [CSS Loaders](https://css-loaders.com/) 45 | - [CSS Loader Generator](https://10015.io/tools/css-loader-generator) 46 | - [Loaders](https://cssloaders.github.io/) 47 | 48 | ::: 49 | 50 | ## 为什么不使用 vite-plugin-app-loading 插件? 51 | 52 | 代码非常的简单,引入插件反而比较复杂。 53 | 54 | 请自行决定,链接在此:https://github.com/hooray/vite-plugin-app-loading 55 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced/monitoring-updates.md: -------------------------------------------------------------------------------- 1 | # 监控网站更新 {#monitoring-updates} 2 | 3 | ## 概述 4 | 5 | 当网站重新部署,有更新内容时,客户端需要检查更新获取最新的版本。 6 | 7 | 项目通过定时检查更新提供了这一功能,在 `preferences.ts` 文件中配置 checkUpdatesInterval 和 enableCheckUpdates 字段,以开启和设置检查更新的时间间隔(单位:分钟)。 8 | 9 | ```ts 10 | /** 11 | * 默认偏好设置 12 | */ 13 | export const DEFAULT_PREFERENCES = { 14 | /* ================== Version Monitor ================== */ 15 | checkUpdatesInterval: 1, 16 | enableCheckUpdates: true, 17 | } satisfies PreferencesState; 18 | ``` 19 | 20 | > 代码在 [version-monitor](https://github.com/condorheroblog/react-antd-admin/tree/main/src/layout/widgets/version-monitor/index.tsx) 21 | 22 | ## 效果 23 | 24 | 检测到新版本时,系统会发送一个通知,询问用户是否刷新页面: 25 | 26 | ![version-monitor.png](/public/guide/version-monitor.png) 27 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced/upgrading.md: -------------------------------------------------------------------------------- 1 | # 项目升级指南 {#upgrading} 2 | 3 | ### 升级依赖 4 | 5 | 直接运行 npm-check 命令,它会列出所有依赖项的最新版本并自动安装。 6 | 7 | ```bash 8 | pnpm run npm-check 9 | ``` 10 | 11 | 如果想要关闭自动安装,需要修改根目录下的 `taze.config.js` 文件,将 `install` 设置为 `false`。 12 | 13 | 更多用法参考 [taze](https://github.com/antfu-collective/taze?tab=readme-ov-file#config-file)。 14 | 15 | ### 对等依赖不兼容 16 | 17 | 某些情况下可能遇到对等依赖不兼容,例如 `react-scroll`、`react-scrollama` 和 `rooks` 对等依赖未列出 `react@19`。 18 | 19 | 可以通过 `package.json` 文件的 overrides 字段解决。 20 | 21 | ```json 22 | { 23 | "overrides": { 24 | "react-pdf": { 25 | "react": "$react", 26 | "react-dom": "$react-dom" 27 | }, 28 | "react-scroll": { 29 | "@types/react": "$react", 30 | "react": "$react", 31 | "react-dom": "$react-dom" 32 | }, 33 | "react-scrollama": { 34 | "react": "$react", 35 | "react-dom": "$react-dom" 36 | }, 37 | "rooks": { 38 | "react": "$react", 39 | "react-dom": "$react-dom" 40 | } 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/zh/guide/fundamentals/mock.md: -------------------------------------------------------------------------------- 1 | # Mock {#mock} 2 | 3 | 借助 [vite-plugin-fake-server](https://github.com/condorheroblog/vite-plugin-fake-server) 插件的力量,dev 环境可以提供真实和后端交互的 HTTP 请求,打通联调的最后一步,支持常用的 post、get 等请求方法,生产环境通过拦截 XHR 和 Fetch 请求,也能完成数据模拟的任务,**一旦和后端联调完成,建议删除 mock 数据,避免请求优先使用 mock 数据**。 4 | 5 | 使用 [@faker-js/faker](https://fakerjs.dev/) 可提供常见的数据格式。 6 | 7 | ## 使用 {#mock-use} 8 | 9 | 所有的 Mock 数据统一存放在 `src/fake` 目录下,这是一个简单的定义假数据的例子。 10 | 11 | ::: info fake/user.ts 12 | 13 | ```ts 14 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 15 | 16 | import { ADMIN_TOKEN } from "./constants"; 17 | import { resultSuccess } from "./utils"; 18 | 19 | export default defineFakeRoute([ 20 | { 21 | url: "/user-info", 22 | timeout: 1000, 23 | method: "get", 24 | response: ({ headers }) => { 25 | if (headers.authorization?.split?.(" ")?.[1] === ADMIN_TOKEN) { 26 | return resultSuccess({ 27 | id: 1, 28 | avatar: "https://avatars.githubusercontent.com/u/47056890", 29 | username: "Admin", 30 | email: "", 31 | phoneNumber: "1234567890", 32 | description: "manager", 33 | roles: ["admin"], 34 | }); 35 | } 36 | else { 37 | return resultSuccess({ 38 | id: 2, 39 | avatar: "https://avatar.vercel.sh/avatar.svg?text=Common", 40 | username: "Tom", 41 | email: "", 42 | phoneNumber: "9876543210", 43 | description: "employee", 44 | roles: ["common"], 45 | }); 46 | } 47 | }, 48 | }, 49 | ]); 50 | ``` 51 | 52 | ::: 53 | 54 | ## 生产环境关闭数据模拟 55 | 56 | 在 `vite.config.ts` 中配置 `enableProd: false` 即可关闭生产环境的数据模拟。 57 | 58 | ::: info vite.config.ts 59 | 60 | ```ts 61 | import { vitePluginFakeServer } from "vite-plugin-fake-server"; 62 | 63 | export default defineConfig({ 64 | plugins: [ 65 | react(), 66 | vitePluginFakeServer({ 67 | basename: "/api", 68 | enableProd: false, // [!code ++] 69 | timeout: 1000, 70 | }), 71 | ], 72 | }); 73 | ``` 74 | 75 | ::: 76 | 77 | ## 其他推荐 78 | 79 | 推荐使用 [MSW(Mock Service Worker)](https://mswjs.io/docs/getting-started) 进行请求拦截,来实现假数据的模拟,MSW 的优点是可以通过拦截请求,在本地实现和后端接口的交互。 80 | -------------------------------------------------------------------------------- /docs/zh/guide/introduction/quick-start.md: -------------------------------------------------------------------------------- 1 | # 快速开始 {#quick-start} 2 | 3 | ## 前置准备 4 | 5 | ::: info 环境要求 6 | 7 | 在启动项目前,你需要确保你的环境满足以下要求: 8 | 9 | - [Node.js](https://nodejs.org/en) 版本大于 18.18.0,推荐使用 [fnm](https://github.com/Schniz/fnm)、[nvm](https://github.com/nvm-sh/nvm) 进行版本管理。 10 | - [Git](https://git-scm.com/) 任意版本。 11 | 12 | 验证环境是否满足以上要求,通过以下命令查看版本: 13 | 14 | ```bash 15 | # 查看 node 版本 16 | node -v 17 | # 查看 git 版本 18 | git -v 19 | ``` 20 | 21 | ::: 22 | 23 | ## 创建项目 24 | 25 | ### 获取源码 26 | 27 | > 点击直接创建模版项目:[使用这个模板创建仓库](https://github.com/new?template_name=react-antd-admin&template_owner=condorheroblog) 28 | 29 | 手动获取源码的方式如下: 30 | 31 | ::: code-group 32 | 33 | ```sh [GitHub] 34 | npx degit condorheroblog/react-antd-admin react-antd-admin 35 | # or npx giget@latest gh:condorheroblog/react-antd-admin react-antd-admin 36 | ``` 37 | 38 | ::: 39 | 40 | ### 安装依赖 41 | 42 | 在你的代码目录内打开终端,并执行以下命令: 43 | 44 | ```bash 45 | # 进入项目目录 46 | cd react-antd-admin 47 | 48 | # 使用项目指定的 pnpm 版本进行依赖安装 49 | corepack enable 50 | 51 | # 安装依赖 52 | pnpm install 53 | ``` 54 | 55 | ::: tip 注意 56 | 57 | - 项目使用 `pnpm` 进行依赖安装,默认会使用 `corepack` 来安装指定版本的 `pnpm`。: 58 | - 如果 corepack 无法访问 npm 源,可以设置系统的环境变量为镜像源 `COREPACK_REGISTRY=https://registry.npmmirror.com`,然后执行 `pnpm install`。 59 | - 如果你不想使用 `corepack`,只需要运行 `corepack disable` 即可禁用,然后使用任意版本的 `pnpm` 进行安装。 60 | 61 | ::: 62 | 63 | ### 开发 64 | 65 | 只需要执行以下命令就可以在 `http://localhost:3333` 中看到页面: 66 | 67 | ```bash 68 | pnpm run dev 69 | ``` 70 | 71 | ### 构建 72 | 73 | 构建该应用只需要执行以下命令: 74 | 75 | ```bash 76 | pnpm build 77 | ``` 78 | 79 | 然后会看到用于发布的 build 文件夹被生成。 80 | 81 | ### 预览 82 | 83 | 预览构建的应用只需要执行以下命令: 84 | 85 | ```bash 86 | pnpm preview 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/zh/guide/introduction/why.md: -------------------------------------------------------------------------------- 1 | # 为什么选择我们? {#why} 2 | 3 | ## 现实问题 {#the-problems} 4 | 5 | 我多年从事前端开发,主要使用 React 框架开发单页面应用。但是每次启动项目想要找一个合适的框架,确实难上加难,大部分框架都是开发者自己学习 React 搭建的,要么就是不维护了,Vue.js 社区的框架百花齐放,React 社区的框架寥寥无几,如同沙漠。但是开发中后台系统 antd 仍是当仁不让的首选,所以使用 React 开发则是更好的选择。 6 | 7 | 此时,就需要一个框架集成路由、状态管理、权限管理、代码格式化等基础功能,开箱即用,而不是从头开始搭建,于是基于本人的开发经验并借鉴社区优秀框架,开发了 react-antd-admin。 8 | 9 | ## 为什么不选择 UmiJS? {#why-not-umijs} 10 | 11 | > 声明:本人认为 UmiJS 非常优秀。 12 | 13 | UmiJS 是一个非常优秀的框架,但是它太重了,自己封装了很多东西,上手成本很高,对于新手来说不太友好,而且遇到问题很难定位,也很难修改代码,最重要的是包不是最新的无法享受新技术带来的体验。 14 | 15 | ## 感谢 16 | 17 | 感谢社区优秀的框架,本项目开发期间主要参考了以下几个项目: 18 | 19 | 1. [vben-admin](https://github.com/anncwb/vben-admin) 20 | 2. [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin) 21 | 22 | ## 其他优秀项目 23 | 24 | 我在 GitHub 建了两个目录,分别收集了优秀的 React 和 Vue 框架。 25 | 26 | ### React 框架 27 | 28 | 点击查看目录:[React Template](https://github.com/stars/condorheroblog/lists/react-template) 29 | 30 | ### Vue 框架 31 | 32 | 点击查看目录:[Vue Template](https://github.com/stars/condorheroblog/lists/vue-template) 33 | -------------------------------------------------------------------------------- /docs/zh/sponsor/index.md: -------------------------------------------------------------------------------- 1 | # 赞助 {#sponsors} 2 | 3 | 如果此项目对你有帮助,可以请作者吃顿外卖。 4 | 5 | ![sponsors](/public/sponsor.png) 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from "@antfu/eslint-config"; 2 | 3 | export default antfu({ 4 | react: true, 5 | rules: { 6 | "style/quotes": ["error", "double"], 7 | "style/semi": ["error", "always"], 8 | "style/indent": ["error", "tab"], 9 | "jsonc/indent": ["error", "tab"], 10 | "style/no-tabs": "off", 11 | "style/jsx-indent-props": ["error", "tab"], 12 | "react-hooks/exhaustive-deps": "off", 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /fake/README.md: -------------------------------------------------------------------------------- 1 | ## Fake 目录介绍 2 | 3 | 模拟后端数据,主要用于前端开发调试。 4 | 5 | | 文件 | 说明 | 6 | |------------------------|------------------------| 7 | | `utils.ts` | 接口响应工具函数 | 8 | | `auth.fake.ts` | 权限接口(登录和登出等) | 9 | | `user.fake.ts` | 用户信息接口 | 10 | | `async-routes.fake.ts` | 动态路由接口 | 11 | | `constants.ts` | 常量化数据 | 12 | | ... | ... | 13 | 14 | ## fake 文件说明 15 | 16 | 一个经典的 fake 文件如下所示: 17 | 18 | > 文件名:`auth.fake.ts`文件名的中间缀(`.fake.`)是必须的。 19 | 20 | ```ts 21 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 22 | 23 | import { resultSuccess } from "./utils"; 24 | 25 | export default defineFakeRoute([ 26 | { 27 | url: "/logout", 28 | timeout: 1000, 29 | method: "post", 30 | response: () => resultSuccess({}), 31 | }, 32 | ]); 33 | ``` 34 | 35 | ## 项目中使用 Fake 的建议 36 | 37 | 推荐一个页面新建一个 fake 文件即可,文件名与页面同名。 38 | -------------------------------------------------------------------------------- /fake/auth.fake.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 2 | 3 | import { ADMIN_REFRESH_TOKEN, ADMIN_TOKEN, COMMON_REFRESH_TOKEN, COMMON_TOKEN, COUNTRIES_CODE } from "./constants"; 4 | import { resultSuccess } from "./utils"; 5 | 6 | export default defineFakeRoute([ 7 | { 8 | url: "/login", 9 | timeout: 0, 10 | method: "post", 11 | // statusCode: 401, 12 | // response: () => ({ code: 401, message: "Unauthorized" }), 13 | // statusCode: 400, 14 | // response: () => ({ code: 404, message: "Not found" }), 15 | response: ({ body }) => { 16 | if (body.username !== "common") { 17 | return resultSuccess({ 18 | token: ADMIN_TOKEN, 19 | refreshToken: ADMIN_REFRESH_TOKEN, 20 | }); 21 | } 22 | else { 23 | return resultSuccess({ 24 | token: COMMON_TOKEN, 25 | refreshToken: COMMON_REFRESH_TOKEN, 26 | }); 27 | } 28 | }, 29 | }, 30 | { 31 | url: "/logout", 32 | timeout: 1000, 33 | method: "post", 34 | response: () => resultSuccess({}), 35 | }, 36 | { 37 | url: "/refresh-token", 38 | timeout: 1000, 39 | method: "post", 40 | response: ({ body }) => { 41 | if (body.refreshToken === ADMIN_REFRESH_TOKEN) { 42 | return resultSuccess({ token: ADMIN_TOKEN, refreshToken: ADMIN_REFRESH_TOKEN }); 43 | } 44 | return resultSuccess({ token: COMMON_TOKEN, refreshToken: COMMON_REFRESH_TOKEN }); 45 | }, 46 | }, 47 | { 48 | url: "/country-calling-codes", 49 | timeout: 1000, 50 | method: "get", 51 | response: () => { 52 | return resultSuccess(COUNTRIES_CODE); 53 | }, 54 | }, 55 | ]); 56 | -------------------------------------------------------------------------------- /fake/notification.fake.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 2 | 3 | import { resultSuccess } from "./utils"; 4 | 5 | export default defineFakeRoute([ 6 | { 7 | url: "/notifications", 8 | timeout: 1000, 9 | method: "get", 10 | response: () => resultSuccess([ 11 | { 12 | avatar: "https://avatar.vercel.sh/vercel.svg?text=VC", 13 | date: "3 小时前", 14 | isRead: true, 15 | message: "描述信息描述信息描述信息", 16 | title: "收到了 14 份新周报", 17 | }, 18 | { 19 | avatar: "https://avatar.vercel.sh/1", 20 | date: "刚刚", 21 | isRead: false, 22 | message: "描述信息描述信息描述信息", 23 | title: "Tom 回复了你", 24 | }, 25 | { 26 | avatar: "https://avatar.vercel.sh/2", 27 | date: "2024-10-10", 28 | isRead: false, 29 | message: "描述信息描述信息描述信息", 30 | title: "Jack 评论了你", 31 | }, 32 | { 33 | avatar: "https://avatar.vercel.sh/Jack", 34 | date: "1 天前", 35 | isRead: false, 36 | message: "描述信息描述信息描述信息", 37 | title: "代办提醒", 38 | }, 39 | ]), 40 | }, 41 | 42 | ]); 43 | -------------------------------------------------------------------------------- /fake/personal-center.fake.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 2 | 3 | import { resultSuccess } from "./utils"; 4 | 5 | export default defineFakeRoute([ 6 | { 7 | url: "/upload", 8 | timeout: 1000, 9 | method: "post", 10 | response: () => resultSuccess("https://avatar.vercel.sh/blur.svg?text=%F0%9F%91%8D"), 11 | }, 12 | ]); 13 | -------------------------------------------------------------------------------- /fake/user.fake.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from "vite-plugin-fake-server/client"; 2 | 3 | // import { systemManagementRouter } from "./async-routes.fake"; 4 | import { ADMIN_TOKEN } from "./constants"; 5 | import { resultSuccess } from "./utils"; 6 | 7 | export default defineFakeRoute([ 8 | { 9 | url: "/user-info", 10 | timeout: 1000, 11 | method: "get", 12 | response: ({ headers }) => { 13 | if (headers.authorization?.split?.(" ")?.[1] === ADMIN_TOKEN) { 14 | return resultSuccess({ 15 | id: 1, 16 | avatar: "https://avatars.githubusercontent.com/u/47056890", 17 | username: "Admin", 18 | email: "", 19 | phoneNumber: "1234567890", 20 | description: "manager", 21 | roles: ["admin"], 22 | // menus: [systemManagementRouter], 23 | }); 24 | } 25 | else { 26 | return resultSuccess({ 27 | id: 2, 28 | avatar: "https://avatar.vercel.sh/avatar.svg?text=Common", 29 | username: "Tom", 30 | email: "", 31 | phoneNumber: "9876543210", 32 | description: "employee", 33 | roles: ["common"], 34 | }); 35 | } 36 | }, 37 | }, 38 | ]); 39 | -------------------------------------------------------------------------------- /fake/utils.ts: -------------------------------------------------------------------------------- 1 | export function resultSuccess(result: unknown, { message = "ok" } = {}) { 2 | return { 3 | code: 200, 4 | result, 5 | message, 6 | success: true, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | %VITE_GLOB_APP_TITLE% 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condorheroblog/react-antd-admin/ed181d0d3eeab2f72c2dfc3039604e95bb1489e0/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Admin", 3 | "name": "React Antd Admin", 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 | Disallow: 4 | -------------------------------------------------------------------------------- /src/api/home.ts: -------------------------------------------------------------------------------- 1 | import { request } from "#src/utils"; 2 | 3 | export interface PieDataType { 4 | value: number 5 | code: string 6 | } 7 | export function fetchPie(data: { by: string | number }) { 8 | return request 9 | .get("home/pie", { searchParams: data }) 10 | .json>(); 11 | } 12 | 13 | export function fetchLine(data: { range: string }) { 14 | return request 15 | .post("home/line", { json: data }) 16 | .json>(); 17 | } 18 | -------------------------------------------------------------------------------- /src/api/notifications/index.ts: -------------------------------------------------------------------------------- 1 | import type { NotificationItem } from "#src/layout/widgets/notification/types"; 2 | import { request } from "#src/utils"; 3 | 4 | export function fetchNotifications() { 5 | return request 6 | .get("notifications") 7 | .json>(); 8 | } 9 | -------------------------------------------------------------------------------- /src/api/system/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./menu"; 2 | export * from "./role"; 3 | -------------------------------------------------------------------------------- /src/api/system/menu/index.ts: -------------------------------------------------------------------------------- 1 | import type { MenuItemType } from "./types"; 2 | import { request } from "#src/utils"; 3 | 4 | export * from "./types"; 5 | 6 | /* 获取菜单列表 */ 7 | export function fetchMenuList(data: any) { 8 | return request.get>("menu-list", { searchParams: data, ignoreLoading: true }).json(); 9 | } 10 | 11 | /* 新增菜单 */ 12 | export function fetchAddMenuItem(data: MenuItemType) { 13 | return request.post>("menu-item", { json: data, ignoreLoading: true }).json(); 14 | } 15 | 16 | /* 修改菜单 */ 17 | export function fetchUpdateMenuItem(data: MenuItemType) { 18 | return request.put>("menu-item", { json: data, ignoreLoading: true }).json(); 19 | } 20 | 21 | /* 删除菜单 */ 22 | export function fetchDeleteMenuItem(id: number) { 23 | return request.delete>("menu-item", { json: id, ignoreLoading: true }).json(); 24 | } 25 | -------------------------------------------------------------------------------- /src/api/system/menu/types.ts: -------------------------------------------------------------------------------- 1 | export interface MenuItemType { 2 | parentId: string // 上级菜单 id 3 | id: number // 菜单 id 4 | menuType: 0 | 1 | 2 | 3 // 菜单类型(0 代表菜单、1 代表 iframe、2 代表外链、3 代表按钮) 5 | name: string // 菜单名称 6 | path: string // 路由路径 7 | component: string // 组件路径 8 | order: number // 菜单顺序 9 | icon: string // 菜单图标 10 | currentActiveMenu: string // 激活路径 11 | iframeLink: string // iframe 链接 12 | keepAlive: number // 是否缓存页面 13 | externalLink: string // 外链地址 14 | hideInMenu: number // 是否在菜单中隐藏 15 | ignoreAccess: number // 是否忽略权限 16 | status: 1 // 状态(0 停用、1 启用) 17 | createTime: number 18 | updateTime: number 19 | } 20 | -------------------------------------------------------------------------------- /src/api/system/role/index.ts: -------------------------------------------------------------------------------- 1 | import type { RoleItemType } from "./types"; 2 | import { request } from "#src/utils"; 3 | 4 | export * from "./types"; 5 | 6 | /* 获取角色列表 */ 7 | export function fetchRoleList(data: any) { 8 | return request.get>("role-list", { searchParams: data, ignoreLoading: true }).json(); 9 | } 10 | 11 | /* 新增角色 */ 12 | export function fetchAddRoleItem(data: RoleItemType) { 13 | return request.post>("role-item", { json: data, ignoreLoading: true }).json(); 14 | } 15 | 16 | /* 修改角色 */ 17 | export function fetchUpdateRoleItem(data: RoleItemType) { 18 | return request.put>("role-item", { json: data, ignoreLoading: true }).json(); 19 | } 20 | 21 | /* 删除角色 */ 22 | export function fetchDeleteRoleItem(id: number) { 23 | return request.delete>("role-item", { json: id, ignoreLoading: true }).json(); 24 | } 25 | 26 | /* 获取菜单 */ 27 | export function fetchRoleMenu() { 28 | return request.get>("role-menu", { ignoreLoading: true }).json(); 29 | } 30 | 31 | /* 角色绑定的菜单 id */ 32 | export function fetchMenuByRoleId(data: { id: number }) { 33 | return request.get>("menu-by-role-id", { searchParams: data, ignoreLoading: false }).json(); 34 | } 35 | -------------------------------------------------------------------------------- /src/api/system/role/types.ts: -------------------------------------------------------------------------------- 1 | export interface RoleItemType { 2 | id: number 3 | createTime: number 4 | updateTime: number 5 | name: string 6 | code: string 7 | status: 1 | 0 8 | remark: string 9 | } 10 | -------------------------------------------------------------------------------- /src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import type { PasswordLoginFormType } from "#src/pages/login/components/password-login"; 2 | import type { AppRouteRecordRaw } from "#src/router/types"; 3 | import type { AuthType, UserInfoType } from "./types"; 4 | import { request } from "#src/utils"; 5 | 6 | export * from "./types"; 7 | 8 | export function fetchLogin(data: PasswordLoginFormType) { 9 | return request 10 | .post("login", { json: data }) 11 | .json>(); 12 | } 13 | 14 | export function fetchLogout() { 15 | return request.post("logout").json(); 16 | } 17 | 18 | export function fetchAsyncRoutes() { 19 | return request.get("get-async-routes").json>(); 20 | } 21 | 22 | export function fetchUserInfo() { 23 | return request.get("user-info").json>(); 24 | } 25 | 26 | export interface RefreshTokenResult { 27 | token: string 28 | refreshToken: string 29 | } 30 | 31 | export const refreshTokenPath = "refresh-token"; 32 | export function fetchRefreshToken(data: { readonly refreshToken: string }) { 33 | return request.post(refreshTokenPath, { json: data }).json>(); 34 | } 35 | -------------------------------------------------------------------------------- /src/api/user/types.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouteRecordRaw } from "#src/router/types"; 2 | 3 | export interface AuthType { 4 | token: string 5 | refreshToken: string 6 | } 7 | 8 | export interface UserInfoType { 9 | id: string 10 | avatar: string 11 | username: string 12 | email: string 13 | phoneNumber: string 14 | description: string 15 | roles: Array 16 | // 路由可以在此处动态添加 17 | menus?: AppRouteRecordRaw[] 18 | } 19 | 20 | export interface AuthListProps { 21 | label: string 22 | name: string 23 | auth: string[] 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/README.md: -------------------------------------------------------------------------------- 1 | ## 静态文件目录 2 | 3 | - images 文件夹用于存放 JPG, PNG 等图片文件,如果图片过多,可以增加子文件夹分类存放 4 | - svg 文件夹用于存放 SVG 文件 5 | …… 6 | -------------------------------------------------------------------------------- /src/components/access-control/index.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import { useAccess } from "#src/hooks"; 4 | 5 | interface AccessControlProps { 6 | // 权限类型,默认为 code 7 | type?: "code" | "role" 8 | // 权限值,可以是字符串或字符串数组 9 | codes?: string | string[] 10 | children?: ReactNode 11 | // 无权限时显示,默认无权限不显示任何内容。 12 | fallback?: ReactNode 13 | } 14 | 15 | /** 16 | * 权限验证组件 17 | * 18 | * @param AccessControlProps 权限验证组件的属性 19 | * @returns 若子组件存在,并且传入的权限值有效,则返回子组件;否则返回 null 20 | */ 21 | export function AccessControl({ type = "code", codes, children, fallback }: AccessControlProps) { 22 | const { hasAccessByCodes, hasAccessByRoles } = useAccess(); 23 | 24 | if (!children) 25 | return null; 26 | 27 | if (!type || type === "code") { 28 | return hasAccessByCodes(codes) ? children : fallback; 29 | } 30 | 31 | if (type === "role") { 32 | return hasAccessByRoles(codes) ? children : fallback; 33 | } 34 | 35 | return fallback; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/antd-app/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import { StaticAntd } from "#src/utils"; 4 | 5 | import { theme as antdTheme, App } from "antd"; 6 | import { useEffect } from "react"; 7 | 8 | import { setupAntdThemeTokensToHtml } from "./setup-antd-theme"; 9 | 10 | export interface AntdAppProps { 11 | children: ReactNode 12 | } 13 | 14 | export function AntdApp({ children }: AntdAppProps) { 15 | const { token: antdTokens } = antdTheme.useToken(); 16 | 17 | useEffect(() => { 18 | /* 打印查看支持的 token */ 19 | // console.log("antdTokens", antdTokens); 20 | setupAntdThemeTokensToHtml(antdTokens); 21 | }, [antdTokens]); 22 | 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/antd-app/setup-antd-theme.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalToken } from "antd"; 2 | import { getCSSVariablesByTokens } from "./utils"; 3 | 4 | /** 5 | * Setup antd theme tokens to html 6 | * @see https://ant.design/docs/spec/colors 7 | */ 8 | export function setupAntdThemeTokensToHtml(antdTokens: GlobalToken) { 9 | const cssVariablesString = getCSSVariablesByTokens(antdTokens); 10 | 11 | const styleId = "antd-theme-tokens"; 12 | const styleSheet = document.querySelector(`#${styleId}`) || document.createElement("style"); 13 | styleSheet.id = styleId; 14 | styleSheet.textContent = `:root { ${cssVariablesString} }`; 15 | document.head.appendChild(styleSheet); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/antd-app/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalToken } from "antd"; 2 | import { baseColorPalettes, neutralColors, prefix, productLevelColorSystem } from "./constants"; 3 | 4 | /** 5 | * 16 进制颜色值转 RGB 颜色值,因为 16 进制的颜色值在 tailwind 中不支持透明度,比如无法使用 bg-blue-500/20 6 | * @see https://tailwindcss.com/docs/customizing-colors#using-css-variables 7 | */ 8 | export function hexToRGB(hex: string) { 9 | // 移除可能存在的 # 号 10 | hex = hex.replace("#", ""); 11 | 12 | // 获取 R、G、B 的值 13 | const r = Number.parseInt(hex.substring(0, 2), 16); 14 | const g = Number.parseInt(hex.substring(2, 4), 16); 15 | const b = Number.parseInt(hex.substring(4, 6), 16); 16 | 17 | return `${r} ${g} ${b}`; 18 | } 19 | 20 | // 判断是否是 RGB 颜色值 21 | export function isRGBColor(color: string) { 22 | return color.trim().startsWith("rgb"); 23 | } 24 | 25 | export function getCSSVariablesByTokens(tokens: GlobalToken) { 26 | return Object.entries(tokens) 27 | .reduce((acc, [key, value]): string => { 28 | // 功能色系,不包含中性色系 29 | if (productLevelColorSystem.includes(key)) { 30 | const rgb = hexToRGB(value); 31 | return `${acc}--${prefix}-${key}:${rgb};`; 32 | } 33 | 34 | // 中性色系 35 | if (neutralColors.includes(key)) { 36 | // 如果颜色值是 rgb 格式,则直接使用 37 | const rgb = isRGBColor(value) ? value : `rgb(${hexToRGB(value)})`; 38 | return `${acc}--${prefix}-${key}:${rgb};`; 39 | } 40 | // 色板 41 | return baseColorPalettes.includes(key) ? `${acc}--${prefix}-${key}:${hexToRGB(value)};` : acc; 42 | }, ""); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/basic-button/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from "antd"; 2 | import type { ReactNode } from "react"; 3 | import { Button } from "antd"; 4 | 5 | interface BasicButtonProps extends ButtonProps { 6 | children?: ReactNode 7 | } 8 | 9 | export function BasicButton(props: BasicButtonProps) { 10 | const { children } = props; 11 | 12 | // 清除自定义属性 13 | const params: Partial = { ...props }; 14 | 15 | return ( 16 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/basic-content/index.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | 3 | interface Props { 4 | style?: React.CSSProperties 5 | className?: string 6 | children: React.ReactNode 7 | } 8 | 9 | export function BasicContent(props: Props) { 10 | const { children, className, style } = props; 11 | 12 | return ( 13 |
25 | { 26 | children 27 | } 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/basic-form/form-items/form-avatar-item.tsx: -------------------------------------------------------------------------------- 1 | import { UploadOutlined } from "@ant-design/icons"; 2 | 3 | import { Avatar, Button, Upload } from "antd"; 4 | import ImgCrop from "antd-img-crop"; 5 | 6 | interface FormAvatarItemProps { 7 | value?: string 8 | onChange?: (value: any) => void 9 | } 10 | 11 | export function FormAvatarItem({ value, onChange }: FormAvatarItemProps) { 12 | // const { t } = useTranslation(); 13 | 14 | // const onSelect: TreeProps["onSelect"] = (selectedKeys) => { 15 | // onChange?.(selectedKeys); 16 | // }; 17 | 18 | return ( 19 | <> 20 |
21 | 22 | 29 | { 38 | // if (info.file.status !== 'uploading') { 39 | // console.log(info.file, info.fileList); 40 | // } 41 | if (info.file.status === "done") { 42 | window.$message?.success(`${info.file.name} file uploaded successfully`); 43 | onChange?.(info.file.response?.result); 44 | } 45 | else if (info.file.status === "error") { 46 | window.$message?.error(`${info.file.name} file upload failed.`); 47 | } 48 | }} 49 | > 50 | 53 | 54 | 55 |
56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/basic-form/form-items/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./form-avatar-item"; 2 | export * from "./form-tree-item"; 3 | -------------------------------------------------------------------------------- /src/components/basic-form/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./form-items"; 2 | -------------------------------------------------------------------------------- /src/components/basic-table/constants.ts: -------------------------------------------------------------------------------- 1 | // 添加到 table 的 classname 2 | export const BASIC_TABLE_ROOT_CLASS_NAME = "basic-table"; 3 | -------------------------------------------------------------------------------- /src/components/basic-table/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-table-scroll"; 2 | -------------------------------------------------------------------------------- /src/components/basic-table/hooks/use-table-scroll/index.ts: -------------------------------------------------------------------------------- 1 | import { useSize } from "ahooks"; 2 | import { useRef } from "react"; 3 | 4 | export function useTableScroll(scrollX: number = 702) { 5 | const tableWrapperRef = useRef(null); 6 | 7 | const size = useSize(tableWrapperRef); 8 | 9 | const height = size?.height; 10 | 11 | const result = height && height < 600 ? height - 160 : undefined; 12 | 13 | const scrollConfig = { 14 | y: result, 15 | x: scrollX, 16 | }; 17 | 18 | return { 19 | tableWrapperRef, 20 | scrollConfig, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/basic-table/styles.ts: -------------------------------------------------------------------------------- 1 | import { createUseStyles } from "react-jss"; 2 | 3 | export const useStyles = createUseStyles(({ prefixCls, isDark }) => { 4 | return { 5 | basicTable: { 6 | [`& .${prefixCls}-table`]: { 7 | [`& .${prefixCls}-table-container`]: { 8 | [`& .${prefixCls}-table-content, & .${prefixCls}-table-body`]: { 9 | "scrollbar-width": "thin", 10 | "scrollbar-color": isDark ? "#909399 transparent" : "#eaeaea transparent", 11 | "scrollbar-gutter": "stable", 12 | }, 13 | }, 14 | }, 15 | }, 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/fullscreen-button/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from "antd"; 2 | import type { RefObject } from "react"; 3 | import { BasicButton } from "#src/components"; 4 | import { FullscreenExitOutlined, FullscreenOutlined } from "@ant-design/icons"; 5 | 6 | import { useFullscreen } from "ahooks"; 7 | 8 | export interface FullscreenButtonProps extends Omit { 9 | target: HTMLElement | (() => Element) | RefObject 10 | fullscreenIcon?: React.ReactNode 11 | fullscreenExitIcon?: React.ReactNode 12 | } 13 | 14 | /** 15 | * 全屏按钮组件 16 | * 17 | * @param target 全屏目标元素 18 | * @param fullscreenIcon 全屏时图标 19 | * @param fullscreenExitIcon 退出全屏时图标 20 | * @param restProps 其他属性 21 | * @returns 返回全屏按钮组件 22 | */ 23 | export const FullscreenButton: React.FC = ({ 24 | target, 25 | fullscreenIcon, 26 | fullscreenExitIcon, 27 | ...restProps 28 | }) => { 29 | const [isFullscreen, { toggleFullscreen }] = useFullscreen(target); 30 | 31 | return ( 32 | ) : (fullscreenExitIcon ?? )} 36 | onClick={toggleFullscreen} 37 | /> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/global-spin/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { useGlobalStore, usePreferencesStore } from "#src/store"; 3 | 4 | import { cn } from "#src/utils"; 5 | import { Spin } from "antd"; 6 | 7 | import { createUseStyles } from "react-jss"; 8 | import { useSpinDelay } from "spin-delay"; 9 | 10 | export interface GlobalSpinProps { 11 | className?: string 12 | children: ReactNode 13 | } 14 | 15 | const useStyles = createUseStyles({ 16 | rootSpin: { 17 | "height": "100%", 18 | "& .ant-spin-container": { 19 | height: "100%", 20 | }, 21 | "& .ant-spin-spinning": { 22 | maxHeight: "100% !important", 23 | }, 24 | }, 25 | }); 26 | 27 | export function GlobalSpin({ children, className }: GlobalSpinProps) { 28 | const classes = useStyles(); 29 | const spinning = useGlobalStore(state => state.globalSpin); 30 | /** 31 | * 接口返回结果时间过短,页面可能会出现闪烁,使用 useSpinDelay 优化 Spin 32 | * 33 | * @see https://github.com/ant-design/ant-design/issues/51828 34 | */ 35 | const loading = useSpinDelay(spinning, { delay: 500, minDuration: 200 }); 36 | const transitionLoading = usePreferencesStore(state => state.transitionLoading); 37 | 38 | if (!transitionLoading) { 39 | return children; 40 | }; 41 | 42 | return ( 43 | 48 | {children} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/iframe/index.tsx: -------------------------------------------------------------------------------- 1 | import { isValidElement } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useMatches } from "react-router"; 4 | 5 | export function Iframe() { 6 | const matches = useMatches(); 7 | const { t } = useTranslation(); 8 | const currentRoute = matches[matches.length - 1]; 9 | const iframeLink = currentRoute.handle?.iframeLink; 10 | const routeTitle = currentRoute.handle?.title; 11 | 12 | const title = ( 13 | isValidElement(routeTitle) ? t(routeTitle?.props.children) : routeTitle 14 | ) as string; 15 | 16 | return iframeLink 17 | ? ( 18 | /** 19 | * use this tool https://iframegenerator.top/ to generate the iframe code 20 | */ 21 |