├── .env ├── .env.development ├── .env.production ├── .env.staging ├── .github ├── pull_request_template.md └── workflows │ ├── auto-merge.yml │ ├── git-pages.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.CN.md ├── README.md ├── build └── vite │ ├── build.ts │ ├── plugins │ ├── index.ts │ ├── mock.ts │ ├── pwa.ts │ └── svg.ts │ ├── resolve.ts │ └── server.ts ├── commitlint.config.js ├── eslint.config.js ├── index.html ├── mock ├── account.mock.ts └── route.mock.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── pwa │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest └── vite.svg ├── src ├── App.tsx ├── assets │ ├── avatar.png │ ├── icons │ │ ├── locales.svg │ │ ├── login_Illustration.svg │ │ ├── moon.svg │ │ └── sun.svg │ ├── logo.png │ └── react.svg ├── components │ ├── AppLocale │ │ └── index.tsx │ ├── AppTheme │ │ ├── index.less │ │ └── index.tsx │ ├── FormattedMessage │ │ └── index.tsx │ ├── LayoutSpin │ │ └── index.tsx │ └── SvgIcon │ │ ├── index.less │ │ └── index.tsx ├── hooks │ ├── useRouteList.tsx │ ├── useTransformTheme.tsx │ └── web │ │ ├── antCharts │ │ ├── theme.json │ │ └── useChartsConfig.tsx │ │ └── useMessage.tsx ├── index.css ├── layout │ ├── Authority │ │ └── index.tsx │ ├── components │ │ ├── AppAccount │ │ │ ├── index.tsx │ │ │ └── style.tsx │ │ ├── AppLogo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── AppMain │ │ │ ├── AppMain.tsx │ │ │ ├── KeepAlive │ │ │ │ └── index.tsx │ │ │ ├── TabsPage │ │ │ │ ├── components │ │ │ │ │ └── TabsItemLabel.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── useTabsChange.ts │ │ │ │ │ └── useTabsState.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.tsx │ │ │ └── style.tsx │ │ ├── Navbart │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Setting │ │ │ ├── ThemeSettings │ │ │ │ ├── index.tsx │ │ │ │ └── style.tsx │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.tsx │ │ └── Sidebar │ │ │ ├── NavSidebar │ │ │ └── index.tsx │ │ │ ├── SidebarInline │ │ │ ├── index.less │ │ │ └── index.tsx │ │ │ └── hooks │ │ │ └── useMenuList.tsx │ ├── index.less │ └── index.tsx ├── locales │ ├── en_US │ │ ├── index.ts │ │ └── modules │ │ │ ├── api.json │ │ │ ├── layout.json │ │ │ └── login.json │ ├── index.tsx │ └── zh_CN │ │ ├── index.ts │ │ └── modules │ │ ├── api.json │ │ ├── layout.json │ │ └── login.json ├── main.tsx ├── router │ ├── index.tsx │ ├── lazy │ │ ├── view.ts │ │ └── whiteList.ts │ ├── modules │ │ └── index.tsx │ ├── route.d.ts │ └── utils.ts ├── server │ ├── route.ts │ └── useInfo.ts ├── store │ ├── hooks.ts │ ├── index.ts │ └── modules │ │ ├── app.ts │ │ ├── route.ts │ │ └── user.ts ├── utils │ ├── axios │ │ ├── axiosConfig.ts │ │ ├── axiosStatus.ts │ │ ├── errorConfig.ts │ │ ├── iAxios.ts │ │ └── index.ts │ ├── is.ts │ ├── operate │ │ └── index.ts │ └── storage │ │ ├── index.ts │ │ └── types.d.ts ├── views │ ├── DetailsPage │ │ ├── DetailsInfo │ │ │ └── index.tsx │ │ ├── DetailsParams │ │ │ └── index.tsx │ │ ├── hooks │ │ │ └── useInfoPageTabs.tsx │ │ └── index.tsx │ ├── Home │ │ ├── components │ │ │ ├── AreaChart.tsx │ │ │ ├── Comment.tsx │ │ │ ├── RoseChart.tsx │ │ │ └── WordCloudChart.tsx │ │ ├── index copy.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ └── style │ │ │ └── index.tsx │ ├── Login │ │ ├── index.tsx │ │ └── type.ts │ ├── Nested │ │ └── Menu1 │ │ │ ├── Menu1-1 │ │ │ └── index.tsx │ │ │ └── Menu1-2 │ │ │ └── index.tsx │ ├── Power │ │ ├── Permissions │ │ │ └── index.tsx │ │ ├── test-permissions-a │ │ │ └── index.tsx │ │ └── test-permissions-b │ │ │ └── index.tsx │ └── core │ │ ├── Refresh.tsx │ │ └── error │ │ ├── 403.tsx │ │ ├── 404.tsx │ │ ├── 500.tsx │ │ └── ErrorElement.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── types ├── axios.d.ts ├── env.d.ts └── global.d.ts └── vite.config.ts /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/.env -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_ENV = development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_ENV = production -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | VITE_ENV = staging 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## PR 标题 2 | 3 | 简要描述本次 Pull Request 的目的或变更内容。 4 | 5 | ## 变更类型 6 | 7 | 请勾选本次 PR 属于以下哪种类型(可多选): 8 | 9 | - [ ] Bug 修复(修复现有问题) 10 | - [ ] 新功能(添加新功能或特性) 11 | - [ ] 文档更新(仅更改文档) 12 | - [ ] 性能优化(提升性能或代码质量) 13 | - [ ] 测试添加(增加或完善测试用例) 14 | - [ ] 重构代码(不影响功能的代码优化) 15 | 16 | ## 相关问题 17 | 18 | 请关联相关的 Issue 编号(如适用): 19 | 20 | - 关联问题:#123 21 | 22 | ## 变更内容描述 23 | 24 | 详细描述本次变更的内容: 25 | 26 | 1. 添加/更新的功能或逻辑。 27 | 2. 影响的模块或文件。 28 | 3. 其他说明信息。 29 | 30 | ## 截图或录屏(如适用) 31 | 32 | 添加截图或录屏帮助审阅者更好地理解变更: 33 | ![示例截图](url-to-image.png) 34 | 35 | ## Checklist 36 | 37 | 在提交前,请确保已完成以下事项: 38 | 39 | - [ ] 我的代码遵循项目的编码规范。 40 | - [ ] 我已对代码进行了自测,并确保功能正常。 41 | - [ ] 我的变更不影响现有功能或测试通过。 42 | - [ ] 文档已更新(如适用)。 43 | 44 | ## 其他说明 45 | 46 | 其他需要告知审阅者的信息: 47 | 48 | - 示例:兼容性注意事项、已知问题等。 49 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto merge main into git-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [closed] 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | sync: 15 | if: github.event.pull_request.merged == true || github.event_name == 'push' 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout main branch 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Configure Git 25 | run: | 26 | git config user.name "github-actions[bot]" 27 | git config user.email "github-actions[bot]@users.noreply.github.com" 28 | 29 | - name: Add git-pages remote 30 | run: | 31 | git remote add git-pages https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git 32 | 33 | - name: Fetch git-pages branch 34 | run: git fetch origin git-pages:git-pages 35 | 36 | - name: Checkout git-page branch 37 | run: git checkout git-pages 38 | 39 | - name: Merge main into git-pages 40 | run: | 41 | git merge main --no-ff -m "feat: ✨ Auto-merge main into git-pages" 42 | 43 | - name: Push changes to git-pages 44 | run: git push origin git-pages 45 | 46 | - name: Repository Dispatch 47 | uses: peter-evans/repository-dispatch@v3 48 | with: 49 | event-type: git-pages-deploy 50 | -------------------------------------------------------------------------------- /.github/workflows/git-pages.yml: -------------------------------------------------------------------------------- 1 | name: Automatic deployment to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - git-pages 7 | repository_dispatch: 8 | types: [git-pages-deploy] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | ref: ${{ github.event_name == 'push' && github.ref || 'git-pages' }} 18 | fetch-depth: 0 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: '22' 27 | cache: pnpm 28 | 29 | - name: Setup Pages 30 | uses: actions/configure-pages@v5 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Build 36 | run: pnpm build 37 | 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: ./dist 42 | 43 | deploy: 44 | environment: 45 | name: github-pages 46 | url: ${{ steps.deployment.outputs.page_url }} 47 | permissions: 48 | contents: read 49 | pages: write 50 | id-token: write 51 | needs: build 52 | runs-on: ubuntu-latest 53 | name: Deploy 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | 3 | name: Release 4 | 5 | permissions: 6 | contents: write 7 | 8 | on: 9 | push: 10 | tags: 11 | - 'v*' 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | registry-url: https://registry.npmjs.org/ 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub # or changelogithub@0.12 if ensure the stable result 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | bin 14 | *.local 15 | 16 | # Editor directories and files 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | 2 | npx --no -- commitlint --edit 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | 2 | npm run lint:staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .local 2 | .output.js 3 | .npmrc 4 | 5 | **/*.svg 6 | **/*.sh 7 | 8 | dist 9 | public 10 | node_modules 11 | dist_electron -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "mikestead.dotenv", 4 | "dbaeumer.vscode-eslint", 5 | "lokalise.i18n-ally", 6 | "esbenp.prettier-vscode", 7 | "bradlc.vscode-tailwindcss", 8 | "naumovs.color-highlight", 9 | "donjayamanne.githistory" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.css": "tailwindcss" 4 | }, 5 | "eslint.validate": ["json", "javascript", "typescript", "typescriptreact", "javascriptreact"], 6 | "stylelint.enable": true, 7 | "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"], 8 | "i18n-ally.localesPaths": ["src/locales"], 9 | "i18n-ally.namespace": false, 10 | "i18n-ally.sortKeys": true, 11 | "i18n-ally.pathMatcher": "{locale}/modules/{namespaces}.json", 12 | "i18n-ally.displayLanguage": "zh_CN", 13 | "i18n-ally.enabledFrameworks": ["react"], 14 | "i18n-ally.sourceLanguage": "zh_CN", 15 | "i18n-ally.keystyle": "flat" 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 (2024-12-25) 2 | 3 | - ci: 🛠 pr 模版 ([05a1d02](https://github.com/jsxiaosi/react-xs-admin/commit/05a1d02)) 4 | - feat: ✨ 权限路由 ([7c98cb5](https://github.com/jsxiaosi/react-xs-admin/commit/7c98cb5)) 5 | - feat: ✨ 添加夜间主题切换 ([06aa81c](https://github.com/jsxiaosi/react-xs-admin/commit/06aa81c)) 6 | - feat: ✨ 跳转登录页 ([0521d53](https://github.com/jsxiaosi/react-xs-admin/commit/0521d53)) 7 | - feat: ✨ 修改 请求报错提示 ([92a9d24](https://github.com/jsxiaosi/react-xs-admin/commit/92a9d24)) 8 | - feat: ✨ 修改主题颜色 ([72e242c](https://github.com/jsxiaosi/react-xs-admin/commit/72e242c)) 9 | - feat: ✨ add @jsxiaosi/eslint-config-react ([13db953](https://github.com/jsxiaosi/react-xs-admin/commit/13db953)) 10 | - feat: ✨ add ant less ([f487d99](https://github.com/jsxiaosi/react-xs-admin/commit/f487d99)) 11 | - feat: ✨ add ant redux ([af754fb](https://github.com/jsxiaosi/react-xs-admin/commit/af754fb)) 12 | - feat: ✨ add home ([b8f381a](https://github.com/jsxiaosi/react-xs-admin/commit/b8f381a)) 13 | - feat: ✨ add project ([c7b9c6b](https://github.com/jsxiaosi/react-xs-admin/commit/c7b9c6b)) 14 | - feat: ✨ add react-intl ([d997cb6](https://github.com/jsxiaosi/react-xs-admin/commit/d997cb6)) 15 | - feat: ✨ ch md ([b8da08f](https://github.com/jsxiaosi/react-xs-admin/commit/b8da08f)) 16 | - feat: ✨ gitHub Actions ([08ac9c8](https://github.com/jsxiaosi/react-xs-admin/commit/08ac9c8)) 17 | - feat: ✨ package ([8246238](https://github.com/jsxiaosi/react-xs-admin/commit/8246238)) 18 | - feat: ✨ pwa ([5f8b604](https://github.com/jsxiaosi/react-xs-admin/commit/5f8b604)) 19 | - feat(app.tsx): ✨ [App] add 标签页 ([7cb8661](https://github.com/jsxiaosi/react-xs-admin/commit/7cb8661)) 20 | - feat(hooks): ✨ 升级react@19 ([2a582a6](https://github.com/jsxiaosi/react-xs-admin/commit/2a582a6)) 21 | - feat(layout): ✨ [layout] add 侧边栏设置 ([0585f2a](https://github.com/jsxiaosi/react-xs-admin/commit/0585f2a)) 22 | - feat(layout): ✨ add 色弱 灰色 redux-persist ([2b2ec65](https://github.com/jsxiaosi/react-xs-admin/commit/2b2ec65)) 23 | - feat(layout): ✨ add emotions css-in-js ([3ee69e5](https://github.com/jsxiaosi/react-xs-admin/commit/3ee69e5)) 24 | - feat(layout): ✨ appAccount ([ca63cf0](https://github.com/jsxiaosi/react-xs-admin/commit/ca63cf0)) 25 | - feat(layout): ✨ sidebar ([4edf8a2](https://github.com/jsxiaosi/react-xs-admin/commit/4edf8a2)) 26 | - feat(layout): ✨ tabs ([f706c2a](https://github.com/jsxiaosi/react-xs-admin/commit/f706c2a)) 27 | - feat(locales): ✨ [locales] 监听路由报错 ([1df5ca4](https://github.com/jsxiaosi/react-xs-admin/commit/1df5ca4)) 28 | - feat(package): ✨ eslint ([e9ce525](https://github.com/jsxiaosi/react-xs-admin/commit/e9ce525)) 29 | - feat(package): ✨ prettier ([9918450](https://github.com/jsxiaosi/react-xs-admin/commit/9918450)) 30 | - feat(views): ✨ [Pages] add 权限切换 ([b345524](https://github.com/jsxiaosi/react-xs-admin/commit/b345524)) 31 | - feat(views): ✨ 详情页 ([d3585e8](https://github.com/jsxiaosi/react-xs-admin/commit/d3585e8)) 32 | - feat(views): ✨ add login axios ([7c713d8](https://github.com/jsxiaosi/react-xs-admin/commit/7c713d8)) 33 | - feat(views): ✨ permissions ([da8262f](https://github.com/jsxiaosi/react-xs-admin/commit/da8262f)) 34 | - chore: 🔨 删除 VITE_KEY_ALIVE 环境变量 ([148a2fb](https://github.com/jsxiaosi/react-xs-admin/commit/148a2fb)) 35 | - refactor: ♻️ store 调用 ([166ba51](https://github.com/jsxiaosi/react-xs-admin/commit/166ba51)) 36 | - refactor(layout): ♻️ keepAllve 重构 keepalive-for-react 插件 ([a56f7cb](https://github.com/jsxiaosi/react-xs-admin/commit/a56f7cb)) 37 | - refactor(locales): ♻️ 国际化 ([69d9bdb](https://github.com/jsxiaosi/react-xs-admin/commit/69d9bdb)) 38 | - refactor(views): ♻️ login ([7ac1d29](https://github.com/jsxiaosi/react-xs-admin/commit/7ac1d29)) 39 | - refactor(vite build): ♻️ mock ([05f68e2](https://github.com/jsxiaosi/react-xs-admin/commit/05f68e2)) 40 | - build: 📦️ ant pro ([bb2ae03](https://github.com/jsxiaosi/react-xs-admin/commit/bb2ae03)) 41 | - build: 📦️ package.json ([21d6e3a](https://github.com/jsxiaosi/react-xs-admin/commit/21d6e3a)) 42 | - build: 📦️ plugin-react-swc ([3fefeaa](https://github.com/jsxiaosi/react-xs-admin/commit/3fefeaa)) 43 | - build: 📦️ pretty-quick ([4bb7b16](https://github.com/jsxiaosi/react-xs-admin/commit/4bb7b16)) 44 | - build: 📦️ update @jsxiaosi/eslint-config ([e9bc9b5](https://github.com/jsxiaosi/react-xs-admin/commit/e9bc9b5)) 45 | - build: 📦️ vite 4.3 ([6c812ce](https://github.com/jsxiaosi/react-xs-admin/commit/6c812ce)) 46 | - build: 📦️ vite4 ([19ef4d4](https://github.com/jsxiaosi/react-xs-admin/commit/19ef4d4)) 47 | - build(package): 📦️ 升级依赖 ([d2b14cb](https://github.com/jsxiaosi/react-xs-admin/commit/d2b14cb)) 48 | - build(router): 📦️ eslint ([828850c](https://github.com/jsxiaosi/react-xs-admin/commit/828850c)) 49 | - fix: 🐛 修改mock请求时间 ([721bd47](https://github.com/jsxiaosi/react-xs-admin/commit/721bd47)) 50 | - fix: 🐛 cN md ([f39e433](https://github.com/jsxiaosi/react-xs-admin/commit/f39e433)) 51 | - fix: 🐛 eslint ([97a884b](https://github.com/jsxiaosi/react-xs-admin/commit/97a884b)) 52 | - fix: 🐛 html loading ([e077c79](https://github.com/jsxiaosi/react-xs-admin/commit/e077c79)) 53 | - fix: 🐛 lint-staged 查找不到文件 ([fcdeb32](https://github.com/jsxiaosi/react-xs-admin/commit/fcdeb32)) 54 | - fix: 🐛 mork ([0d211a1](https://github.com/jsxiaosi/react-xs-admin/commit/0d211a1)) 55 | - fix: emotion WebkitLineClamp ([6ecee07](https://github.com/jsxiaosi/react-xs-admin/commit/6ecee07)) 56 | - fix(layout): 🐛 [layout] tabs size="small" ([7517410](https://github.com/jsxiaosi/react-xs-admin/commit/7517410)) 57 | - fix(layout): 🐛 keepalive Authority judgment ([a4e9aee](https://github.com/jsxiaosi/react-xs-admin/commit/a4e9aee)) 58 | - fix(layout): 🐛 layoutApp ([2c8f484](https://github.com/jsxiaosi/react-xs-admin/commit/2c8f484)) 59 | - fix(layout): 🐛 memu 国际化 ([cbb17c7](https://github.com/jsxiaosi/react-xs-admin/commit/cbb17c7)) 60 | - fix(layout): 🐛 repair that the switch permission page did not refresh ([7d2d898](https://github.com/jsxiaosi/react-xs-admin/commit/7d2d898)) 61 | - fix(layout): 🐛 sidebar ([a85089b](https://github.com/jsxiaosi/react-xs-admin/commit/a85089b)) 62 | - fix(layout): 🐛 sitebar ([7636e1d](https://github.com/jsxiaosi/react-xs-admin/commit/7636e1d)) 63 | - fix(layout): 🐛 tabsPage ([e43e018](https://github.com/jsxiaosi/react-xs-admin/commit/e43e018)), closes [#1](https://github.com/jsxiaosi/react-xs-admin/issues/1) 64 | - fix(locales): 🐛 国际化类型,index.html ([b4564e6](https://github.com/jsxiaosi/react-xs-admin/commit/b4564e6)) 65 | - fix(package): 🐛 husky ([e127169](https://github.com/jsxiaosi/react-xs-admin/commit/e127169)) 66 | - fix(router): 🐛 datainfo page Display Error ([624dc0d](https://github.com/jsxiaosi/react-xs-admin/commit/624dc0d)) 67 | - fix(router): 🐛 when the routing setting alwaysShow is false, undefined problems will occur ([7925de9](https://github.com/jsxiaosi/react-xs-admin/commit/7925de9)) 68 | - fix(store): 🐛 user ([7198854](https://github.com/jsxiaosi/react-xs-admin/commit/7198854)) 69 | - fix(utils): 🐛 axios type error ([e2f9779](https://github.com/jsxiaosi/react-xs-admin/commit/e2f9779)) 70 | - style: 💄 console.log ([3b72771](https://github.com/jsxiaosi/react-xs-admin/commit/3b72771)) 71 | - style(eslint): 💄 eslint-style ([14a3787](https://github.com/jsxiaosi/react-xs-admin/commit/14a3787)) 72 | - fix(router,views): 🐛 permissions ([43f167d](https://github.com/jsxiaosi/react-xs-admin/commit/43f167d)) 73 | - perf(hooks,router): ⚡️ 路由参数优化 ([8e37a65](https://github.com/jsxiaosi/react-xs-admin/commit/8e37a65)) 74 | - perf: ⚡️ html、README.md ([19a0ebe](https://github.com/jsxiaosi/react-xs-admin/commit/19a0ebe)) 75 | - perf(layout): ⚡️ 优化layout ([94f21f9](https://github.com/jsxiaosi/react-xs-admin/commit/94f21f9)) 76 | - perf(layout): ⚡️ sitebar,route ([7251e3d](https://github.com/jsxiaosi/react-xs-admin/commit/7251e3d)) 77 | - docs: 📝 add README LICENSE ([44d526f](https://github.com/jsxiaosi/react-xs-admin/commit/44d526f)) 78 | - docs: 📝 img ([5122396](https://github.com/jsxiaosi/react-xs-admin/commit/5122396)) 79 | - docs: 📝 main English ([3dfcaaf](https://github.com/jsxiaosi/react-xs-admin/commit/3dfcaaf)) 80 | - docs: md ([ec74e9d](https://github.com/jsxiaosi/react-xs-admin/commit/ec74e9d)) 81 | - docs: md ([6d97cbc](https://github.com/jsxiaosi/react-xs-admin/commit/6d97cbc)) 82 | - wip(router): 🚀 [App] add route ([1bb2274](https://github.com/jsxiaosi/react-xs-admin/commit/1bb2274)) 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 小斯(xiaosi) 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.CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

react-xs-admin

4 |
5 |
6 | 7 | **中文** | [English](./README.md) 8 | 9 | ## 简介 10 | 11 | 基于`React18`,`And Design`,`Vite4`等主流技术开发的开箱即用后台模板,内置国际化、路由权限控制等方案能够满足多数企业管理系统需求! 12 | 13 | ## 特性 14 | 15 | - **最新技术栈**:使用 React18/Vite4 等前端前沿技术开发 16 | - **主题**:可配置的主题 17 | - **国际化**:内置完善的国际化方案 18 | - **权限**:权限路由 19 | 20 | ## 预览 21 | 22 | - [github 站点](https://jsxiaosi.github.io/react-xs-admin/) 23 | - [gitee 站点](https://jsxiaosi.gitee.io/react-xs-admin/) 24 | 25 | 26 | 27 | 28 | 29 | ## 准备 30 | 31 | - [Node](http://nodejs.org/) 和 [Git](https://git-scm.com/) -项目开发环境 32 | - [Vite](https://cn.vitejs.dev/) - 熟悉 Vite 特性 33 | - [React18](https://reactjs.org/) - 熟悉 React18 基础语法 34 | - [Es6+](http://es6.ruanyifeng.com/) - 熟悉 Es6 基本语法 35 | - [React Router V6](https://reactrouter.com/en/main) - 熟悉 React Router V6 基本使用 36 | - [And Design 5](https://ant.design/docs/react/introduce-cn) - Ui 基本使用 37 | - [Emotion](https://emotion.sh/docs/introduction) - 基本使用 38 | 39 | ## 安装使用 40 | 41 | ### 1. 获取项目代码(Https or SSH) 42 | 43 | ```bash 44 | git clone https://github.com/jsxiaosi/react-xs-admin.git 45 | 46 | git clone git@github.com:jsxiaosi/react-xs-admin.git 47 | ``` 48 | 49 | 或者通过[`xs-cli`](https://github.com/jsxiaosi/xs-cli)快速创建 50 | 51 | ```bash 52 | npx @jsxiaosi/xs-cli create [project-name] 53 | ``` 54 | 55 | ### 2.安装依赖 56 | 57 | ```bash 58 | cd react-xs-admin 59 | ``` 60 | 61 | 推荐使用`pnpm` 62 | 63 | ```bash 64 | pnpm i 65 | ``` 66 | 67 | `npm`安装 68 | 69 | ```bash 70 | npm install 71 | 72 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 73 | # 如果下载依赖慢可以使用淘宝镜像源安装依赖 74 | npm install --registry=https://registry.npm.taobao.org 75 | 76 | ``` 77 | 78 | ### 3.运行 79 | 80 | ```bash 81 | npm run dev 82 | ``` 83 | 84 | ### 4.打包 85 | 86 | ```bash 87 | npm run build 88 | ``` 89 | 90 | ## 项目地址 91 | 92 | - [react-xs-admin](https://github.com/jsxiaosi/react-xs-admin) 93 | 94 | ## 如何贡献 95 | 96 | **Pull Request:** 97 | 98 | 1. Fork 代码! 99 | 2. 创建自己的分支: `git checkout -b feature/xxxx` 100 | 3. 提交你的修改: `git commit -m 'feature: add xxxxx'` 101 | 4. 推送您的分支: `git push origin feature/xxxx` 102 | 5. 提交`pull request` 103 | 104 | ## Git 贡献提交规范 105 | 106 | - 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 107 | 108 | - `feat` 新增功能 109 | - `fix` 修复缺陷 110 | - `docs` 文档变更 111 | - `style` 代码格式 112 | - `refactor` 代码重构 113 | - `perf` 性能优化 114 | - `test` 添加疏漏测试或已有测试改动 115 | - `build` 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等) 116 | - `ci` 修改 CI 配置、脚本 117 | - `revert` 回滚 commit 118 | - `chore` 对构建过程或辅助工具和库的更改 (不影响源文件) 119 | - `wip` 正在开发中 120 | - `types` 类型定义文件修改 121 | 122 | ## 浏览器支持 123 | 124 | 本地开发推荐使用`Chrome 80+` 浏览器 125 | 126 | 支持现代浏览器, 不支持 IE 127 | 128 | | [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 129 | | :-: | :-: | :-: | :-: | :-: | 130 | | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 131 | 132 | ## 维护者 133 | 134 | [@jsxiaosi](https://github.com/jsxiaosi) 135 | 136 | ## License 137 | 138 | [MIT © 2022](./LICENSE) 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

react-xs-admin

4 |
5 |
6 | 7 | **English** | [中文](./README.CN.md) 8 | 9 | ## Brief Introduction 10 | 11 | The out of the box background template developed based on mainstream technologies such as `React18`,`And Design`,`Vite4`, and the built-in internationalization, routing permission control and other solutions can meet the needs of most enterprise management systems! 12 | 13 | ## Characteristic 14 | 15 | - **The latest technology stack**: Developed using front-end cutting-edge technologies such as React18/Vite4 16 | 17 | - **Theme**: configurable theme 18 | 19 | - **Internationalization**: built-in perfect internationalization scheme 20 | 21 | - **Permission**: Permission Routing 22 | 23 | ## Preview 24 | 25 | - [github site](https://jsxiaosi.github.io/react-xs-admin/) 26 | - [gitee site](http://jsxiaosi.gitee.io/react-xs-admin/) 27 | 28 | 29 | 30 | 31 | 32 | ## Prepare 33 | 34 | - [Node](http://nodejs.org/) And [Git](https://git-scm.com/) - Project development environment 35 | - [Vite](https://cn.vitejs.dev/) - Familiar with Vite features 36 | - [React18](https://reactjs.org/) - Familiar with the basic syntax of React18 37 | - [Es6+](http://es6.ruanyifeng.com/) - familiar with the basic syntax of Es6 38 | - [React Router V6](https://reactrouter.com/en/main) - Familiar with the basic use of React Router V6 39 | - [And Design 5](https://ant.design/docs/react/introduce-cn) - Ui basic use 40 | - [Emotion](https://emotion.sh/docs/introduction) - Basic use 41 | 42 | ## Installation and use 43 | 44 | ### 1. Get project code (Https or SSH) 45 | 46 | ```bash 47 | git clone https://github.com/jsxiaosi/react-xs-admin.git 48 | 49 | git clone git@github.com:jsxiaosi/react-xs-admin.git 50 | ``` 51 | 52 | Alternatively, you can use the [`xs-cli`](https://github.com/jsxiaosi/xs-cli)to quickly create one 53 | 54 | ```bash 55 | npx @jsxiaosi/xs-cli create [project-name] 56 | ``` 57 | 58 | ### 2.Installation Dependencies 59 | 60 | ```bash 61 | cd react-xs-admin 62 | ``` 63 | 64 | Recommended`pnpm` 65 | 66 | ```bash 67 | pnpm i 68 | ``` 69 | 70 | `npm` install 71 | 72 | ```bash 73 | npm install 74 | ``` 75 | 76 | ### 3.Developer 77 | 78 | ```bash 79 | npm run dev 80 | ``` 81 | 82 | ### 4.Production 83 | 84 | ```bash 85 | npm run build 86 | ``` 87 | 88 | ## Project address 89 | 90 | - [react-xs-admin](https://github.com/jsxiaosi/react-xs-admin) 91 | 92 | ## How to contribute 93 | 94 | **Pull Request:** 95 | 96 | 1. Fork Code! 97 | 2. Create your own branch: `git checkout -b feature/xxxx` 98 | 3. Submit your changes: `git commit -m 'feature: add xxxxx'` 99 | 4. Push your branch: `git push origin feature/xxxx` 100 | 5. Submit: `pull request` 101 | 102 | ## Git Contribution submission specification 103 | 104 | - Refer to [Vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification 105 | 106 | - `feat` New Features 107 | - `fix` Repair defects 108 | - `docs` Document change 109 | - `style` Code format 110 | - `refactor` Code refactoring 111 | - `perf` Performance optimization 112 | - `test` Add neglected tests or changes to existing tests 113 | - `build` Build processes, external dependency changes (such as upgrading npm packages, modifying packaging configurations, etc.) 114 | - `ci` Modify CI configuration and scripts 115 | - `revert` Roll back the commit 116 | - `chore` Changes to the build process or tools and libraries (do not affect source files) 117 | - `wip` Under development 118 | - `types` Type definition file modification 119 | 120 | ## Browser Support 121 | 122 | Chrome 80+ is recommended for local development 123 | 124 | Supports modern browsers, not Internet Explorer 125 | 126 | | [ Edge](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 127 | | :-: | :-: | :-: | :-: | :-: | 128 | | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 129 | 130 | ## maintainers 131 | 132 | [@jsxiaosi](https://github.com/jsxiaosi) 133 | 134 | ## License 135 | 136 | [MIT © 2022](./LICENSE) 137 | -------------------------------------------------------------------------------- /build/vite/build.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions } from 'vite'; 2 | 3 | export function createViteBuild(): BuildOptions { 4 | const viteBuild = { 5 | target: 'es2015', 6 | // 指定输出路径 7 | outDir: 'dist', 8 | cssTarget: 'chrome80', 9 | 10 | // 指定生成静态资源的存放路径 11 | assetsDir: 'static', 12 | // 启用/禁用 CSS 代码拆分。当启用时,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在块加载时插入 如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。 13 | cssCodeSplit: true, 14 | // 构建后是否生成 source map 文件。 15 | sourcemap: false, 16 | // 启用/禁用 brotli 压缩大小报告。压缩大型输出文件可能会很慢,因此禁用该功能可能会提高大型项目的构建性能。 17 | brotliSize: false, 18 | // minify: 'terser', 19 | // terserOptions: { 20 | // compress: { 21 | // // 打包清除console 22 | // drop_console: true, 23 | // }, 24 | // }, 25 | // chunk 大小警告的限制(以 kbs 为单位) 26 | chunkSizeWarningLimit: 2000, 27 | }; 28 | return viteBuild; 29 | } 30 | -------------------------------------------------------------------------------- /build/vite/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | // import react from '@vitejs/plugin-react-swc'; 3 | import type { ConfigEnv, PluginOption } from 'vite'; 4 | import { configMockPlugin } from './mock'; 5 | 6 | // svg配置 7 | import { configSvgPlugin } from './svg'; 8 | 9 | export function createVitePlugins(_isBuild = false, _configEnv: ConfigEnv) { 10 | const vitePlugins: PluginOption[] = []; 11 | 12 | vitePlugins.push( 13 | react({ 14 | jsxImportSource: '@emotion/react', 15 | babel: { 16 | plugins: ['@emotion/babel-plugin'], 17 | }, 18 | }), 19 | ); 20 | 21 | vitePlugins.push(configSvgPlugin()); 22 | 23 | vitePlugins.push(configMockPlugin()); 24 | 25 | return vitePlugins; 26 | } 27 | -------------------------------------------------------------------------------- /build/vite/plugins/mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock plugin for development and production. 3 | * https://github.com/anncwb/vite-plugin-mock 4 | */ 5 | import { vitePluginFakeServer } from 'vite-plugin-fake-server'; 6 | 7 | export function configMockPlugin() { 8 | return vitePluginFakeServer({ 9 | logger: false, 10 | include: 'mock', 11 | infixName: false, 12 | enableProd: true, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /build/vite/plugins/pwa.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * pwa 3 | * https://vite-plugin-pwa.netlify.app 4 | */ 5 | import { VitePWA } from 'vite-plugin-pwa'; 6 | 7 | export function configPwaPlugin() { 8 | const options = { 9 | includeAssets: ['favicon.svg', 'favicon.ico', 'robots.txt', 'apple-touch-icon.png'], 10 | logLevel: 'silent', 11 | manifest: { 12 | name: '小斯管理后台模板', 13 | short_name: '小斯后台模板', 14 | description: '基于 vue3+vite+element-push 搭建的后台模板', 15 | theme_color: '#ffffff', 16 | icons: [ 17 | { 18 | src: '/pwa/android-chrome-192x192.png', 19 | sizes: '192x192', 20 | type: 'image/png', 21 | }, 22 | { 23 | src: '/pwa/android-chrome-512x512.png', 24 | sizes: '512x512', 25 | type: 'image/png', 26 | }, 27 | { 28 | src: '/pwa/android-chrome-512x512.png', 29 | sizes: '512x512', 30 | type: 'image/png', 31 | purpose: 'any maskable', 32 | }, 33 | ], 34 | }, 35 | }; 36 | 37 | return VitePWA(options); 38 | } 39 | -------------------------------------------------------------------------------- /build/vite/plugins/svg.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * svg 3 | * https://github.com/anncwb/vite-plugin-svg-icons/blob/main/README.zh_CN.md 4 | */ 5 | import path from 'path'; 6 | import process from 'process'; 7 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; 8 | 9 | export function configSvgPlugin() { 10 | const svgPlugin = createSvgIconsPlugin({ 11 | // 指定需要缓存的图标文件夹 12 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], 13 | // 压缩配置 14 | // svgoOptions: false, 15 | // 指定symbolId格式 16 | symbolId: 'icon-[dir]-[name]', 17 | }); 18 | return svgPlugin; 19 | } 20 | -------------------------------------------------------------------------------- /build/vite/resolve.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { AliasOptions, ResolveOptions } from 'vite'; 3 | 4 | type myResolveOptions = ResolveOptions & { alias?: AliasOptions }; 5 | 6 | export function createViteResolve(myDirname: string): myResolveOptions { 7 | const viteResolve: myResolveOptions = { 8 | // 引用别名配置 9 | alias: { 10 | // 配置@别名 11 | '@': `${path.resolve(myDirname, 'src')}`, 12 | // 配置#别名 13 | '#': `${path.resolve(myDirname, 'types')}`, 14 | }, 15 | // 导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会干扰 IDE 和类型支持。 16 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'], 17 | }; 18 | 19 | return viteResolve; 20 | } 21 | -------------------------------------------------------------------------------- /build/vite/server.ts: -------------------------------------------------------------------------------- 1 | import type { ServerOptions } from 'vite'; 2 | 3 | export function createViteServer(): ServerOptions { 4 | const viteServer: ServerOptions = { 5 | // 服务器主机名,如果允许外部访问,可设置为"0.0.0.0" 6 | host: true, 7 | // 服务器端口号 8 | port: 5173, 9 | // 端口已被占用时是否尝试使用下一个可用的端口 true:直接退出,而不是尝试下一个可用端口 false:尝试下一个可用端口 10 | strictPort: false, 11 | // boolean | string 启动项目时自动在浏览器打开应用程序;如果为string,比如"/index.html",会打开http://localhost:5173/index.html 12 | // open: true, 13 | // boolean | CorsOptions 为开发服务器配置 CORS。默认启用并允许任何源,传递一个 选项对象 来调整行为或设为 false 表示禁用。 14 | // cors: true, 15 | // 设置为 true 强制使依赖预构建。 16 | // force: false, 17 | // 自定义代理规则 18 | proxy: { 19 | '/api': { 20 | target: '', 21 | changeOrigin: true, 22 | rewrite: path => path.replace(/^\/api/, ''), 23 | }, 24 | }, 25 | }; 26 | return viteServer; 27 | } 28 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const scopes = fs 7 | .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'src')) 8 | .map(i => i.toLowerCase()); 9 | 10 | const gitStatus = execSync('git status --porcelain || true').toString().trim().split('\n'); 11 | 12 | const scopeComplete = gitStatus 13 | .find(r => ~r.indexOf('M src')) 14 | ?.replace(/(\/)/g, '%%') 15 | ?.match(/src%%((\w|-)*)/)?.[1]; 16 | 17 | const subjectComplete = gitStatus 18 | .find(r => ~r.indexOf('M src')) 19 | ?.replace(/\//g, '%%') 20 | ?.match(/src%%((\w|-)*)/)?.[1]; 21 | 22 | const Configuration = { 23 | extends: ['@jsxiaosi/commitlint-config'], 24 | prompt: { 25 | // 范围设置 26 | scopes: [...scopes, 'mock'], 27 | // 范围是否可以多选 28 | enableMultipleScopes: true, 29 | // 多选范围后用标识符隔开 30 | scopeEnumSeparator: ',', 31 | // 设置 选择范围 中 为空选项(empty) 和 自定义选项(custom) 的 位置 32 | customScopesAlign: !scopeComplete ? 'top' : 'bottom', 33 | // 如果 defaultScope 与在选择范围列表项中的 value 相匹配就会进行星标置顶操作。 34 | defaultScope: scopeComplete, 35 | // 描述预设值 36 | defaultSubject: subjectComplete && `[${subjectComplete}] `, 37 | }, 38 | }; 39 | 40 | export default Configuration; 41 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import jsxiaosi from '@jsxiaosi/eslint-config'; 2 | 3 | export default jsxiaosi({ 4 | typescript: true, 5 | react: true, 6 | prettier: { 7 | usePrettierrc: true, 8 | }, 9 | rules: { 10 | 'react/no-unknown-property': ['error', { ignore: ['css'] }], 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /mock/account.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from 'vite-plugin-fake-server/client'; 2 | 3 | const userInfo = { 4 | name: '爱喝蜂蜜绿的小斯斯', 5 | userid: '00000001', 6 | email: '1531733886@qq.com', 7 | signature: '甜甜的蜂蜜,甘甜的绿茶,蜂蜜中和了绿茶的苦涩保留了绿茶回甘,绝妙啊', 8 | introduction: '微笑着,努力着,欣赏着', 9 | title: '小斯斯', 10 | token: '', 11 | power: 'admin', 12 | }; 13 | 14 | const userInfo2 = { 15 | name: 'test', 16 | userid: '00000002', 17 | email: '12312311223@qq.com', 18 | signature: '小啊小啊浪', 19 | introduction: '一个只会喝蜂蜜绿的小前端', 20 | title: '咪咪咪', 21 | token: '', 22 | power: 'test', 23 | }; 24 | 25 | export default defineFakeRoute([ 26 | { 27 | url: '/mock_api/login', 28 | timeout: 1000, 29 | method: 'post', 30 | response: ({ body }: { body: Recordable }) => { 31 | const { username, password } = body; 32 | if (username === 'admin' && password === 'admin123') { 33 | userInfo.token = genID(16); 34 | return { 35 | data: userInfo, 36 | code: 1, 37 | message: 'ok', 38 | }; 39 | } else if (username === 'test' && password === 'test123') { 40 | userInfo2.token = genID(16); 41 | return { 42 | data: userInfo2, 43 | code: 1, 44 | message: 'ok', 45 | }; 46 | } else { 47 | return { 48 | data: null, 49 | code: -1, 50 | message: '账号密码错误', 51 | }; 52 | } 53 | }, 54 | // rawResponse: async (req, res) => { 55 | // console.log(req, res); 56 | // let reqbody = {}; 57 | // res.setHeader('Content-Type', 'application/json'); 58 | // reqbody = { data: userInfo }; 59 | // res.statusCode = 500; 60 | // res.end(JSON.stringify(reqbody)); 61 | // }, 62 | }, 63 | { 64 | url: '/mock_api/getUserInfo', 65 | timeout: 1000, 66 | method: 'get', 67 | response: () => { 68 | return userInfo; 69 | }, 70 | }, 71 | ]); 72 | 73 | function genID(length: number) { 74 | return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36); 75 | } 76 | -------------------------------------------------------------------------------- /mock/route.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineFakeRoute } from 'vite-plugin-fake-server/client'; 2 | 3 | const power = [ 4 | { 5 | path: '/home', 6 | id: 'Home', 7 | }, 8 | { 9 | path: '/nested', 10 | id: 'Nested', 11 | children: [ 12 | { 13 | path: 'menu1', 14 | id: 'Menu1', 15 | children: [ 16 | { 17 | path: 'menu1-1', 18 | id: 'Menu1-1', 19 | }, 20 | { 21 | path: 'menu1-2', 22 | id: 'Menu1-2', 23 | }, 24 | ], 25 | }, 26 | ], 27 | }, 28 | ]; 29 | 30 | const adminRoute = [ 31 | { 32 | path: '/power', 33 | id: 'Power', 34 | children: [ 35 | { 36 | path: 'permissions', 37 | id: 'Permissions', 38 | }, 39 | { 40 | path: 'test-permissions-a', 41 | id: 'TestPermissionsA', 42 | }, 43 | ], 44 | }, 45 | ]; 46 | 47 | const testRoute = [ 48 | { 49 | path: '/power', 50 | id: 'Power', 51 | children: [ 52 | { 53 | path: 'permissions', 54 | id: 'Permissions', 55 | }, 56 | { 57 | path: 'test-permissions-b', 58 | id: 'TestPermissionsB', 59 | }, 60 | ], 61 | }, 62 | ]; 63 | 64 | export default defineFakeRoute([ 65 | { 66 | url: '/mock_api/getRoute', 67 | timeout: 500, 68 | method: 'post', 69 | response: ({ body }: { body: Recordable }) => { 70 | const { name } = body; 71 | if (name === 'admin') { 72 | return { 73 | data: [...power, ...adminRoute], 74 | code: 1, 75 | message: 'ok', 76 | }; 77 | } else if (name === 'test') { 78 | return { 79 | data: [...power, ...testRoute], 80 | code: 1, 81 | message: 'ok', 82 | }; 83 | } else { 84 | return { 85 | data: [], 86 | code: -1, 87 | message: '账号错误', 88 | }; 89 | } 90 | }, 91 | }, 92 | ]); 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-xs-admin", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "packageManager": "pnpm@9.0.0", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">=18", 10 | "pnpm": ">=9" 11 | }, 12 | "scripts": { 13 | "dev": "vite", 14 | "build": "tsc && vite build", 15 | "staging": "tsc && vite build --mode staging", 16 | "preview": "vite preview", 17 | "lint:eslint": "eslint \"{src,mock,build}/**/*.{ts,js,tsx,jsx}\" package.json --fix", 18 | "lint:prettier": "prettier . --write", 19 | "lint:staged": "lint-staged", 20 | "check:type": "tsc --noEmit", 21 | "lint:eslint-insppect-config": "npx @eslint/config-inspector --config eslint.config.js", 22 | "log": "conventional-changelog -p cmyr-config -i CHANGELOG.md -s -r 0", 23 | "tsc": "tsc --noEmit --skipLibCheck", 24 | "cz": "czg", 25 | "prepare": "husky" 26 | }, 27 | "dependencies": { 28 | "@ant-design/charts": "^1.4.3", 29 | "@ant-design/colors": "^7.1.0", 30 | "@ant-design/icons": "^5.5.2", 31 | "@emotion/react": "^11.14.0", 32 | "@emotion/styled": "^11.14.0", 33 | "@reduxjs/toolkit": "^2.5.0", 34 | "ahooks": "^3.8.4", 35 | "antd": "^5.22.5", 36 | "axios": "^1.7.9", 37 | "classnames": "^2.5.1", 38 | "dayjs": "^1.11.13", 39 | "keepalive-for-react": "^3.0.7", 40 | "localforage": "^1.10.0", 41 | "lodash": "^4.17.21", 42 | "lodash-es": "^4.17.21", 43 | "match-sorter": "^8.0.0", 44 | "react": "^19.0.0", 45 | "react-dom": "^19.0.0", 46 | "react-intl": "^7.0.4", 47 | "react-redux": "^9.2.0", 48 | "react-router": "^7.1.0", 49 | "react-router-dom": "^7.1.0", 50 | "redux-persist": "^6.0.0", 51 | "sort-by": "^1.2.0" 52 | }, 53 | "devDependencies": { 54 | "@emotion/babel-plugin": "^11.13.5", 55 | "@emotion/eslint-plugin": "^11.12.0", 56 | "@eslint-react/eslint-plugin": "^1.22.0", 57 | "@jsxiaosi/commitlint-config": "^1.0.9", 58 | "@jsxiaosi/eslint-config": "^1.0.9", 59 | "@jsxiaosi/eslint-config-prettier": "^1.0.9", 60 | "@types/crypto-js": "^4.2.2", 61 | "@types/lodash-es": "^4.17.12", 62 | "@types/react": "^19.0.2", 63 | "@types/react-dom": "^19.0.2", 64 | "@vitejs/plugin-react": "^4.3.4", 65 | "@vitejs/plugin-react-swc": "^3.7.2", 66 | "autoprefixer": "^10.4.20", 67 | "commitlint": "^19.6.1", 68 | "conventional-changelog-cli": "^5.0.0", 69 | "crypto-js": "^4.2.0", 70 | "czg": "^1.11.0", 71 | "eslint": "^9.17.0", 72 | "eslint-plugin-prettier": "^5.2.1", 73 | "eslint-plugin-react": "^7.37.3", 74 | "eslint-plugin-react-hooks": "^5.1.0", 75 | "eslint-plugin-react-refresh": "^0.4.16", 76 | "husky": "^9.1.7", 77 | "less": "^4.2.1", 78 | "lint-staged": "^15.2.11", 79 | "postcss": "^8.4.49", 80 | "prettier": "^3.4.2", 81 | "tailwindcss": "^3.4.17", 82 | "typescript": "^5.7.2", 83 | "vite": "^6.0.5", 84 | "vite-plugin-fake-server": "^2.1.4", 85 | "vite-plugin-pwa": "^0.21.1", 86 | "vite-plugin-svg-icons": "^2.0.1" 87 | }, 88 | "pnpm": { 89 | "peerDependencyRules": { 90 | "ignoreMissing": [ 91 | "rollup", 92 | "@babel/core" 93 | ] 94 | } 95 | }, 96 | "config": { 97 | "commitizen": { 98 | "path": "node_modules/cz-git" 99 | } 100 | }, 101 | "lint-staged": { 102 | "{src,mock,build}/**/*.{ts,js,tsx,jsx}": [ 103 | "eslint --fix", 104 | "prettier --write" 105 | ], 106 | "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [ 107 | "eslint --fix" 108 | ], 109 | "package.json": [ 110 | "prettier --write" 111 | ], 112 | "*.md": [ 113 | "eslint --fix", 114 | "prettier --write" 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import prettierConfig from '@jsxiaosi/eslint-config-prettier'; 2 | 3 | export default { 4 | ...prettierConfig, 5 | }; 6 | -------------------------------------------------------------------------------- /public/pwa/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/pwa/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/pwa/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/apple-touch-icon.png -------------------------------------------------------------------------------- /public/pwa/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/pwa/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/favicon-16x16.png -------------------------------------------------------------------------------- /public/pwa/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/favicon-32x32.png -------------------------------------------------------------------------------- /public/pwa/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/favicon.ico -------------------------------------------------------------------------------- /public/pwa/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/public/pwa/mstile-150x150.png -------------------------------------------------------------------------------- /public/pwa/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-xs-admin", 3 | "short_name": "react-xs-admin", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider, theme } from 'antd'; 2 | import enUS from 'antd/locale/en_US'; 3 | import zhCN from 'antd/locale/zh_CN'; 4 | import dayjs from 'dayjs'; 5 | import { Suspense, useEffect, useMemo } from 'react'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { shallowEqual } from 'react-redux'; 8 | import LayoutSpin from './components/LayoutSpin'; 9 | import { localeConfig, setIntl } from './locales'; 10 | 11 | import RouteView from './router'; 12 | import { initAsyncRoute } from './router/utils'; 13 | import { useAppSelector } from './store/hooks'; 14 | import 'dayjs/locale/zh-cn'; 15 | import 'dayjs/locale/en'; 16 | import 'antd/dist/reset.css'; 17 | 18 | function App() { 19 | const { locale, color, themeMode } = useAppSelector( 20 | state => ({ 21 | locale: state.app.locale, 22 | color: state.app.color, 23 | themeMode: state.app.themeMode, 24 | }), 25 | shallowEqual, 26 | ); 27 | const { userInfo } = useAppSelector(state => state.user); 28 | const asyncRouter = useAppSelector(state => state.route.asyncRouter); 29 | 30 | const getLocale = useMemo(() => { 31 | setIntl(locale); 32 | if (locale === 'en-US') { 33 | dayjs.locale('en'); 34 | return enUS; 35 | } else { 36 | dayjs.locale('zh-cn'); 37 | return zhCN; 38 | } 39 | }, [locale]); 40 | 41 | useEffect(() => { 42 | if (!asyncRouter.length && userInfo) { 43 | initAsyncRoute(userInfo.power); 44 | } 45 | }, []); 46 | 47 | const loading = useMemo(() => { 48 | if (!asyncRouter.length && userInfo) { 49 | return true; 50 | } 51 | return false; 52 | }, [asyncRouter]); 53 | 54 | return ( 55 | 64 | 65 | {loading ? ( 66 | 67 | ) : ( 68 | // 69 | }> 70 | 71 | 72 | // 73 | )} 74 | 75 | 76 | ); 77 | } 78 | 79 | export default App; 80 | -------------------------------------------------------------------------------- /src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/src/assets/avatar.png -------------------------------------------------------------------------------- /src/assets/icons/locales.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 10 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 15 | 17 | 19 | 22 | 25 | 28 | 31 | 33 | 36 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsxiaosi/react-xs-admin/e162ee5f9241be4e4cc3c56cd8a59b1dd3b2f910/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AppLocale/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 2 | import { setAppLocale } from '@/store/modules/app'; 3 | import { Dropdown } from 'antd'; 4 | import { memo, useMemo } from 'react'; 5 | import type { LocaleType } from '@/locales'; 6 | import type { MenuProps } from 'antd'; 7 | import SvgIcon from '../SvgIcon'; 8 | 9 | const Locale = memo(() => { 10 | const dispatch = useAppDispatch(); 11 | const locale = useAppSelector(state => state.app.locale); 12 | 13 | const menuItems: MenuProps['items'] = useMemo(() => { 14 | return [ 15 | { label: '中文', key: 'zh-CN', disabled: locale === 'zh-CN' }, // 菜单项务必填写 key 16 | { label: 'English', key: 'en-US', disabled: locale === 'en-US' }, 17 | ]; 18 | }, [locale]); 19 | 20 | const menuClick: MenuProps['onClick'] = info => { 21 | dispatch(setAppLocale(info.key as LocaleType)); 22 | }; 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }); 32 | 33 | export default Locale; 34 | -------------------------------------------------------------------------------- /src/components/AppTheme/index.less: -------------------------------------------------------------------------------- 1 | .app-theme { 2 | position: relative; 3 | width: 48px; 4 | height: 26px; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | background-color: 151515; 9 | padding: 0 5px; 10 | font-size: 0.8em; 11 | border-radius: 30px; 12 | .theme-inner { 13 | position: absolute; 14 | z-index: 1; 15 | width: 16px; 16 | height: 16px; 17 | border-radius: 50%; 18 | transition: 19 | transform 0.5s, 20 | background-color 0.5s; 21 | will-change: transform; 22 | } 23 | } 24 | .app-theme-dark { 25 | .theme-inner { 26 | transform: translateX(calc(100% + 4px)); 27 | } 28 | } 29 | .icon { 30 | font-size: 1em; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/AppTheme/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 2 | import { setAppThemeMode } from '@/store/modules/app'; 3 | import { theme } from 'antd'; 4 | import SvgIcon from '../SvgIcon'; 5 | import './index.less'; 6 | 7 | const AppTheme = () => { 8 | const dispatch = useAppDispatch(); 9 | const themeMode = useAppSelector(state => state.app.themeMode); 10 | 11 | const thme = theme.useToken(); 12 | 13 | return ( 14 |
{ 18 | dispatch(setAppThemeMode(themeMode === 'dark' ? 'light' : 'dark')); 19 | }} 20 | > 21 |
22 | 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default AppTheme; 29 | -------------------------------------------------------------------------------- /src/components/FormattedMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage as IntFormattedMessage } from 'react-intl'; 2 | import type { Props } from '@/locales'; 3 | 4 | export const FormattedMessage: React.FC = ({ id, ...props }) => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/LayoutSpin/index.tsx: -------------------------------------------------------------------------------- 1 | import { Spin, theme } from 'antd'; 2 | import { memo, useMemo } from 'react'; 3 | 4 | interface LayoutSpinProps { 5 | position?: 'fixed' | 'absolute'; 6 | } 7 | 8 | const LayoutSpin = memo((props: LayoutSpinProps) => { 9 | const thme = theme.useToken(); 10 | 11 | const position = useMemo(() => { 12 | if (props.position) return `${props.position} top-0 left-0 z-40`; 13 | else return ''; 14 | }, [props.position]); 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }); 22 | 23 | export default LayoutSpin; 24 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.less: -------------------------------------------------------------------------------- 1 | .svg-icon { 2 | height: 1em; 3 | line-height: 1em; 4 | font-size: 1em; 5 | display: block; 6 | 7 | .svg { 8 | width: 1em; 9 | height: 1em; 10 | 11 | use { 12 | fill: currentColor; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import './index.less'; 3 | 4 | interface SvgIconType { 5 | prefix?: string; 6 | name: string; 7 | color?: string; 8 | className?: string; 9 | } 10 | 11 | const SvgIcon = memo((props: SvgIconType) => { 12 | const { prefix, name, color, className } = props; 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }); 21 | 22 | export default SvgIcon; 23 | -------------------------------------------------------------------------------- /src/hooks/useRouteList.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorElement } from '@/router/lazy/whiteList'; 2 | import { Typography } from 'antd'; 3 | import { redirect } from 'react-router'; 4 | import type { MenuItem, RouteList } from '@/router/route'; 5 | import type { RouteObject } from 'react-router'; 6 | 7 | const { Text } = Typography; 8 | 9 | export const useRouteList = () => { 10 | function handleRouteList(list: RouteList[]): RouteObject[] { 11 | return list.map((i: RouteList) => { 12 | const item: RouteObject = { 13 | path: i.path, 14 | id: i.id, 15 | element: i.element, 16 | }; 17 | 18 | if (i.element) { 19 | item.errorElement = ; 20 | } 21 | 22 | if (i.children) { 23 | item.children = handleRouteList(i.children); 24 | if (i.redirect && item.children.length) { 25 | item.children.push({ 26 | index: true, 27 | loader() { 28 | return redirect(i.redirect || ''); 29 | }, 30 | }); 31 | } 32 | } 33 | 34 | return item; 35 | }); 36 | } 37 | 38 | function routeListToMenu(rtList: RouteList[], path?: React.Key): MenuItem[] { 39 | const menuList: MenuItem[] = []; 40 | rtList.forEach((i: RouteList) => { 41 | const item = i; 42 | if (item.handle.hideSidebar) return; 43 | 44 | if (!item.alwaysShow && item.alwaysShow !== undefined && !item.element) { 45 | if (item.children && item.children[0]) { 46 | menuList.push(routeListToMenu([item.children[0]], item.path)[0]); 47 | return; 48 | } 49 | } 50 | 51 | let rtItem: MenuItem = { 52 | key: item.path, 53 | label: '', 54 | }; 55 | 56 | if (path) { 57 | rtItem.key = item.path ? `${path}/${item.path}` : path; 58 | } 59 | 60 | rtItem = { 61 | ...rtItem, 62 | label: ( 63 | 64 | {item.handle.label} 65 | 66 | ), 67 | icon: item.handle.icon, 68 | }; 69 | 70 | if (item.children && !item.element) { 71 | rtItem.children = routeListToMenu(item.children, rtItem.key); 72 | } 73 | 74 | menuList.push(rtItem); 75 | }); 76 | 77 | return menuList; 78 | } 79 | 80 | return { handleRouteList, routeListToMenu }; 81 | }; 82 | -------------------------------------------------------------------------------- /src/hooks/useTransformTheme.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export const useTransformTheme = () => { 4 | const body = useRef(document.documentElement); 5 | 6 | const themeHtmlClassName = (className: string, isShow: boolean) => { 7 | if (isShow) { 8 | body.current.className = `${body.current.className} ${className}`; 9 | } else { 10 | const exp = new RegExp(` ${className}`, 'g'); 11 | body.current.className = body.current.className.replace(exp, ''); 12 | } 13 | }; 14 | 15 | return { themeHtmlClassName }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/hooks/web/antCharts/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "#ffffff", 3 | "subColor": "rgba(0,0,0,0.05)", 4 | "semanticRed": "#F4664A", 5 | "semanticGreen": "#30BF78", 6 | "padding": "auto", 7 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 8 | "columnWidthRatio": 0.5, 9 | "maxColumnWidth": null, 10 | "minColumnWidth": null, 11 | "roseWidthRatio": 0.9999999, 12 | "multiplePieWidthRatio": 0.7692307692307692, 13 | "sequenceColors": [ 14 | "#d8daff", 15 | "#aeb6ff", 16 | "#7d94ff", 17 | "#5d80fd", 18 | "#4973fa", 19 | "#2d66f6", 20 | "#0359ef", 21 | "#034ddf", 22 | "#0241cf", 23 | "#0035c0" 24 | ], 25 | "shapes": { 26 | "point": [ 27 | "hollow-circle", 28 | "hollow-square", 29 | "hollow-bowtie", 30 | "hollow-diamond", 31 | "hollow-hexagon", 32 | "hollow-triangle", 33 | "hollow-triangle-down", 34 | "circle", 35 | "square", 36 | "bowtie", 37 | "diamond", 38 | "hexagon", 39 | "triangle", 40 | "triangle-down", 41 | "cross", 42 | "tick", 43 | "plus", 44 | "hyphen", 45 | "line" 46 | ], 47 | "line": ["line", "dash", "dot", "smooth"], 48 | "area": ["area", "smooth", "line", "smooth-line"], 49 | "interval": ["rect", "hollow-rect", "line", "tick"] 50 | }, 51 | "sizes": [1, 10], 52 | "components": { 53 | "axis": { 54 | "common": { 55 | "title": { 56 | "autoRotate": true, 57 | "position": "center", 58 | "spacing": 12, 59 | "style": { 60 | "fill": "#595959", 61 | "fontSize": 12, 62 | "lineHeight": 12, 63 | "textBaseline": "middle", 64 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"" 65 | } 66 | }, 67 | "label": { 68 | "autoRotate": false, 69 | "autoEllipsis": false, 70 | "autoHide": { "type": "equidistance", "cfg": { "minGap": 6 } }, 71 | "offset": 8, 72 | "style": { 73 | "fill": "#8C8C8C", 74 | "fontSize": 12, 75 | "lineHeight": 12, 76 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"" 77 | } 78 | }, 79 | "line": { "style": { "lineWidth": 1, "stroke": "#BFBFBF" } }, 80 | "grid": { 81 | "line": { 82 | "type": "line", 83 | "style": { "stroke": "#D9D9D9", "lineWidth": 1, "lineDash": null } 84 | }, 85 | "alignTick": true, 86 | "animate": true 87 | }, 88 | "tickLine": { 89 | "style": { "lineWidth": 1, "stroke": "#BFBFBF" }, 90 | "alignTick": true, 91 | "length": 4 92 | }, 93 | "subTickLine": null, 94 | "animate": true 95 | }, 96 | "top": { "position": "top", "grid": null, "title": null, "verticalLimitLength": 0.5 }, 97 | "bottom": { "position": "bottom", "grid": null, "title": null, "verticalLimitLength": 0.5 }, 98 | "left": { 99 | "position": "left", 100 | "title": null, 101 | "line": null, 102 | "tickLine": null, 103 | "verticalLimitLength": 0.3333333333333333 104 | }, 105 | "right": { 106 | "position": "right", 107 | "title": null, 108 | "line": null, 109 | "tickLine": null, 110 | "verticalLimitLength": 0.3333333333333333 111 | }, 112 | "circle": { 113 | "title": null, 114 | "grid": { 115 | "line": { 116 | "type": "line", 117 | "style": { "stroke": "#D9D9D9", "lineWidth": 1, "lineDash": null } 118 | }, 119 | "alignTick": true, 120 | "animate": true 121 | } 122 | }, 123 | "radius": { 124 | "title": null, 125 | "grid": { 126 | "line": { 127 | "type": "circle", 128 | "style": { "stroke": "#D9D9D9", "lineWidth": 1, "lineDash": null } 129 | }, 130 | "alignTick": true, 131 | "animate": true 132 | } 133 | } 134 | }, 135 | "legend": { 136 | "common": { 137 | "title": null, 138 | "marker": { "symbol": "circle", "spacing": 8, "style": { "r": 4, "fill": "#5B8FF9" } }, 139 | "itemName": { 140 | "spacing": 5, 141 | "style": { 142 | "fill": "#595959", 143 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 144 | "fontSize": 12, 145 | "lineHeight": 12, 146 | "fontWeight": "normal", 147 | "textAlign": "start", 148 | "textBaseline": "middle" 149 | } 150 | }, 151 | "itemStates": { 152 | "active": { "nameStyle": { "opacity": 0.8 } }, 153 | "unchecked": { 154 | "nameStyle": { "fill": "#D8D8D8" }, 155 | "markerStyle": { "fill": "#D8D8D8", "stroke": "#D8D8D8" } 156 | }, 157 | "inactive": { "nameStyle": { "fill": "#D8D8D8" }, "markerStyle": { "opacity": 0.2 } } 158 | }, 159 | "flipPage": true, 160 | "pageNavigator": { 161 | "marker": { 162 | "style": { 163 | "size": 12, 164 | "inactiveFill": "#000", 165 | "inactiveOpacity": 0.45, 166 | "fill": "#000", 167 | "opacity": 1 168 | } 169 | }, 170 | "text": { "style": { "fill": "#8C8C8C", "fontSize": 12 } } 171 | }, 172 | "animate": false, 173 | "maxItemWidth": 200, 174 | "itemSpacing": 24, 175 | "itemMarginBottom": 12, 176 | "padding": [8, 8, 8, 8] 177 | }, 178 | "right": { "layout": "vertical", "padding": [0, 8, 0, 8] }, 179 | "left": { "layout": "vertical", "padding": [0, 8, 0, 8] }, 180 | "top": { "layout": "horizontal", "padding": [8, 0, 8, 0] }, 181 | "bottom": { "layout": "horizontal", "padding": [8, 0, 8, 0] }, 182 | "continuous": { 183 | "title": null, 184 | "background": null, 185 | "track": {}, 186 | "rail": { 187 | "type": "color", 188 | "size": 12, 189 | "defaultLength": 100, 190 | "style": { "fill": "#D9D9D9", "stroke": null, "lineWidth": 0 } 191 | }, 192 | "label": { 193 | "align": "rail", 194 | "spacing": 4, 195 | "formatter": null, 196 | "style": { 197 | "fill": "#8C8C8C", 198 | "fontSize": 12, 199 | "lineHeight": 12, 200 | "textBaseline": "middle", 201 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"" 202 | } 203 | }, 204 | "handler": { "size": 10, "style": { "fill": "#F0F0F0", "stroke": "#BFBFBF" } }, 205 | "slidable": true, 206 | "padding": [8, 8, 8, 8] 207 | } 208 | }, 209 | "tooltip": { 210 | "showContent": true, 211 | "follow": true, 212 | "showCrosshairs": false, 213 | "showMarkers": true, 214 | "shared": false, 215 | "enterable": false, 216 | "position": "auto", 217 | "marker": { 218 | "symbol": "circle", 219 | "stroke": "#fff", 220 | "shadowBlur": 10, 221 | "shadowOffsetX": 0, 222 | "shadowOffsetY": 0, 223 | "shadowColor": "rgba(0,0,0,0.09)", 224 | "lineWidth": 2, 225 | "r": 4 226 | }, 227 | "crosshairs": { 228 | "line": { "style": { "stroke": "#BFBFBF", "lineWidth": 1 } }, 229 | "text": null, 230 | "textBackground": { 231 | "padding": 2, 232 | "style": { "fill": "rgba(0, 0, 0, 0.25)", "lineWidth": 0, "stroke": null } 233 | }, 234 | "follow": false 235 | }, 236 | "domStyles": { 237 | "g2-tooltip": { 238 | "position": "absolute", 239 | "visibility": "hidden", 240 | "zIndex": 8, 241 | "transition": "left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s", 242 | "backgroundColor": "rgb(255, 255, 255)", 243 | "opacity": 0.95, 244 | "boxShadow": "0px 0px 10px #aeaeae", 245 | "borderRadius": "3px", 246 | "color": "#595959", 247 | "fontSize": "12px", 248 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 249 | "lineHeight": "12px", 250 | "padding": "0 12px 0 12px" 251 | }, 252 | "g2-tooltip-title": { "marginBottom": "12px", "marginTop": "12px" }, 253 | "g2-tooltip-list": { "margin": 0, "listStyleType": "none", "padding": 0 }, 254 | "g2-tooltip-list-item": { 255 | "listStyleType": "none", 256 | "padding": 0, 257 | "marginBottom": "12px", 258 | "marginTop": "12px", 259 | "marginLeft": 0, 260 | "marginRight": 0 261 | }, 262 | "g2-tooltip-marker": { 263 | "width": "8px", 264 | "height": "8px", 265 | "borderRadius": "50%", 266 | "display": "inline-block", 267 | "marginRight": "8px" 268 | }, 269 | "g2-tooltip-value": { "display": "inline-block", "float": "right", "marginLeft": "30px" } 270 | } 271 | }, 272 | "annotation": { 273 | "arc": { "style": { "stroke": "#D9D9D9", "lineWidth": 1 }, "animate": true }, 274 | "line": { 275 | "style": { "stroke": "#BFBFBF", "lineDash": null, "lineWidth": 1 }, 276 | "text": { 277 | "position": "start", 278 | "autoRotate": true, 279 | "style": { 280 | "fill": "#595959", 281 | "stroke": null, 282 | "lineWidth": 0, 283 | "fontSize": 12, 284 | "textAlign": "start", 285 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 286 | "textBaseline": "bottom" 287 | } 288 | }, 289 | "animate": true 290 | }, 291 | "text": { 292 | "style": { 293 | "fill": "#595959", 294 | "stroke": null, 295 | "lineWidth": 0, 296 | "fontSize": 12, 297 | "textBaseline": "middle", 298 | "textAlign": "start", 299 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"" 300 | }, 301 | "animate": true 302 | }, 303 | "region": { 304 | "top": false, 305 | "style": { "lineWidth": 0, "stroke": null, "fill": "#000", "fillOpacity": 0.06 }, 306 | "animate": true 307 | }, 308 | "image": { "top": false, "animate": true }, 309 | "dataMarker": { 310 | "top": true, 311 | "point": { "style": { "r": 3, "stroke": "#5B8FF9", "lineWidth": 2 } }, 312 | "line": { "style": { "stroke": "#BFBFBF", "lineWidth": 1 }, "length": 16 }, 313 | "text": { 314 | "style": { 315 | "textAlign": "start", 316 | "fill": "#595959", 317 | "stroke": null, 318 | "lineWidth": 0, 319 | "fontSize": 12, 320 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"" 321 | } 322 | }, 323 | "direction": "upward", 324 | "autoAdjust": true, 325 | "animate": true 326 | }, 327 | "dataRegion": { 328 | "style": { 329 | "region": { "fill": "#000", "fillOpacity": 0.06 }, 330 | "text": { 331 | "textAlign": "center", 332 | "textBaseline": "bottom", 333 | "fill": "#595959", 334 | "stroke": null, 335 | "lineWidth": 0, 336 | "fontSize": 12, 337 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"" 338 | } 339 | }, 340 | "animate": true 341 | } 342 | }, 343 | "slider": { 344 | "common": { 345 | "padding": [8, 8, 8, 8], 346 | "backgroundStyle": { "fill": "#416180", "opacity": 0.05 }, 347 | "foregroundStyle": { "fill": "#5B8FF9", "opacity": 0.15 }, 348 | "handlerStyle": { 349 | "width": 10, 350 | "height": 24, 351 | "fill": "#F7F7F7", 352 | "opacity": 1, 353 | "stroke": "#BFBFBF", 354 | "lineWidth": 1, 355 | "radius": 2, 356 | "highLightFill": "#FFF" 357 | }, 358 | "textStyle": { 359 | "fill": "#000", 360 | "opacity": 0.45, 361 | "fontSize": 12, 362 | "lineHeight": 12, 363 | "fontWeight": "normal", 364 | "stroke": null, 365 | "lineWidth": 0 366 | } 367 | } 368 | }, 369 | "scrollbar": { 370 | "common": { "padding": [8, 8, 8, 8] }, 371 | "default": { "style": { "trackColor": "rgba(0,0,0,0)", "thumbColor": "rgba(0,0,0,0.15)" } }, 372 | "hover": { "style": { "thumbColor": "rgba(0,0,0,0.2)" } } 373 | } 374 | }, 375 | "labels": { 376 | "offset": 12, 377 | "style": { 378 | "fill": "#595959", 379 | "fontSize": 12, 380 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 381 | "stroke": null, 382 | "lineWidth": 0 383 | }, 384 | "fillColorDark": "#2c3542", 385 | "fillColorLight": "#ffffff", 386 | "autoRotate": true 387 | }, 388 | "innerLabels": { 389 | "style": { 390 | "fill": "#FFFFFF", 391 | "fontSize": 12, 392 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 393 | "stroke": null, 394 | "lineWidth": 0 395 | }, 396 | "autoRotate": true 397 | }, 398 | "overflowLabels": { 399 | "style": { 400 | "fill": "#595959", 401 | "fontSize": 12, 402 | "fontFamily": "\"Segoe UI\", Roboto, \"Helvetica Neue\", Arial,\n \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",\n \"Noto Color Emoji\"", 403 | "stroke": "#FFFFFF", 404 | "lineWidth": 1 405 | } 406 | }, 407 | "pieLabels": { 408 | "labelHeight": 14, 409 | "offset": 10, 410 | "labelLine": { "style": { "lineWidth": 1 } }, 411 | "autoRotate": true 412 | }, 413 | "styleSheet": { 414 | "brandColor": "rgba(0, 177, 255, 1)", 415 | "paletteQualitative10": [ 416 | "rgba(0, 177, 255, 1)", 417 | "rgba(43, 222, 228, 1)", 418 | "#2498D1", 419 | "#BBBDE6", 420 | "#4045B2", 421 | "#21A97A", 422 | "#FF745A", 423 | "#007E99", 424 | "#FFA8A8", 425 | "#2391FF" 426 | ], 427 | "paletteQualitative20": [ 428 | "#025DF4", 429 | "#DB6BCF", 430 | "#2498D1", 431 | "#BBBDE6", 432 | "#4045B2", 433 | "#21A97A", 434 | "#FF745A", 435 | "#007E99", 436 | "#FFA8A8", 437 | "#2391FF", 438 | "#FFC328", 439 | "#A0DC2C", 440 | "#946DFF", 441 | "#626681", 442 | "#EB4185", 443 | "#CD8150", 444 | "#36BCCB", 445 | "#327039", 446 | "#803488", 447 | "#83BC99" 448 | ] 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/hooks/web/antCharts/useChartsConfig.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '@/store/hooks'; 2 | import themeChart from './theme.json'; 3 | export const useChartsConfig = () => { 4 | const themeMode = useAppSelector(state => state.app.themeMode); 5 | 6 | const theme = themeMode === 'dark' ? 'dark' : themeChart; 7 | 8 | return { theme }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/web/useMessage.tsx: -------------------------------------------------------------------------------- 1 | import { getIntlText } from '@/locales'; 2 | import { message, Modal } from 'antd'; 3 | 4 | import type { ModalFuncProps } from 'antd'; 5 | 6 | function createElMessageBox(messageg: string, title: string, options: ModalFuncProps) { 7 | Modal.error({ title, content: messageg, ...options }); 8 | } 9 | 10 | export function createErrorModal(msg: string) { 11 | createElMessageBox(msg, getIntlText('api.errorTip'), { centered: true }); 12 | } 13 | 14 | export function createErrorMsg(msg: string) { 15 | message.error(msg); 16 | } 17 | 18 | export function useMessage() { 19 | return { 20 | createErrorModal, 21 | createErrorMsg, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | width: 100%; 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | #root { 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | ul { 19 | list-style-type: none; 20 | padding: 0; 21 | margin: 0; 22 | } 23 | 24 | .supense-loading { 25 | width: 100%; 26 | height: 100%; 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | } 31 | 32 | .cursor { 33 | cursor: pointer; 34 | } 35 | 36 | /* 灰色 */ 37 | .html-grey { 38 | filter: grayscale(100%); 39 | } 40 | /* 色弱 */ 41 | .html-weakness { 42 | filter: invert(80%); 43 | } 44 | -------------------------------------------------------------------------------- /src/layout/Authority/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '@/store/hooks'; 2 | import { Navigate } from 'react-router'; 3 | 4 | interface AuthorityType { 5 | children: React.ReactNode; 6 | } 7 | 8 | const Authority = ({ children }: AuthorityType) => { 9 | const user = useAppSelector(state => state.user); 10 | 11 | if (!user?.power) return ; 12 | 13 | return <>{children}; 14 | }; 15 | 16 | export default Authority; 17 | -------------------------------------------------------------------------------- /src/layout/components/AppAccount/index.tsx: -------------------------------------------------------------------------------- 1 | import avatar from '@/assets/avatar.png'; 2 | import { useAppDispatch } from '@/store/hooks'; 3 | import { setSignOut } from '@/store/modules/user'; 4 | import { removeStorage } from '@/utils/storage'; 5 | import { Dropdown, Image } from 'antd'; 6 | import { useNavigate } from 'react-router'; 7 | import type { MenuProps } from 'antd'; 8 | import { getAccountStyle } from './style'; 9 | 10 | const AppAccount = () => { 11 | const { AccountDiv } = getAccountStyle(); 12 | 13 | const dispatch = useAppDispatch(); 14 | 15 | const navigate = useNavigate(); 16 | 17 | const items: MenuProps['items'] = [ 18 | { 19 | key: '1', 20 | label: '退出登录', 21 | }, 22 | ]; 23 | 24 | const memuChange: MenuProps['onClick'] = _e => { 25 | removeStorage('userInfo'); 26 | dispatch(setSignOut()); 27 | 28 | navigate('/login'); 29 | }; 30 | 31 | return ( 32 | 33 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default AppAccount; 50 | -------------------------------------------------------------------------------- /src/layout/components/AppAccount/style.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import type { CSSObject } from '@emotion/react'; 3 | 4 | const getAccountDivStyle = (): CSSObject => { 5 | return { 6 | display: 'flex', 7 | alignItems: 'center', 8 | '.wave': { 9 | width: 30, 10 | height: 30, 11 | borderRadius: '50%', 12 | overflow: 'hidden', 13 | 14 | '.ant-image': { 15 | display: 'flex', 16 | }, 17 | }, 18 | }; 19 | }; 20 | 21 | export const getAccountStyle = () => { 22 | const AccountDiv = styled.div` 23 | ${getAccountDivStyle()} 24 | `; 25 | 26 | return { AccountDiv }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/layout/components/AppLogo/index.less: -------------------------------------------------------------------------------- 1 | .app-logo { 2 | display: flex; 3 | align-items: center; 4 | .logo { 5 | height: 50px; 6 | width: 55px; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | .name { 12 | width: 100%; 13 | flex: 1; 14 | font-size: 22px; 15 | font-weight: 500; 16 | transition: all 0.5s; 17 | overflow: hidden; 18 | color: #000; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/layout/components/AppLogo/index.tsx: -------------------------------------------------------------------------------- 1 | import logo from '@/assets/logo.png'; 2 | import { Image, theme } from 'antd'; 3 | import { memo } from 'react'; 4 | import './index.less'; 5 | 6 | const AppLogo = memo(() => { 7 | const thme = theme.useToken(); 8 | 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 | xiaosiAdmin 16 |
17 |
18 | ); 19 | }); 20 | 21 | export default AppLogo; 22 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/AppMain.tsx: -------------------------------------------------------------------------------- 1 | import LayoutSpin from '@/components/LayoutSpin'; 2 | import { Layout } from 'antd'; 3 | import { KeepAlive, useKeepAliveRef } from 'keepalive-for-react'; 4 | import { memo, Suspense, useMemo } from 'react'; 5 | import { useLocation, useOutlet } from 'react-router'; 6 | import { getAppMainStyle } from './style'; 7 | import TabsPage from './TabsPage'; 8 | 9 | const { Content } = Layout; 10 | 11 | const AppMain = memo(() => { 12 | const location = useLocation(); 13 | const maxLen = 10; 14 | const aliveRef = useKeepAliveRef(); 15 | 16 | const activeCacheKey = useMemo(() => { 17 | return location.pathname + location.search; 18 | }, [location.pathname, location.search]); 19 | 20 | const outlet = useOutlet(); 21 | 22 | return ( 23 | 24 | 25 | 26 |
27 | }>{outlet} 28 |
29 |
30 |
31 | ); 32 | }); 33 | 34 | export default AppMain; 35 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/KeepAlive/index.tsx: -------------------------------------------------------------------------------- 1 | import LayoutSpin from '@/components/LayoutSpin'; 2 | import { useAppSelector } from '@/store/hooks'; 3 | import React, { memo, Suspense, useEffect, useRef, useState } from 'react'; 4 | import { createPortal } from 'react-dom'; 5 | import { useLocation, useOutlet, useParams } from 'react-router'; 6 | import type { ReactNode, RefObject } from 'react'; 7 | 8 | export interface ComponentReactElement { 9 | children?: ReactNode | ReactNode[]; 10 | } 11 | 12 | interface ComponentProps extends ComponentReactElement { 13 | active: boolean; 14 | name: string; 15 | renderDiv: RefObject; 16 | } 17 | 18 | export const Component: React.FC = ({ active, children, name, renderDiv }) => { 19 | const [targetElement] = useState(() => document.createElement('div')); 20 | const activatedRef = useRef(false); 21 | activatedRef.current = activatedRef.current || active; 22 | 23 | useEffect(() => { 24 | if (active) { 25 | renderDiv.current?.appendChild(targetElement); 26 | } else { 27 | try { 28 | renderDiv.current?.removeChild(targetElement); 29 | } catch (e) { 30 | console.error(e); 31 | } 32 | } 33 | }, [active, name, renderDiv, targetElement]); 34 | 35 | useEffect(() => { 36 | targetElement.setAttribute('id', name); 37 | }, [name, targetElement]); 38 | 39 | return }>{activatedRef.current && createPortal(children, targetElement)}; 40 | }; 41 | 42 | interface Props extends ComponentReactElement { 43 | maxLen?: number; 44 | } 45 | export const KeepAlive = memo(({ maxLen = 10 }: Props) => { 46 | const ele = useOutlet(); 47 | const location = useLocation(); 48 | const params = useParams(); 49 | const activeName = location.pathname + location.search; 50 | const multiTabs = useAppSelector(state => state.route.multiTabs); 51 | 52 | const containerRef = useRef(null); 53 | const [cacheReactNodes, setCacheReactNodes] = useState>([]); 54 | 55 | useEffect(() => { 56 | if (!activeName) { 57 | return; 58 | } 59 | const include = multiTabs.map(i => i.key); 60 | setCacheReactNodes(reactNodes => { 61 | reactNodes = reactNodes.filter(i => !i.name.startsWith('/refresh')); 62 | 63 | if (location.pathname.startsWith('/refresh')) { 64 | const reactIndex = reactNodes.findIndex(res => res.name === `/${params['*']}${location.search}`); 65 | if (reactIndex !== -1) reactNodes.splice(reactIndex, 1); 66 | reactNodes.push({ 67 | name: activeName, 68 | ele, 69 | }); 70 | return reactNodes; 71 | } 72 | 73 | // 缓存超过上限的 74 | if (reactNodes.length >= maxLen) { 75 | reactNodes.splice(0, 1); 76 | } 77 | // 添加 78 | const reactNode = reactNodes.find(res => res.name === activeName); 79 | if (!reactNode) { 80 | reactNodes.push({ 81 | name: activeName, 82 | ele, 83 | }); 84 | } 85 | 86 | // 缓存路由列表和标签页列表同步 87 | if (include) { 88 | return reactNodes.filter(i => include.includes(i.name)); 89 | } 90 | return reactNodes; 91 | }); 92 | }, [activeName, maxLen, multiTabs]); 93 | 94 | return ( 95 | <> 96 |
97 | {cacheReactNodes.map(i => { 98 | return ( 99 | 100 | {i.ele} 101 | 102 | ); 103 | })} 104 | 105 | ); 106 | }); 107 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/TabsPage/components/TabsItemLabel.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'antd'; 2 | import { useMemo, useState } from 'react'; 3 | import type { Interpolation, Theme } from '@emotion/react'; 4 | import type { MenuProps } from 'antd'; 5 | import type { CSSProperties, ReactNode } from 'react'; 6 | import { useTabsChange } from '../hooks/useTabsChange'; 7 | import { useTabsState } from '../hooks/useTabsState'; 8 | import type { RightClickTags } from '../hooks/useTabsState'; 9 | 10 | interface TabsItemLabelProps { 11 | pathKey: string; 12 | children: ReactNode; 13 | eventType?: 'click' | 'ContextMenu'; 14 | className?: string; 15 | style?: CSSProperties; 16 | css?: Interpolation; 17 | } 18 | 19 | const TabsItemLabel = ({ pathKey, eventType, className, style, css, children }: TabsItemLabelProps) => { 20 | const [open, setOpen] = useState(false); 21 | const { menuItems } = useTabsState(pathKey, open); 22 | const { onTabsDropdownChange } = useTabsChange(); 23 | 24 | const menuClick: MenuProps['onClick'] = e => { 25 | e.domEvent.stopPropagation(); 26 | onTabsDropdownChange(e.key as RightClickTags['code'], pathKey); 27 | }; 28 | 29 | const contentProps: React.DOMAttributes = useMemo(() => { 30 | if (eventType === 'click') { 31 | return { 32 | onClick: e => { 33 | e.preventDefault(); 34 | setOpen(!open); 35 | }, 36 | }; 37 | } else { 38 | return { 39 | onContextMenu: e => { 40 | e.preventDefault(); 41 | setOpen(!open); 42 | }, 43 | }; 44 | } 45 | }, [eventType]); 46 | 47 | return ( 48 | !visible && setOpen(visible)} 55 | > 56 |
57 | {children} 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default TabsItemLabel; 64 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/TabsPage/hooks/useTabsChange.ts: -------------------------------------------------------------------------------- 1 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 2 | import { type MultiTabsType, setStoreMultiTabs } from '@/store/modules/route'; 3 | import { useKeepAliveContext } from 'keepalive-for-react'; 4 | import { useLocation, useNavigate } from 'react-router'; 5 | import type { RightClickTags } from './useTabsState'; 6 | 7 | export const useTabsChange = () => { 8 | const multiTabs = useAppSelector(state => state.route.multiTabs); 9 | const location = useLocation(); 10 | const navigate = useNavigate(); 11 | const dispatch = useAppDispatch(); 12 | // const { refresh } = useRefresh(); 13 | const { refresh, destroy } = useKeepAliveContext(); 14 | 15 | const handleTabsList = (pathName: string, type: 'add' | 'delete') => { 16 | dispatch( 17 | setStoreMultiTabs({ 18 | type, 19 | tabs: { 20 | key: pathName, 21 | }, 22 | }), 23 | ); 24 | }; 25 | 26 | const getCurrentPathname = (): string => { 27 | return location.pathname + location.search; 28 | }; 29 | 30 | // 添加标签 31 | const addRouteTabs = () => { 32 | handleTabsList(getCurrentPathname(), 'add'); 33 | }; 34 | 35 | // 关闭当前导航 36 | const removeTab = (pathKey: string) => { 37 | const item = multiTabs.findIndex(i => i.key === pathKey); 38 | const tabsLength = multiTabs.length; 39 | 40 | let value: MultiTabsType; 41 | if (multiTabs[item].key === getCurrentPathname()) { 42 | if (item === tabsLength - 1) { 43 | value = multiTabs[item - 1]; 44 | } else { 45 | value = multiTabs[tabsLength - 1]; 46 | } 47 | navigate(value.key); 48 | } 49 | 50 | destroy(pathKey); 51 | handleTabsList(pathKey, 'delete'); 52 | }; 53 | 54 | const closeTabsRoute = (pathKey: string, type: 'other' | 'left' | 'right') => { 55 | const selectItemIndex = multiTabs.findIndex(i => i.key === pathKey); 56 | const mapList = multiTabs.filter((i, index) => { 57 | if (i.key !== pathKey && type === 'other') return true; 58 | else if (index < selectItemIndex && type === 'left') return true; 59 | else if (index > selectItemIndex && type === 'right') return true; 60 | return false; 61 | }); 62 | if (mapList.find(i => i.key === getCurrentPathname())) { 63 | const { key } = multiTabs[selectItemIndex]; 64 | navigate(key); 65 | } 66 | 67 | mapList.forEach(i => { 68 | destroy(i.key); 69 | i.key && handleTabsList(i.key, 'delete'); 70 | }); 71 | }; 72 | 73 | const onTabsDropdownChange = (code: RightClickTags['code'], pathKey: string) => { 74 | switch (code) { 75 | case 'refresh': 76 | refresh(pathKey); 77 | break; 78 | case 'close': 79 | removeTab(pathKey); 80 | break; 81 | case 'closeLeftOther': 82 | closeTabsRoute(pathKey, 'left'); 83 | break; 84 | case 'closeRightOther': 85 | closeTabsRoute(pathKey, 'right'); 86 | break; 87 | case 'closeOther': 88 | closeTabsRoute(pathKey, 'other'); 89 | break; 90 | default: 91 | break; 92 | } 93 | }; 94 | 95 | return { onTabsDropdownChange, addRouteTabs, removeTab, refresh }; 96 | }; 97 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/TabsPage/hooks/useTabsState.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '@/store/hooks'; 2 | import { useMemo, useState } from 'react'; 3 | 4 | export interface RightClickTags { 5 | text: string; 6 | disabled: boolean; 7 | code: 'refresh' | 'close' | 'closeOther' | 'closeLeftOther' | 'closeRightOther'; 8 | } 9 | 10 | export const useTabsState = (pathKey: string, openDropdown: boolean) => { 11 | const multiTabs = useAppSelector(state => state.route.multiTabs); 12 | 13 | const [rightClickTags] = useState([ 14 | { 15 | text: '刷新', 16 | disabled: false, 17 | code: 'refresh', 18 | }, 19 | { 20 | text: '关闭', 21 | disabled: false, 22 | code: 'close', 23 | }, 24 | { 25 | text: '关闭其他标签', 26 | disabled: false, 27 | code: 'closeOther', 28 | }, 29 | { 30 | text: '关闭左侧其他标签', 31 | disabled: false, 32 | code: 'closeLeftOther', 33 | }, 34 | { 35 | text: '关闭右侧其他标签', 36 | disabled: false, 37 | code: 'closeRightOther', 38 | }, 39 | ]); 40 | 41 | const getDisabledStatus = (code: string, multFindIndex: number, multlength: number) => { 42 | const isFirstTab = multFindIndex === 0 && multlength > 1; 43 | const isLastTab = multFindIndex === multlength - 1 && multlength > 1; 44 | const isOnlyTab = multlength === 1; 45 | 46 | const disableCodesForOnlyTab = ['close', 'closeOther', 'closeLeftOther', 'closeRightOther']; 47 | 48 | if (isFirstTab && code === 'closeLeftOther') return true; 49 | if (isLastTab && code === 'closeRightOther') return true; 50 | if (isOnlyTab && disableCodesForOnlyTab.includes(code)) return true; 51 | 52 | return false; 53 | }; 54 | 55 | const rightClickTagsList = useMemo(() => { 56 | const multFindIndex = multiTabs.findIndex(i => i.key === pathKey); 57 | const multlength = multiTabs.length; 58 | 59 | return rightClickTags.map(item => ({ 60 | ...item, 61 | disabled: getDisabledStatus(item.code, multFindIndex, multlength), 62 | })); 63 | }, [openDropdown]); 64 | 65 | const menuItems = useMemo(() => { 66 | return rightClickTagsList.map(i => { 67 | return { 68 | label: i.text, 69 | key: i.code, 70 | disabled: i.disabled, 71 | }; 72 | }); 73 | }, [rightClickTagsList]); 74 | 75 | return { menuItems, rightClickTagsList }; 76 | }; 77 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/TabsPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from '@/components/FormattedMessage'; 2 | import { useRouteList } from '@/hooks/useRouteList'; 3 | import { defaultRoute } from '@/router/modules'; 4 | import { findRouteByPath } from '@/router/utils'; 5 | import { useAppSelector } from '@/store/hooks'; 6 | import { CaretDownFilled, ReloadOutlined } from '@ant-design/icons'; 7 | import { Tabs, theme } from 'antd'; 8 | import { memo, useEffect, useMemo } from 'react'; 9 | import { useLocation, useMatch, useNavigate } from 'react-router'; 10 | import type { TabsProps } from 'antd'; 11 | import TabsItemLabel from './components/TabsItemLabel'; 12 | import { useTabsChange } from './hooks/useTabsChange'; 13 | import { getTabsStyle } from './style'; 14 | 15 | interface Props { 16 | maxLen?: number; 17 | } 18 | 19 | const TabsPage = memo((_props: Props) => { 20 | const location = useLocation(); 21 | const navigate = useNavigate(); 22 | const mark = useMatch(location.pathname); 23 | const { routeListToMenu } = useRouteList(); 24 | const menuList = routeListToMenu(defaultRoute); 25 | const asyncRouter = useAppSelector(state => state.route.asyncRouter); 26 | const multiTabs = useAppSelector(state => state.route.multiTabs); 27 | const { addRouteTabs, removeTab, refresh } = useTabsChange(); 28 | 29 | const thme = theme.useToken(); 30 | 31 | const tabsItem = useMemo(() => { 32 | return multiTabs.map(i => { 33 | let routeBy = null; 34 | if (!i.label) routeBy = findRouteByPath(i.key, menuList); 35 | return { 36 | key: i.key, 37 | label: ( 38 | 39 |
40 | {i.localeLabel ? : ''} 41 | {i.label || routeBy?.label} 42 |
43 |
44 | ), 45 | }; 46 | }); 47 | }, [multiTabs]); 48 | 49 | const onEdit: TabsProps['onEdit'] = (targetKey, action) => { 50 | if (action === 'remove') { 51 | removeTab(targetKey as string); 52 | } 53 | }; 54 | 55 | useEffect(() => { 56 | if (location.pathname === '/') { 57 | if (asyncRouter.length) navigate(asyncRouter[0].path); 58 | return; 59 | } 60 | 61 | const findRoute = findRouteByPath(location.pathname, menuList); 62 | if (findRoute && !findRoute.hideTabs) { 63 | addRouteTabs(); 64 | } 65 | }, [location.pathname, mark]); 66 | 67 | return ( 68 | 1 ? 'editable-card' : 'card'} 74 | onChange={key => navigate(key)} 75 | onEdit={onEdit} 76 | tabBarExtraContent={{ 77 | right: ( 78 |
79 |
refresh()}> 80 | 81 |
82 | 87 | 88 | 89 |
90 | ), 91 | }} 92 | tabBarStyle={{ 93 | margin: 0, 94 | }} 95 | items={tabsItem} 96 | /> 97 | ); 98 | }); 99 | 100 | export default TabsPage; 101 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/TabsPage/style.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSObject } from '@emotion/react'; 2 | import type { GlobalToken } from 'antd'; 3 | 4 | export const getTabsStyle = (token: GlobalToken): CSSObject => { 5 | return { 6 | '& .ant-tabs-nav::before': { 7 | borderBottom: `1px solid ${token.colorBorder}`, 8 | }, 9 | '& .ant-tabs-nav .ant-tabs-tab': { 10 | padding: 0, 11 | paddingRight: token.padding, 12 | border: `1px solid ${token.colorBorder}`, 13 | '.tabs-tab-label': { 14 | padding: `${token.paddingContentVerticalSM}px 0 ${token.paddingContentVerticalSM}px ${token.paddingContentVerticalLG}px`, 15 | }, 16 | }, 17 | '&.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab': { 18 | padding: 0, 19 | paddingRight: token.padding, 20 | '.tabs-tab-label': { 21 | padding: `${token.paddingContentVerticalSM - 2}px 0 ${ 22 | token.paddingContentVerticalSM - 2 23 | }px ${token.paddingContentVerticalLG}px`, 24 | }, 25 | }, 26 | '& .ant-tabs-nav .tabs-right-content': { 27 | display: 'flex', 28 | }, 29 | '& .ant-tabs-nav .tabs-right-content .right-down-fukked': { 30 | borderLeft: `1px solid ${token.colorBorder}`, 31 | borderBottom: 'none', 32 | borderTop: 'none', 33 | cursor: 'pointer', 34 | padding: `${token.paddingContentVerticalSM}px ${token.padding}px`, 35 | }, 36 | '&.ant-tabs-small > .ant-tabs-nav .tabs-right-content .right-down-fukked': { 37 | padding: `${token.paddingContentVerticalSM - 2}px ${token.paddingContentVertical}px`, 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/style.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSObject } from '@emotion/react'; 2 | 3 | export const getAppMainStyle = (): CSSObject => { 4 | return { 5 | display: 'flex', 6 | flexDirection: 'column', 7 | '.main-content': { 8 | padding: 12, 9 | height: '100%', 10 | overflowY: 'auto', 11 | position: 'relative', 12 | }, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/layout/components/Navbart/index.less: -------------------------------------------------------------------------------- 1 | .layout .site-layout-sub-header { 2 | height: 50px; 3 | line-height: 50px; 4 | // background-color: #fff; 5 | // border-bottom: 1px solid #e4e7ed; 6 | font-size: 16px; 7 | 8 | .layout-header { 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | box-sizing: border-box; 15 | 16 | .layout-header-left { 17 | .layout-header-collapsed { 18 | cursor: pointer; 19 | margin: 10px; 20 | } 21 | } 22 | 23 | .layout-header-conter { 24 | flex: auto; 25 | min-width: 0; 26 | .ant-typography { 27 | line-height: inherit; 28 | } 29 | } 30 | 31 | .layout-header-right { 32 | display: grid; 33 | grid-auto-flow: column; 34 | align-items: center; 35 | grid-gap: 10px; 36 | margin-right: 10px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/layout/components/Navbart/index.tsx: -------------------------------------------------------------------------------- 1 | import AppLocale from '@/components/AppLocale'; 2 | import AppTheme from '@/components/AppTheme'; 3 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 4 | import { setAppCollapsed } from '@/store/modules/app'; 5 | import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; 6 | import { useResponsive } from 'ahooks'; 7 | import { Layout, theme } from 'antd'; 8 | import { memo } from 'react'; 9 | import { shallowEqual } from 'react-redux'; 10 | import AppAccount from '../AppAccount'; 11 | import AppLogo from '../AppLogo'; 12 | import Setting from '../Setting'; 13 | import NavSidebar from '../Sidebar/NavSidebar'; 14 | import './index.less'; 15 | 16 | const { Header } = Layout; 17 | 18 | const Navbart = memo(() => { 19 | const dispatch = useAppDispatch(); 20 | const { collapsed, sidebarMode } = useAppSelector( 21 | state => ({ 22 | collapsed: state.app.collapsed, 23 | sidebarMode: state.app.sidebarMode, 24 | }), 25 | shallowEqual, 26 | ); 27 | const thme = theme.useToken(); 28 | const responsive = useResponsive(); 29 | 30 | const render = () => { 31 | return ( 32 |
40 |
41 | {(sidebarMode !== 'blend' || !responsive.sm) && ( 42 |
43 | {(sidebarMode === 'vertical' || !responsive.sm) && ( 44 |
{ 47 | dispatch(setAppCollapsed(!collapsed)); 48 | }} 49 | > 50 | {collapsed ? : } 51 |
52 | )} 53 | {sidebarMode === 'horizontal' && responsive.sm && } 54 |
55 | )} 56 |
57 | {sidebarMode !== 'vertical' && responsive.sm ? : null} 58 |
59 | 60 |
61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | return render(); 72 | }); 73 | 74 | export default Navbart; 75 | -------------------------------------------------------------------------------- /src/layout/components/Setting/ThemeSettings/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTransformTheme } from '@/hooks/useTransformTheme'; 2 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 3 | import { setAppColor } from '@/store/modules/app'; 4 | import { CheckOutlined } from '@ant-design/icons'; 5 | import { Switch, theme } from 'antd'; 6 | import { memo } from 'react'; 7 | import { getThemeSettingsStyle } from './style'; 8 | 9 | const ThemeSettings = memo(() => { 10 | const dispatch = useAppDispatch(); 11 | const then = theme.useToken(); 12 | const color = useAppSelector(state => state.app.color); 13 | const { themeHtmlClassName } = useTransformTheme(); 14 | 15 | const colorList = ['#722ed1', '#eb2f96', '#52c41a', '#13c2c2', '#fadb14', '#fa541c', '#f5222d']; 16 | 17 | const { ThemeSettingsDiv } = getThemeSettingsStyle(then.token); 18 | 19 | return ( 20 | 21 |
22 | {colorList.map(i => { 23 | return ( 24 |
{ 29 | dispatch(setAppColor(i)); 30 | }} 31 | > 32 | {color === i && } 33 | {/* */} 34 |
35 | ); 36 | })} 37 |
38 |
39 | 灰色模式 40 | themeHtmlClassName('html-grey', e)} /> 41 |
42 |
43 | 色弱模式 44 | themeHtmlClassName('html-weakness', e)} /> 45 |
46 |
47 | ); 48 | }); 49 | 50 | export default ThemeSettings; 51 | -------------------------------------------------------------------------------- /src/layout/components/Setting/ThemeSettings/style.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import type { CSSObject } from '@emotion/react'; 3 | import type { GlobalToken } from 'antd/es/theme/interface'; 4 | 5 | const getColorListStyle = (): CSSObject => { 6 | return { 7 | '.color-list': { 8 | display: 'flex', 9 | alignItems: 'center', 10 | justifyContent: 'space-between', 11 | '.color-list-item': { 12 | width: 20, 13 | height: 20, 14 | display: 'flex', 15 | alignItems: 'center', 16 | justifyContent: 'space-between', 17 | }, 18 | }, 19 | }; 20 | }; 21 | 22 | const getOptions = (token: GlobalToken): CSSObject => { 23 | return { 24 | '.options': { 25 | marginTop: token.margin, 26 | display: 'flex', 27 | alignItems: 'center', 28 | justifyContent: 'space-between', 29 | }, 30 | }; 31 | }; 32 | 33 | const getThemeSettingsStyle = (token: GlobalToken) => { 34 | const ThemeSettingsDiv = styled.div` 35 | ${getColorListStyle()} 36 | ${getOptions(token)} 37 | `; 38 | 39 | return { ThemeSettingsDiv }; 40 | }; 41 | 42 | export { getThemeSettingsStyle }; 43 | -------------------------------------------------------------------------------- /src/layout/components/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 3 | import { setAppSidebarMode } from '@/store/modules/app'; 4 | import { SettingOutlined } from '@ant-design/icons'; 5 | 6 | import { Divider, Drawer, theme, Tooltip } from 'antd'; 7 | import classNames from 'classnames'; 8 | import { memo, useState } from 'react'; 9 | import type { AppConfigMode } from '@/store/modules/app'; 10 | import { getSidebarMode } from './style'; 11 | import ThemeSettings from './ThemeSettings'; 12 | 13 | const Setting = memo(() => { 14 | const [drawerOpen, setDrawerOpen] = useState(false); 15 | const dispatch = useAppDispatch(); 16 | const sidebarMode = useAppSelector(state => state.app.sidebarMode); 17 | 18 | const thme = theme.useToken(); 19 | 20 | const intl = useLocale(); 21 | 22 | const sidebarSeting: { label: string; value: AppConfigMode['sidebarMode'] }[] = [ 23 | { 24 | label: '左侧菜单模式', 25 | value: 'vertical', 26 | }, 27 | { 28 | label: '顶部菜单模式', 29 | value: 'horizontal', 30 | }, 31 | { 32 | label: '混合菜单模式', 33 | value: 'blend', 34 | }, 35 | ]; 36 | 37 | return ( 38 | <> 39 | setDrawerOpen(true)} /> 40 | setDrawerOpen(false)} 47 | open={drawerOpen} 48 | > 49 |
50 | {intl.formatMessage({ id: 'layout.setting.layoutSettings' })} 51 |
52 | {sidebarSeting.map(i => { 53 | return ( 54 | 55 |
{ 60 | dispatch(setAppSidebarMode(i.value)); 61 | }} 62 | > 63 |
64 |
65 |
66 | 67 | ); 68 | })} 69 |
70 | {intl.formatMessage({ id: 'layout.setting.themeSettings' })} 71 | 72 | 73 |
74 | 75 | 76 | ); 77 | }); 78 | 79 | export default Setting; 80 | -------------------------------------------------------------------------------- /src/layout/components/Setting/style/index.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSObject } from '@emotion/react'; 2 | import type { GlobalToken } from 'antd/es/theme/interface'; 3 | 4 | export const getSidebarMode = (token: GlobalToken): CSSObject => { 5 | return { 6 | padding: `0 ${token.paddingSM}px`, 7 | color: token.colorText, 8 | '.sidebar_seting': { 9 | display: 'flex', 10 | justifyContent: 'space-between', 11 | alignItems: 'center', 12 | '.sidebar_mode': { 13 | position: 'relative', 14 | width: '80px', 15 | height: '60px', 16 | background: '#f0f2f5', 17 | borderRadius: 5, 18 | boxShadow: '0 1px 2.5px 0 rgb(0 0 0 / 18%)', 19 | overflow: 'hidden', 20 | '&:nth-of-type(1)': { 21 | div: { 22 | '&:nth-of-type(1)': { 23 | width: '30%', 24 | height: '100%', 25 | background: token.colorPrimary, 26 | }, 27 | 28 | '&:nth-of-type(2)': { 29 | position: 'absolute', 30 | top: 0, 31 | right: 0, 32 | width: '70%', 33 | height: '30%', 34 | background: '#fff', 35 | boxShadow: '0 0 1px #888', 36 | }, 37 | }, 38 | }, 39 | '&:nth-of-type(2)': { 40 | div: { 41 | '&:nth-of-type(1)': { 42 | width: '100%', 43 | height: '30%', 44 | boxShadow: '0 0 1px #888', 45 | background: token.colorPrimary, 46 | }, 47 | }, 48 | }, 49 | '&:nth-of-type(3)': { 50 | div: { 51 | '&:nth-of-type(1)': { 52 | width: '30%', 53 | height: '100%', 54 | background: '#fff', 55 | }, 56 | 57 | '&:nth-of-type(2)': { 58 | position: 'absolute', 59 | top: 0, 60 | right: 0, 61 | width: '100%', 62 | height: '30%', 63 | background: token.colorPrimary, 64 | boxShadow: '0 0 1px #888', 65 | }, 66 | }, 67 | }, 68 | }, 69 | '.sidebar_mode-select': { 70 | border: `2px solid ${token.colorPrimary}`, 71 | }, 72 | }, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/NavSidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import { getParentPaths } from '@/router/utils'; 2 | import { useAppSelector } from '@/store/hooks'; 3 | import { Menu } from 'antd'; 4 | import { memo, useMemo, useState } from 'react'; 5 | import { useLocation, useNavigate } from 'react-router'; 6 | import type { MenuProps } from 'antd'; 7 | import { useMenuList } from '../hooks/useMenuList'; 8 | 9 | const NavSidebar = memo(() => { 10 | const [openKeys, setOpenKeys] = useState([]); 11 | const sidebarMode = useAppSelector(state => state.app.sidebarMode); 12 | 13 | const { pathname } = useLocation(); 14 | const navigate = useNavigate(); 15 | const { menuList } = useMenuList(); 16 | 17 | const onOpenChange: MenuProps['onOpenChange'] = keys => { 18 | setOpenKeys(keys); 19 | }; 20 | 21 | const selectOpenKey = useMemo(() => { 22 | if (sidebarMode === 'blend') { 23 | const routeKey = getParentPaths(pathname, menuList); 24 | return [routeKey[0]]; 25 | } else { 26 | return [pathname]; 27 | } 28 | }, [pathname, sidebarMode]); 29 | 30 | const menuItems = useMemo(() => { 31 | if (sidebarMode === 'blend') { 32 | return menuList.map(i => { 33 | const { key, label, icon } = i; 34 | return { 35 | key, 36 | label, 37 | icon, 38 | }; 39 | }); 40 | } else { 41 | return menuList; 42 | } 43 | }, [sidebarMode, menuList]); 44 | 45 | return ( 46 | navigate(e.key)} 53 | style={{ border: 'none' }} 54 | /> 55 | ); 56 | }); 57 | export default NavSidebar; 58 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarInline/index.less: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | .ant-menu-inline-collapsed { 3 | width: 54px; 4 | } 5 | .ant-typography { 6 | line-height: inherit; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarInline/index.tsx: -------------------------------------------------------------------------------- 1 | import { findRouteByPath, getParentPaths } from '@/router/utils'; 2 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 3 | import { setAppCollapsed } from '@/store/modules/app'; 4 | import { useResponsive } from 'ahooks'; 5 | import { Drawer, Layout, Menu, theme } from 'antd'; 6 | import { memo, useEffect, useMemo, useState } from 'react'; 7 | import { shallowEqual } from 'react-redux'; 8 | import { useLocation, useNavigate } from 'react-router'; 9 | import type { MenuProps, SiderProps } from 'antd'; 10 | import AppLogo from '../../AppLogo'; 11 | import { useMenuList } from '../hooks/useMenuList'; 12 | import './index.less'; 13 | 14 | const { Sider } = Layout; 15 | 16 | const Sidebar = memo(() => { 17 | const { pathname } = useLocation(); 18 | const dispatch = useAppDispatch(); 19 | const { collapsed, sidebarMode } = useAppSelector( 20 | state => ({ 21 | collapsed: state.app.collapsed, 22 | sidebarMode: state.app.sidebarMode, 23 | }), 24 | shallowEqual, 25 | ); 26 | const [openKeys, setOpenKeys] = useState([]); 27 | const thme = theme.useToken(); 28 | const responsive = useResponsive(); 29 | const navigate = useNavigate(); 30 | const { menuList } = useMenuList(); 31 | 32 | useEffect(() => { 33 | if (!collapsed) { 34 | setOpenKeys(getParentPaths(pathname, menuList)); 35 | } else { 36 | setOpenKeys([]); 37 | } 38 | }, [collapsed, pathname]); 39 | 40 | const onOpenChange: MenuProps['onOpenChange'] = keys => { 41 | setOpenKeys(keys); 42 | }; 43 | 44 | const onBreakpoint: SiderProps['onBreakpoint'] = broken => { 45 | let collapsedValue = collapsed; 46 | if (broken) collapsedValue = true; 47 | else collapsedValue = false; 48 | dispatch(setAppCollapsed(collapsedValue)); 49 | }; 50 | 51 | const menuItems = useMemo(() => { 52 | if (sidebarMode === 'blend') { 53 | // path的父级路由组成的数组 54 | const parentPathArr = getParentPaths(pathname, menuList); 55 | // 当前路由的信息 56 | const parenetRoute = findRouteByPath(parentPathArr[0], menuList); 57 | if (parenetRoute) { 58 | if (parenetRoute.children) return parenetRoute.children; 59 | else return [parenetRoute]; 60 | } 61 | return []; 62 | } else { 63 | return menuList; 64 | } 65 | }, [sidebarMode, pathname, menuList]); 66 | 67 | const MenuRender = ( 68 | <> 69 | 70 | navigate(e.key)} 77 | style={{ border: 'none' }} 78 | /> 79 | 80 | ); 81 | 82 | return ( 83 | <> 84 | {(sidebarMode !== 'horizontal' || !responsive.sm) && ( 85 | <> 86 | {responsive.sm ? ( 87 | 101 | {MenuRender} 102 | 103 | ) : ( 104 | dispatch(setAppCollapsed(!collapsed))} 111 | open={!collapsed} 112 | > 113 |
{MenuRender}
114 |
115 | )} 116 | 117 | )} 118 | 119 | ); 120 | }); 121 | 122 | export default Sidebar; 123 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/hooks/useMenuList.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteList } from '@/hooks/useRouteList'; 2 | import { handlePowerRoute } from '@/router/utils'; 3 | import { useAppSelector } from '@/store/hooks'; 4 | import { useMemo } from 'react'; 5 | 6 | export const useMenuList = () => { 7 | const asyncRouter = useAppSelector(state => state.route.asyncRouter); 8 | const { routeListToMenu } = useRouteList(); 9 | 10 | const menuList = useMemo(() => { 11 | return routeListToMenu(handlePowerRoute(asyncRouter)); 12 | }, [asyncRouter]); 13 | 14 | return { menuList }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/layout/index.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | width: 100%; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, theme } from 'antd'; 2 | import React from 'react'; 3 | 4 | import AppMain from './components/AppMain/AppMain'; 5 | import Navbart from './components/Navbart'; 6 | import SidebarInline from './components/Sidebar/SidebarInline'; 7 | import './index.less'; 8 | 9 | const { Footer } = Layout; 10 | 11 | const LayoutApp: React.FC = () => { 12 | const thme = theme.useToken(); 13 | 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 |
Ant Design ©2018 Created by Ant UED
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default LayoutApp; 27 | -------------------------------------------------------------------------------- /src/locales/en_US/index.ts: -------------------------------------------------------------------------------- 1 | import api from './modules/api.json'; 2 | import layout from './modules/layout.json'; 3 | import login from './modules/login.json'; 4 | 5 | const en_US = { 6 | ...layout, 7 | ...api, 8 | ...login, 9 | }; 10 | 11 | export default en_US; 12 | -------------------------------------------------------------------------------- /src/locales/en_US/modules/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "api.errorTip": "Error Tip", 3 | "api.errMsg401": "The user does not have permission!", 4 | "api.errMsg403": "The user is authorized, but access is forbidden!", 5 | "api.errMsg404": "Network request error, the resource was not found!", 6 | "api.errMsg405": "Network request error, request method not allowed!", 7 | "api.errMsg408": "Network request timed out!", 8 | "api.errMsg500": "Server error, please contact the administrator!", 9 | "api.errMsg501": "The network is not implemented!", 10 | "api.errMsg502": "Network Error!", 11 | "api.errMsg503": "The service is unavailable, the server is temporarily overloaded or maintained!", 12 | "api.errMsg504": "Network timeout!", 13 | "api.errMsg505": "The http version does not support the request!" 14 | } 15 | -------------------------------------------------------------------------------- /src/locales/en_US/modules/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout.setting.title": "Setting", 3 | "layout.setting.layoutSettings": "Layout Settings", 4 | "layout.setting.themeSettings": "Theme Settings", 5 | "layout.memu.home": "Home", 6 | "layout.memu.nesting": "Nesting", 7 | "layout.memu.permissions": "Permissions", 8 | "layout.memu.permissionsPage": "Permissions Toggle", 9 | "layout.memu.testPermissionsPage1": "Admin Permissions", 10 | "layout.memu.testPermissionsPage2": "Test Permissions", 11 | "layout.memu.detailsPage": "DetailsPage", 12 | "layout.error.403": "Sorry, you are not authorized to access this page.", 13 | "layout.error.404": "Sorry, the page you visited does not exist.", 14 | "layout.error.500": "Sorry, something went wrong.", 15 | "layout.error.element.content": "The content you page has the following error:" 16 | } 17 | -------------------------------------------------------------------------------- /src/locales/en_US/modules/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "login.button": "Login", 3 | "login.forgotPassword": "Forgot password", 4 | "login.password": "Password:admin123", 5 | "login.passwordRules": "Please input your Password!", 6 | "login.rememberPassword": "Remember password", 7 | "login.userNameRules": "Please input your Username!", 8 | "login.username": "UserName:admin" 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/index.tsx: -------------------------------------------------------------------------------- 1 | import { createIntl, useIntl } from 'react-intl'; 2 | import type { IntlShape, MessageDescriptor } from 'react-intl'; 3 | import en_US from './en_US'; 4 | import zh_CN from './zh_CN'; 5 | 6 | export const localeConfig = { 7 | 'zh-CN': zh_CN, 8 | 'en-US': en_US, 9 | }; 10 | 11 | export type LocaleType = keyof typeof localeConfig; 12 | 13 | export type LocaleId = keyof typeof en_US; 14 | 15 | export interface Props extends MessageDescriptor { 16 | id: LocaleId; 17 | } 18 | 19 | export type FormatMessageProps = (descriptor: Props) => string; 20 | 21 | export const useLocale = () => { 22 | const { formatMessage: intlFormatMessage, ...rest } = useIntl(); 23 | const formatMessage: FormatMessageProps = intlFormatMessage; 24 | 25 | return { 26 | ...rest, 27 | formatMessage, 28 | }; 29 | }; 30 | 31 | let g_intl: IntlShape; 32 | 33 | /** 34 | * 获取当前的 intl 对象,可以在 node 中使用 35 | * @param locale 需要切换的语言类型 36 | * @param changeIntl 是否不使用 g_intl 37 | * @returns IntlShape 38 | */ 39 | export const getIntl = (locale?: LocaleType, changeIntl?: boolean) => { 40 | // 如果全局的 g_intl 存在,且不是 setIntl 调用 41 | if (g_intl && !changeIntl && !locale) { 42 | return g_intl; 43 | } 44 | // 如果存在于 localeInfo 中 45 | if (locale && localeConfig[locale]) { 46 | return createIntl({ 47 | locale, 48 | messages: localeConfig[locale], 49 | }); 50 | } 51 | // 使用 zh-CN 52 | if (localeConfig['zh-CN']) 53 | return createIntl({ 54 | locale: 'zh-CN', 55 | messages: localeConfig['zh-CN'], 56 | }); 57 | 58 | // 如果还没有,返回一个空的 59 | return createIntl({ 60 | locale: 'zh-CN', 61 | messages: {}, 62 | }); 63 | }; 64 | 65 | export const getIntlText = (id: LocaleId) => { 66 | return getIntl().formatMessage({ id }); 67 | }; 68 | 69 | /** 70 | * 切换全局的 intl 的设置 71 | * @param locale 语言的key 72 | */ 73 | export const setIntl = (locale: LocaleType) => { 74 | g_intl = getIntl(locale, true); 75 | }; 76 | -------------------------------------------------------------------------------- /src/locales/zh_CN/index.ts: -------------------------------------------------------------------------------- 1 | import api from './modules/api.json'; 2 | import layout from './modules/layout.json'; 3 | import login from './modules/login.json'; 4 | 5 | const zh_CN = { 6 | ...layout, 7 | ...api, 8 | ...login, 9 | }; 10 | 11 | export default zh_CN; 12 | -------------------------------------------------------------------------------- /src/locales/zh_CN/modules/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "api.errorTip": "错误提示", 3 | "api.errMsg401": "用户没有权限!", 4 | "api.errMsg403": "用户得到授权,但是访问是被禁止的。!", 5 | "api.errMsg404": "网络请求错误,未找到该资源!", 6 | "api.errMsg405": "网络请求错误,请求方法未允许!", 7 | "api.errMsg408": "网络请求超时!", 8 | "api.errMsg500": "服务器错误,请联系管理员!", 9 | "api.errMsg501": "网络未实现!", 10 | "api.errMsg502": "网络错误!", 11 | "api.errMsg503": "服务不可用,服务器暂时过载或维护!", 12 | "api.errMsg504": "网络超时!", 13 | "api.errMsg505": "http版本不支持该请求!" 14 | } 15 | -------------------------------------------------------------------------------- /src/locales/zh_CN/modules/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout.error.403": "抱歉,您无权访问此页面。", 3 | "layout.error.404": "抱歉,您访问的页面不存在。", 4 | "layout.error.500": "抱歉,出了问题。", 5 | "layout.error.element.content": "页面内容有以下错误:", 6 | "layout.memu.home": "首页", 7 | "layout.memu.nesting": "嵌套页面", 8 | "layout.memu.permissions": "权限管理", 9 | "layout.memu.permissionsPage": "权限切换", 10 | "layout.memu.testPermissionsPage1": "Admin权限测试页面", 11 | "layout.memu.testPermissionsPage2": "Test权限测试页面", 12 | "layout.memu.detailsPage": "详情页", 13 | "layout.setting.layoutSettings": "布局设置", 14 | "layout.setting.themeSettings": "主题设置", 15 | "layout.setting.title": "设置" 16 | } 17 | -------------------------------------------------------------------------------- /src/locales/zh_CN/modules/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "login.button": "登录", 3 | "login.forgotPassword": "忘记密码", 4 | "login.password": "密码:admin123", 5 | "login.passwordRules": "请输入您的密码!", 6 | "login.rememberPassword": "记住密码", 7 | "login.userNameRules": "请输入您的用户名!", 8 | "login.username": "用户名:admin" 9 | } 10 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { PersistGate } from 'redux-persist/integration/react'; 5 | import App from './App'; 6 | import store, { persistor } from './store'; 7 | import './index.css'; 8 | import 'virtual:svg-icons-register'; 9 | 10 | const routeDOM = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 11 | 12 | export const RootRender = ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | routeDOM.render(RootRender); 21 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteList } from '@/hooks/useRouteList'; 2 | import { useAppSelector } from '@/store/hooks'; 3 | import { memo, useEffect, useState } from 'react'; 4 | import { createBrowserRouter, Navigate, RouterProvider } from 'react-router'; 5 | import type { AsyncRouteType } from '@/store/modules/route'; 6 | import type { RouteObject } from 'react-router'; 7 | import { baseRouter, whiteList } from './modules'; 8 | import { handlePowerRoute } from './utils'; 9 | import type { RouteList } from './route'; 10 | 11 | const RouteView = memo(() => { 12 | const asyncRouter = useAppSelector(state => state.route.asyncRouter); 13 | const { handleRouteList } = useRouteList(); 14 | 15 | // 为“/”根路由添加重定向 16 | const handleRedirect = (asyncRouter: AsyncRouteType[]) => { 17 | const routerList = handleRouteList(handlePowerRoute(asyncRouter)); 18 | if (routerList.length) { 19 | routerList.push({ 20 | path: '', 21 | element: , 22 | }); 23 | } 24 | return [...routerList, ...whiteList]; 25 | }; 26 | 27 | const mapBaseRouter = (baseRouter: RouteList[], asyncRouter: AsyncRouteType[]) => { 28 | return baseRouter.map(i => { 29 | const routeItem = i; 30 | if (routeItem.path === '/') { 31 | routeItem.children = handleRedirect(asyncRouter); 32 | } 33 | return routeItem; 34 | }); 35 | }; 36 | 37 | const [route, setRoute] = useState(mapBaseRouter(baseRouter, asyncRouter)); 38 | 39 | // 更新路由列表 40 | useEffect(() => { 41 | setRoute(mapBaseRouter(baseRouter, asyncRouter)); 42 | }, [asyncRouter]); 43 | 44 | const routeElemt = createBrowserRouter(route as RouteObject[]); 45 | 46 | return ; 47 | }); 48 | 49 | export default RouteView; 50 | -------------------------------------------------------------------------------- /src/router/lazy/view.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const Home = lazy(() => import('@/views/Home')); 4 | export const Menu1_1 = lazy(() => import('@/views/Nested/Menu1/Menu1-1')); 5 | export const Menu1_2 = lazy(() => import('@/views/Nested/Menu1/Menu1-2')); 6 | export const Permissions = lazy(() => import('@/views/Power/Permissions')); 7 | export const TestPermissionsA = lazy(() => import('@/views/Power/test-permissions-a')); 8 | export const TestPermissionsB = lazy(() => import('@/views/Power/test-permissions-b')); 9 | export const DetailsPage = lazy(() => import('@/views/DetailsPage')); 10 | export const DetailsInfo = lazy(() => import('@/views/DetailsPage/DetailsInfo')); 11 | export const DetailsParams = lazy(() => import('@/views/DetailsPage/DetailsParams')); 12 | -------------------------------------------------------------------------------- /src/router/lazy/whiteList.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const ErrorPage403 = lazy(() => import('@/views/core/error/403')); 4 | export const ErrorElement = lazy(() => import('@/views/core/error/ErrorElement')); 5 | 6 | export const Refresh = lazy(() => import('@/views/core/Refresh')); 7 | 8 | export const Login = lazy(() => import('@/views/Login')); 9 | -------------------------------------------------------------------------------- /src/router/modules/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from '@/components/FormattedMessage'; 2 | import Layout from '@/layout'; 3 | import Authority from '@/layout/Authority'; 4 | import { AppstoreOutlined, DatabaseOutlined, HomeOutlined, UserSwitchOutlined } from '@ant-design/icons'; 5 | import type { RouteList } from '@/router/route'; 6 | import { 7 | DetailsInfo, 8 | DetailsPage, 9 | DetailsParams, 10 | Home, 11 | Menu1_1, 12 | Menu1_2, 13 | Permissions, 14 | TestPermissionsA, 15 | TestPermissionsB, 16 | } from '../lazy/view'; 17 | import { ErrorElement, ErrorPage403, Login, Refresh } from '../lazy/whiteList'; 18 | 19 | export const defaultRoute: RouteList[] = [ 20 | { 21 | path: '/home', 22 | id: 'Home', 23 | element: , 24 | handle: { label: , icon: }, 25 | }, 26 | { 27 | path: '/nested', 28 | id: 'Nested', 29 | redirect: '/nested/menu1', 30 | handle: { label: , icon: }, 31 | children: [ 32 | { 33 | path: 'menu1', 34 | id: 'Menu1', 35 | redirect: '/nested/menu1/menu1-1', 36 | handle: { label: 'menu-1' }, 37 | children: [ 38 | { 39 | path: 'menu1-1', 40 | id: 'Menu1-1', 41 | element: , 42 | handle: { label: 'menu-1-1' }, 43 | }, 44 | { 45 | path: 'menu1-2', 46 | id: 'Menu1-2', 47 | element: , 48 | handle: { label: 'menu-1-2' }, 49 | }, 50 | ], 51 | }, 52 | ], 53 | }, 54 | { 55 | path: '/power', 56 | id: 'Power', 57 | redirect: '/power/permissions', 58 | handle: { 59 | label: , 60 | icon: , 61 | }, 62 | children: [ 63 | { 64 | path: 'permissions', 65 | id: 'Permissions', 66 | element: , 67 | handle: { label: }, 68 | }, 69 | { 70 | path: 'test-permissions-a', 71 | id: 'TestPermissionsA', 72 | element: , 73 | handle: { label: }, 74 | }, 75 | { 76 | path: 'test-permissions-b', 77 | id: 'TestPermissionsB', 78 | element: , 79 | handle: { label: }, 80 | }, 81 | ], 82 | }, 83 | { 84 | path: '/details-page', 85 | id: 'DetailsPage', 86 | alwaysShow: false, 87 | handle: { label: , whiteList: true }, 88 | children: [ 89 | { 90 | path: '', 91 | id: 'DetailsList', 92 | element: , 93 | handle: { 94 | label: , 95 | icon: , 96 | }, 97 | }, 98 | { 99 | path: 'details-info', 100 | id: 'DetailsInfo', 101 | element: , 102 | handle: { label: '详情页', hideSidebar: true }, 103 | }, 104 | { 105 | path: 'details-params/:id', 106 | id: 'DetailsParams', 107 | element: , 108 | handle: { label: '详情页', hideSidebar: true }, 109 | }, 110 | ], 111 | }, 112 | ]; 113 | 114 | export const whiteList: RouteList[] = [ 115 | { 116 | path: '*', 117 | element: , 118 | }, 119 | { 120 | path: '/refresh/*', 121 | element: , 122 | handle: { label: '', hideSidebar: true, whiteList: true }, 123 | }, 124 | ]; 125 | 126 | export const baseRouter: RouteList[] = [ 127 | { 128 | path: '/', 129 | element: ( 130 | 131 | 132 | 133 | ), 134 | errorElement: , 135 | children: [...whiteList], 136 | }, 137 | { 138 | path: '/login', 139 | element: , 140 | }, 141 | ]; 142 | -------------------------------------------------------------------------------- /src/router/route.d.ts: -------------------------------------------------------------------------------- 1 | import type { RouteObject } from 'react-router'; 2 | 3 | export interface MenuItem { 4 | label: React.ReactNode; 5 | key?: React.Key; 6 | icon?: React.ReactNode; 7 | children?: MenuItem[]; 8 | type?: 'group'; 9 | whiteList?: boolean; 10 | hideSidebar?: boolean; 11 | // 是否隐藏标签 12 | hideTabs?: boolean; 13 | } 14 | 15 | export type RouteList = Omit & { 16 | redirect?: string; 17 | children?: RouteList[]; 18 | alwaysShow?: boolean; 19 | handle?: MenuItem; 20 | // meta: MenuItem; 21 | }; 22 | -------------------------------------------------------------------------------- /src/router/utils.ts: -------------------------------------------------------------------------------- 1 | import { getRouteApi } from '@/server/route'; 2 | import store from '@/store'; 3 | import { setStoreAsyncRouter } from '@/store/modules/route'; 4 | import { cloneDeep } from 'lodash-es'; 5 | import { createBrowserRouter } from 'react-router'; 6 | import type { MenuItem, RouteList } from '@/router/route'; 7 | import type { AsyncRouteType } from '@/store/modules/route'; 8 | import type { Key } from 'react'; 9 | import type { RouteObject } from 'react-router'; 10 | import { defaultRoute } from './modules'; 11 | 12 | // import { HomeOutlined } from '@ant-design/icons'; 13 | 14 | export async function initAsyncRoute(power: string) { 15 | store.dispatch(setStoreAsyncRouter([])); 16 | 17 | const res = await getRouteApi({ name: power }); 18 | if (res.data.length) { 19 | store.dispatch(setStoreAsyncRouter(res.data)); 20 | } 21 | return ''; 22 | } 23 | 24 | export function handlePowerRoute(dataRouter: AsyncRouteType[], routerList: RouteList[] = defaultRoute) { 25 | const newRouteList: RouteList[] = []; 26 | routerList.forEach(i => { 27 | const item = cloneDeep(i); 28 | if (!item.handle.whiteList) { 29 | const rItem = dataRouter.find(r => r.id === item.id); 30 | if (rItem) { 31 | if (rItem.children && item.children && item.children.length) { 32 | const children = handlePowerRoute(rItem.children, item.children); 33 | item.children = children; 34 | if (children) newRouteList.push(item); 35 | } else { 36 | newRouteList.push(item); 37 | } 38 | } 39 | } else { 40 | newRouteList.push(item); 41 | } 42 | }); 43 | return newRouteList; 44 | } 45 | 46 | export function createRouterList(routeList: RouteObject[]) { 47 | return createBrowserRouter(routeList); 48 | } 49 | 50 | // 通过path获取父级路径 51 | export function getParentPaths(routePath: string, routes: MenuItem[]): string[] { 52 | // 深度遍历查找 53 | function dfs(routes: MenuItem[], key: string, parents: string[]) { 54 | for (let i = 0; i < routes.length; i++) { 55 | const item = routes[i]; 56 | // 找到key则返回父级key 57 | if (item.key === key) return [item.key]; 58 | // children不存在或为空则不递归 59 | if (!item.children || !item.children.length) continue; 60 | // 往下查找时将当前key入栈 61 | parents.push(item.key as string); 62 | 63 | if (dfs(item.children, key, parents).length) return parents; 64 | // 深度遍历查找未找到时当前path 出栈 65 | parents.pop(); 66 | } 67 | // 未找到时返回空数组 68 | return []; 69 | } 70 | return dfs(routes, routePath, []); 71 | } 72 | 73 | // 查找对应path的路由信息 74 | export function findRouteByPath(path: Key, routes: MenuItem[]): MenuItem | null { 75 | const res = routes.find(item => item.key === path) || null; 76 | if (res) { 77 | return res; 78 | } else { 79 | for (let i = 0; i < routes.length; i++) { 80 | if (routes[i].children instanceof Array && routes[i].children?.length) { 81 | const miRes = findRouteByPath(path, routes[i].children as MenuItem[]); 82 | if (miRes) { 83 | return miRes; 84 | } else { 85 | if (routes[i].key === path) return routes[i]; 86 | } 87 | } 88 | } 89 | return null; 90 | } 91 | } 92 | 93 | // 拼接路径 伪path resolve 94 | function pathResolve(...paths: string[]) { 95 | let resolvePath = ''; 96 | let isAbsolutePath = false; 97 | for (let i = paths.length - 1; i > -1; i--) { 98 | const path = paths[i]; 99 | if (isAbsolutePath) { 100 | break; 101 | } 102 | if (!path) { 103 | continue; 104 | } 105 | resolvePath = `${path}/${resolvePath}`; 106 | isAbsolutePath = path.charCodeAt(0) === 47; 107 | } 108 | if (/^\/+$/.test(resolvePath)) { 109 | resolvePath = resolvePath.replace(/(\/+)/, '/'); 110 | } else { 111 | resolvePath = resolvePath 112 | .replace(/(?!^)\w+\/+\.{2}\//g, '') 113 | .replace(/(?!^)\.\//g, '') 114 | .replace(/\/+$/, ''); 115 | } 116 | return resolvePath; 117 | } 118 | 119 | // 设置完整路由path, 120 | export function setUpRoutePath(routeList: AsyncRouteType[], pathName = '') { 121 | for (const node of routeList) { 122 | if (pathName) { 123 | node.path = pathResolve(pathName, node.path || ''); 124 | } 125 | if (node.children && node.children.length) { 126 | setUpRoutePath(node.children, node.path); 127 | } 128 | } 129 | return routeList; 130 | } 131 | 132 | // 扁平路由 133 | export function formatFlatteningRoutes(routesList: AsyncRouteType[]) { 134 | if (routesList.length === 0) return routesList; 135 | let hierarchyList = routesList; 136 | for (let i = 0; i < hierarchyList.length; i++) { 137 | if (hierarchyList[i].children) { 138 | hierarchyList = hierarchyList.slice(0, i + 1).concat(hierarchyList[i].children || [], hierarchyList.slice(i + 1)); 139 | } 140 | } 141 | return hierarchyList; 142 | } 143 | -------------------------------------------------------------------------------- /src/server/route.ts: -------------------------------------------------------------------------------- 1 | import { deffHttp } from '@/utils/axios'; 2 | import type { AsyncRouteType } from '@/store/modules/route'; 3 | 4 | enum Api { 5 | ROUTE_LIST = '/mock_api/getRoute', 6 | } 7 | 8 | interface Param { 9 | name: string; 10 | } 11 | 12 | export const getRouteApi = (data: Param) => deffHttp.post({ url: Api.ROUTE_LIST, data }); 13 | -------------------------------------------------------------------------------- /src/server/useInfo.ts: -------------------------------------------------------------------------------- 1 | import { deffHttp } from '@/utils/axios'; 2 | 3 | export interface UseInfoType { 4 | name: string; 5 | userid: string; 6 | email: string; 7 | signature: string; 8 | introduction: string; 9 | title: string; 10 | token: string; 11 | power: 'test' | 'admin'; 12 | } 13 | 14 | export const getUserInfo = (user: string, pwd: string) => 15 | deffHttp.post( 16 | { 17 | url: '/mock_api/login', 18 | data: { username: user, password: pwd }, 19 | }, 20 | { errorMessageMode: 'modal', withToken: false }, 21 | ); 22 | -------------------------------------------------------------------------------- /src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import type { TypedUseSelectorHook } from 'react-redux'; 3 | import type { AppDispatch, RootState } from './index'; 4 | 5 | // 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector` 6 | export const useAppDispatch: () => AppDispatch = useDispatch; 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; 8 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'; 3 | import storage from 'redux-persist/lib/storage'; 4 | // import thunk from 'redux-thunk'; 5 | import appReducer from './modules/app'; 6 | import routeReducer from './modules/route'; 7 | import userReducer from './modules/user'; 8 | 9 | const reducers = combineReducers({ 10 | app: appReducer, 11 | route: routeReducer, 12 | user: userReducer, 13 | }); 14 | 15 | const persistConfig = { 16 | key: 'react-xs', 17 | storage, 18 | // 白名单 19 | whitelist: ['app', 'route', 'user'], 20 | // 黑名单 21 | blacklist: [], 22 | }; 23 | 24 | const persistedReducer = persistReducer(persistConfig, reducers); 25 | 26 | const store = configureStore({ 27 | reducer: persistedReducer, 28 | middleware: getDefaultMiddleware => 29 | getDefaultMiddleware({ 30 | serializableCheck: { 31 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 32 | }, 33 | }), 34 | }); 35 | 36 | // 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型 37 | export type RootState = ReturnType; 38 | // 推断出类型: {posts: PostsState, comments: CommentsState, users: UsersState} 39 | export type AppDispatch = typeof store.dispatch; 40 | 41 | export const persistor = persistStore(store); 42 | 43 | export default store; 44 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import type { LocaleType } from '@/locales'; 3 | import type { PayloadAction } from '@reduxjs/toolkit'; 4 | 5 | export type ThemeMode = 'dark' | 'light'; 6 | export type SidebarMode = 'vertical' | 'horizontal' | 'blend'; 7 | 8 | export interface AppConfigMode { 9 | collapsed: boolean; 10 | locale: LocaleType; 11 | themeMode: ThemeMode; 12 | sidebarMode: SidebarMode; 13 | color: string; 14 | } 15 | 16 | const initialState: AppConfigMode = { 17 | collapsed: false, 18 | locale: 'zh-CN', 19 | themeMode: 'light', 20 | sidebarMode: 'vertical', 21 | color: '#409eff', 22 | }; 23 | 24 | export const appSlice = createSlice({ 25 | name: 'appConfig', 26 | initialState, 27 | reducers: { 28 | setAppCollapsed: (state, action: PayloadAction) => { 29 | state.collapsed = action.payload; 30 | }, 31 | setAppLocale: (state, action: PayloadAction) => { 32 | state.locale = action.payload; 33 | }, 34 | setAppThemeMode: (state, action: PayloadAction) => { 35 | state.themeMode = action.payload; 36 | }, 37 | setAppSidebarMode: (state, action: PayloadAction) => { 38 | state.sidebarMode = action.payload; 39 | }, 40 | setAppColor: (state, action: PayloadAction) => { 41 | state.color = action.payload; 42 | }, 43 | }, 44 | }); 45 | // 每个 case reducer 函数会生成对应的 Action creators 46 | export const { setAppCollapsed, setAppColor, setAppLocale, setAppSidebarMode, setAppThemeMode } = appSlice.actions; 47 | 48 | export default appSlice.reducer; 49 | -------------------------------------------------------------------------------- /src/store/modules/route.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import type { LocaleId } from '@/locales'; 3 | import type { PayloadAction } from '@reduxjs/toolkit'; 4 | // import { formatFlatteningRoutes, setUpRoutePath } from '@/router/utils'; 5 | 6 | export interface AsyncRouteType { 7 | path: string; 8 | id: string; 9 | children: AsyncRouteType[]; 10 | } 11 | 12 | export interface MultiTabsType { 13 | label?: string; 14 | localeLabel?: LocaleId; 15 | key: string; 16 | } 17 | 18 | interface RouteState { 19 | asyncRouter: AsyncRouteType[]; 20 | // levelAsyncRouter: AsyncRouteType[]; 21 | multiTabs: MultiTabsType[]; 22 | } 23 | 24 | const initialState: RouteState = { 25 | asyncRouter: [], 26 | // levelAsyncRouter: [], 27 | multiTabs: [], 28 | }; 29 | 30 | export const routeSlice = createSlice({ 31 | name: 'route', 32 | initialState, 33 | reducers: { 34 | setStoreAsyncRouter: (state, action: PayloadAction) => { 35 | state.asyncRouter = action.payload; 36 | // state.levelAsyncRouter = formatFlatteningRoutes(setUpRoutePath(action.payload)); 37 | }, 38 | setStoreMultiTabs: (state, action: PayloadAction<{ type: 'add' | 'delete' | 'update'; tabs: MultiTabsType }>) => { 39 | const { type, tabs } = action.payload; 40 | const tabIndex = state.multiTabs.findIndex(i => i.key === tabs.key); 41 | switch (type) { 42 | case 'add': 43 | if (tabIndex === -1) state.multiTabs.push(tabs); 44 | break; 45 | case 'delete': 46 | if (tabIndex !== -1) state.multiTabs.splice(tabIndex, 1); 47 | break; 48 | case 'update': 49 | if (tabIndex !== -1) state.multiTabs[tabIndex] = tabs; 50 | break; 51 | default: 52 | break; 53 | } 54 | }, 55 | }, 56 | }); 57 | // 每个 case reducer 函数会生成对应的 Action creators 58 | export const { setStoreAsyncRouter, setStoreMultiTabs } = routeSlice.actions; 59 | 60 | export default routeSlice.reducer; 61 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import type { UseInfoType } from '@/server/useInfo'; 3 | import type { PayloadAction } from '@reduxjs/toolkit'; 4 | 5 | interface UserSliceType { 6 | userInfo?: UseInfoType; 7 | power?: UseInfoType['power']; 8 | } 9 | 10 | const initialState: UserSliceType = {}; 11 | 12 | export const UserSlice = createSlice({ 13 | name: 'userInfo', 14 | initialState, 15 | reducers: { 16 | setUserInfo: (state, action: PayloadAction) => { 17 | state.userInfo = action.payload; 18 | state.power = action.payload.power; 19 | }, 20 | setPower: (state, action: PayloadAction) => { 21 | state.power = action.payload; 22 | if (state.userInfo) { 23 | state.userInfo.power = action.payload; 24 | } 25 | }, 26 | setSignOut: state => { 27 | delete state.userInfo; 28 | delete state.power; 29 | }, 30 | }, 31 | }); 32 | 33 | export const { setUserInfo, setPower, setSignOut } = UserSlice.actions; 34 | 35 | export default UserSlice.reducer; 36 | -------------------------------------------------------------------------------- /src/utils/axios/axiosConfig.ts: -------------------------------------------------------------------------------- 1 | import type { RequestOptions, Result } from '#/axios'; 2 | /** 3 | * axios 数据处理类 4 | */ 5 | import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 6 | 7 | export interface CreateAxiosOptions extends AxiosRequestConfig { 8 | requestOptions?: RequestOptions; 9 | interceptor?: AxiosInterceptor; 10 | } 11 | 12 | type RequestInterceptorsConfig = Pick & 13 | InternalAxiosRequestConfig; 14 | 15 | export abstract class AxiosInterceptor { 16 | /** 17 | * @description: 请求前的配置 18 | */ 19 | beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig; 20 | 21 | /** 22 | * @description: 请求成功的处理 23 | */ 24 | requestHook?: (res: AxiosResponse, options: RequestOptions) => any; 25 | 26 | /** 27 | * @description: 请求失败处理 28 | */ 29 | requestCatchHook?: (e: Error, options: RequestOptions) => Promise; 30 | 31 | /** 32 | * @description: 请求之前的拦截器 33 | */ 34 | requestInterceptors?: (config: RequestInterceptorsConfig) => RequestInterceptorsConfig; 35 | 36 | /** 37 | * @description: 请求之前的拦截器错误处理 38 | */ 39 | requestInterceptorsCatch?: (error: Error) => void; 40 | 41 | /** 42 | * @description: 请求之后的拦截器 43 | */ 44 | responseInterceptors?: (res: AxiosResponse) => AxiosResponse; 45 | 46 | /** 47 | * @description: 请求之后的拦截器错误处理 48 | */ 49 | responseInterceptorsCatch?: (error: Error) => void; 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/axios/axiosStatus.ts: -------------------------------------------------------------------------------- 1 | import { createErrorModal, createErrorMsg } from '@/hooks/web/useMessage'; 2 | import { getIntlText } from '@/locales'; 3 | import type { ErrorMessageMode } from '#/axios'; 4 | 5 | export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = 'message'): void { 6 | let errMessage = ''; 7 | 8 | switch (status) { 9 | case 400: 10 | errMessage = `${msg}`; 11 | break; 12 | case 401: 13 | errMessage = getIntlText('api.errMsg401'); 14 | break; 15 | case 403: 16 | errMessage = getIntlText('api.errMsg403'); 17 | break; 18 | case 404: 19 | errMessage = getIntlText('api.errMsg404'); 20 | break; 21 | case 405: 22 | errMessage = getIntlText('api.errMsg405'); 23 | break; 24 | case 408: 25 | errMessage = getIntlText('api.errMsg408'); 26 | break; 27 | case 500: 28 | errMessage = getIntlText('api.errMsg500'); 29 | break; 30 | case 501: 31 | errMessage = getIntlText('api.errMsg501'); 32 | break; 33 | case 502: 34 | errMessage = getIntlText('api.errMsg502'); 35 | break; 36 | case 503: 37 | errMessage = getIntlText('api.errMsg503'); 38 | break; 39 | case 504: 40 | errMessage = getIntlText('api.errMsg504'); 41 | break; 42 | case 505: 43 | errMessage = getIntlText('api.errMsg505'); 44 | break; 45 | default: 46 | } 47 | if (errMessage) { 48 | if (errorMessageMode === 'modal') { 49 | createErrorModal(errMessage); 50 | } else if (errorMessageMode === 'message') { 51 | createErrorMsg(errMessage); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/axios/errorConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from '#/axios'; 2 | import type { AxiosResponse } from 'axios'; 3 | 4 | export const errorData = (res: AxiosResponse>) => { 5 | return { 6 | data: null, 7 | message: res.data.message, 8 | code: res.data.code, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/axios/iAxios.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@/utils/is'; 2 | import axios from 'axios'; 3 | import { cloneDeep } from 'lodash-es'; 4 | import type { RequestOptions, Result } from '#/axios'; 5 | import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; 6 | import type { CreateAxiosOptions } from './axiosConfig'; 7 | 8 | /** 9 | * @description: axios 模块 10 | */ 11 | export class IAxios { 12 | private axiosInstance: AxiosInstance; 13 | private readonly options: CreateAxiosOptions; 14 | 15 | constructor(options: CreateAxiosOptions) { 16 | this.options = options; 17 | this.axiosInstance = axios.create(options); 18 | this.setupInterceptors(); 19 | } 20 | 21 | /** 22 | * @description: 创建axios 23 | */ 24 | private createAxios(config: CreateAxiosOptions): void { 25 | this.axiosInstance = axios.create(config); 26 | } 27 | 28 | /** 29 | * @description 获取拦截器配置 30 | */ 31 | private getInterceptor() { 32 | const { interceptor } = this.options; 33 | return interceptor; 34 | } 35 | 36 | /** 37 | * @description: 重新配置 axios 38 | */ 39 | configAxios(config: CreateAxiosOptions) { 40 | if (!this.axiosInstance) { 41 | return; 42 | } 43 | this.createAxios(config); 44 | } 45 | 46 | /** 47 | * @description 挂载拦截器 48 | */ 49 | private setupInterceptors() { 50 | const interceptor = this.getInterceptor(); 51 | if (!interceptor) { 52 | return; 53 | } 54 | const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } = 55 | interceptor; 56 | 57 | // 此方法为了过滤不挂载interceptor没配置的拦截器 58 | 59 | // 请求拦截器配置 60 | requestInterceptors && 61 | isFunction(requestInterceptors) && 62 | this.axiosInstance.interceptors.request.use(requestInterceptors, undefined); 63 | 64 | // 请求拦截器失败配置 65 | requestInterceptorsCatch && 66 | isFunction(requestInterceptorsCatch) && 67 | this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch); 68 | 69 | // 响应拦截器配置 70 | responseInterceptors && 71 | isFunction(responseInterceptors) && 72 | this.axiosInstance.interceptors.response.use(responseInterceptors, undefined); 73 | 74 | // 响应拦截器失败配置 75 | responseInterceptorsCatch && 76 | isFunction(responseInterceptorsCatch) && 77 | this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch); 78 | } 79 | 80 | /** 81 | * @description get请求(config:axios请求配置, options:数据的特殊处理) 82 | */ 83 | get(config: AxiosRequestConfig, options?: RequestOptions): Promise> { 84 | return this.request({ ...config, method: 'GET' }, options); 85 | } 86 | 87 | /** 88 | * @description post请求(config:axios请求配置, options:数据的特殊处理) 89 | */ 90 | post(config: AxiosRequestConfig, options?: RequestOptions): Promise> { 91 | return this.request({ ...config, method: 'POST' }, options); 92 | } 93 | 94 | /** 95 | * @description put请求(config:axios请求配置, options:数据的特殊处理) 96 | */ 97 | put(config: AxiosRequestConfig, options?: RequestOptions): Promise> { 98 | return this.request({ ...config, method: 'PUT' }, options); 99 | } 100 | 101 | /** 102 | * @description delete请求(config:axios请求配置, options:数据的特殊处理) 103 | */ 104 | delete(config: AxiosRequestConfig, options?: RequestOptions): Promise> { 105 | return this.request({ ...config, method: 'DELETE' }, options); 106 | } 107 | 108 | /** 109 | * @description 请求体 110 | */ 111 | request(config: AxiosRequestConfig, options?: RequestOptions): Promise> { 112 | let conf: CreateAxiosOptions = cloneDeep(config); 113 | 114 | const interceptor = this.getInterceptor(); 115 | 116 | const { requestOptions } = this.options; 117 | 118 | const opt: RequestOptions = Object.assign({}, requestOptions, options); 119 | 120 | const { beforeRequestHook, requestCatchHook, requestHook } = interceptor || {}; 121 | if (beforeRequestHook && isFunction(beforeRequestHook)) { 122 | conf = beforeRequestHook(conf, opt); 123 | } 124 | 125 | conf.requestOptions = opt; 126 | 127 | return new Promise((resolve, reject) => { 128 | this.axiosInstance 129 | .request>>(conf) 130 | .then((res: AxiosResponse>) => { 131 | if (requestHook && isFunction(requestHook)) { 132 | try { 133 | resolve(requestHook(res, opt)); 134 | } catch (err) { 135 | reject(err || new Error('request error!')); 136 | } 137 | return; 138 | } 139 | resolve(res as unknown as Promise>); 140 | }) 141 | .catch((e: Error | AxiosError) => { 142 | if (requestCatchHook && isFunction(requestCatchHook)) { 143 | reject(requestCatchHook(e, opt)); 144 | return; 145 | } 146 | if (axios.isAxiosError(e)) { 147 | // rewrite error message from axios in here 148 | } 149 | reject(e); 150 | }); 151 | }); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/utils/axios/index.ts: -------------------------------------------------------------------------------- 1 | import { createErrorModal, createErrorMsg } from '@/hooks/web/useMessage'; 2 | import { isString } from 'lodash-es'; 3 | import { checkStatus } from './axiosStatus'; 4 | import { errorData } from './errorConfig'; 5 | import { IAxios } from './iAxios'; 6 | import type { AxiosInterceptor, CreateAxiosOptions } from './axiosConfig'; 7 | 8 | /** 9 | * @description:一下所有拦截器请根据自身使用场景更改 10 | */ 11 | const interceptor: AxiosInterceptor = { 12 | /** 13 | * @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误 14 | */ 15 | requestHook: (res, options) => { 16 | /** 17 | * 此处方法是对请求回来的数据进行处理, 18 | * 根据自己的使用场景更改 19 | */ 20 | const { data } = res; 21 | const { errorMessageMode } = options; 22 | if (data) { 23 | if (data.code === -1) { 24 | if (errorMessageMode === 'modal') { 25 | createErrorModal(data.message); 26 | } else if (errorMessageMode === 'message') { 27 | createErrorMsg(data.message); 28 | } 29 | return errorData(res); 30 | } else { 31 | const { code, data: dataInfo, message } = data; 32 | if (!code && !dataInfo && !message) { 33 | const toData = { 34 | code: 1, 35 | data, 36 | message: 'ok', 37 | }; 38 | return toData; 39 | } 40 | } 41 | } 42 | return data; 43 | }, 44 | 45 | /** 46 | * @description: 请求失败的错误处理 47 | */ 48 | requestCatchHook: (e, _options) => { 49 | return Promise.reject(e); 50 | }, 51 | 52 | /** 53 | * @description: 请求之前处理config 54 | */ 55 | beforeRequestHook: (config, options) => { 56 | const { urlPrefix } = options; 57 | if (urlPrefix && isString(urlPrefix)) config.url = `${urlPrefix}${config.url}`; 58 | return config; 59 | }, 60 | 61 | /** 62 | * @description: 请求拦截器处理 63 | */ 64 | requestInterceptors: config => { 65 | const { requestOptions } = config; 66 | if (requestOptions?.withToken) { 67 | (config as Recordable).headers._token = 'myToken'; 68 | if (requestOptions?.specialToken) (config as Recordable).headers._token = requestOptions?.specialToken; 69 | } 70 | 71 | return config; 72 | }, 73 | 74 | /** 75 | * @description: 请求拦截器错误处理 76 | */ 77 | requestInterceptorsCatch: error => { 78 | return error; 79 | }, 80 | 81 | /** 82 | * @description: 响应拦截器处理 83 | */ 84 | responseInterceptors: res => { 85 | return res; 86 | }, 87 | 88 | /** 89 | * @description: 响应拦截器错误处理 90 | */ 91 | responseInterceptorsCatch: (error: any) => { 92 | const { response, message, config } = error || {}; 93 | const errorMessageMode = config.requestOptions.errorMessageMode || 'none'; 94 | checkStatus(response ? response.status : 404, message, errorMessageMode); 95 | return error; 96 | }, 97 | }; 98 | 99 | function createAxios(opt?: Partial) { 100 | return new IAxios({ 101 | ...{ 102 | acoisadmisf: '', 103 | // 请求时间 104 | timeout: 10 * 1000, 105 | // (拦截器)数据处理方式 106 | interceptor, 107 | headers: { 'Content-Type': 'application/json' }, 108 | // 配置项(需要在拦截器中做的处理),下面的选项都可以在独立的接口请求中覆盖 109 | requestOptions: { 110 | withToken: true, 111 | errorMessageMode: 'message', 112 | }, 113 | }, 114 | ...(opt || {}), 115 | }); 116 | } 117 | export const deffHttp = createAxios(); 118 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | export function is(val: unknown, type: string) { 4 | return toString.call(val) === `[object ${type}]`; 5 | } 6 | 7 | export function isDef(val?: T): val is T { 8 | return typeof val !== 'undefined'; 9 | } 10 | 11 | export function isUnDef(val?: T): val is T { 12 | return !isDef(val); 13 | } 14 | 15 | export function isObject(val: any): val is Record { 16 | return val !== null && is(val, 'Object'); 17 | } 18 | 19 | export function isEmpty(val: T): val is T { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0; 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0; 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function isDate(val: unknown): val is Date { 36 | return is(val, 'Date'); 37 | } 38 | 39 | export function isNull(val: unknown): val is null { 40 | return val === null; 41 | } 42 | 43 | export function isNullAndUnDef(val: unknown): val is null | undefined { 44 | return isUnDef(val) && isNull(val); 45 | } 46 | 47 | export function isNullOrUnDef(val: unknown): val is null | undefined { 48 | return isUnDef(val) || isNull(val); 49 | } 50 | 51 | export function isNumber(val: unknown): val is number { 52 | return is(val, 'Number'); 53 | } 54 | 55 | export function isPromise(val: unknown): val is Promise { 56 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); 57 | } 58 | 59 | export function isString(val: unknown): val is string { 60 | return is(val, 'String'); 61 | } 62 | 63 | export function isFunction any>(val: unknown): val is T { 64 | return typeof val === 'function'; 65 | } 66 | 67 | export function isBoolean(val: unknown): val is boolean { 68 | return is(val, 'Boolean'); 69 | } 70 | 71 | export function isRegExp(val: unknown): val is RegExp { 72 | return is(val, 'RegExp'); 73 | } 74 | 75 | export function isArray(val: any): val is Array { 76 | return val && Array.isArray(val); 77 | } 78 | 79 | export function isWindow(val: any): val is Window { 80 | return typeof window !== 'undefined' && is(val, 'Window'); 81 | } 82 | 83 | export function isElement(val: unknown): val is Element { 84 | return isObject(val) && !!val.tagName; 85 | } 86 | 87 | export function isMap(val: unknown): val is Map { 88 | return is(val, 'Map'); 89 | } 90 | 91 | export const isServer = typeof window === 'undefined'; 92 | 93 | export const isClient = !isServer; 94 | 95 | export function isUrl(path: string): boolean { 96 | const reg = 97 | /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 98 | return reg.test(path); 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/operate/index.ts: -------------------------------------------------------------------------------- 1 | export const hasClass = (ele: RefType, cls: string): any => { 2 | return !!ele.className.match(new RegExp(`(\\s|^)${cls}(\\s|$)`)); 3 | }; 4 | 5 | export const addClass = (ele: RefType, cls: string, extracls?: string): any => { 6 | if (!hasClass(ele, cls)) ele.className += ` ${cls}`; 7 | if (extracls) { 8 | if (!hasClass(ele, extracls)) ele.className += ` ${extracls}`; 9 | } 10 | }; 11 | 12 | export const removeClass = (ele: RefType, cls: string, extracls?: string): any => { 13 | if (hasClass(ele, cls)) { 14 | const reg = new RegExp(`(\\s|^)${cls}(\\s|$)`); 15 | ele.className = ele.className.replace(reg, ' ').trim(); 16 | } 17 | if (extracls) { 18 | if (hasClass(ele, extracls)) { 19 | const regs = new RegExp(`(\\s|^)${extracls}(\\s|$)`); 20 | ele.className = ele.className.replace(regs, ' ').trim(); 21 | } 22 | } 23 | }; 24 | 25 | export const toggleClass = (flag: boolean, clsName: string, target?: RefType): any => { 26 | const targetEl = target || document.body; 27 | let { className } = targetEl; 28 | className = className.replace(clsName, ''); 29 | targetEl.className = flag ? `${className} ${clsName} ` : className; 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | import type { StorageConfig, StorageType, StorageValue } from './types'; 3 | 4 | // 十六位十六进制数作为密钥 5 | const SECRET_KEY = CryptoJS.enc.Utf8.parse('3333e6e143439161'); 6 | // 十六位十六进制数作为密钥偏移量 7 | const SECRET_IV = CryptoJS.enc.Utf8.parse('e3bbe7e3ba84431a'); 8 | 9 | // 类型 window.localStorage,window.sessionStorage, 10 | let config: StorageConfig = { 11 | prefix: 'xiaosiAdmin', // 名称前缀 建议:项目名 + 项目版本 12 | expire: 0, //过期时间 单位:秒 13 | isEncrypt: false, // 默认加密 为了调试方便, 开发过程中可以不加密 14 | }; 15 | 16 | /** 17 | * 加密方法 18 | * @param data 19 | * @returns {string} 20 | */ 21 | const encrypt = (data: string): string => { 22 | if (typeof data === 'object') { 23 | try { 24 | data = JSON.stringify(data); 25 | } catch (error) { 26 | console.error('encrypt error:', error); 27 | } 28 | } 29 | const dataHex = CryptoJS.enc.Utf8.parse(data); 30 | const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, { 31 | iv: SECRET_IV, 32 | mode: CryptoJS.mode.CBC, 33 | padding: CryptoJS.pad.Pkcs7, 34 | }); 35 | return encrypted.ciphertext.toString(); 36 | }; 37 | 38 | /** 39 | * 解密方法 40 | * @param data 41 | * @returns {string} 42 | */ 43 | const decrypt = (data: string): string => { 44 | const encryptedHexStr = CryptoJS.enc.Hex.parse(data); 45 | const str = CryptoJS.enc.Base64.stringify(encryptedHexStr); 46 | const decrypt = CryptoJS.AES.decrypt(str, SECRET_KEY, { 47 | iv: SECRET_IV, 48 | mode: CryptoJS.mode.CBC, 49 | padding: CryptoJS.pad.Pkcs7, 50 | }); 51 | const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); 52 | return decryptedStr.toString(); 53 | }; 54 | 55 | // 名称前自动添加前缀 56 | const autoAddPrefix = (key: string): string => { 57 | const prefix = config.prefix ? `${config.prefix}_` : ''; 58 | return prefix + key; 59 | }; 60 | 61 | // 移除已添加的前缀 62 | const autoRemovePrefix = (key: string) => { 63 | const len = config.prefix ? config.prefix.length + 1 : 0; 64 | return key.substr(len); 65 | }; 66 | 67 | // 获取全部 getAllStorage 68 | export const getStorageAll = (type: StorageType = 'localStorage') => { 69 | const len = window[type].length; // 获取长度 70 | const arr = []; // 定义数据集 71 | for (let i = 0; i < len; i++) { 72 | // 获取key 索引从0开始 73 | const getKey = window[type].key(i) || ''; 74 | // 获取key对应的值 75 | const getVal = window[type].getItem(getKey); 76 | // 放进数组 77 | arr[i] = { key: getKey, val: getVal }; 78 | } 79 | return arr; 80 | }; 81 | 82 | // 根据请求配置替换默认config 83 | export const setStorageConfig = (info: StorageConfig) => { 84 | config = { ...config, ...info }; 85 | }; 86 | 87 | // 判断是否支持 Storage 88 | export const isSupportStorage = () => { 89 | return typeof Storage !== 'undefined'; 90 | }; 91 | 92 | // 设置 setStorage 93 | export const setStorage = (key: string, value: StorageValue, expire = 0, type: StorageType = 'localStorage') => { 94 | if (value === null || value === undefined) { 95 | value = null; 96 | } 97 | 98 | if (isNaN(expire) || expire < 0) throw new Error('Expire 必须是数字'); 99 | 100 | if (config.expire > 0 || expire > 0) expire = (expire || config.expire) * 1000; 101 | const data = { 102 | value, // 存储值 103 | time: Date.now(), //存值时间戳 104 | expire, // 过期时间 105 | }; 106 | 107 | const encryptString = config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data); 108 | 109 | window[type].setItem(autoAddPrefix(key), encryptString); 110 | }; 111 | 112 | // 删除 removeStorage 113 | export const removeStorage = (key: string, type: StorageType = 'localStorage') => { 114 | window[type].removeItem(autoAddPrefix(key)); 115 | }; 116 | 117 | // 获取 getStorage 118 | export const getStorage = (key: string, type: StorageType = 'localStorage'): StorageValue => { 119 | key = autoAddPrefix(key); 120 | // key 不存在判断 121 | if (!window[type].getItem(key) || JSON.stringify(window[type].getItem(key)) === 'null') { 122 | return null; 123 | } 124 | 125 | // 优化 持续使用中续期 126 | const storage = config.isEncrypt 127 | ? JSON.parse(decrypt(window[type].getItem(key) || '')) 128 | : JSON.parse(window[type].getItem(key) || ''); 129 | 130 | const nowTime = Date.now(); 131 | 132 | // 过期删除 133 | if (storage.expire && config.expire * 6000 < nowTime - storage.time) { 134 | removeStorage(key); 135 | return null; 136 | } else { 137 | // 未过期期间被调用 则自动续期 进行保活 138 | setStorage(autoRemovePrefix(key), storage.value); 139 | return storage.value; 140 | } 141 | }; 142 | 143 | // 是否存在 hasStorage 144 | export const hasStorage = (key: string): boolean => { 145 | key = autoAddPrefix(key); 146 | const arr = getStorageAll().filter(item => { 147 | return item.key === key; 148 | }); 149 | return !!arr.length; 150 | }; 151 | 152 | // 获取所有key 153 | export const getStorageKeys = (): (string | null)[] => { 154 | const items = getStorageAll(); 155 | const keys = []; 156 | for (let index = 0; index < items.length; index++) { 157 | keys.push(items[index].key); 158 | } 159 | return keys; 160 | }; 161 | 162 | // 根据索引获取key 163 | export const getStorageForIndex = (index: number, type: StorageType = 'localStorage') => { 164 | return window[type].key(index); 165 | }; 166 | 167 | // 获取localStorage长度 168 | export const getStorageLength = (type: StorageType = 'localStorage') => { 169 | return window[type].length; 170 | }; 171 | 172 | // 清空 clearStorage 173 | export const clearStorage = (type: StorageType = 'localStorage') => { 174 | window[type].clear(); 175 | }; 176 | 177 | /** SessionStorage */ 178 | 179 | export const setSessionStorage = (key: string, value: StorageValue, expire = 0) => { 180 | return setStorage(key, value, expire, 'sessionStorage'); 181 | }; 182 | 183 | export const getSessionStorage = (key: string): StorageValue => { 184 | return getStorage(key, 'sessionStorage'); 185 | }; 186 | 187 | export const getSessionStorageForIndex = (index: number) => { 188 | return getStorageForIndex(index, 'sessionStorage'); 189 | }; 190 | 191 | export const getSessionStorageLength = () => { 192 | return getStorageLength('sessionStorage'); 193 | }; 194 | 195 | export const getSessionStorageAll = () => { 196 | return getStorageAll('sessionStorage'); 197 | }; 198 | 199 | export const removeSessionStorage = (key: string) => { 200 | return removeStorage(key, 'sessionStorage'); 201 | }; 202 | 203 | export const clearSessionStorage = () => { 204 | return clearStorage('sessionStorage'); 205 | }; 206 | -------------------------------------------------------------------------------- /src/utils/storage/types.d.ts: -------------------------------------------------------------------------------- 1 | export type StorageValue = T | null | undefined; 2 | 3 | export type StorageType = 'localStorage' | 'sessionStorage'; 4 | 5 | export interface StorageConfig { 6 | prefix: string; 7 | expire: number; 8 | isEncrypt: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/DetailsPage/DetailsInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { memo, useEffect, useState } from 'react'; 3 | import { useSearchParams } from 'react-router'; 4 | import { useInfoPageTabs } from '../hooks/useInfoPageTabs'; 5 | 6 | const DatailsInfo = memo(() => { 7 | const [id, setId] = useState(); 8 | const [searchParams] = useSearchParams(); 9 | const { handleTabs } = useInfoPageTabs(); 10 | const intl = useLocale(); 11 | 12 | useEffect(() => { 13 | setId(searchParams.get('id') || ''); 14 | handleTabs('qurey', 'update', Number(searchParams.get('id'))); 15 | }, []); 16 | return ( 17 | <> 18 | {intl.formatMessage({ id: 'layout.memu.detailsPage' })}-{id} 19 | 20 | ); 21 | }); 22 | 23 | export default DatailsInfo; 24 | -------------------------------------------------------------------------------- /src/views/DetailsPage/DetailsParams/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { memo, useEffect, useState } from 'react'; 3 | import { useParams } from 'react-router'; 4 | import { useInfoPageTabs } from '../hooks/useInfoPageTabs'; 5 | 6 | const DetailsParams = memo(() => { 7 | const [id, setId] = useState(); 8 | const params = useParams(); 9 | const { handleTabs } = useInfoPageTabs(); 10 | const intl = useLocale(); 11 | 12 | useEffect(() => { 13 | setId(params.id); 14 | handleTabs('params', 'update', Number(params.id)); 15 | }, []); 16 | return ( 17 | <> 18 | {intl.formatMessage({ id: 'layout.memu.detailsPage' })}Params-{id} 19 | 20 | ); 21 | }); 22 | 23 | export default DetailsParams; 24 | -------------------------------------------------------------------------------- /src/views/DetailsPage/hooks/useInfoPageTabs.tsx: -------------------------------------------------------------------------------- 1 | import { setStoreMultiTabs } from '@/store/modules/route'; 2 | import { useDispatch } from 'react-redux'; 3 | import type { MultiTabsType } from '@/store/modules/route'; 4 | 5 | export const useInfoPageTabs = () => { 6 | const dispatch = useDispatch(); 7 | const handleTabs = (pateType: 'qurey' | 'params', type: 'add' | 'update', id: number) => { 8 | let tabs: MultiTabsType; 9 | 10 | if (pateType === 'params') { 11 | tabs = { 12 | key: `/details-page/details-params/${id}`, 13 | label: `Params-${id}`, 14 | localeLabel: `layout.memu.detailsPage`, 15 | }; 16 | } else { 17 | tabs = { 18 | key: `/details-page/details-info?id=${id}`, 19 | label: `-${id}`, 20 | localeLabel: `layout.memu.detailsPage`, 21 | }; 22 | } 23 | dispatch(setStoreMultiTabs({ type, tabs })); 24 | }; 25 | 26 | return { handleTabs }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/views/DetailsPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { Button } from 'antd'; 3 | import { useNavigate } from 'react-router'; 4 | import { useInfoPageTabs } from './hooks/useInfoPageTabs'; 5 | 6 | const DateilsPage = () => { 7 | const navigate = useNavigate(); 8 | const intl = useLocale(); 9 | 10 | const { handleTabs } = useInfoPageTabs(); 11 | const qureyChange = (pateType: 'qurey' | 'params', i: number) => { 12 | let path = `/details-page/details-info?id=${i}`; 13 | if (pateType === 'params') { 14 | path = `/details-page/details-params/${i}`; 15 | } 16 | handleTabs(pateType, 'add', i); 17 | navigate(path); 18 | }; 19 | 20 | return ( 21 |
22 |
23 | {[1, 2, 3, 4, 5].map(i => { 24 | return ( 25 | 28 | ); 29 | })} 30 |
31 |
32 | {[1, 2, 3, 4, 5].map(i => { 33 | return ( 34 | 37 | ); 38 | })} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default DateilsPage; 45 | -------------------------------------------------------------------------------- /src/views/Home/components/AreaChart.tsx: -------------------------------------------------------------------------------- 1 | import { useChartsConfig } from '@/hooks/web/antCharts/useChartsConfig'; 2 | import { Area } from '@ant-design/charts'; 3 | import { memo } from 'react'; 4 | import type { AreaConfig } from '@ant-design/charts'; 5 | 6 | const data = [ 7 | { 8 | week: '星期天', 9 | value: 150, 10 | category: '目标任务', 11 | }, 12 | { 13 | week: '星期一', 14 | value: 154, 15 | category: '目标任务', 16 | }, 17 | { 18 | week: '星期二', 19 | value: 201, 20 | category: '目标任务', 21 | }, 22 | { 23 | week: '星期三', 24 | value: 299, 25 | category: '目标任务', 26 | }, 27 | { 28 | week: '星期四', 29 | value: 190, 30 | category: '目标任务', 31 | }, 32 | { 33 | week: '星期五', 34 | value: 330, 35 | category: '目标任务', 36 | }, 37 | { 38 | week: '星期六', 39 | value: 410, 40 | category: '目标任务', 41 | }, 42 | { 43 | week: '星期天', 44 | value: 220, 45 | category: '事项', 46 | }, 47 | { 48 | week: '星期一', 49 | value: 182, 50 | category: '事项', 51 | }, 52 | { 53 | week: '星期二', 54 | value: 191, 55 | category: '事项', 56 | }, 57 | { 58 | week: '星期三', 59 | value: 234, 60 | category: '事项', 61 | }, 62 | { 63 | week: '星期四', 64 | value: 290, 65 | category: '事项', 66 | }, 67 | { 68 | week: '星期五', 69 | value: 330, 70 | category: '事项', 71 | }, 72 | { 73 | week: '星期六', 74 | value: 310, 75 | category: '事项', 76 | }, 77 | { 78 | week: '星期天', 79 | value: 50, 80 | category: '任务', 81 | }, 82 | { 83 | week: '星期一', 84 | value: 124, 85 | category: '任务', 86 | }, 87 | { 88 | week: '星期二', 89 | value: 191, 90 | category: '任务', 91 | }, 92 | { 93 | week: '星期三', 94 | value: 280, 95 | category: '任务', 96 | }, 97 | { 98 | week: '星期四', 99 | value: 90, 100 | category: '任务', 101 | }, 102 | { 103 | week: '星期五', 104 | value: 30, 105 | category: '任务', 106 | }, 107 | { 108 | week: '星期六', 109 | value: 10, 110 | category: '任务', 111 | }, 112 | ]; 113 | 114 | const AreaChart = memo(() => { 115 | const { theme } = useChartsConfig(); 116 | const config: AreaConfig = { 117 | data, 118 | theme, 119 | height: 362, 120 | xField: 'week', 121 | yField: 'value', 122 | seriesField: 'category', 123 | smooth: true, 124 | legend: false, 125 | xAxis: { 126 | range: [0, 1], 127 | nice: true, 128 | grid: { 129 | alignTick: true, 130 | line: { 131 | style: { 132 | stroke: '#ddd', 133 | opacity: 0.5, 134 | }, 135 | }, 136 | }, 137 | }, 138 | yAxis: { 139 | nice: true, 140 | tickCount: 7, 141 | min: 0, 142 | max: 1000, 143 | grid: { 144 | alignTick: true, 145 | line: { 146 | style: { 147 | stroke: '#ddd', 148 | opacity: 0.5, 149 | }, 150 | }, 151 | }, 152 | }, 153 | }; 154 | 155 | return ; 156 | }); 157 | 158 | export default AreaChart; 159 | -------------------------------------------------------------------------------- /src/views/Home/components/Comment.tsx: -------------------------------------------------------------------------------- 1 | import avatar from '@/assets/avatar.png'; 2 | import { UserOutlined } from '@ant-design/icons'; 3 | import { Avatar, Divider, theme } from 'antd'; 4 | import { memo } from 'react'; 5 | import type { CSSObject } from '@emotion/react'; 6 | import type { GlobalToken } from 'antd/es/theme/interface'; 7 | 8 | const getCommentItem = (token: GlobalToken): CSSObject => { 9 | return { 10 | height: 450, 11 | overflowY: 'auto', 12 | '.item': { 13 | display: 'flex', 14 | '.item-content': { 15 | marginLeft: 14, 16 | display: 'flex', 17 | flexDirection: 'column', 18 | flex: 1, 19 | '.title': { 20 | fontSize: token.fontSize, 21 | }, 22 | time: { 23 | fontSize: token.fontSizeSM, 24 | }, 25 | '.text': { 26 | textOverflow: 'ellipsis', 27 | overflow: 'hidden', 28 | display: '-webkit-box', 29 | WebkitLineClamp: '2', 30 | ' -webkit-box-orient': 'vertical', 31 | }, 32 | }, 33 | }, 34 | }; 35 | }; 36 | 37 | const Comment = memo(() => { 38 | const thme = theme.useToken(); 39 | 40 | return ( 41 |
42 |
    43 | {[1, 2, 3, 4].map(i => { 44 | return ( 45 |
  • 46 |
    47 | } src={avatar} /> 48 |
    49 | 某某某 50 | 2021-12-31 51 | 52 | 据我观察,你今天吃意大利面遇到不良商家,没有拌42号混凝土,以及使用劣质螺丝钉,严重影响你的扭矩,从而影响UFO的产量,吸收不足引发内蜂蜜失调,导致排放系统紊乱,对整个太平洋以及充电器造成严重的核污染。我个人认为,这个意大利面就应该拌42号混凝土,因为这个螺丝钉的长度,它很容易会直接影响到挖掘机的扭矩你知道吧,你往里砸的时候,一瞬间它就会产生大量的高能蛋白。 53 | 54 |
    55 |
    56 | 57 |
  • 58 | ); 59 | })} 60 |
61 |
62 | ); 63 | }); 64 | 65 | export default Comment; 66 | -------------------------------------------------------------------------------- /src/views/Home/components/RoseChart.tsx: -------------------------------------------------------------------------------- 1 | import { useChartsConfig } from '@/hooks/web/antCharts/useChartsConfig'; 2 | import { Pie } from '@ant-design/charts'; 3 | import { memo } from 'react'; 4 | import type { PieConfig } from '@ant-design/charts'; 5 | 6 | const data = [ 7 | { 8 | type: 'Vue', 9 | value: 70, 10 | }, 11 | { 12 | type: 'React', 13 | value: 20, 14 | }, 15 | { 16 | type: 'Angular', 17 | value: 10, 18 | }, 19 | ]; 20 | 21 | const RoseChart = memo(() => { 22 | const { theme } = useChartsConfig(); 23 | 24 | // 分组玫瑰图 25 | const config: PieConfig = { 26 | appendPadding: 10, 27 | data, 28 | theme, 29 | height: 362, 30 | angleField: 'value', 31 | colorField: 'type', 32 | radius: 1, 33 | innerRadius: 0.64, 34 | meta: { 35 | value: { 36 | formatter: (v: number) => `${v}`, 37 | }, 38 | }, 39 | legend: { 40 | layout: 'horizontal', 41 | position: 'bottom', 42 | }, 43 | label: { 44 | type: 'inner', 45 | offset: '-50%', 46 | style: { 47 | textAlign: 'center', 48 | }, 49 | autoRotate: false, 50 | content: '{value}', 51 | }, 52 | statistic: { 53 | title: { 54 | customHtml: (_container, _view, datum, _data) => { 55 | if (datum) { 56 | return datum.type; 57 | } 58 | return '卷'; 59 | }, 60 | }, 61 | // content: { 62 | // offsetY: 4, 63 | // style: { 64 | // fontSize: '32px', 65 | // }, 66 | // customHtml: (container, view, datum, data) => { 67 | // return '123123'; 68 | // }, 69 | // }, 70 | }, 71 | // 添加 中心统计文本 交互 72 | interactions: [ 73 | { 74 | type: 'element-selected', 75 | }, 76 | { 77 | type: 'element-active', 78 | }, 79 | { 80 | type: 'pie-statistic-active', 81 | }, 82 | ], 83 | }; 84 | 85 | return ; 86 | }); 87 | 88 | export default RoseChart; 89 | -------------------------------------------------------------------------------- /src/views/Home/components/WordCloudChart.tsx: -------------------------------------------------------------------------------- 1 | import { useChartsConfig } from '@/hooks/web/antCharts/useChartsConfig'; 2 | import { WordCloud } from '@ant-design/charts'; 3 | import { memo } from 'react'; 4 | import type { WordCloudConfig } from '@ant-design/charts'; 5 | 6 | let data = [ 7 | { name: 'Vue', value: 0 }, 8 | { name: 'React', value: 0 }, 9 | { name: 'Angular', value: 0 }, 10 | { name: 'ECharts', value: 0 }, 11 | { name: 'Wechat', value: 0 }, 12 | { name: 'Element', value: 0 }, 13 | { name: 'Vite', value: 0 }, 14 | { name: 'Node', value: 0 }, 15 | { name: 'Router', value: 0 }, 16 | { name: 'I18n', value: 0 }, 17 | { name: 'VitePress', value: 0 }, 18 | { name: 'Umi', value: 0 }, 19 | { name: 'And Design', value: 0 }, 20 | ]; 21 | 22 | const WordCloudChart = memo(() => { 23 | const { theme } = useChartsConfig(); 24 | 25 | data = data.map(i => { 26 | i.value = Math.random() * 30 + 8; 27 | return i; 28 | }); 29 | const config: WordCloudConfig = { 30 | data, 31 | theme, 32 | wordField: 'name', 33 | weightField: 'value', 34 | height: 450, 35 | color: ['#e3e3e3', '#e3e3e3', '#e3e3e3'], 36 | wordStyle: { 37 | fontFamily: 'Verdana', 38 | fontSize: [18, 26], 39 | }, 40 | // 设置交互类型 41 | interactions: [ 42 | { 43 | type: 'element-active', 44 | }, 45 | ], 46 | state: { 47 | active: { 48 | // 这里可以设置 active 时的样式 49 | style: { 50 | lineWidth: 3, 51 | }, 52 | }, 53 | }, 54 | }; 55 | 56 | return ; 57 | }); 58 | 59 | export default WordCloudChart; 60 | -------------------------------------------------------------------------------- /src/views/Home/index copy.tsx: -------------------------------------------------------------------------------- 1 | import reactLogo from '@/assets/react.svg'; 2 | import { Button } from 'antd'; 3 | import { memo, useState } from 'react'; 4 | import './index.less'; 5 | 6 | const Home = memo(() => { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 |
11 |
12 | 20 |

Vite + React

21 |
22 | 25 |

26 | Edit src/App.tsx and save to test HMR 27 |

28 |
29 |

Click on the Vite and React logos to learn more

30 |
31 |
32 | ); 33 | }); 34 | 35 | export default Home; 36 | -------------------------------------------------------------------------------- /src/views/Home/index.less: -------------------------------------------------------------------------------- 1 | .home { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | text-align: center; 6 | width: 100%; 7 | height: 100%; 8 | 9 | .content { 10 | .logo { 11 | height: 6em; 12 | padding: 1.5em; 13 | will-change: filter; 14 | } 15 | .logo:hover { 16 | filter: drop-shadow(0 0 2em #646cffaa); 17 | } 18 | .logo.react:hover { 19 | filter: drop-shadow(0 0 2em #61dafbaa); 20 | } 21 | 22 | @keyframes logo-spin { 23 | from { 24 | transform: rotate(0deg); 25 | } 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | @media (prefers-reduced-motion: no-preference) { 32 | a:nth-of-type(2) .logo { 33 | animation: logo-spin infinite 20s linear; 34 | } 35 | } 36 | 37 | .card { 38 | padding: 2em; 39 | } 40 | 41 | .read-the-docs { 42 | color: #888; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { RightOutlined } from '@ant-design/icons'; 2 | import { Card, Col, Progress, Row, theme } from 'antd'; 3 | import { memo } from 'react'; 4 | import AreaChart from './components/AreaChart'; 5 | import Comment from './components/Comment'; 6 | import RoseChart from './components/RoseChart'; 7 | import WordCloudChart from './components/WordCloudChart'; 8 | import { getNumericalValue } from './style'; 9 | import './index.less'; 10 | 11 | const Home = memo(() => { 12 | const thme = theme.useToken(); 13 | 14 | const speedList = [ 15 | { 16 | title: '待办事项', 17 | online: 24, 18 | total: 70, 19 | }, 20 | { 21 | title: '待办任务', 22 | online: 39, 23 | total: 100, 24 | }, 25 | { 26 | title: '目标计划', 27 | online: 5, 28 | total: 10, 29 | }, 30 | { 31 | title: '评论回复', 32 | online: 10, 33 | total: 40, 34 | }, 35 | ]; 36 | 37 | const value = (online: number, total: number) => { 38 | return Math.round((online / total) * 100); 39 | }; 40 | 41 | return ( 42 |
43 | 44 | {speedList.map(i => { 45 | return ( 46 | 47 | }> 48 |
49 |
50 | 51 | {i.online}/{i.total} 52 | 53 | Online/Total 54 |
55 | 60 |
61 |
62 | 63 | ); 64 | })} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
87 |
88 | ); 89 | }); 90 | 91 | export default Home; 92 | -------------------------------------------------------------------------------- /src/views/Home/style/index.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSObject } from '@emotion/react'; 2 | import type { GlobalToken } from 'antd/es/theme/interface'; 3 | 4 | export const getNumericalValue = (token: GlobalToken): CSSObject => { 5 | return { 6 | '.numerical-value': { 7 | display: 'flex', 8 | justifyContent: 'space-between', 9 | alignItems: 'flex-end', 10 | marginBottom: '10px', 11 | '.number': { 12 | color: token.colorText, 13 | fontSize: token.fontSizeHeading4, 14 | fontWeight: 600, 15 | }, 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/views/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import logo from '@/assets/logo.png'; 2 | import AppLocale from '@/components/AppLocale'; 3 | import AppTheme from '@/components/AppTheme'; 4 | import { useLocale } from '@/locales'; 5 | import { initAsyncRoute } from '@/router/utils'; 6 | import { getUserInfo } from '@/server/useInfo'; 7 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 8 | import { setUserInfo } from '@/store/modules/user'; 9 | import { LockOutlined, UserOutlined } from '@ant-design/icons'; 10 | import { Button, Checkbox, Form, Image, Input, theme } from 'antd'; 11 | import { memo, useEffect, useState } from 'react'; 12 | import { useNavigate } from 'react-router'; 13 | import type { LoginForm } from './type'; 14 | 15 | const Login = memo(() => { 16 | const intl = useLocale(); 17 | 18 | const thme = theme.useToken(); 19 | 20 | const dispatch = useAppDispatch(); 21 | 22 | const navigate = useNavigate(); 23 | 24 | const [loading, setLoading] = useState(false); 25 | 26 | const onFinish = async (values: LoginForm) => { 27 | setLoading(true); 28 | const res = await getUserInfo(values.username, values.password); 29 | if (res.code === 1) { 30 | await initAsyncRoute(res.data.power); 31 | dispatch(setUserInfo(res.data)); 32 | } 33 | 34 | setLoading(false); 35 | }; 36 | 37 | const userStore = useAppSelector(state => state.user); 38 | 39 | useEffect(() => { 40 | if (userStore.power) { 41 | navigate('/home'); 42 | } 43 | }, [userStore]); 44 | 45 | return ( 46 |
50 |
51 | 52 | 53 |
54 |
55 |
56 | 57 |

React Xs Admin

58 |
59 |
66 | 67 | name="username" 68 | rules={[{ required: true, message: intl.formatMessage({ id: 'login.userNameRules' }) }]} 69 | > 70 | } placeholder={intl.formatMessage({ id: 'login.username' })} allowClear /> 71 | 72 | 73 | name="password" 74 | rules={[{ required: true, message: intl.formatMessage({ id: 'login.passwordRules' }) }]} 75 | > 76 | } 78 | placeholder={intl.formatMessage({ id: 'login.password' })} 79 | allowClear 80 | /> 81 | 82 | > 83 |
84 | 85 | {intl.formatMessage({ id: 'login.rememberPassword' })} 86 | 87 | 88 | 91 |
92 | 93 | 94 | 95 | 98 | 99 | 100 |
101 |
102 | ); 103 | }); 104 | 105 | export default Login; 106 | -------------------------------------------------------------------------------- /src/views/Login/type.ts: -------------------------------------------------------------------------------- 1 | export interface LoginForm { 2 | username: string; 3 | password: string; 4 | checked: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/Nested/Menu1/Menu1-1/index.tsx: -------------------------------------------------------------------------------- 1 | function Menu1_1() { 2 | return
menu1-1
; 3 | } 4 | 5 | export default Menu1_1; 6 | -------------------------------------------------------------------------------- /src/views/Nested/Menu1/Menu1-2/index.tsx: -------------------------------------------------------------------------------- 1 | function Menu1_2() { 2 | return
menu1-2
; 3 | } 4 | 5 | export default Menu1_2; 6 | -------------------------------------------------------------------------------- /src/views/Power/Permissions/index.tsx: -------------------------------------------------------------------------------- 1 | import { initAsyncRoute } from '@/router/utils'; 2 | import { useAppDispatch, useAppSelector } from '@/store/hooks'; 3 | import { setPower } from '@/store/modules/user'; 4 | import { Button } from 'antd'; 5 | 6 | const Permissions = () => { 7 | const dispatch = useAppDispatch(); 8 | 9 | const power = useAppSelector(state => state.user.power); 10 | 11 | const setCount = async () => { 12 | const newPower = power === 'admin' ? 'test' : 'admin'; 13 | dispatch(setPower(newPower)); 14 | initAsyncRoute(newPower); 15 | }; 16 | 17 | return ( 18 | 21 | ); 22 | }; 23 | 24 | export default Permissions; 25 | -------------------------------------------------------------------------------- /src/views/Power/test-permissions-a/index.tsx: -------------------------------------------------------------------------------- 1 | export default function TestPermissionsA() { 2 | return
admin 可见
; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/Power/test-permissions-b/index.tsx: -------------------------------------------------------------------------------- 1 | export default function TestPermissionsB() { 2 | return
Test 可见
; 3 | } 4 | -------------------------------------------------------------------------------- /src/views/core/Refresh.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation, useParams } from 'react-router'; 2 | 3 | const Redirect = () => { 4 | const params = useParams(); 5 | const location = useLocation(); 6 | 7 | return ; 8 | }; 9 | 10 | export default Redirect; 11 | -------------------------------------------------------------------------------- /src/views/core/error/403.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { Button, Result } from 'antd'; 3 | import { memo } from 'react'; 4 | import { useNavigate } from 'react-router'; 5 | 6 | function Error403() { 7 | const init = useLocale(); 8 | 9 | const navigate = useNavigate(); 10 | 11 | return ( 12 | { 20 | navigate('/'); 21 | }} 22 | > 23 | Back Home 24 | 25 | } 26 | /> 27 | ); 28 | } 29 | 30 | export default memo(Error403); 31 | -------------------------------------------------------------------------------- /src/views/core/error/404.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { Button, Result } from 'antd'; 3 | import { memo } from 'react'; 4 | 5 | function Error404() { 6 | const init = useLocale(); 7 | 8 | return ( 9 | Back Home} 14 | /> 15 | ); 16 | } 17 | 18 | export default memo(Error404); 19 | -------------------------------------------------------------------------------- /src/views/core/error/500.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { Button, Result } from 'antd'; 3 | import { memo } from 'react'; 4 | 5 | function Error500() { 6 | const init = useLocale(); 7 | 8 | return ( 9 | Back Home} 14 | /> 15 | ); 16 | } 17 | 18 | export default memo(Error500); 19 | -------------------------------------------------------------------------------- /src/views/core/error/ErrorElement.tsx: -------------------------------------------------------------------------------- 1 | import { useLocale } from '@/locales'; 2 | import { Result, Typography } from 'antd'; 3 | import { memo } from 'react'; 4 | import { useRouteError } from 'react-router'; 5 | 6 | const { Paragraph, Text } = Typography; 7 | 8 | interface ErrorElementType { 9 | pageType: 'Layout' | 'Page'; 10 | } 11 | 12 | const ErrorElement = memo((props: ErrorElementType) => { 13 | const errorText = useRouteError() as Error; 14 | const intl = useLocale(); 15 | return ( 16 | 17 |
18 | 19 | 25 | {intl.formatMessage({ id: 'layout.error.element.content' })} 26 | 27 | 28 | {errorText.stack} 29 |
30 |
31 | ); 32 | }); 33 | 34 | export default ErrorElement; 35 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | darkMode: 'class', 4 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 5 | corePlugins: { 6 | preflight: false, // 关闭默认样式 7 | }, 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: 'var(--main-color)', 12 | }, 13 | }, 14 | }, 15 | plugins: [], 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "@emotion/react", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "useDefineForClassFields": true, 8 | "baseUrl": ".", 9 | "module": "ESNext", 10 | "moduleResolution": "Node", 11 | "paths": { 12 | "@/*": ["src/*"], 13 | "#/*": ["types/*"] 14 | }, 15 | "resolveJsonModule": true, 16 | "allowJs": false, 17 | "strict": true, 18 | "noEmit": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": false, 21 | "forceConsistentCasingInFileNames": true, 22 | "isolatedModules": true, 23 | "skipLibCheck": true 24 | }, 25 | "references": [ 26 | { 27 | "path": "./tsconfig.node.json" 28 | } 29 | ], 30 | "include": ["src", "types"] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "types": ["vite/client"] 8 | }, 9 | "include": ["vite.config.ts", "build", "types", "mock"] 10 | } 11 | -------------------------------------------------------------------------------- /types/axios.d.ts: -------------------------------------------------------------------------------- 1 | export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined; 2 | export interface RequestOptions { 3 | // 网址前缀 留空使用默认 4 | urlPrefix?: string; 5 | // 设置token 6 | specialToken?: string; 7 | // 是否开启自定义请求报错提示 8 | errorMassge?: boolean; 9 | // 是否携带token 10 | withToken?: boolean; 11 | // 错误消息提示类型 12 | errorMessageMode?: ErrorMessageMode; 13 | } 14 | export interface Result { 15 | code: number; 16 | message: string; 17 | data: T; 18 | } 19 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare interface ViteEnv { 4 | readonly VITE_ENV: string; 5 | } 6 | 7 | interface ImportMetaEnv extends ViteEnv { 8 | _: unknown; 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv; 13 | } 14 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare type RefType = T | null; 2 | 3 | declare type Recordable = Record; 4 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigEnv, UserConfig } from 'vite'; 2 | import { createViteResolve } from './build/vite/resolve'; 3 | import { createVitePlugins } from './build/vite/plugins'; 4 | import { createViteBuild } from './build/vite/build'; 5 | import { createViteServer } from './build/vite/server'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default (configEnv: ConfigEnv): UserConfig => { 9 | const { command } = configEnv; 10 | 11 | const isBuild = command === 'build'; 12 | 13 | return { 14 | // 解析配置 15 | resolve: createViteResolve(__dirname), 16 | // 插件配置 17 | plugins: createVitePlugins(isBuild, configEnv), 18 | // 打包配置 19 | build: createViteBuild(), 20 | // 服务配置 21 | server: createViteServer(), 22 | }; 23 | }; 24 | --------------------------------------------------------------------------------