├── .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 | 
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 |
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 | | [
](http://godban.github.io/browsers-support-badges/)IE | [
](http://godban.github.io/browsers-support-badges/)Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](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 |
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 | | [
](http://godban.github.io/browsers-support-badges/)IE | [
](http://godban.github.io/browsers-support-badges/)Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](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 |
17 |
--------------------------------------------------------------------------------
/src/assets/icons/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
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 |