├── src ├── utils │ ├── business │ │ ├── index.ts │ │ ├── localStorage.ts │ │ └── token.ts │ ├── setCssVar.ts │ ├── browser.ts │ └── scrollStyle.ts ├── styles │ ├── vars.less │ ├── index.less │ ├── reset.less │ └── utils.less ├── locales │ ├── en │ │ ├── components.json │ │ ├── http.json │ │ ├── error.json │ │ ├── login.json │ │ ├── layout.json │ │ ├── index.ts │ │ └── menu.json │ ├── zh-Hans │ │ ├── components.json │ │ ├── http.json │ │ ├── error.json │ │ ├── login.json │ │ ├── layout.json │ │ ├── index.ts │ │ └── menu.json │ └── index.ts ├── layouts │ ├── Tabs │ │ ├── ContextMenu │ │ │ ├── const.ts │ │ │ ├── index.less │ │ │ ├── useContextMenu.ts │ │ │ └── style.ts │ │ ├── FixAntdTabTranslate │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── AnimationWrap │ │ │ └── index.tsx │ │ ├── index.less │ │ ├── useDraggable.ts │ │ └── TabChrome │ │ │ └── index.less │ ├── ConsoleLayout │ │ ├── consts.ts │ │ ├── animations.ts │ │ ├── store │ │ │ ├── index.ts │ │ │ └── Provider.tsx │ │ └── index.less │ ├── Language │ │ ├── index.less │ │ └── index.tsx │ ├── components │ │ └── TooltipIcon │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── Collapse │ │ ├── index.less │ │ └── index.tsx │ ├── Footer │ │ ├── index.tsx │ │ └── index.less │ ├── Breadcrumb │ │ ├── index.tsx │ │ ├── index.less │ │ ├── useBreadcrumb.ts │ │ └── utils.tsx │ ├── Github │ │ └── index.tsx │ ├── Avatar │ │ ├── index.less │ │ └── index.tsx │ ├── DarkSwitch │ │ └── index.tsx │ ├── Header │ │ ├── index.less │ │ └── index.tsx │ ├── FullScreen │ │ ├── index.tsx │ │ ├── hooks.ts │ │ └── utils.ts │ ├── Refresh │ │ └── index.tsx │ └── SideMenu │ │ ├── hooks.ts │ │ ├── index.less │ │ └── index.tsx ├── components │ ├── AntdProvider │ │ ├── index.less │ │ ├── index.ts │ │ ├── hooks.ts │ │ └── Provider.tsx │ ├── Loading │ │ ├── index.less │ │ └── index.tsx │ ├── Back │ │ ├── index.less │ │ └── index.tsx │ ├── Hover │ │ └── index.tsx │ ├── Progress │ │ ├── utils.ts │ │ └── index.tsx │ ├── TablePage │ │ └── index.tsx │ ├── business │ │ └── withAuth │ │ │ └── index.tsx │ ├── store │ │ └── index.tsx │ └── SvgIcon │ │ └── index.tsx ├── pages │ ├── nest │ │ ├── index.less │ │ ├── index.tsx │ │ ├── nest1.tsx │ │ ├── nest2-1.tsx │ │ ├── nest2-2-1.tsx │ │ ├── nest2-2-2.tsx │ │ ├── nest2.tsx │ │ └── nest2-2.tsx │ ├── grid │ │ ├── index.less │ │ └── locales │ │ │ ├── zh-Hans │ │ │ └── grid.json │ │ │ └── en │ │ │ └── grid.json │ ├── home │ │ └── index.less │ ├── permission │ │ ├── local │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── ChangeUser │ │ │ └── index.less │ │ ├── locales │ │ │ ├── zh-Hans │ │ │ │ └── permission.json │ │ │ └── en │ │ │ │ └── permission.json │ │ └── route │ │ │ └── index.tsx │ ├── tablePage │ │ ├── components │ │ │ └── PoweredByAdminSearchList │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── scrollLoadModeList │ │ │ └── index.less │ │ ├── locales │ │ │ ├── zh-Hans │ │ │ │ └── tablePage.json │ │ │ └── en │ │ │ │ └── tablePage.json │ │ ├── extraSearchModel │ │ │ └── index.less │ │ ├── table.mock.ts │ │ └── simpleTablePage │ │ │ └── index.tsx │ ├── noAccess │ │ ├── index.less │ │ └── index.tsx │ ├── notFound │ │ ├── index.less │ │ └── index.tsx │ ├── singleSider │ │ └── index.tsx │ ├── profile │ │ ├── index.less │ │ └── index.tsx │ ├── router │ │ ├── tempRoute │ │ │ └── index.tsx │ │ ├── locales │ │ │ ├── zh-Hans │ │ │ │ └── router.json │ │ │ └── en │ │ │ │ └── router.json │ │ └── meta │ │ │ └── index.tsx │ ├── separation │ │ └── index.tsx │ ├── login │ │ ├── index.tsx │ │ ├── Tools.tsx │ │ └── index.less │ └── alive │ │ └── index.tsx ├── mock │ ├── res.ts │ ├── browser.ts │ └── passthrough.ts ├── consts │ └── index.ts ├── http │ ├── utils.ts │ └── API.d.ts ├── vite-env.d.ts ├── router │ └── index.ts ├── hooks │ ├── usePrevious.ts │ └── useTablePage.ts ├── services │ ├── withAuth.ts │ ├── login.ts │ ├── withAuth.mock.ts │ └── login.mock.ts ├── assets │ └── svg │ │ ├── close.svg │ │ ├── rectangle.svg │ │ ├── location.svg │ │ ├── menu.svg │ │ ├── single_slider.svg │ │ ├── back.svg │ │ ├── search.svg │ │ ├── menu_unfold.svg │ │ ├── unchecked.svg │ │ ├── dialog.svg │ │ ├── copyright.svg │ │ ├── format.svg │ │ ├── forbidden.svg │ │ ├── error.svg │ │ ├── signup.svg │ │ ├── checked.svg │ │ ├── profile.svg │ │ ├── stay.svg │ │ ├── nintendo.svg │ │ ├── locked.svg │ │ ├── email.svg │ │ ├── not_found.svg │ │ ├── theme_dark.svg │ │ ├── close_circle.svg │ │ ├── scroll_list.svg │ │ ├── table_simple.svg │ │ ├── web.svg │ │ ├── home.svg │ │ ├── lock.svg │ │ ├── sign.svg │ │ ├── search_table.svg │ │ ├── table.svg │ │ ├── color_skin.svg │ │ ├── menu2.svg │ │ ├── fullscreen.svg │ │ ├── external_link.svg │ │ ├── route.svg │ │ ├── fullscreen_exit.svg │ │ ├── user.svg │ │ ├── color_data.svg │ │ ├── grid.svg │ │ ├── btn.svg │ │ ├── github.svg │ │ ├── color_picker.svg │ │ ├── language.svg │ │ ├── refresh.svg │ │ ├── help.svg │ │ ├── color_tabs.svg │ │ ├── detail.svg │ │ └── color_building.svg ├── App.tsx ├── models │ ├── base │ │ └── index.ts │ └── withAuth │ │ └── permissions.ts └── main.tsx ├── .husky └── pre-commit ├── docs ├── .vitepress │ └── theme │ │ ├── my-fonts.css │ │ └── index.ts ├── public │ ├── logo.png │ └── favicon.ico ├── guide │ ├── begin.md │ ├── backend.md │ └── what.md ├── development │ ├── lint.md │ ├── style.md │ ├── env.md │ ├── icon.md │ ├── layout.md │ ├── qa.md │ ├── structure.md │ ├── mock.md │ ├── keep-alive.md │ ├── auth.md │ └── i18n.md ├── index.md └── components │ ├── Layout.vue │ └── Structure.vue ├── .env.dev ├── .env.test ├── .env.uat ├── .env.lib ├── .env.prod ├── postcss.config.cjs ├── public └── images │ ├── logo.png │ ├── favicon.ico │ └── login_bg.jpg ├── .env.localhost ├── tsconfig.node.json ├── .eslintignore ├── typings.d.ts ├── index.html ├── .gitignore ├── LICENSE ├── .eslintrc.cjs └── tsconfig.json /src/utils/business/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/vars.less: -------------------------------------------------------------------------------- 1 | :root {} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/my-fonts.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=dev 2 | VITE_API_HOST='/api' -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=test 2 | VITE_API_HOST='/api' -------------------------------------------------------------------------------- /.env.uat: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=uat 2 | VITE_API_HOST='/api' -------------------------------------------------------------------------------- /.env.lib: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=prod 2 | VITE_API_HOST='/api' 3 | -------------------------------------------------------------------------------- /src/locales/en/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "返回": "Back" 3 | } -------------------------------------------------------------------------------- /src/locales/zh-Hans/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "返回": "返回" 3 | } -------------------------------------------------------------------------------- /src/locales/zh-Hans/http.json: -------------------------------------------------------------------------------- 1 | { 2 | "请检查网络": "请检查网络" 3 | } 4 | -------------------------------------------------------------------------------- /src/layouts/Tabs/ContextMenu/const.ts: -------------------------------------------------------------------------------- 1 | export const MENU_ID = 'menu-id'; 2 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=prod 2 | VITE_API_HOST='/api' 3 | # VITE_BASENAME='/subpath' -------------------------------------------------------------------------------- /src/locales/en/http.json: -------------------------------------------------------------------------------- 1 | { 2 | "请检查网络": "Please check your network" 3 | } 4 | -------------------------------------------------------------------------------- /src/components/AntdProvider/index.less: -------------------------------------------------------------------------------- 1 | .console-antd-app { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/nest/index.less: -------------------------------------------------------------------------------- 1 | .console-nest__wrap { 2 | border: 1px solid #333; 3 | margin: 24px; 4 | } -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diandian18/react-antd-console/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diandian18/react-antd-console/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diandian18/react-antd-console/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diandian18/react-antd-console/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/login_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diandian18/react-antd-console/HEAD/public/images/login_bg.jpg -------------------------------------------------------------------------------- /src/pages/grid/index.less: -------------------------------------------------------------------------------- 1 | .console-grid__item { 2 | height: 150px; 3 | border-radius: 8px; 4 | margin: 8px; 5 | } 6 | -------------------------------------------------------------------------------- /src/layouts/ConsoleLayout/consts.ts: -------------------------------------------------------------------------------- 1 | export const ClassName__ConsoleLayout_RightSideMain = 'console-layout__right-side-main'; 2 | -------------------------------------------------------------------------------- /src/layouts/Language/index.less: -------------------------------------------------------------------------------- 1 | .console-layout-language__dropdown { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './reset.less'; 2 | @import './vars.less'; 3 | 4 | @import 'admin-search-list/dist-lib/style.css'; 5 | -------------------------------------------------------------------------------- /src/locales/zh-Hans/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "对不起,您没有访问权限": "对不起,您没有访问权限", 3 | "回到主页": "回到主页", 4 | "对不起,您访问的页面不存在": "对不起,您访问的页面不存在" 5 | } -------------------------------------------------------------------------------- /.env.localhost: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VITE_APP_ENV=localhost 3 | # 本地开发如果要修改环境,在此修改 4 | VITE_API_HOST='/api' 5 | # VITE_BASENAME='/subpath' -------------------------------------------------------------------------------- /src/mock/res.ts: -------------------------------------------------------------------------------- 1 | export function axiosRes(data: T) { 2 | return { 3 | code: '200', 4 | message: 'success', 5 | data, 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/consts/index.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TITLE = 'react-antd-console'; 2 | export const logo = '/images/logo.png'; 3 | export const tab_title = 'tab_title'; 4 | -------------------------------------------------------------------------------- /src/http/utils.ts: -------------------------------------------------------------------------------- 1 | export function statusClass(status: number) { 2 | if (status >= 500) { 3 | return '5xx'; 4 | } 5 | return String(status); 6 | } 7 | -------------------------------------------------------------------------------- /src/layouts/Tabs/ContextMenu/index.less: -------------------------------------------------------------------------------- 1 | .console-layout__context-menu { 2 | .anticon { 3 | margin-right: 2px; 4 | } 5 | svg { 6 | margin-right: 10px; 7 | } 8 | } -------------------------------------------------------------------------------- /src/layouts/Tabs/FixAntdTabTranslate/index.less: -------------------------------------------------------------------------------- 1 | .console-layout-fix-antd-tab-translate { 2 | position: absolute; 3 | visibility: hidden; 4 | pointer-events: none; 5 | } 6 | -------------------------------------------------------------------------------- /src/locales/en/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "对不起,您没有访问权限": "Sorry, you don't have access", 3 | "回到主页": "Back to home", 4 | "对不起,您访问的页面不存在": "Sorry, the page you are visiting does not exist" 5 | } -------------------------------------------------------------------------------- /src/layouts/components/TooltipIcon/index.less: -------------------------------------------------------------------------------- 1 | .console-tooltipIcon__button { 2 | border-radius: 50%; 3 | transition: all 0.1s; 4 | &:hover { 5 | transform: scale(1.2); 6 | } 7 | } -------------------------------------------------------------------------------- /src/pages/home/index.less: -------------------------------------------------------------------------------- 1 | .console-home__desc { 2 | line-height: 2; 3 | } 4 | 5 | .console-home__function { 6 | .ant-card-body { 7 | padding: 12px 24px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/permission/local/index.less: -------------------------------------------------------------------------------- 1 | .console-permission-local__btn-wrap { 2 | margin-top: 16px; 3 | } 4 | 5 | .console-permission-local__label { 6 | display: inline-block; 7 | width: 140px; 8 | } -------------------------------------------------------------------------------- /src/pages/grid/locales/zh-Hans/grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "改变窗口宽度,观察布局的变化": "改变窗口宽度,观察布局的变化", 3 | "当前尺寸": "当前尺寸", 4 | "第一排": "第一排", 5 | "第二排": "第二排", 6 | "尺寸": "尺寸", 7 | "备注": "备注", 8 | "屏幕": "屏幕" 9 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /lambda/ 3 | /scripts 4 | /config 5 | .history 6 | public 7 | dist 8 | 9 | /public/js/ 10 | 11 | *.config.js 12 | 13 | .eslintrc.js 14 | typings.d.ts 15 | postcss.config.cjs -------------------------------------------------------------------------------- /src/utils/setCssVar.ts: -------------------------------------------------------------------------------- 1 | export function setCssVar(vars: Record) { 2 | Object.keys(vars).forEach(key => { 3 | document.documentElement.style.setProperty(key, vars[key]); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/locales/zh-Hans/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "请输入帐号": "请输入帐号", 3 | "请输入密码": "请输入密码", 4 | "记住我": "记住我", 5 | "忘记密码": "忘记密码", 6 | "登录": "登录", 7 | "或": "或", 8 | "注册": "注册", 9 | "登录成功": "登录成功" 10 | } 11 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | type BlEnum = 0 | 1; 2 | 3 | type Key = string | number; 4 | 5 | interface Token { 6 | accessToken: string; 7 | refreshToken: string; 8 | /** 9 | * 过期时间, 单位毫秒 10 | */ 11 | expiration: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/locales/zh-Hans/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "已登出": "已登出", 3 | "个人中心": "个人中心", 4 | "退出登录": "退出登录", 5 | "切换语言成功": "已切换为{{language}}", 6 | "语言": "语言", 7 | "刷新页面": "刷新页面", 8 | "主题色": "主题色", 9 | "亮色": "亮色", 10 | "暗色": "暗色" 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/business/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { LRULocalStorage } from '../LRUCache'; 2 | 3 | export class MyLocalStorage extends LRULocalStorage { 4 | getDataKey() { 5 | return 'common'; // 可以设置动态的命名空间,例如根据路由区分。此处先先死 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/grid/locales/en/grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "改变窗口宽度,观察布局的变化": "Change the window width and observe the layout changes", 3 | "当前尺寸": "Current size", 4 | "第一排": "First row", 5 | "第二排": "Second row", 6 | "尺寸": "Size", 7 | "备注": "Remark", 8 | "屏幕": "Screen" 9 | } -------------------------------------------------------------------------------- /src/components/Loading/index.less: -------------------------------------------------------------------------------- 1 | .console-loading { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | .anticon { 11 | font-size: 50px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/utils/browser.ts: -------------------------------------------------------------------------------- 1 | const UA = window.navigator.userAgent; 2 | 3 | /** 小程序算作移动端 */ 4 | export const isMobile = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|miniProgram)/i.test(UA); 5 | -------------------------------------------------------------------------------- /src/locales/en/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "请输入帐号": "Please enter your account", 3 | "请输入密码": "Please enter your password", 4 | "记住我": "Remember me", 5 | "忘记密码": "Forgot password", 6 | "登录": "Login", 7 | "或": "Or", 8 | "注册": "Sign up", 9 | "登录成功": "Login successful" 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/permission/ChangeUser/index.less: -------------------------------------------------------------------------------- 1 | .console-change-user { 2 | .ant-table { 3 | width: 320px; 4 | } 5 | } 6 | 7 | .console-change-user__radio-label { 8 | margin-right: 6px; 9 | } 10 | 11 | .console-change-user__title { 12 | margin-top: 24px; 13 | margin-bottom: 10px; 14 | } -------------------------------------------------------------------------------- /docs/guide/begin.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 环境 4 | 5 | - node 18+(推荐) 6 | - npm 10+ (推荐) 7 | 8 | ## 安装 9 | 10 | ```shell 11 | npm i 12 | ``` 13 | 14 | ## 启动 15 | 16 | ```shell 17 | npm start 18 | ``` 19 | 20 | ## 构建 21 | 22 | ```shell 23 | npm run build:prod # 生产环境 - 环境变量定义在.env.prod文件 24 | ``` 25 | -------------------------------------------------------------------------------- /src/locales/en/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "已登出": "Logout successfully", 3 | "个人中心": "Profile", 4 | "退出登录": "Logout", 5 | "切换语言成功": "Switch to {{language}} successfully", 6 | "语言": "Language", 7 | "刷新页面": "Refresh", 8 | "主题色": "Theme color", 9 | "亮色": "Light mode", 10 | "暗色": "Dark mode" 11 | } 12 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_ENV: string; 5 | readonly VITE_API_HOST: string; 6 | readonly VITE_BASENAME: string; 7 | // 更多环境变量... 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv; 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/tablePage/components/PoweredByAdminSearchList/index.less: -------------------------------------------------------------------------------- 1 | .console-layout-powered-by { 2 | display: inline-flex; 3 | align-items: center; 4 | padding: 6px 12px; 5 | border-radius: 8px; 6 | border: 1px solid #ddd; 7 | // box-shadow: 2px 2px 12px #ddd; 8 | a { 9 | font-style: italic; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'react-router-toolset'; 2 | import { routesConfig } from './config'; 3 | 4 | const basename = import.meta.env.VITE_BASENAME; 5 | 6 | const router = new Router(routesConfig, { 7 | basename, 8 | }); 9 | 10 | export default router; 11 | 12 | export * from 'react-router-toolset'; 13 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function usePrevious(currentState: T) { 4 | const [previousState, setPreviousState] = useState(currentState); 5 | if (currentState !== previousState) { 6 | setPreviousState(currentState); 7 | } 8 | return previousState; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/pages/noAccess/index.less: -------------------------------------------------------------------------------- 1 | .no-access { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100%; 6 | height: 100%; 7 | text-align: center; 8 | } 9 | 10 | .no-access__icon { 11 | width: 320px; 12 | } 13 | 14 | .no-access__tips { 15 | margin: 12px 0; 16 | font-size: 16px; 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/notFound/index.less: -------------------------------------------------------------------------------- 1 | .not-found { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100%; 6 | height: 100%; 7 | text-align: center; 8 | } 9 | 10 | .not-found__icon { 11 | width: 320px; 12 | } 13 | 14 | .not-found__tips { 15 | margin: 12px 0; 16 | font-size: 16px; 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/singleSider/index.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const SingleSider = () => { 5 | const { t: t_menu } = useTranslation('menu'); 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default SingleSider; 12 | -------------------------------------------------------------------------------- /src/layouts/Collapse/index.less: -------------------------------------------------------------------------------- 1 | .console-layout-tabs__collapse { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 30px; 6 | height: 30px; 7 | background: var(--container-background-color); 8 | border-radius: 10px; 9 | box-shadow: var(--layout-box-shdow); 10 | cursor: pointer; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/profile/index.less: -------------------------------------------------------------------------------- 1 | .console-profile__wrap { 2 | margin-top: 100px; 3 | text-align: center; 4 | } 5 | 6 | .console-profile__contact { 7 | text-align: left; 8 | margin-top: 12px; 9 | p { 10 | display: flex; 11 | align-items: center; 12 | margin-top: 4px; 13 | span, a { 14 | margin-left: 16px; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/router/tempRoute/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | 3 | /** 4 | * 临时路由 5 | */ 6 | const TempRoute = () => { 7 | const { t: t_router } = useTranslation('router'); 8 | return ( 9 |
10 | {t_router('我是通过router.setSiblings()方法新增的临时路由')} 11 |
12 | ); 13 | }; 14 | 15 | export default TempRoute; 16 | -------------------------------------------------------------------------------- /src/layouts/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon from '@/components/SvgIcon'; 2 | import './index.less'; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 | Copyright {new Date().getFullYear()} react-antd-console 8 |
9 | ); 10 | }; 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /src/pages/tablePage/scrollLoadModeList/index.less: -------------------------------------------------------------------------------- 1 | .scrollLoadPage__list { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .scrollLoadPage__list-item { 7 | width: calc(33.33333% - 16px); 8 | margin: 0 16px 16px 0; 9 | } 10 | 11 | .scrollLoadPage__list-item-bottom { 12 | margin-top: 10px; 13 | .ant-btn { 14 | margin-right: 6px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Back/index.less: -------------------------------------------------------------------------------- 1 | .console__back { 2 | display: inline-flex; 3 | align-items: center; 4 | margin-bottom: 16px; 5 | .ant-divider { 6 | margin-inline: 16px; 7 | } 8 | } 9 | 10 | .console__back-icon { 11 | margin-right: 4px; 12 | } 13 | 14 | .console__back-action { 15 | display: flex; 16 | align-items: center; 17 | cursor: pointer; 18 | } 19 | -------------------------------------------------------------------------------- /src/services/withAuth.ts: -------------------------------------------------------------------------------- 1 | import request from '@/http'; 2 | 3 | interface HttpGetBaseInfoRes { 4 | userId: number; 5 | userAccount: string; 6 | avatar: string; 7 | permissions: string[]; 8 | } 9 | 10 | /** 11 | * 获取个人基本信息 12 | */ 13 | export async function httpGetBaseInfo() { 14 | return request.get>('/user/mine'); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Hover/index.tsx: -------------------------------------------------------------------------------- 1 | import { useHover } from 'react-use'; 2 | import type { JSX } from "react"; 3 | 4 | type Props = { 5 | children: ((isHovering: boolean) => JSX.Element); 6 | } 7 | 8 | const Hover = (props: Props) => { 9 | const hoverChildren = useHover((isHovering: boolean) => { 10 | return props.children(isHovering); 11 | }); 12 | return hoverChildren; 13 | }; 14 | 15 | export default Hover; 16 | -------------------------------------------------------------------------------- /src/layouts/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb as AntdBreadcrumb } from 'antd'; 2 | import useBreadcrumb from './useBreadcrumb'; 3 | import './index.less'; 4 | 5 | const Breadcrumb = () => { 6 | const items = useBreadcrumb(); 7 | return ( 8 | 12 | ); 13 | }; 14 | 15 | export default Breadcrumb; 16 | 17 | -------------------------------------------------------------------------------- /src/mock/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw/browser'; 2 | import { passthroughHandlers } from './passthrough'; 3 | import { loginMock } from '@/services/login.mock'; 4 | import { withAuthMock } from '@/services/withAuth.mock'; 5 | import { tableMock } from '@/pages/tablePage/table.mock'; 6 | 7 | export const worker = setupWorker( 8 | ...passthroughHandlers, 9 | ...loginMock, 10 | ...withAuthMock, 11 | ...tableMock, 12 | ); 13 | -------------------------------------------------------------------------------- /src/pages/nest/index.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router'; 2 | import { Card, Space } from 'antd'; 3 | import './index.less'; 4 | 5 | const Nest = () => { 6 | return ( 7 | 8 | 9 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Nest; 19 | -------------------------------------------------------------------------------- /src/pages/permission/locales/zh-Hans/permission.json: -------------------------------------------------------------------------------- 1 | { 2 | "切换为 Assistant 帐号后,会跳转到 403 页,因为 Assistant 帐号没有本路由权限": "切换为 Assistant 帐号后,会跳转到 403 页,因为 Assistant 帐号没有本路由权限", 3 | "切换为 Assistant 帐号后,有的按钮会隐藏": "切换为 Assistant 帐号后,有的按钮会隐藏", 4 | "帐号": "帐号", 5 | "路由权限页": "路由权限页", 6 | "局部权限页": "局部权限页", 7 | "按钮A": "按钮A", 8 | "按钮B": "按钮B", 9 | "切换帐号": "切换帐号", 10 | "只有Admin能看到": "只有Admin能看到", 11 | "都能看到": "都能看到", 12 | "切换帐号成功": "切换帐号成功" 13 | } -------------------------------------------------------------------------------- /src/layouts/Github/index.tsx: -------------------------------------------------------------------------------- 1 | import TooltipIcon from '../components/TooltipIcon'; 2 | import SvgIcon from '@/components/SvgIcon'; 3 | 4 | const Github = () => { 5 | return ( 6 | } 9 | onClick={() => { 10 | window.open('https://github.com/diandian18/react-antd-console'); 11 | }} 12 | /> 13 | ); 14 | }; 15 | 16 | export default Github; 17 | -------------------------------------------------------------------------------- /src/layouts/Footer/index.less: -------------------------------------------------------------------------------- 1 | .console-layout__right-footer { 2 | flex-shrink: 0; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | height: 42px; 7 | background-color: var(--container-background-color); 8 | border-top: 1px solid #0505050f; 9 | margin-top: var(--layout-gutter); 10 | border-radius: var(--layout-border-radius); 11 | box-shadow: var(--layout-box-shdow); 12 | .anticon, svg { 13 | margin: 0 4px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mock/passthrough.ts: -------------------------------------------------------------------------------- 1 | import { http, passthrough } from 'msw'; 2 | 3 | export const passthroughHandlers = [ 4 | http.get('*.ts', () => passthrough()), 5 | http.get('*.tsx', () => passthrough()), 6 | http.get('*.json', () => passthrough()), 7 | http.get('*.js', () => passthrough()), 8 | http.get('*.css', () => passthrough()), 9 | http.get('*.less', () => passthrough()), 10 | http.get('*.svg', () => passthrough()), 11 | http.get('*.png', () => passthrough()), 12 | ]; 13 | -------------------------------------------------------------------------------- /src/pages/tablePage/components/PoweredByAdminSearchList/index.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon from '@/components/SvgIcon'; 2 | import './index.less'; 3 | 4 | const PoweredBy = () => { 5 | return ( 6 | 7 |  admin-search-list 8 | 9 | ); 10 | } 11 | 12 | export default PoweredBy; 13 | -------------------------------------------------------------------------------- /src/pages/nest/nest1.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const Nest1 = () => { 5 | const { t: t_menu } = useTranslation('menu'); 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Nest1; 19 | -------------------------------------------------------------------------------- /src/pages/nest/nest2-1.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const Nest21 = () => { 5 | const { t: t_menu } = useTranslation('menu'); 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Nest21; 19 | -------------------------------------------------------------------------------- /src/pages/nest/nest2-2-1.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const Nest221 = () => { 5 | const { t: t_menu } = useTranslation('menu'); 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Nest221; 19 | -------------------------------------------------------------------------------- /src/pages/nest/nest2-2-2.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const Nest222 = () => { 5 | const { t: t_menu } = useTranslation('menu'); 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Nest222; 19 | -------------------------------------------------------------------------------- /docs/development/lint.md: -------------------------------------------------------------------------------- 1 | # 编码规范 2 | 3 | ## 概述 4 | 5 | 项目总体按最小约束原则约束编码规范,只使用了 `eslint`。你可以根据自己的需求自行添加各规范,如 [stylelint](https://stylelint.io/)、[prettier](https://prettier.io/)等 6 | 7 | :::tip 8 | 我们认为在编码规范方面,应当在工程**统一性**和**灵活性**之间找到一个平衡,而不是一味地使用各种lint类工具作强制约束。你可以找到自己团队的平衡点,定制适合自己团队的编码规范 9 | ::: 10 | 11 | ## eslint规则 12 | 13 | 本项目采用官方建议的通用 `eslint` 规则,如下: 14 | 15 | - `@typescript-eslint/recommended` (`eslint` 官方赞助的社区 `typescript` 规则) 16 | - `eslint:recommended` (`eslint` 官方推荐规则) 17 | - `eslint-plugin-react` (社区流行的 `react` 规则) 18 | 19 | 详见 `.eslintrc.cjs` 20 | -------------------------------------------------------------------------------- /src/layouts/Breadcrumb/index.less: -------------------------------------------------------------------------------- 1 | .console-layout-breadcrumb { 2 | &.ant-breadcrumb ol { 3 | flex-wrap: nowrap; 4 | } 5 | .ant-breadcrumb-overlay-link { 6 | display: inline-flex; 7 | align-items: center; 8 | &:first-child > span { 9 | > .anticon { 10 | margin-right: 3px; 11 | } 12 | } 13 | } 14 | .ant-breadcrumb-link { 15 | display: flex; 16 | &:first-child > span { 17 | display: inline-flex; 18 | align-items: center; 19 | > svg { 20 | margin-right: 4px; 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/pages/nest/nest2.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import { Outlet } from 'react-router'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const Nest2 = () => { 6 | const { t: t_menu } = useTranslation('menu'); 7 | return ( 8 | 9 | 10 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Nest2; 21 | -------------------------------------------------------------------------------- /src/pages/separation/index.tsx: -------------------------------------------------------------------------------- 1 | import withAuth from '@/components/business/withAuth'; 2 | import Back from '@/components/Back'; 3 | import { Alert, Card } from 'antd'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | const Separation = withAuth(() => { 7 | const { t: t_menu } = useTranslation('menu'); 8 | return ( 9 | 10 | 11 | 12 | 13 | ); 14 | }); 15 | 16 | export default Separation; 17 | 18 | -------------------------------------------------------------------------------- /src/pages/nest/nest2-2.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import { Outlet } from 'react-router'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const Nest22 = () => { 6 | const { t: t_menu } = useTranslation('menu'); 7 | return ( 8 | 9 | 10 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Nest22; 21 | -------------------------------------------------------------------------------- /src/assets/svg/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm'; 2 | import Tools from './Tools'; 3 | import SvgIcon from '@/components/SvgIcon'; 4 | import './index.less'; 5 | 6 | const Login = () => { 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 |
14 | Copyright {new Date().getFullYear()} react-antd-console 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Login; 21 | -------------------------------------------------------------------------------- /src/locales/en/index.ts: -------------------------------------------------------------------------------- 1 | import menu from './menu.json'; 2 | import layout from './layout.json'; 3 | import login from './login.json'; 4 | import grid from '@/pages/grid/locales/en/grid.json'; 5 | import permission from '@/pages/permission/locales/en/permission.json'; 6 | import router from '@/pages/router/locales/en/router.json'; 7 | import tablePage from '@/pages/tablePage/locales/en/tablePage.json'; 8 | import error from './error.json'; 9 | 10 | const en = { 11 | menu, 12 | layout, 13 | login, 14 | grid, 15 | permission, 16 | router, 17 | tablePage, 18 | error, 19 | }; 20 | 21 | export default en; 22 | 23 | -------------------------------------------------------------------------------- /src/pages/permission/locales/en/permission.json: -------------------------------------------------------------------------------- 1 | { 2 | "切换为 Assistant 帐号后,会跳转到 403 页,因为 Assistant 帐号没有本路由权限": "After switching to the assistant account, you will be redirected to the 403 page because the assistant account does not have the permission of this route", 3 | "切换为 Assistant 帐号后,有的按钮会隐藏": "After switching to assistant account, some buttons will be hidden", 4 | "帐号": "Account", 5 | "路由权限页": "Route", 6 | "局部权限页": "Local", 7 | "按钮A": "Button A", 8 | "按钮B": "Button B", 9 | "切换帐号": "Switch account", 10 | "只有Admin能看到": "Only admin can see", 11 | "都能看到": "Everyone can see", 12 | "切换帐号成功": "Account switched" 13 | } -------------------------------------------------------------------------------- /src/layouts/Tabs/AnimationWrap/index.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { HTMLMotionProps, motion } from 'framer-motion'; 3 | 4 | const AnimationSpan = ({ children, ...props }: PropsWithChildren>) => { 5 | return ( 6 | 14 | { children } 15 | 16 | ); 17 | } 18 | 19 | export default AnimationSpan; 20 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import SvgIcon from '@/components/SvgIcon'; 3 | import './index.less'; 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default Loading; 23 | -------------------------------------------------------------------------------- /src/pages/login/Tools.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from 'antd'; 2 | import DarkSwitch from '@/layouts/DarkSwitch'; 3 | import Language from '@/layouts/Language'; 4 | import FullScreen from '@/layouts/FullScreen'; 5 | import Refresh from '@/layouts/Refresh'; 6 | import ColorPicker from '@/layouts/ColorPicker'; 7 | 8 | const Tools = () => { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default Tools; 23 | -------------------------------------------------------------------------------- /src/pages/permission/route/index.tsx: -------------------------------------------------------------------------------- 1 | import ChangeUser from '../ChangeUser'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { Alert } from 'antd'; 4 | 5 | /** 6 | * 路由权限 7 | */ 8 | const RoutePermission = () => { 9 | const { t: t_permission } = useTranslation('permission'); 10 | return ( 11 |
12 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default RoutePermission; 21 | -------------------------------------------------------------------------------- /docs/development/style.md: -------------------------------------------------------------------------------- 1 | # 样式 2 | 3 | 样式方案采用 [less](https://lesscss.org/) ,并遵循 [BEM](https://getbem.com/) 规范。 4 | 5 | :::tip 6 | 你可以根据需要,自行采用其他样式方案,如 [tailwindcss](https://tailwindcss.com/)、[styled-components](https://styled-components.com/) 等。 7 | ::: 8 | 9 | ## 目录结构 10 | 11 | ```shell 12 | ├── src 13 | │   ├── styles # 全局样式 14 | │   │   ├── index.less # 入口文件 15 | │   │   ├── reset.less # 重置样式 16 | │   │   ├── utils.less # 工具样式 17 | │   │   └── vars.less # less样式变量 18 | │   ├── pages 19 | │   │   ├── login 20 | │ │ │   ├── index.less 21 | │ │ │   └── index.tsx 22 | ``` 23 | 24 | ::: tip 25 | 对于业务组件,建议把**样式文件**和**组件文件**放在一起 26 | ::: 27 | -------------------------------------------------------------------------------- /src/locales/zh-Hans/index.ts: -------------------------------------------------------------------------------- 1 | import menu from './menu.json'; 2 | import layout from './layout.json'; 3 | import login from './login.json'; 4 | import grid from '@/pages/grid/locales/zh-Hans/grid.json'; 5 | import permission from '@/pages/permission/locales/zh-Hans/permission.json'; 6 | import router from '@/pages/router/locales/zh-Hans/router.json'; 7 | import tablePage from '@/pages/tablePage/locales/zh-Hans/tablePage.json'; 8 | import error from './error.json'; 9 | 10 | const zh_Hans = { 11 | menu, 12 | layout, 13 | login, 14 | grid, 15 | permission, 16 | router, 17 | tablePage, 18 | error, 19 | }; 20 | 21 | export default zh_Hans; 22 | 23 | -------------------------------------------------------------------------------- /src/pages/router/locales/zh-Hans/router.json: -------------------------------------------------------------------------------- 1 | { 2 | "路由的变化会导致整体组件重新渲染,建议在渲染页面之前完成路由的变化操作": "路由的变化会导致整体组件重新渲染,建议在渲染页面之前完成路由的变化操作", 3 | "动态新增路由": "动态新增路由", 4 | "新增尾部": "新增尾部", 5 | "新增头部": "新增头部", 6 | "新增中间": "新增中间", 7 | "动态删除路由": "动态删除路由", 8 | "删除尾部": "删除尾部", 9 | "删除头部": "删除头部", 10 | "删除中间": "删除中间", 11 | "在指定位置动态新增路由": "在指定位置动态新增路由", 12 | "指定在“外链”后新增": "指定在“外链”后新增", 13 | "删除新增的路由": "删除新增的路由", 14 | "动态修改路由": "动态修改路由", 15 | "修改当前路由": "修改当前路由", 16 | "重置": "重置", 17 | "标题": "标题", 18 | "修改标题为rac": "修改标题为rac", 19 | "修改logo": "修改logo", 20 | "修改Icon": "修改Icon", 21 | "我是通过router.setSiblings()方法新增的临时路由": "我是通过router.setSiblings()方法新增的临时路由" 22 | } -------------------------------------------------------------------------------- /src/layouts/ConsoleLayout/animations.ts: -------------------------------------------------------------------------------- 1 | import type { Variants } from 'framer-motion'; 2 | 3 | export const Animations: Record = { 4 | ['bounceInRight']: { 5 | initial: { x: '3%' }, 6 | in: { x: 0 }, 7 | }, 8 | ['bounceInLeft']: { 9 | initial: { x: '-3%' }, 10 | in: { x: 0 }, 11 | }, 12 | ['bounceInUp']: { 13 | initial: { y: '3%' }, 14 | in: { y: 0 }, 15 | }, 16 | ['bounceInDown']: { 17 | initial: { y: '-3%' }, 18 | in: { y: 0 }, 19 | }, 20 | ['fadeIn']: { 21 | initial: { 22 | opacity: 0, 23 | scale: 0.96, 24 | }, 25 | in: { 26 | opacity: 1, 27 | scale: 1, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/styles/reset.less: -------------------------------------------------------------------------------- 1 | html, body, ul, li, ol, dl, dd, dt, p, h1, h2, h3, h4, h5, h6, form, fieldset, legend, img { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, 7 | body, 8 | #root { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | html, 14 | body { 15 | overflow: hidden; 16 | } 17 | 18 | .colorWeak { 19 | filter: invert(80%); 20 | } 21 | 22 | canvas { 23 | display: block; 24 | } 25 | 26 | body { 27 | text-rendering: optimizeLegibility; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | } 31 | 32 | ul, 33 | ol { 34 | list-style: none; 35 | } 36 | 37 | *, *::before, *::after { 38 | box-sizing: border-box; 39 | } 40 | -------------------------------------------------------------------------------- /src/services/login.ts: -------------------------------------------------------------------------------- 1 | import request from '@/http'; 2 | 3 | interface HttpPostLoginReq { 4 | userAccount: string; 5 | userPassword: string; 6 | } 7 | 8 | interface HttpPostLoginRes { 9 | userAccount: string; 10 | userId: number; 11 | permissions: string[]; 12 | accessToken: string; 13 | refreshToken: string; 14 | expiration: number; 15 | } 16 | 17 | /** 18 | * 登录 19 | */ 20 | export async function httpPostLogin(data: HttpPostLoginReq) { 21 | return request.post>('/user/login', data); 22 | } 23 | 24 | /** 25 | * 登出 26 | */ 27 | export function httpPostLogout() { 28 | return request.post('/user/logout'); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/layouts/Avatar/index.less: -------------------------------------------------------------------------------- 1 | @import '@/styles/utils.less'; 2 | 3 | .console-layout__avatar { 4 | margin-left: 6px; 5 | .anticon-user { 6 | font-size: 22px; 7 | margin-right: 8px; 8 | } 9 | } 10 | 11 | .console-layout__avatar-click { 12 | display: flex; 13 | align-items: center; 14 | cursor: pointer; 15 | } 16 | 17 | .console-layout__avatar-image-wrap { 18 | display: inline-block; 19 | width: 42px; 20 | height: 42px; 21 | border-radius: 50%; 22 | padding: 4px; 23 | img { 24 | width: 100%; 25 | height: 100%; 26 | padding: 4px; 27 | } 28 | } 29 | 30 | .console-layout__name { 31 | display: inline-block; 32 | width: 60px; 33 | .ellipsis(); 34 | } 35 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import { h } from 'vue' 3 | import type { Theme } from 'vitepress' 4 | import DefaultTheme from 'vitepress/theme-without-fonts' 5 | import './my-fonts.css' 6 | import './style.css' 7 | 8 | export default { 9 | extends: DefaultTheme, 10 | Layout: () => { 11 | return h(DefaultTheme.Layout, null, { 12 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 13 | }) 14 | }, 15 | enhanceApp({ app, router, siteData }) { 16 | // ... 17 | } 18 | } satisfies Theme 19 | 20 | // :root { 21 | // --vp-font-family-base: /* normal text font */ 22 | // --vp-font-family-mono: /* code font */ 23 | // } 24 | -------------------------------------------------------------------------------- /src/assets/svg/rectangle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/Tabs/FixAntdTabTranslate/index.tsx: -------------------------------------------------------------------------------- 1 | import { JSXElementConstructor, PropsWithChildren, ReactElement } from 'react'; 2 | import './index.less'; 3 | 4 | interface Props { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | node: ReactElement>; 7 | } 8 | 9 | /** 10 | * 自定义tab的包裹容器 11 | * 用于继承antd tab的位移定位功能 12 | */ 13 | const FixAntdTabTranslate = ({ node, children }: PropsWithChildren) => { 14 | return ( 15 |
16 | {children} 17 | 18 | {node} 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default FixAntdTabTranslate; 25 | -------------------------------------------------------------------------------- /src/assets/svg/location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/single_slider.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/noAccess/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import router from '@/router'; 3 | import { useTranslation } from 'react-i18next'; 4 | import SvgIcon from '@/components/SvgIcon'; 5 | import './index.less'; 6 | 7 | /** 8 | * 403页 9 | */ 10 | const NoAccess = () => { 11 | const { t: t_error } = useTranslation('error'); 12 | return ( 13 |
14 |
15 |

16 |

{t_error('对不起,您没有访问权限')}

17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default NoAccess; 24 | -------------------------------------------------------------------------------- /src/services/withAuth.mock.ts: -------------------------------------------------------------------------------- 1 | import { axiosRes } from '@/mock/res'; 2 | import { HttpResponse, http } from 'msw'; 3 | import { commonInfo, userAdmin, userAssistant } from './login.mock'; 4 | 5 | export const withAuthMock = [ 6 | http.get('/api/user/mine', async({ request }) => { 7 | let token = ''; 8 | // @ts-expect-error entries 9 | const entries = request.headers.entries(); 10 | for (const [key, value] of entries) { 11 | if (key === 'authorization') { 12 | token = (value ?? '').split(' ')[1]; 13 | } 14 | } 15 | const userInfo = token === 'aaaa' ? userAdmin : userAssistant; 16 | return HttpResponse.json(axiosRes({ 17 | ...commonInfo, 18 | ...userInfo, 19 | })); 20 | }), 21 | ]; 22 | -------------------------------------------------------------------------------- /src/pages/notFound/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | import router from '@/router'; 3 | import { useTranslation } from 'react-i18next'; 4 | import SvgIcon from '@/components/SvgIcon'; 5 | import './index.less'; 6 | 7 | /** 8 | * 404页 9 | */ 10 | const NotFound = () => { 11 | const { t: t_error } = useTranslation('error'); 12 | 13 | return ( 14 |
15 |
16 |

17 |

{t_error('对不起,您访问的页面不存在')}

18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default NotFound; 25 | -------------------------------------------------------------------------------- /src/layouts/Tabs/ContextMenu/useContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { useContextMenu as useContextMenuByReactContexify } from 'react-contexify'; 2 | import { ItemType } from '@/layouts/SideMenu/utils'; 3 | import { MouseEvent } from 'react'; 4 | import { MENU_ID } from './const'; 5 | 6 | interface Params { 7 | item?: ItemType; 8 | pathname: string; 9 | } 10 | 11 | export function useContextMenu({ item, pathname }: Params) { 12 | const { show } = useContextMenuByReactContexify({ 13 | id: MENU_ID, 14 | }); 15 | 16 | function onContextMenu(event: MouseEvent) { 17 | show({ 18 | event, 19 | props: { 20 | item, 21 | pathname, 22 | }, 23 | }); 24 | } 25 | 26 | return { 27 | onContextMenu, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/tablePage/locales/zh-Hans/tablePage.json: -------------------------------------------------------------------------------- 1 | { 2 | "姓名": "姓名", 3 | "邮箱": "邮箱", 4 | "操作": "操作", 5 | "详情": "详情", 6 | "编辑": "编辑", 7 | "确认删除吗?": "确认删除吗?", 8 | "删除后保持滚动条位置": "删除后保持滚动条位置", 9 | "删除": "删除", 10 | "请输入": "请输入", 11 | "新增": "新增", 12 | "常见表格 - 详情": "常见表格 - 详情", 13 | "日志": "日志", 14 | "导出": "导出", 15 | "人力资源部": "人力资源部", 16 | "财务部": "财务部", 17 | "销售部": "销售部", 18 | "市场部": "市场部", 19 | "技术部": "技术部", 20 | "运营部": "运营部", 21 | "部门": "部门", 22 | "部门作为额外参数": "部门作为额外参数", 23 | "时间范围": "时间范围", 24 | "TalbePage也可以用在弹窗里, 或更多地方": "TalbePage也可以用在弹窗里, 或更多地方", 25 | "弹窗内TablePage": "弹窗内TablePage", 26 | "打开弹窗": "打开弹窗", 27 | "自定义搜索按钮": "自定义搜索按钮", 28 | "按钮顺序": "按钮顺序", 29 | "自定义渲染": "自定义渲染", 30 | "自定义": "自定义", 31 | "默认": "默认" 32 | } -------------------------------------------------------------------------------- /src/layouts/Tabs/index.less: -------------------------------------------------------------------------------- 1 | .console-layout-tabs { 2 | display: flex; 3 | align-items: center; 4 | width: calc(100% - 34px); 5 | padding-right: 8px; 6 | user-select: none; 7 | margin-left: 4px; 8 | &.CHROME { 9 | .ant-tabs-top > .ant-tabs-nav { 10 | &:before { 11 | border-bottom: none; 12 | } 13 | } 14 | } 15 | .ant-tabs { 16 | width: 100%; 17 | } 18 | .ant-tabs-nav-list { 19 | position: relative; 20 | padding-top: var(--layout-gutter) 21 | } 22 | .ant-tabs-nav { 23 | margin-bottom: 0; 24 | } 25 | } 26 | 27 | .console-layout-tabs__popup { 28 | .ant-tabs-dropdown-menu-item >span >div { 29 | display: flex; 30 | align-items: center; 31 | svg { 32 | margin-right: 4px; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/development/env.md: -------------------------------------------------------------------------------- 1 | # 环境变量 2 | 3 | 环境变量采用 `vite` 内置的方案。 4 | 5 | - 当使用 `vite --mode localhost` 启动项目时,环境的配置文件,对应的是根目录的 `.env.localhost` 文件。 6 | - `.env.localhost` 文件中定义的环境变量,可通过 `const { VITE_API_HOST } = import.meta.env` 在代码中引入。 7 | 8 | ## 如何新增环境,并新增环境变量 9 | 10 | 1. 在根目录新建 `.env.newEnv` 文件 11 | 2. 在 `.env.newEnv` 文件中定义环境变量: `VITE_SOME_KEY = someValue` 12 | 3. 在 `src/vite-env.d.ts` 定义 `VITE_SOME_KEY` 的类型 13 | 4. 在项目的 `ts/tsx` 文件中引入环境变量 `const { VITE_SOME_KEY } = import.meta.env;` 14 | 15 | ## 使用环境构建 16 | 17 | 添加构建命令: 18 | 19 | ::: code-group 20 | 21 | ```json [package.json] 22 | { 23 | "scripts": { 24 | "build:newEnv": "vite build --mode newEnv", // [!code ++] 25 | } 26 | } 27 | ``` 28 | 29 | ::: 30 | 31 | 执行构建 32 | 33 | ```shell 34 | npm run build:newEnv 35 | ``` 36 | -------------------------------------------------------------------------------- /src/assets/svg/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react-antd-console 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/DarkSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | import { themeModel } from '@/models/theme'; 2 | import { useModel } from '@zhangsai/model'; 3 | import TooltipIcon from '../components/TooltipIcon'; 4 | import SvgIcon from '@/components/SvgIcon'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | const DarkSwitch = () => { 8 | const curDarkMode = useModel(themeModel, 'curDarkMode'); 9 | const { t: t_layout } = useTranslation('layout'); 10 | return ( 11 | : } 14 | onClick={() => { 15 | themeModel.setThemeState({ curDarkMode: !curDarkMode }); 16 | }} 17 | /> 18 | ); 19 | }; 20 | 21 | export default DarkSwitch; 22 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { initReactI18next } from 'react-i18next'; 2 | import i18n from 'i18next'; 3 | import { baseModel } from '@/models/base'; 4 | // import en from './en'; 5 | // import zh_Hans from './zh-Hans'; 6 | 7 | let mounted = false; 8 | 9 | export async function i18nInit() { 10 | if (mounted) return; 11 | 12 | const en = (await import('./en')).default; 13 | const zh_Hans = (await import('./zh-Hans')).default; 14 | 15 | const resources = { 16 | en, 17 | ['zh_Hans']: zh_Hans, 18 | }; 19 | 20 | i18n 21 | .use(initReactI18next) 22 | .init({ 23 | fallbackLng: baseModel.state.language ?? 'zh_Hans', 24 | resources, 25 | interpolation: { 26 | escapeValue: false, 27 | }, 28 | }); 29 | 30 | mounted = true; 31 | } 32 | 33 | export default i18n; 34 | 35 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "react-antd-console" 7 | text: "后台管理系统前端解决方案" 8 | tagline: 简单、专注、高效 9 | image: https://static.react-antd-console.site/template.png 10 | actions: 11 | - theme: brand 12 | text: 快速开始 13 | link: /guide/what 14 | - theme: alt 15 | text: 在线预览 16 | link: https://template.react-antd-console.site 17 | 18 | features: 19 | - title: 最新技术栈 20 | icon: 🔥 21 | details: 使用 React 19、Ant design 5、TypeScript、Vite 等新版本 22 | - title: 上手简单 23 | icon: 🔧 24 | details: 结构清晰,模块独立,内易修改,外易拆换 25 | - title: 专注业务 26 | icon: 🎯 27 | details: 封装好了登录、鉴权、菜单、面包屑、标签页等功能,只需专注于业务开发 28 | - title: 主题定制 29 | icon: 🎨 30 | details: 支持深/浅肤色模式下的任意颜色切换,以及多种主题风格选择 31 | --- 32 | -------------------------------------------------------------------------------- /src/components/Progress/utils.ts: -------------------------------------------------------------------------------- 1 | interface Opts { 2 | force?: boolean; 3 | } 4 | 5 | export function setNProgressColor(color: string, opts?: Opts) { 6 | const { force = false } = opts ?? {}; 7 | const styleDom = document.querySelector('#nprogressThemeColor') ?? document.createElement('style'); 8 | if (!force && styleDom.id) return; 9 | styleDom.id = 'nprogressThemeColor'; 10 | styleDom.innerHTML = ` 11 | #nprogress .bar { 12 | background: ${color}!important; 13 | box-shadow: 0 0 2px ${color}; 14 | } 15 | 16 | #nprogress .peg { 17 | box-shadow: 0 0 10px ${color}, 0 0 5px ${color}; 18 | } 19 | 20 | #nprogress .spinner-icon { 21 | border-top-color: ${color}; 22 | border-inline-start-color: ${color}; 23 | } 24 | `; 25 | document.body.appendChild(styleDom); 26 | } 27 | -------------------------------------------------------------------------------- /.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-lib 13 | dist-version 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | # dependencies 29 | /node_modules 30 | /package-lock.json 31 | /yarn.lock 32 | 33 | # misc 34 | .DS_Store 35 | /coverage 36 | .idea 37 | *bak 38 | .vscode 39 | 40 | # visual studio code 41 | .history 42 | *.log 43 | functions/* 44 | .temp/** 45 | 46 | # screenshot 47 | screenshot 48 | .firebase 49 | .eslintcache 50 | 51 | # docs 52 | docs/.vitepress/dist/* 53 | docs/.vitepress/cache/* 54 | 55 | # other 56 | 57 | *.local 58 | stats.html 59 | -------------------------------------------------------------------------------- /src/locales/zh-Hans/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "登录": "登录", 3 | "首页": "首页", 4 | "栅格布局": "栅格布局", 5 | "个人中心": "个人中心", 6 | "权限": "权限", 7 | "路由权限": "路由权限", 8 | "局部权限": "局部权限", 9 | "路由": "路由", 10 | "动态路由": "动态路由", 11 | "动态meta": "动态meta", 12 | "搜索表格": "搜索表格", 13 | "常见表格": "常见表格", 14 | "常见表格详情": "常见表格详情", 15 | "滚动加载表格": "滚动加载表格", 16 | "滚动加载列表": "滚动加载列表", 17 | "额外参数": "额外参数", 18 | "格式化搜索参数": "格式化搜索参数", 19 | "简单表格": "简单表格", 20 | "弹窗内使用": "弹窗内使用", 21 | "自定义搜索按钮": "自定义搜索按钮", 22 | "嵌套路由": "嵌套路由", 23 | "菜单1": "菜单1", 24 | "菜单2": "菜单2", 25 | "菜单2-1": "菜单2-1", 26 | "菜单2-2": "菜单2-2", 27 | "菜单2-2-1": "菜单2-2-1", 28 | "菜单2-2-2": "菜单2-2-2", 29 | "错误页": "错误页", 30 | "外链": "外链", 31 | "单栏": "单栏", 32 | "单栏示例": "单栏示例", 33 | "独立布局": "独立布局", 34 | "本页面独立于默认布局": "本页面独立于默认布局", 35 | "出错了": "出错了", 36 | "页面不存在": "页面不存在" 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/svg/menu_unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/unchecked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/Breadcrumb/useBreadcrumb.ts: -------------------------------------------------------------------------------- 1 | import router from '@/router'; 2 | import { useMemo } from 'react'; 3 | import { useLocation } from 'react-router'; 4 | import { genBreadcrumb } from './utils'; 5 | import { useModel } from '@zhangsai/model'; 6 | import { baseModel } from '@/models/base'; 7 | 8 | export default function useBreadcrumb() { 9 | const language = useModel(baseModel, 'language'); 10 | const location = useLocation(); 11 | // 当前菜单变化时,重新计算面包屑数据 12 | const items = useMemo(() => { 13 | const route = router.flattenRoutes.get(router.getRoutePath(location.pathname)); 14 | const breadcrumb = genBreadcrumb(route, { 15 | showIcon: true, 16 | showDropdownMenu: false, 17 | }); 18 | return breadcrumb; 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, [location.pathname, language]); 21 | 22 | return items; 23 | } -------------------------------------------------------------------------------- /src/layouts/Collapse/index.tsx: -------------------------------------------------------------------------------- 1 | import useStore from '@/layouts/ConsoleLayout/store'; 2 | import SvgIcon from '@/components/SvgIcon'; 3 | import { motion } from 'framer-motion'; 4 | import './index.less'; 5 | 6 | const Collapse = () => { 7 | const { collapsed, setCollapsed } = useStore(); 8 | 9 | function onClickCollapseMenu() { 10 | setCollapsed(!collapsed); 11 | } 12 | 13 | return ( 14 | 18 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export default Collapse; 30 | -------------------------------------------------------------------------------- /src/styles/utils.less: -------------------------------------------------------------------------------- 1 | .ellipsis() { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | } 6 | 7 | .scroll-bar-none { 8 | &::-webkit-scrollbar{ 9 | width: 0; 10 | height: 0; 11 | } 12 | 13 | /* 隐藏滚动条,当IE下溢出,仍然可以滚动 */ 14 | -ms-overflow-style:none; 15 | 16 | /* 火狐下隐藏滚动条 */ 17 | overflow:-moz-scrollbars-none; 18 | } 19 | 20 | // 重置scroll样式: 部分浏览器不支持eg:Firefox 21 | .reset-scrollbar() { 22 | &::-webkit-scrollbar { 23 | width: 6px; 24 | height: 6px 25 | } 26 | &::-webkit-scrollbar-track { 27 | background: hsla(0, 0%, 100%, .15); 28 | border-radius: 3px; 29 | -webkit-box-shadow: inset 0 0 5px rgba(37, 37, 37, .05); 30 | } 31 | &::-webkit-scrollbar-thumb { 32 | background: hsla(0, 0%, 100%, .2); 33 | border-radius: 3px; 34 | -webkit-box-shadow: inset 0 0 5px hsla(0, 0%, 100%, .05); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/svg/dialog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/copyright.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/ConsoleLayout/store/index.ts: -------------------------------------------------------------------------------- 1 | import { ItemType } from '@/layouts/SideMenu/utils'; 2 | import { createStore } from '@/components/store'; 3 | 4 | export interface StoreContextType { 5 | /** 菜单(树型) */ 6 | menuItems: ItemType[]; 7 | /** 菜单(一维) */ 8 | flattenMenuItems: Map; 9 | /** 菜单(一维), 包含被hidden的 */ 10 | allFlattenMenuItems: Map; 11 | /** 菜单收起 */ 12 | collapsed: boolean; 13 | setCollapsed: React.Dispatch>; 14 | /** 移动端菜单收起 */ 15 | mobileCollapsed: boolean; 16 | setMobileCollapsed: React.Dispatch>; 17 | ref1: React.RefObject; 18 | ref2: React.RefObject; 19 | ref3: React.RefObject; 20 | } 21 | 22 | const store = createStore(); 23 | const { useStore, Context } = store; 24 | export { Context }; 25 | export default useStore; 26 | 27 | -------------------------------------------------------------------------------- /src/assets/svg/format.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/forbidden.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/svg/signup.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/checked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useRoutes } from 'react-router'; 2 | import router, { useRouter } from '@/router'; 3 | import { DEFAULT_TITLE, logo } from './consts'; 4 | import { useTranslation } from 'react-i18next'; 5 | import Progress from '@/components/Progress'; 6 | import { AnimatePresence } from 'framer-motion'; 7 | import '@/styles/index.less'; 8 | 9 | function App() { 10 | const { curRoute, reactRoutes } = useRouter(router); 11 | // 这里类型会报错。因为react-router和react-router-dom的类型不一致 12 | const element = useRoutes(reactRoutes); 13 | const { t: t_menu } = useTranslation('menu'); 14 | 15 | return ( 16 | <> 17 | {curRoute?.name ? `${t_menu(curRoute.name)} | ${DEFAULT_TITLE}` : DEFAULT_TITLE} 18 | 19 | 20 | 21 | { element } 22 | 23 | 24 | ); 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/assets/svg/profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/stay.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TablePage/index.tsx: -------------------------------------------------------------------------------- 1 | import SearchList, { HttpGet, RefProps, SearchListProps } from 'admin-search-list'; 2 | import request from '@/http'; 3 | import { useImperativeHandle, useRef, Ref } from 'react'; 4 | 5 | const axiosHttpGet: HttpGet = async(url, opts) => { 6 | return request.get(url, { 7 | method: 'get', 8 | params: opts.params, 9 | headers: opts.headers, 10 | }).then((res) => { 11 | return res.data; 12 | }); 13 | }; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | const TablePage = , D>(props: Omit, 'httpGet'>, ref: Ref>) => { 17 | 18 | const tempRef = useRef>(null); 19 | // @ts-expect-error who can help? 20 | useImperativeHandle(ref, () => tempRef.current); 21 | 22 | return ( 23 | 28 | ); 29 | }; 30 | 31 | export default TablePage; -------------------------------------------------------------------------------- /src/pages/tablePage/locales/en/tablePage.json: -------------------------------------------------------------------------------- 1 | { 2 | "姓名": "Name", 3 | "邮箱": "Email", 4 | "操作": "Actions", 5 | "详情": "Detail", 6 | "编辑": "Edit", 7 | "确认删除吗?": "Are you sure to delete?", 8 | "删除后保持滚动条位置": "Keep scroll bar position after deletion", 9 | "删除": "Delete", 10 | "请输入": "Please input", 11 | "新增": "Add", 12 | "常见表格 - 详情": "Table detail - Detail", 13 | "日志": "Log", 14 | "导出": "Export", 15 | "人力资源部": "Human resources", 16 | "财务部": "Finance", 17 | "销售部": "Sales", 18 | "市场部": "Marketing", 19 | "技术部": "Technology", 20 | "运营部": "Operations", 21 | "部门": "Department", 22 | "部门作为额外参数": "Department as an additional parameter", 23 | "时间范围": "Time Range", 24 | "TalbePage也可以用在弹窗里, 或更多地方": "TalbePage can also be used in modal, or more places", 25 | "弹窗内TablePage": "TablePage in modal", 26 | "打开弹窗": "Open modal", 27 | "自定义搜索按钮": "Custom search buttons", 28 | "按钮顺序": "Button Order", 29 | "自定义渲染": "Custom Rendering", 30 | "自定义": "Custom", 31 | "默认": "Default" 32 | } -------------------------------------------------------------------------------- /src/layouts/Header/index.less: -------------------------------------------------------------------------------- 1 | @import '@/styles/utils.less'; 2 | 3 | .console-layout__header { 4 | display: flex; 5 | justify-content: space-between; 6 | width: 100%; 7 | height: 42px; 8 | background-color: var(--container-background-color); 9 | border-radius: var(--layout-border-radius); 10 | box-shadow: var(--layout-box-shdow); 11 | } 12 | .isMobile .console-layout__header { 13 | justify-content: end; 14 | } 15 | 16 | .console-layout__header-left { 17 | flex: 1; 18 | display: flex; 19 | align-items: center; 20 | padding-left: 18px; 21 | overflow-x: auto; 22 | white-space: nowrap; 23 | .scroll-bar-none(); 24 | } 25 | 26 | .console-layout__header-right { 27 | display: flex; 28 | align-items: center; 29 | margin: 0 14px; 30 | } 31 | 32 | .console-layout__header-right-icon-wrap { 33 | display: flex; 34 | align-items: center; 35 | height: 100%; 36 | padding: 0 2px; 37 | margin-left: 2px; 38 | .ant-btn { 39 | width: 28px; 40 | height: 28px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/svg/nintendo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AntdProvider/index.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, useEffect } from 'react'; 2 | import { App as AntdApp } from 'antd'; 3 | import type { MessageInstance } from 'antd/lib/message/interface'; 4 | import type { ModalStaticFunctions } from 'antd/lib/modal/confirm'; 5 | import type { NotificationInstance } from 'antd/lib/notification/interface'; 6 | 7 | let message: MessageInstance; 8 | let notification: NotificationInstance; 9 | let modal: Omit; 10 | 11 | const AntdStaticFnInit = ({ children }: PropsWithChildren) => { 12 | const { 13 | message: antdMessage, 14 | notification: antdNotification, 15 | modal: antdModal, 16 | } = AntdApp.useApp(); 17 | 18 | useEffect(() => { 19 | message = antdMessage; 20 | notification = antdNotification; 21 | modal = antdModal; 22 | // eslint-disable-next-line react-hooks/exhaustive-deps 23 | }, []); 24 | 25 | return children; 26 | }; 27 | 28 | export default AntdStaticFnInit; 29 | 30 | export { message, notification, modal }; 31 | -------------------------------------------------------------------------------- /src/pages/router/locales/en/router.json: -------------------------------------------------------------------------------- 1 | { 2 | "路由的变化会导致整体组件重新渲染,建议在渲染页面之前完成路由的变化操作": "Changes in routing will cause the entire component to be re-rendered. It is recommended to complete the routing change operation before rendering the page", 3 | "动态新增路由": "Add route Dynamically", 4 | "新增尾部": "Add to tail", 5 | "新增头部": "Add to head", 6 | "新增中间": "Add in middle", 7 | "动态删除路由": "Delete Route Dynamically", 8 | "删除尾部": "Remove tail", 9 | "删除头部": "Remove head", 10 | "删除中间": "Remove middle", 11 | "在指定位置动态新增路由": "Add a route at a specified location dynamically", 12 | "指定在“外链”后新增": "Specify to add after external link", 13 | "删除新增的路由": "Delete the newly added route", 14 | "动态修改路由": "Modify route Dynamically", 15 | "修改当前路由": "Modify current route", 16 | "重置": "Reset", 17 | "标题": "Title", 18 | "修改标题为rac": "Change the title to rac", 19 | "修改logo": "Modify logo", 20 | "修改Icon": "Modify icon", 21 | "我是通过router.setSiblings()方法新增的临时路由": "I am the route which added temporarily through the router.setSiblings() method" 22 | } -------------------------------------------------------------------------------- /src/locales/en/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "登录": "Login", 3 | "首页": "Home", 4 | "栅格布局": "Grid", 5 | "个人中心": "Profile", 6 | "权限": "Access", 7 | "路由权限": "Route access", 8 | "局部权限": "Local access", 9 | "路由": "Route", 10 | "动态路由": "Dynamic Route", 11 | "动态meta": "Dynamic meta", 12 | "搜索表格": "Table", 13 | "常见表格": "General table", 14 | "常见表格详情": "Table detail", 15 | "滚动加载表格": "Scrolling table", 16 | "滚动加载列表": "Scrolling list", 17 | "额外参数": "Extra params", 18 | "格式化搜索参数": "Format params", 19 | "简单表格": "Simple table", 20 | "弹窗内使用": "Table in Modal", 21 | "自定义搜索按钮": "Custom btn", 22 | "嵌套路由": "Nest", 23 | "菜单1": "Menu1", 24 | "菜单2": "Menu2", 25 | "菜单2-1": "Menu2-1", 26 | "菜单2-2": "Menu2-2", 27 | "菜单2-2-1": "Menu2-2-1", 28 | "菜单2-2-2": "Menu2-2-2", 29 | "错误页": "Error", 30 | "外链": "Link", 31 | "单栏": "1 side", 32 | "单栏示例": "Single layout example", 33 | "独立布局": "0 side", 34 | "本页面独立于默认布局": "This page is independent of the default layout", 35 | "出错了": "Someting wrong", 36 | "页面不存在": "Not found" 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/svg/locked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AntdProvider/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useModel } from '@zhangsai/model'; 2 | import { themeColors, themeModel } from '@/models/theme'; 3 | import { ThemeConfig, theme } from 'antd'; 4 | 5 | export function getAntdThemeConfigByThemeModel(isDark: boolean, colorPrimary: string) { 6 | return { 7 | algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm, 8 | token: { 9 | colorPrimary, 10 | colorBgLayout: isDark ? themeColors.dark['--layout-background-color'] : themeColors.light['--layout-background-color'], 11 | colorBgContainer: isDark ? themeColors.dark['--container-background-color'] : themeColors.light['--container-background-color'], 12 | }, 13 | }; 14 | } 15 | 16 | /** 根据themeModel映射一份antd theme的配置 */ 17 | export function useAntdTheme(darkMode?: boolean): ThemeConfig { 18 | const curDarkMode = useModel(themeModel, 'curDarkMode'); 19 | const colorPrimary = useModel(themeModel, 'colorPrimary'); 20 | const isDark = (darkMode ?? curDarkMode); 21 | 22 | return getAntdThemeConfigByThemeModel(isDark, colorPrimary); 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/svg/not_found.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/hooks/useTablePage.ts: -------------------------------------------------------------------------------- 1 | import { ClassName__ConsoleLayout_RightSideMain } from '@/layouts/ConsoleLayout/consts'; 2 | import { SEARCH_LIST_DOM_CLASS } from 'admin-search-list' 3 | 4 | const headerFixed = true; 5 | 6 | /** 7 | * 头部不固定时 -> console-layout__right-side 8 | * 头部固定 + listScroll -> SEARCH_LIST_DOM_CLASS 9 | * 头部固定 + !listScroll -> ClassName__ConsoleLayout_RightSideMain 10 | */ 11 | export function useScrollContainer(listScroll?: boolean) { 12 | const backTopTarget = headerFixed ? 13 | ((listScroll ?? true) ? SEARCH_LIST_DOM_CLASS : `.${ClassName__ConsoleLayout_RightSideMain}`) : 14 | '.console-layout__right-side'; 15 | return backTopTarget; 16 | } 17 | 18 | /** 19 | * 20 | */ 21 | export function useTableSticky(listScroll?: boolean) { 22 | return { 23 | offsetHeader: (listScroll ?? true) ? 24 | (headerFixed ? 0 : 0) : 25 | (headerFixed ? -24 : 0), 26 | }; 27 | } 28 | 29 | /** 30 | * 头部不固定时,tableScroll一定不能滚动 31 | */ 32 | export function useListScroll(listScroll?: boolean) { 33 | return headerFixed ? (listScroll ?? true) : false; 34 | } 35 | -------------------------------------------------------------------------------- /src/http/API.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | 3 | export interface BaseHttpResult { 4 | code: string; 5 | message: string; 6 | } 7 | export interface HttpResult { 8 | code: string; 9 | message: string; 10 | data: D; 11 | } 12 | 13 | export interface ItemsHttpResult { 14 | code: string; 15 | message: string; 16 | data: { 17 | items: D 18 | }; 19 | } 20 | export interface ItemsPaginationHttpResult { 21 | code: string; 22 | message: string; 23 | data: { 24 | items: D, 25 | total: number; 26 | page: number; 27 | perPage: number; 28 | sort?: string; 29 | order?: string; 30 | }; 31 | } 32 | 33 | export interface ItemsPaginationHasTotalAllHttpResult { 34 | code: string; 35 | message: string; 36 | data: { 37 | datas: { 38 | items: D, 39 | total: number; 40 | page: number; 41 | perPage: number; 42 | sort?: string; 43 | order?: string; 44 | } 45 | totalAll: number 46 | }; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/components/Back/index.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from 'antd'; 2 | import router, { history } from '@/router'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { FC } from 'react'; 5 | import SvgIcon from '@/components/SvgIcon'; 6 | import './index.less'; 7 | 8 | interface Props { 9 | title?: string; 10 | backUrl?: string; 11 | } 12 | 13 | /** 14 | * 页面返回 15 | */ 16 | const Back: FC = ({ title, backUrl }) => { 17 | const { t: t_components } = useTranslation('components'); 18 | 19 | function onClickBack() { 20 | if (backUrl) { 21 | router.push(backUrl); 22 | } else { 23 | history.back(); 24 | } 25 | } 26 | 27 | return ( 28 |
29 | 30 | 31 | {t_components('返回')} 32 | 33 | {title && <> 34 | 35 |

{title}

36 | } 37 |
38 | ); 39 | }; 40 | 41 | export default Back; 42 | -------------------------------------------------------------------------------- /src/layouts/Tabs/useDraggable.ts: -------------------------------------------------------------------------------- 1 | import { useSortable } from '@dnd-kit/sortable'; 2 | import { CSS } from '@dnd-kit/utilities'; 3 | 4 | export interface DraggableTabPaneProps extends React.HTMLAttributes { 5 | 'data-node-key': string; 6 | } 7 | 8 | const useDraggable = ({ ...props }: DraggableTabPaneProps) => { 9 | const { attributes, listeners, setNodeRef, transform, transition, isDragging } = 10 | useSortable({ 11 | id: props['data-node-key'], 12 | transition: null, 13 | }); 14 | 15 | const style: React.CSSProperties = { 16 | ...props.style, 17 | transform: CSS.Transform.toString(transform && { ...transform, y: 0, scaleX: 1 }), 18 | transition: isDragging ? 'none' : transition, 19 | cursor: isDragging ? 'grabbing' : 'default', 20 | zIndex: isDragging ? 2 : 1, 21 | }; 22 | 23 | return { 24 | draggableProps: { 25 | key: props['data-node-key'], 26 | ref: setNodeRef, 27 | style, 28 | ...attributes, 29 | ...listeners, 30 | }, 31 | isDragging, 32 | }; 33 | }; 34 | 35 | export default useDraggable; 36 | -------------------------------------------------------------------------------- /src/layouts/Tabs/ContextMenu/style.ts: -------------------------------------------------------------------------------- 1 | export const themeColors = { 2 | light: { 3 | '--contexify-menu-bgColor': '#fff', 4 | '--contexify-separator-color': 'rgba(0,0,0,.2)', 5 | '--contexify-item-color': '#333', 6 | '--contexify-activeItem-color': 'var(--console-antd-colorPrimary)', 7 | '--contexify-activeItem-bgColor': 'var(--console-antd-colorPrimaryBg)', 8 | '--contexify-rightSlot-color': '#6f6e77', 9 | '--contexify-activeRightSlot-color': '#fff', 10 | '--contexify-arrow-color': '#6f6e77', 11 | '--contexify-activeArrow-color': '#fff', 12 | }, 13 | dark: { 14 | '--contexify-menu-bgColor': 'rgba(40,40,40,.98)', 15 | '--contexify-separator-color': '#4c4c4c', 16 | '--contexify-item-color': '#fff', 17 | '--contexify-activeItem-color': 'var(--console-antd-colorPrimary)', 18 | '--contexify-activeItem-bgColor': 'var(--console-antd-colorPrimaryBg)', 19 | '--contexify-rightSlot-color': '#6f6e77', 20 | '--contexify-activeRightSlot-color': '#fff', 21 | '--contexify-arrow-color': '#6f6e77', 22 | '--contexify-activeArrow-color': '#fff', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/assets/svg/theme_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sai 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. -------------------------------------------------------------------------------- /src/utils/business/token.ts: -------------------------------------------------------------------------------- 1 | import { MyLocalStorage } from './localStorage'; 2 | import type { GetOptions } from '@/utils/LRUCache'; 3 | 4 | const STORE_KEY = 'accessTokens'; 5 | const MAX_LENGTH = 20; 6 | 7 | interface Token { 8 | accessToken: string; 9 | refreshToken: string; 10 | expiration: number; 11 | } 12 | 13 | const myLs = new MyLocalStorage(STORE_KEY, MAX_LENGTH); 14 | 15 | /** 16 | * 获取当前命名空间下的token 17 | * 不存在,则返回'' 18 | */ 19 | export function lsGetToken(options?: GetOptions) { 20 | return myLs.get(options); 21 | } 22 | 23 | /** 24 | * 设置当前命名空间下的token 25 | * 不在命名空间,则不设置 26 | * 本方法有两个副作用: 27 | * 1. 开始refreshToken定时器 28 | * 2. 更新ramToken 29 | */ 30 | export function lsSetToken(accessToken: string, refreshToken: string, expiration: number) { 31 | myLs.set({ 32 | accessToken, 33 | refreshToken, 34 | expiration, 35 | }); 36 | } 37 | 38 | /** 39 | * 移除当前命名空间下的token 40 | * 不在命名空间,则不移除 41 | */ 42 | export function lsRemoveToken() { 43 | myLs.remove(); 44 | } 45 | 46 | /** 47 | * 移除localStorage中所有活动的token 48 | */ 49 | export function lsRemoveAllToken() { 50 | myLs.removeAll(); 51 | } 52 | -------------------------------------------------------------------------------- /docs/development/icon.md: -------------------------------------------------------------------------------- 1 | # Icon 2 | 3 | 项目利用了 [vite-plugin-svg-icons](https://github.com/vbenjs/vite-plugin-svg-icons) 封装了 `SvgIcon` 组件,可以像引入图片资源一样,方便地引入图标资源。 4 | 5 | ## 定义 6 | 7 | ```ts 8 | interface SvgIconProps extends SVGAttributes { 9 | className?: string; 10 | style?: CSSProperties; 11 | prefix?: string; 12 | /** [dir]-[filename] */ 13 | name: string; 14 | color?: string; 15 | size?: number | string; 16 | width?: number | string; 17 | height?: number | string; 18 | } 19 | ``` 20 | 21 | ## 使用 22 | 23 | ```tsx 24 | import SvgIcon from '@/components/SvgIcon'; 25 | 26 | const MyComponent = () => { 27 | return ( 28 | // 通过name属性找到svg文件 29 | ; 30 | ); 31 | } 32 | ``` 33 | 34 | ## `svg` 大小 35 | 36 | 默认 `svg` 大小为 `16px`。可通过两种方式设置: 37 | 38 | - `size`: 宽高值相等,优先级大于 `width` 和 `height` 属性 39 | - `width/height` 属性 40 | 41 | ## `name` 属性是怎么找到 `svg` 文件位置的? 42 | 43 | - `vite-plugin-svg-icons` 通过我们定义的 `symbolId` 找到 `svg` 文件 44 | - `SvgIcon` 的 `name` 属性代表 `assets/svg/` 目录下的 `svg` 文件。格式为: 45 | - 如果在 `assets/svg/` 下的根目录,则为 `[filename]` 46 | - 如果在 `assets/svg/` 下的子目录,则为 `[dir]-[filename]` 47 | -------------------------------------------------------------------------------- /src/assets/svg/close_circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/FullScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import { exitFullscreen, requestFullscreen, isFullscreenEnabled } from './utils'; 2 | import TooltipIcon from '../components/TooltipIcon'; 3 | import { useTranslation } from 'react-i18next'; 4 | import SvgIcon from '@/components/SvgIcon'; 5 | import { useFullScreen } from './hooks'; 6 | 7 | interface Props { 8 | element?: Element | string; 9 | } 10 | 11 | const FullScreen: React.FC = ({ 12 | element = document.documentElement, 13 | }) => { 14 | const isFullScreen = useFullScreen(); 15 | const { t: t_layout } = useTranslation('layout'); 16 | 17 | if (!isFullscreenEnabled) return null; 18 | 19 | if (!isFullScreen) { 20 | return ( 21 | } 24 | onClick={() => requestFullscreen(element)} 25 | /> 26 | ); 27 | } else { 28 | return ( 29 | } 32 | onClick={() => exitFullscreen()} 33 | /> 34 | ); 35 | } 36 | }; 37 | 38 | export default FullScreen; 39 | -------------------------------------------------------------------------------- /src/assets/svg/scroll_list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/table_simple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/web.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es2021': true, 5 | }, 6 | 'extends': [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:react/jsx-runtime', 11 | 'plugin:react-hooks/recommended', 12 | ], 13 | 'overrides': [ 14 | { 15 | 'env': { 16 | 'node': true, 17 | }, 18 | 'files': [ 19 | '.eslintrc.{js,cjs}', 20 | ], 21 | 'parserOptions': { 22 | 'sourceType': 'script', 23 | }, 24 | }, 25 | ], 26 | 'parser': '@typescript-eslint/parser', 27 | 'parserOptions': { 28 | 'ecmaVersion': 'latest', 29 | 'sourceType': 'module', 30 | }, 31 | 'plugins': [ 32 | '@typescript-eslint', 33 | 'react', 34 | 'react-refresh', 35 | // 'import', 36 | ], 37 | 'settings': { 38 | 'react': { 39 | 'version': 'detect', 40 | }, 41 | }, 42 | 'rules': { 43 | '@typescript-eslint/no-unused-expressions': 0, 44 | 'react/display-name': 0, 45 | 'react/prop-types': 0, 46 | '@typescript-eslint/no-unused-vars': [0, { 47 | 'caughtErrors': 'none', 48 | }] 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/assets/svg/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/Refresh/index.tsx: -------------------------------------------------------------------------------- 1 | import { baseModel } from '@/models/base'; 2 | import TooltipIcon from '../components/TooltipIcon'; 3 | import { useTranslation } from 'react-i18next'; 4 | import SvgIcon from '@/components/SvgIcon'; 5 | import { motion, useAnimation } from 'framer-motion'; 6 | 7 | const Refresh = () => { 8 | const { t: t_layout } = useTranslation('layout'); 9 | const controls = useAnimation(); 10 | 11 | return ( 12 | // @ts-expect-error forwardRef is no need any more. 13 | 24 | 25 | )} 26 | onClick={() => { 27 | baseModel.refresh(); 28 | controls.start({ 29 | rotate: [0, 360], 30 | }); 31 | setTimeout(() => { 32 | controls.stop(); 33 | }, 300); 34 | }} 35 | /> 36 | ); 37 | }; 38 | 39 | export default Refresh; 40 | -------------------------------------------------------------------------------- /src/components/Progress/index.tsx: -------------------------------------------------------------------------------- 1 | import { getRandomNumber } from '@/utils'; 2 | import NProgress from 'nprogress'; 3 | import { useLocation } from 'react-router'; 4 | import { useEffect, useRef } from 'react'; 5 | import { setNProgressColor } from './utils'; 6 | import { useModel } from '@zhangsai/model'; 7 | import { themeModel } from '@/models/theme'; 8 | import 'nprogress/nprogress.css'; 9 | 10 | NProgress.configure({ showSpinner: false }); 11 | 12 | function act() { 13 | NProgress.start(); 14 | NProgress.inc(getRandomNumber(0.2, 0.8)); 15 | NProgress.done(); 16 | } 17 | 18 | /** 19 | * 进度条 20 | */ 21 | const Progress = () => { 22 | const { pathname } = useLocation(); 23 | const mountedRef = useRef(false); 24 | const colorPrimary = useModel(themeModel, 'colorPrimary'); 25 | 26 | // 主题色变化时更新NProgress颜色 27 | useEffect(() => { 28 | setNProgressColor(colorPrimary, { force: mountedRef.current }); 29 | }, [colorPrimary]); 30 | 31 | useEffect(() => { 32 | act(); 33 | mountedRef.current = true; 34 | }, []); 35 | 36 | useEffect(() => { 37 | if (mountedRef.current) { 38 | act(); 39 | } 40 | }, [pathname]); 41 | 42 | return null; 43 | }; 44 | 45 | export default Progress; 46 | -------------------------------------------------------------------------------- /docs/guide/backend.md: -------------------------------------------------------------------------------- 1 | # 我是后端? 2 | 3 | 如果你是前端开发,并了解 [Node.js](https://nodejs.org/zh-cn) 和 [npm](https://www.npmjs.com/) 的基本知识,则可以跳过本节。如果你是没有前端经验的后端,可以按照下面说明,准备开发环境并作一些基础知识学习 4 | 5 | ## 安装 Node.js 6 | 7 | 可以使用 [nvm](https://github.com/nvm-sh/nvm) 工具,下载安装和管理node 8 | 9 | 下载 nvm 以后,安装 Node.js 10 | 11 | ```shell 12 | # 安装 lts 版本 13 | nvm install --lts 14 | ``` 15 | 16 | 切换当前 node 版本为 lts 版本 17 | 18 | ```shell 19 | nvm use --lts 20 | ``` 21 | 22 | 确认 node 和 npm 被正确安装 23 | 24 | ```shell 25 | node -v 26 | npm -v 27 | ``` 28 | 29 | ## 使用 npm 镜像加速 30 | 31 | npm 官方仓库的包,可能因为网络问题下载不了,可以使用 [nrm](https://github.com/Pana/nrm) 工具,切换镜像源以加速 32 | 33 | 查看有哪些源 34 | 35 | ```shell 36 | nrm ls 37 | ``` 38 | 39 | 切换到淘宝镜像 40 | 41 | ```shell 42 | nrm use taobao 43 | ``` 44 | 45 | npm 的 [package.json 官方文档](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) 46 | 47 | ## TypeScript 48 | 49 | [TypeScript 官方文档](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) 50 | 51 | ## React 52 | 53 | [React 官方文档](https://zh-hans.react.dev/learn) 54 | 55 | ## React Router 56 | 57 | [React Router 官方文档](https://reactrouter.com/en/main) 58 | 59 | ## Ant Design 60 | 61 | [Ant Design 官方文档](https://ant.design/components/overview-cn) 62 | -------------------------------------------------------------------------------- /docs/guide/what.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | ## react-antd-console 是什么? 4 | 5 | react-antd-console 是一个后台管理系统的前端解决方案,封装了后台管理系统必要功能(如登录、鉴权、菜单、面包屑、标签页等),帮助开发人员专注于业务快速开发。项目基于 `React 19`、`Ant design 5`、`Vite` 和 `TypeScript` 等新版本。对于使用到的各项技术,会被持续更新至最新版本 6 | 7 | ## 谁适合使用? 8 | 9 | 如果你正在寻找一款极简的后台管理的前端模板,技术栈能先进且稳定,希望毫不费力地掌握其中的原理,或希望只专注于业务开发,那么可以尝试本项目,本项目是作者多年经验的总结 10 | 11 | ## 尽可能简单 12 | 13 | 无论是使用本项目做开发,还是学习的目的,保持简单是必要的。因此本项目专注于:良好的代码层次设计、定义清晰明确的目录结构、容易改造和拆换的模块分类等。本项目最小化的封装了一些必要的功能,例如登录、鉴权、菜单、面包屑、标签页等。如果你没有自己的UI设计,那么可以直接使用本项目封装的功能;如果你有自己的UI设计,那么也可以在本项目基础上作方便的改造 14 | 15 | ## 功能 16 | 17 | - **🔥 最新技术栈**: `Vite`(支持`热更新`)、`React19`、`Ant Design5`、`TypeScript`(近乎`100%`的类型覆盖) 18 | - **🎯 专注业务**: 封装好的布局(侧边菜单、面包屑、标签页、页头页脚等),只需要`专注于业务开发` 19 | - **🔒 权限管理**: 支持`菜单级`和`按钮级`权限 20 | - **🛠️ 路由配置**: 一份极简配置,自动生成路由、菜单、面包屑等,支持嵌套路由、单/无布局等配置,支持路由动态变化等 21 | - **💾 数据管理**: `分层`(数据和视图)架构设计,数据管理方案理论上支持接入任意UI渲染库/框架(包括不限于React/Vue/Angular) 22 | - **🎨 颜色换肤**: 支持深/浅肤色模式下的任意颜色切换 23 | - **🏷️ 多标签页**: 可拖拽的多标签页,支持持久化、右键菜单等 24 | - **✨ 页面缓存**: 支持页面状态缓存,切换回页面后,保留切换前的页面状态 25 | - **🎬 优雅动画**: 支持路由切换动画,标签页、菜单、功能按钮动画等 26 | - **🧩 其他功能**: 如`响应式设计`、`国际化`、`Mock`、`环境配置`、`工程化规范`等 27 | 28 | ## 浏览器兼容 29 | 30 | 兼容支持es2015的浏览器,不兼容IE,建议不低于: 31 | 32 | - Chrome >=87 33 | - Firefox >=78 34 | - Safari >=14 35 | - Edge >=88 36 | -------------------------------------------------------------------------------- /src/assets/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AntdProvider/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import { ConfigProvider, App as AntdApp } from 'antd'; 3 | import { useAntdTheme } from './hooks'; 4 | import enUS from 'antd/locale/en_US'; 5 | import zhCN from 'antd/locale/zh_CN'; 6 | import { useModel } from '@zhangsai/model'; 7 | import { baseModel } from '@/models/base'; 8 | import { Locale } from 'antd/lib/locale'; 9 | import AntdStaticFnInit from '.'; 10 | import { themeModel } from '@/models/theme'; 11 | import classNames from 'classnames'; 12 | import './index.less'; 13 | 14 | const languageMap: Record = { 15 | ['zh_Hans']: zhCN, 16 | ['en']: enUS, 17 | }; 18 | 19 | const AntdProvider = ({ children }: PropsWithChildren) => { 20 | const language = useModel(baseModel, 'language'); 21 | const curDarkMode = useModel(themeModel, 'curDarkMode'); 22 | const antdTheme = useAntdTheme(); 23 | return ( 24 | 25 | 28 | 29 | { children } 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default AntdProvider; 37 | -------------------------------------------------------------------------------- /docs/development/layout.md: -------------------------------------------------------------------------------- 1 | # 布局 2 | 3 | 项目封装了统一的布局,所有的布局相关代码都在 `src/layouts/ConsoleLayout/` 中。主要包含了**侧边菜单 ``**、**面包屑 ``**、**头 `
`**、**脚 `