├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .history └── src │ ├── App_20220114211902.tsx │ ├── App_20220125111937.tsx │ ├── App_20220125111950.tsx │ ├── main_20220103200715.tsx │ ├── main_20220207004057.tsx │ ├── main_20220207004102.tsx │ ├── main_20220207004103.tsx │ ├── main_20220207004117.tsx │ ├── main_20220207004124.tsx │ ├── main_20220207004125.tsx │ ├── main_20220207004126.tsx │ └── styles │ └── tailwind │ ├── base_20220125112234.css │ ├── base_20220125112300.css │ ├── components_20220125112242.css │ ├── components_20220125112306.css │ ├── utilities_20220125112254.css │ └── utilities_20220125112311.css ├── .pnpm-debug.log ├── .pnpmfile.cjs ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── back ├── context.ts.bak ├── hooks.ts.bak ├── hooks.tsx.bak ├── hooks2.ts.bak └── storage.tsx.bak ├── build ├── config.ts ├── index.ts ├── plugins │ ├── antd.ts │ ├── icon.ts │ ├── index.ts │ ├── mock.ts │ └── windicss.ts ├── proxy.ts ├── types.ts └── utils │ ├── index.ts │ └── paths.ts ├── index.html ├── mock ├── _util.ts ├── chart.ts ├── servers.ts ├── types.ts └── user.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── App.tsx ├── assets │ ├── icons │ │ ├── download-count.svg │ │ ├── dynamic-avatar-1.svg │ │ ├── dynamic-avatar-2.svg │ │ ├── dynamic-avatar-3.svg │ │ ├── dynamic-avatar-4.svg │ │ ├── dynamic-avatar-5.svg │ │ ├── dynamic-avatar-6.svg │ │ ├── moon.svg │ │ ├── sun.svg │ │ ├── test.svg │ │ ├── total-sales.svg │ │ ├── transaction.svg │ │ └── visit-count.svg │ ├── images │ │ ├── demo.png │ │ ├── header.jpg │ │ └── logo.png │ └── svg │ │ ├── illustration.svg │ │ ├── login-bg-dark.svg │ │ ├── login-bg.svg │ │ ├── login-box-bg.svg │ │ ├── net-error.svg │ │ └── no-data.svg ├── components │ ├── Auth │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── required.tsx │ │ ├── store.ts │ │ └── types.ts │ ├── Chart │ │ ├── _default.config.ts │ │ ├── chart.tsx │ │ ├── components │ │ │ └── PercentGaugeChart.tsx │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── store.ts │ │ └── types.ts │ ├── Config │ │ ├── _default.config.ts │ │ ├── constants.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── setup.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── Fetcher │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── provider.tsx │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── Icon │ │ ├── _default.config.ts │ │ ├── constants.ts │ │ ├── hooks.tsx │ │ ├── icon.tsx │ │ ├── index.ts │ │ ├── store.ts │ │ └── types.ts │ ├── KeepAlive │ │ ├── constants.ts │ │ ├── hooks.tsx │ │ ├── index.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── view.tsx │ ├── Layout │ │ ├── constants.ts │ │ ├── default.config.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── provider.tsx │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── Menu │ │ ├── _default.config.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.tsx │ ├── Router │ │ ├── _default.config.tsx │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── provider.tsx │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── factory │ │ │ ├── filter.ts │ │ │ ├── generate.tsx │ │ │ └── index.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ └── views.tsx │ ├── Spinner │ │ ├── collection │ │ │ ├── babel │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── block-reserve │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── box │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── coffee │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── disappeared │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── eat │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── index.ts │ │ │ ├── jump-circle │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── loop-circle │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── rain │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── rotate-circle │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── wave │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ └── wind-mill │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── loading.tsx │ │ ├── spinner.tsx │ │ ├── types.ts │ │ └── utils.ts │ └── Storage │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── types.ts │ │ └── utils.ts ├── config │ ├── app.ts │ ├── index.ts │ ├── layout.ts │ ├── router.tsx │ └── routes │ │ ├── constants.tsx │ │ ├── dynamic.tsx │ │ └── loading.tsx ├── favicon.svg ├── index.css ├── logo.svg ├── main.tsx ├── styles │ ├── antd.less │ ├── index.css │ └── tailwind │ │ ├── base.css │ │ ├── components.css │ │ └── utilities.css ├── utils │ ├── constants.ts │ ├── helpers.ts │ ├── hooks.ts │ ├── index.ts │ ├── store │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── store.ts │ │ └── types.ts │ └── timer.ts ├── views │ ├── account │ │ ├── center │ │ │ └── index.blade.tsx │ │ └── setting │ │ │ └── index.blade.tsx │ ├── auth │ │ ├── components │ │ │ ├── credential.form.tsx │ │ │ └── index.ts │ │ ├── login.blade.tsx │ │ ├── login.module.less │ │ └── signup.blade.tsx │ ├── charts │ │ ├── line.blade.tsx │ │ ├── percent.blade.tsx │ │ └── wave.blade.tsx │ ├── content │ │ ├── articles │ │ │ ├── create.blade.tsx │ │ │ └── list.blade.tsx │ │ ├── categories │ │ │ └── list.blade.tsx │ │ ├── comments │ │ │ └── list.blade.tsx │ │ └── tags │ │ │ └── list.blade.tsx │ ├── dashboard │ │ ├── anlysis │ │ │ └── index.blade.tsx │ │ ├── monitor │ │ │ ├── components │ │ │ │ ├── app.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── data.tsx │ │ │ │ ├── index.ts │ │ │ │ └── info.tsx │ │ │ ├── constants.ts │ │ │ ├── dnd.tsx │ │ │ ├── index.blade.tsx │ │ │ ├── index.blade.tsx.back │ │ │ ├── index.module.less │ │ │ └── types.ts │ │ ├── utils.ts │ │ └── workspace │ │ │ └── index.blade.tsx │ ├── errors │ │ ├── 403.blade.tsx │ │ ├── 404.blade.tsx │ │ └── 500.blade.tsx │ ├── layouts │ │ ├── components │ │ │ ├── drawer │ │ │ │ ├── a.css.bak │ │ │ │ ├── constants.ts │ │ │ │ ├── hooks.ts │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── header │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── sidebar │ │ │ │ ├── index.tsx │ │ │ │ ├── logo.tsx │ │ │ │ └── menu.tsx │ │ │ └── tabs │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── master.blade.tsx │ │ └── styles │ │ │ ├── index.module.less │ │ │ ├── mixins.less │ │ │ └── variables.less │ ├── media │ │ └── index.blade.tsx │ └── setting │ │ └── index.blade.tsx └── vite-env.d.ts ├── stylelint.config.js ├── tailwind.config.js ├── tsconfig.eslint.json ├── tsconfig.json ├── typings ├── global.d.ts └── module.d.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | back 7 | .history -------------------------------------------------------------------------------- /.history/src/App_20220114211902.tsx: -------------------------------------------------------------------------------- 1 | import { useSetupAuth } from './components/Auth'; 2 | import { useSetupConfig } from './components/Config'; 3 | import { SWRFetcher, useSetupFetcher } from './components/Fetcher'; 4 | import { useSetupIcon } from './components/Icon'; 5 | import { useSetupKeepAlive } from './components/KeepAlive'; 6 | import { useSetupMenu } from './components/Menu'; 7 | import { Router, useSetupRouter } from './components/Router'; 8 | import { useSetupStorage } from './components/Storage'; 9 | import { config, router } from './config'; 10 | 11 | const useSetup = () => { 12 | // 初始化本地存储 13 | useSetupStorage(); 14 | useSetupConfig(config); 15 | // 初始化请求库和SWR,如果不需要自定义全局配置可不写 16 | useSetupFetcher(); 17 | // 通过本地存储的Token获取远程用户信息 18 | useSetupAuth('/user/info'); 19 | // 通过用户信息初始化路由 20 | useSetupRouter(router); 21 | useSetupKeepAlive('/'); 22 | // 通过路由或用户信息初始化菜单 23 | useSetupMenu(); 24 | // 初始化图标配置 25 | useSetupIcon({ 26 | iconfont_urls: ['//at.alicdn.com/t/font_2497975_4zt848h920t.js'], 27 | }); 28 | // useSetupChart(); 29 | }; 30 | const App = () => { 31 | useSetup(); 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /.history/src/App_20220125111937.tsx: -------------------------------------------------------------------------------- 1 | import { useSetupAuth } from './components/Auth'; 2 | import { useSetupConfig } from './components/Config'; 3 | import { SWRFetcher, useSetupFetcher } from './components/Fetcher'; 4 | import { useSetupIcon } from './components/Icon'; 5 | import { useSetupKeepAlive } from './components/KeepAlive'; 6 | import { useSetupMenu } from './components/Menu'; 7 | import { Router, useSetupRouter } from './components/Router'; 8 | import { useSetupStorage } from './components/Storage'; 9 | import { config, router } from './config'; 10 | 11 | const useSetup = () => { 12 | // 初始化本地存储 13 | useSetupStorage(); 14 | useSetupConfig(config); 15 | // 初始化请求库和SWR,如果不需要自定义全局配置可不写 16 | useSetupFetcher(); 17 | // 通过本地存储的Token获取远程用户信息 18 | useSetupAuth('/user/info'); 19 | // 通过用户信息初始化路由 20 | useSetupRouter(router); 21 | useSetupKeepAlive('/'); 22 | // 通过路由或用户信息初始化菜单 23 | useSetupMenu(); 24 | // 初始化图标配置 25 | useSetupIcon({ 26 | iconfont_urls: ['//at.alicdn.com/t/font_2497975_4zt848h920t.js'], 27 | }); 28 | // useSetupChart(); 29 | }; 30 | const App = () => { 31 | useSetup(); 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /.history/src/App_20220125111950.tsx: -------------------------------------------------------------------------------- 1 | import { useSetupAuth } from './components/Auth'; 2 | import { useSetupConfig } from './components/Config'; 3 | import { SWRFetcher, useSetupFetcher } from './components/Fetcher'; 4 | import { useSetupIcon } from './components/Icon'; 5 | import { useSetupKeepAlive } from './components/KeepAlive'; 6 | import { useSetupMenu } from './components/Menu'; 7 | import { Router, useSetupRouter } from './components/Router'; 8 | import { useSetupStorage } from './components/Storage'; 9 | import { config, router } from './config'; 10 | 11 | const useSetup = () => { 12 | // 初始化本地存储 13 | useSetupStorage(); 14 | useSetupConfig(config); 15 | // 初始化请求库和SWR,如果不需要自定义全局配置可不写 16 | useSetupFetcher(); 17 | // 通过本地存储的Token获取远程用户信息 18 | useSetupAuth('/user/info'); 19 | // 通过用户信息初始化路由 20 | useSetupRouter(router); 21 | useSetupKeepAlive({ path: '/' }); 22 | // 通过路由或用户信息初始化菜单 23 | useSetupMenu(); 24 | // 初始化图标配置 25 | useSetupIcon({ 26 | iconfont_urls: ['//at.alicdn.com/t/font_2497975_4zt848h920t.js'], 27 | }); 28 | // useSetupChart(); 29 | }; 30 | const App = () => { 31 | useSetup(); 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /.history/src/main_20220103200715.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | 21 | import '@/styles/index.css'; 22 | 23 | import App from './App'; 24 | 25 | // if (import.meta.env.DEV) { 26 | // import('antd/dist/antd.less'); 27 | // } 28 | enableMapSet(); 29 | ReactDOM.render(, document.getElementById('root')); 30 | -------------------------------------------------------------------------------- /.history/src/main_20220207004057.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | import 'react'; 21 | 22 | import '@/styles/index.css'; 23 | 24 | import App from './App'; 25 | 26 | // if (import.meta.env.DEV) { 27 | // import('antd/dist/antd.less'); 28 | // } 29 | enableMapSet(); 30 | ReactDOM.render(, document.getElementById('root')); 31 | -------------------------------------------------------------------------------- /.history/src/main_20220207004102.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | import React from 'react'; 21 | 22 | import '@/styles/index.css'; 23 | 24 | import App from './App'; 25 | 26 | // if (import.meta.env.DEV) { 27 | // import('antd/dist/antd.less'); 28 | // } 29 | enableMapSet(); 30 | ReactDOM.render(, document.getElementById('root')); 31 | -------------------------------------------------------------------------------- /.history/src/main_20220207004103.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | import React from 'react'; 21 | 22 | import '@/styles/index.css'; 23 | 24 | import App from './App'; 25 | 26 | // if (import.meta.env.DEV) { 27 | // import('antd/dist/antd.less'); 28 | // } 29 | enableMapSet(); 30 | ReactDOM.render(, document.getElementById('root')); 31 | -------------------------------------------------------------------------------- /.history/src/main_20220207004117.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | 21 | import '@/styles/index.css'; 22 | 23 | import App from './App'; 24 | 25 | // if (import.meta.env.DEV) { 26 | // import('antd/dist/antd.less'); 27 | // } 28 | enableMapSet(); 29 | ReactDOM.render(, document.getElementById('root')); 30 | -------------------------------------------------------------------------------- /.history/src/main_20220207004124.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | 21 | import '@/styles/index.css'; 22 | 23 | import App from './App'; 24 | 25 | // if (import.meta.env.DEV) { 26 | // import('antd/dist/antd.less'); 27 | // } 28 | 29 | enableMapSet(); 30 | ReactDOM.render(, document.getElementById('root')); 31 | -------------------------------------------------------------------------------- /.history/src/main_20220207004125.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | 21 | import '@/styles/index.css'; 22 | 23 | import App from './App'; 24 | 25 | // if (import.meta.env.DEV) { 26 | // import('antd/dist/antd.less'); 27 | // } 28 | 29 | enableMapSet(); 30 | ReactDOM.render(, document.getElementById('root')); 31 | -------------------------------------------------------------------------------- /.history/src/main_20220207004126.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2021-12-16 14:56:38 +0800 7 | * @Path : /src/main.tsx 8 | * @Description : 入口文件,在此启动项目 9 | * @LastEditors : pincman 10 | * Copyright 2021 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { enableMapSet } from 'immer'; 14 | import ReactDOM from 'react-dom'; 15 | // import 'virtual:windi-base.css'; 16 | // import 'virtual:windi-components.css'; 17 | // import 'virtual:windi-utilities.css'; 18 | // import 'virtual:windi-devtools'; 19 | import 'virtual:svg-icons-register'; 20 | 21 | import '@/styles/index.css'; 22 | 23 | import App from './App'; 24 | 25 | // if (import.meta.env.DEV) { 26 | // import('antd/dist/antd.less'); 27 | // } 28 | 29 | enableMapSet(); 30 | ReactDOM.render(, document.getElementById('root')); 31 | -------------------------------------------------------------------------------- /.history/src/styles/tailwind/base_20220125112234.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toomejs/toome/39fcfe82b97a8684615d68d6b99296cfc943f76a/.history/src/styles/tailwind/base_20220125112234.css -------------------------------------------------------------------------------- /.history/src/styles/tailwind/base_20220125112300.css: -------------------------------------------------------------------------------- 1 | /* 添加自定义tailwind基础层样式,一般用于覆盖一些tailwind中默认的基础样式 */ 2 | 3 | /* 如果要引用tailwind自带的值或tailwind.config.js的theme中配置的值,可以通过 "@apply"指令或"theme"函数获取 */ 4 | 5 | /* 在"@layer"中添加的样式如果在程序中没有用到会在编译后被清除,如果需要强制存在于编译后的样式表,请在"@layer"外定义 */ 6 | 7 | /* 示例: 8 | h1 { 9 | @apply text-2xl; 10 | } */ 11 | 12 | @layer base { 13 | } 14 | -------------------------------------------------------------------------------- /.history/src/styles/tailwind/components_20220125112242.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toomejs/toome/39fcfe82b97a8684615d68d6b99296cfc943f76a/.history/src/styles/tailwind/components_20220125112242.css -------------------------------------------------------------------------------- /.history/src/styles/tailwind/components_20220125112306.css: -------------------------------------------------------------------------------- 1 | /* 添加自定义tailwind组件层样式,一般无特殊需求可以用react组件抽象而不是在这里定义css类 */ 2 | 3 | /* 如果要引用tailwind自带的值或tailwind.config.js的theme中配置的值,可以通过 "@apply"指令或"theme"函数获取 */ 4 | 5 | /* 在"@layer"中添加的样式如果在程序中没有用到会在编译后被清除,如果需要强制存在于编译后的样式表,请在"@layer"外定义 */ 6 | 7 | /* 示例: 8 | .btn-primary { 9 | @apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75; 10 | } */ 11 | 12 | @layer components { 13 | } 14 | -------------------------------------------------------------------------------- /.history/src/styles/tailwind/utilities_20220125112254.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toomejs/toome/39fcfe82b97a8684615d68d6b99296cfc943f76a/.history/src/styles/tailwind/utilities_20220125112254.css -------------------------------------------------------------------------------- /.history/src/styles/tailwind/utilities_20220125112311.css: -------------------------------------------------------------------------------- 1 | /* 添加自定义tailwindg工具层样式,可以在这里添加一些tailwind中不存在的一些样式类 */ 2 | 3 | /* 如果要引用tailwind自带的值或tailwind.config.js的theme中配置的值,可以通过 "@apply"指令或"theme"函数获取 */ 4 | 5 | /* 在"@layer"中添加的样式如果在程序中没有用到会在编译后被清除,如果需要强制存在于编译后的样式表,请在"@layer"外定义 */ 6 | 7 | /* 示例: 8 | .content-auto { 9 | content-visibility: auto; 10 | } */ 11 | 12 | @layer utilities { 13 | } 14 | -------------------------------------------------------------------------------- /.pnpm-debug.log: -------------------------------------------------------------------------------- 1 | { 2 | "0 info pnpm": { 3 | "message": "Using hooks from: /data/code/reacx/.pnpmfile.cjs", 4 | "prefix": "/data/code/reacx" 5 | }, 6 | "1 info pnpm": { 7 | "message": "readPackage hook is declared. Manifests of dependencies might get overridden", 8 | "prefix": "/data/code/reacx" 9 | }, 10 | "2 debug pnpm:scope": { 11 | "selected": 1 12 | }, 13 | "3 error pnpm": { 14 | "code": "ELIFECYCLE", 15 | "errno": "ENOENT", 16 | "syscall": "spawn", 17 | "file": "sh", 18 | "pkgid": "gkradmin@0.0.0", 19 | "stage": "dev", 20 | "script": "vite", 21 | "pkgname": "gkradmin", 22 | "err": { 23 | "name": "pnpm", 24 | "message": "gkradmin@0.0.0 dev: `vite`\nspawn ENOENT", 25 | "code": "ELIFECYCLE", 26 | "stack": "pnpm: gkradmin@0.0.0 dev: `vite`\nspawn ENOENT\n at ChildProcess. (/home/pincman/.node_modules/pnpm-global/5/node_modules/.pnpm/pnpm@6.29.1/node_modules/pnpm/dist/pnpm.cjs:92187:22)\n at ChildProcess.emit (node:events:390:28)\n at maybeClose (node:internal/child_process:1064:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:301:5)" 27 | } 28 | }, 29 | "4 warn pnpm:global": " Local package.json exists, but node_modules missing, did you mean to install?" 30 | } -------------------------------------------------------------------------------- /.pnpmfile.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | readPackage(pkg) { 4 | if (pkg.name === 'react-loadingg') { 5 | pkg.peerDependencies = {}; 6 | } 7 | return pkg; 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | **/*.svg 4 | **/*.md 5 | **/*.svg 6 | **/*.ejs 7 | **/*.html 8 | **/*.png 9 | **/*.toml 10 | .dockerignore 11 | .DS_Store 12 | .eslintignore 13 | docker 14 | .editorconfig 15 | Dockerfile* 16 | .gitignore 17 | .prettierignore 18 | LICENSE 19 | .eslintcache 20 | *.lock 21 | yarn-error.log 22 | .umi 23 | .umi-production 24 | .umi-test 25 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | module.exports = { 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | printWidth: 100, 6 | proseWrap: 'never', 7 | endOfLine: 'auto', 8 | semi: true, 9 | tabWidth: 4, 10 | vueIndentScriptAndStyle: true, 11 | htmlWhitespaceSensitivity: 'strict', 12 | overrides: [ 13 | { 14 | files: '.prettierrc', 15 | options: { 16 | parser: 'json', 17 | }, 18 | }, 19 | { 20 | files: 'document.ejs', 21 | options: { 22 | parser: 'html', 23 | }, 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.tsx 3 | *.ts 4 | *.json 5 | *.png 6 | *.eot 7 | *.ttf 8 | *.woff 9 | src/styles/antd/* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true, 5 | "source.fixAll.stylelint": true 6 | }, 7 | "emmet.includeLanguages": { 8 | "postcss": "css" 9 | }, 10 | "stylelint.validate": ["css", "scss", "less", "postcss"], 11 | "javascript.preferences.importModuleSpecifier": "project-relative", 12 | "less.lint.unknownAtRules": "ignore", 13 | "typescript.suggest.jsdoc.generateReturns": false 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 pincman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toome Admin 2 | ![](https://pic.pincman.com/media/202207021843636.png) 3 | 4 | 5 | > 目前版本是使用React17写的,React18版本正在开发中,升级完毕正式上线,目前版本最好学习使用,不保证BUG和问题处理* 6 | 7 | 8 | 9 | 一款支持KeepLive,标签拖动,Echarts,多种Loading,嵌套配置式路由等功能 10 | 11 | 12 | 13 | 主要使用以下技术 14 | 15 | - 路由[React Router v6](https://reactrouter.com/) 16 | - 状态管理[Zustand](https://github.com/pmndrs/zustand) 17 | - 数据获取[Axios](https://github.com/axios/axios)+[Swr](https://swr.vercel.app/zh-CN) 18 | - 图标[Echarts](https://echarts.apache.org/zh/index.html) 19 | - 组件库[Antd](https://ant.design/index-cn),React18版本会换成[Arco](https://arco.design/) 20 | - 动画[React-Spring](https://react-spring.dev/) 21 | - 拖动[React-dnd](https://react-dnd.github.io/react-dnd/) 22 | - 图标[iconify](https://iconify.design/) 23 | - 其它如[ahooks](https://ahooks.gitee.io/zh-CN),[immer](https://github.com/immerjs/immer),[react-loadingg](https://github.com/Summer-andy/react-loading),[react-use-measure](https://github.com/pmndrs/react-use-measure)等 24 | 25 | ![](https://pic.pincman.com/media/202207021831221.png) 26 | ![](https://pic.pincman.com/media/202207021832509.png) 27 | 28 | 支持网站[平克小站](https://pincman.com),实现过程学习[react18最佳实践](https://v.pincman.com/courses/66.html) -------------------------------------------------------------------------------- /back/context.ts.bak: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import createContext from 'zustand/context'; 3 | 4 | import type { RouterState } from './types'; 5 | 6 | const { Provider, useStore, useStoreApi } = createContext(); 7 | export const RouterContextProvider = memo(Provider); 8 | export { useStore, useStoreApi }; 9 | -------------------------------------------------------------------------------- /back/storage.tsx.bak: -------------------------------------------------------------------------------- 1 | import { FC, useContext, useReducer } from 'react'; 2 | 3 | import { StorageConfigContext, StorageDispatchContext, StorageStateContext } from './hooks'; 4 | import type { StorageConfig } from './types'; 5 | import { storageReducer, initStorage } from './utils'; 6 | 7 | const StateProvider: FC = ({ children }) => { 8 | const config = useContext(StorageConfigContext); 9 | const [state, dispatch] = useReducer(storageReducer, config, initStorage); 10 | return ( 11 | // eslint-disable-next-line react/jsx-no-constructed-context-values 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | }; 19 | const Storage: FC<{ config?: StorageConfig }> = ({ config = {}, children }) => ( 20 | 21 | {children} 22 | 23 | ); 24 | 25 | export default Storage; 26 | -------------------------------------------------------------------------------- /build/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import merge from 'deepmerge'; 3 | import { ConfigEnv, UserConfig } from 'vite'; 4 | import { getThemeVariables } from 'antd/dist/theme'; 5 | 6 | import { getPlugins } from './plugins'; 7 | import { createProxy } from './proxy'; 8 | 9 | import { Configure } from './types'; 10 | import { pathResolve } from './utils'; 11 | 12 | export const getConfig = (params: ConfigEnv, configure?: Configure): UserConfig => { 13 | const isBuild = params.command === 'build'; 14 | const modifyVars = getThemeVariables(); 15 | console.log(modifyVars.hack); 16 | return merge( 17 | { 18 | resolve: { 19 | alias: { 20 | '@': pathResolve('src'), 21 | '~antd': 'antd', 22 | '~@ant-design': '@ant-design', 23 | }, 24 | }, 25 | css: { 26 | modules: { 27 | localsConvention: 'camelCaseOnly', 28 | }, 29 | preprocessorOptions: { 30 | less: { 31 | javascriptEnabled: true, 32 | modifyVars: { 33 | hack: `true;@import (reference) "${pathResolve( 34 | 'src/styles/antd.less', 35 | )}";`, 36 | }, 37 | }, 38 | }, 39 | }, 40 | plugins: getPlugins(isBuild), 41 | server: { 42 | host: true, 43 | port: 3100, 44 | proxy: createProxy([ 45 | ['/api', 'http://localhost:3000'], 46 | ['/upload', 'http://localhost:3300/upload'], 47 | ]), 48 | }, 49 | }, 50 | typeof configure === 'function' ? configure(params, isBuild) : {}, 51 | { 52 | arrayMerge: (_d, s, _o) => Array.from(new Set([..._d, ...s])), 53 | }, 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /build/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './utils'; 3 | export * from './config'; 4 | export * from './proxy'; 5 | -------------------------------------------------------------------------------- /build/plugins/antd.ts: -------------------------------------------------------------------------------- 1 | import styleImport from 'vite-plugin-style-import'; 2 | 3 | export function configAntdPlugin(isBuild: boolean) { 4 | // if (!isBuild) return []; 5 | const antdPlugin = styleImport({ 6 | libs: [ 7 | { 8 | libraryName: 'antd', 9 | esModule: true, 10 | resolveStyle: (name) => { 11 | return `antd/es/${name}/style/index`; 12 | }, 13 | }, 14 | ], 15 | }); 16 | return antdPlugin; 17 | } 18 | -------------------------------------------------------------------------------- /build/plugins/icon.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import viteSvgIcons from 'vite-plugin-svg-icons'; 4 | // import Icons from 'unplugin-icons/vite'; 5 | 6 | export function configIconPlugin(isBuild: boolean) { 7 | return viteSvgIcons({ 8 | // 指定需要缓存的图标文件夹 9 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], 10 | svgoOptions: isBuild, 11 | // 指定symbolId格式 12 | symbolId: 'svg-[dir]-[name]', 13 | }); 14 | // Icons({ compiler: 'jsx' }), 15 | // ]; 16 | } 17 | -------------------------------------------------------------------------------- /build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import Icons from 'unplugin-icons/vite'; 3 | import { PluginOption } from 'vite'; 4 | 5 | import { configIconPlugin } from './icon'; 6 | import { configMockPlugin } from './mock'; 7 | import { configAntdPlugin } from './antd'; 8 | // import { configWindiCssPlugin } from './windicss'; 9 | 10 | export function getPlugins(isBuild: boolean) { 11 | const vitePlugins: (PluginOption | PluginOption[])[] = []; 12 | vitePlugins.push(react()); 13 | vitePlugins.push(configMockPlugin(isBuild)); 14 | vitePlugins.push(configIconPlugin(isBuild)); 15 | vitePlugins.push(Icons({ compiler: 'jsx', jsx: 'react' })); 16 | vitePlugins.push(configAntdPlugin(isBuild)); 17 | // vitePlugins.push(configWindiCssPlugin()); 18 | return vitePlugins; 19 | } 20 | -------------------------------------------------------------------------------- /build/plugins/mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock plugin for development and production. 3 | * https://github.com/anncwb/vite-plugin-mock 4 | */ 5 | import { viteMockServe } from 'vite-plugin-mock'; 6 | 7 | export function configMockPlugin(isBuild: boolean) { 8 | return viteMockServe({ 9 | // eslint-disable-next-line no-useless-escape 10 | ignore: /^\_/, 11 | mockPath: 'mock', 12 | localEnabled: !isBuild, 13 | prodEnabled: false, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /build/plugins/windicss.ts: -------------------------------------------------------------------------------- 1 | import WindiCSS from 'vite-plugin-windicss'; 2 | 3 | export function configWindiCssPlugin() { 4 | return WindiCSS(); 5 | } 6 | -------------------------------------------------------------------------------- /build/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to parse the .env.development proxy configuration 3 | */ 4 | import type { ProxyOptions } from 'vite'; 5 | 6 | type ProxyItem = [string, string]; 7 | 8 | type ProxyList = ProxyItem[]; 9 | 10 | type ProxyTargetList = Record; 11 | 12 | const httpsRE = /^https:\/\//; 13 | 14 | /** 15 | * Generate proxy 16 | * @param list 17 | */ 18 | export function createProxy(list: ProxyList = []) { 19 | const ret: ProxyTargetList = {}; 20 | for (const [prefix, target] of list) { 21 | const isHttps = httpsRE.test(target); 22 | 23 | // https://github.com/http-party/node-http-proxy#options 24 | ret[prefix] = { 25 | target, 26 | changeOrigin: true, 27 | ws: true, 28 | rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''), 29 | // https is require secure=false 30 | ...(isHttps ? { secure: false } : {}), 31 | }; 32 | } 33 | return ret; 34 | } 35 | -------------------------------------------------------------------------------- /build/types.ts: -------------------------------------------------------------------------------- 1 | import { ConfigEnv, UserConfig } from 'vite'; 2 | 3 | export type Configure = (params: ConfigEnv, isBuild: boolean) => UserConfig; 4 | -------------------------------------------------------------------------------- /build/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paths'; 2 | -------------------------------------------------------------------------------- /build/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export const pathResolve = (dir: string) => resolve(__dirname, '../../', dir); 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /mock/_util.ts: -------------------------------------------------------------------------------- 1 | // Interface data format used to return a unified format 2 | export function resultItems>(data: T, meta: Record = {}) { 3 | return { 4 | data, 5 | meta, 6 | }; 7 | } 8 | 9 | export function resultSuccess>(result: T, { message = 'ok' } = {}) { 10 | return { 11 | code: 0, 12 | result, 13 | message, 14 | type: 'success', 15 | }; 16 | } 17 | 18 | export function resultPageSuccess( 19 | page: number, 20 | pageSize: number, 21 | list: T[], 22 | { message = 'ok' } = {}, 23 | ) { 24 | const pageData = pagination(page, pageSize, list); 25 | 26 | return { 27 | ...resultSuccess({ 28 | items: pageData, 29 | total: list.length, 30 | }), 31 | message, 32 | }; 33 | } 34 | 35 | export function resultError(message = 'Request failed', { code = -1, result = null } = {}) { 36 | return { 37 | code, 38 | result, 39 | message, 40 | type: 'error', 41 | }; 42 | } 43 | 44 | export function pagination(pageNo: number, pageSize: number, array: T[]): T[] { 45 | const offset = (pageNo - 1) * Number(pageSize); 46 | const ret = 47 | offset + Number(pageSize) >= array.length 48 | ? array.slice(offset, array.length) 49 | : array.slice(offset, offset + Number(pageSize)); 50 | return ret; 51 | } 52 | 53 | export interface RequestParams { 54 | method: string; 55 | body: any; 56 | headers?: { authorization?: string }; 57 | query: any; 58 | } 59 | 60 | /** 61 | * @description 本函数用于从request数据中获取token,请根据项目的实际情况修改 62 | * 63 | */ 64 | export function getRequestToken({ headers }: RequestParams): string | undefined { 65 | return headers?.authorization; 66 | } 67 | 68 | export const randomIntFrom = (min: number, max: number) => { 69 | const minc = Math.ceil(min); 70 | const maxc = Math.floor(max); 71 | return Math.floor(Math.random() * (maxc - minc + 1)) + minc; // 含最大值,含最小值 72 | }; 73 | export const randomArray = (...some: number[]) => some[randomIntFrom(0, some.length - 1)]; 74 | -------------------------------------------------------------------------------- /mock/servers.ts: -------------------------------------------------------------------------------- 1 | import type { ServerItem } from '@/views/dashboard/monitor/types'; 2 | 3 | import { RequestParams, resultError } from './_util'; 4 | import type { MockItem } from './types'; 5 | 6 | export const servers: ServerItem[] = [ 7 | { 8 | id: '1', 9 | os: 'Debian Bullseye 11.2 64bit', 10 | cpu: 4, 11 | memory: 16 * 1024, 12 | disk: [ 13 | { path: '/', value: 50 * 1024 }, 14 | { path: '/data', value: 100 * 1024 }, 15 | ], 16 | status: 'running', 17 | insetIp: '10.120.118.4', 18 | publicIp: ' 139.198.177.23', 19 | }, 20 | ]; 21 | export default [ 22 | { 23 | url: '/api/servers', 24 | method: 'get', 25 | response(request: RequestParams) { 26 | const id = request.query?.id; 27 | if (!id) return servers; 28 | const server = servers.find((s) => s.id === id); 29 | if (!server) { 30 | (this.res as any).statusCode = 500; 31 | return resultError('server not exits!'); 32 | } 33 | return server; 34 | }, 35 | }, 36 | ] as MockItem[]; 37 | -------------------------------------------------------------------------------- /mock/types.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock'; 2 | 3 | export interface MockItem extends MockMethod { 4 | res: any; 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | module.exports = { 3 | plugins: [ 4 | require('postcss-import'), 5 | require('tailwindcss/nesting'), 6 | require('tailwindcss'), 7 | require('autoprefixer'), 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useSetupAuth } from './components/Auth'; 2 | import { useSetupConfig } from './components/Config'; 3 | import { SWRFetcher, useSetupFetcher } from './components/Fetcher'; 4 | import { useSetupIcon } from './components/Icon'; 5 | import { useSetupKeepAlive } from './components/KeepAlive'; 6 | import { useSetupMenu } from './components/Menu'; 7 | import { Router, useSetupRouter } from './components/Router'; 8 | import { useSetupStorage } from './components/Storage'; 9 | import { config, router } from './config'; 10 | 11 | const useSetup = () => { 12 | // 初始化本地存储 13 | useSetupStorage(); 14 | useSetupConfig(config); 15 | // 初始化请求库和SWR,如果不需要自定义全局配置可不写 16 | useSetupFetcher(); 17 | // 通过本地存储的Token获取远程用户信息 18 | useSetupAuth('/user/info'); 19 | // 通过用户信息初始化路由 20 | useSetupRouter(router); 21 | useSetupKeepAlive({ path: '/' }); 22 | // 通过路由或用户信息初始化菜单 23 | useSetupMenu(); 24 | // 初始化图标配置 25 | useSetupIcon({ 26 | iconfont_urls: ['//at.alicdn.com/t/font_2497975_4zt848h920t.js'], 27 | }); 28 | // useSetupChart(); 29 | }; 30 | const App = () => { 31 | useSetup(); 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /src/assets/icons/download-count.svg: -------------------------------------------------------------------------------- 1 | Asset 91 -------------------------------------------------------------------------------- /src/assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 10 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/icons/test.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon1@3x 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/icons/total-sales.svg: -------------------------------------------------------------------------------- 1 | Asset 500 -------------------------------------------------------------------------------- /src/assets/icons/transaction.svg: -------------------------------------------------------------------------------- 1 | Asset 480% -------------------------------------------------------------------------------- /src/assets/icons/visit-count.svg: -------------------------------------------------------------------------------- 1 | Asset 510 -------------------------------------------------------------------------------- /src/assets/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toomejs/toome/39fcfe82b97a8684615d68d6b99296cfc943f76a/src/assets/images/demo.png -------------------------------------------------------------------------------- /src/assets/images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toomejs/toome/39fcfe82b97a8684615d68d6b99296cfc943f76a/src/assets/images/header.jpg -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toomejs/toome/39fcfe82b97a8684615d68d6b99296cfc943f76a/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/svg/login-bg-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/svg/login-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/Auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './types'; 3 | export * from './required'; 4 | export * from './store'; 5 | -------------------------------------------------------------------------------- /src/components/Auth/required.tsx: -------------------------------------------------------------------------------- 1 | import { trim } from 'lodash-es'; 2 | import { FC, ReactElement } from 'react'; 3 | import { Navigate, useLocation } from 'react-router-dom'; 4 | 5 | import { useAuthInited, useToken } from './hooks'; 6 | /** 7 | * 该组件已废弃 8 | * @param param0 9 | */ 10 | export const RequirdAuth: FC<{ 11 | basename: string; 12 | path?: string; 13 | element: ReactElement; 14 | }> = ({ element, basename, path = '/auth/login' }) => { 15 | const location = useLocation(); 16 | const token = useToken(); 17 | const tokened = useAuthInited(); 18 | if (tokened && token !== undefined) { 19 | let redirect = ''; 20 | if (location.pathname !== path && trim(location.pathname, '/') !== trim(basename, '/')) { 21 | redirect = `?redirect=${location.pathname}`; 22 | if (location.search) redirect = `${redirect}${location.search}`; 23 | } 24 | return token ? element : ; 25 | } 26 | return element; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Auth/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-26 12:03:29 +0800 6 | * @Updated_at : 2022-01-16 00:27:49 +0800 7 | * @Path : /src/components/Auth/store.ts 8 | * @Description : Auth组件状态池 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { createStore } from '@/utils'; 14 | 15 | import { AuthStoreType } from './types'; 16 | 17 | /** 18 | * 账户状态储存池 19 | */ 20 | export const AuthStore = createStore(() => ({ 21 | token: null, 22 | user: null, 23 | inited: false, 24 | })); 25 | -------------------------------------------------------------------------------- /src/components/Auth/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2022-01-09 14:28:55 +0800 7 | * @Path : /src/components/Auth/types.ts 8 | * @Description : Auth组件类型 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | /** 14 | * 角色类型 15 | */ 16 | export type Role = RecordScalable< 17 | { 18 | /** 角色ID */ 19 | id: string; 20 | /** 角色名称 */ 21 | name: string; 22 | }, 23 | T 24 | >; 25 | /** 26 | * 权限类型 27 | */ 28 | export type Permission = RecordScalable< 29 | { 30 | /** 权限ID */ 31 | id: string; 32 | /** 权限名称 */ 33 | name: string; 34 | }, 35 | T 36 | >; 37 | 38 | /** 39 | * 用户类型 40 | */ 41 | export type User< 42 | T extends RecordAnyOrNever = RecordNever, 43 | R extends RecordAnyOrNever = RecordNever, 44 | P extends RecordAnyOrNever = RecordNever, 45 | > = RecordScalable< 46 | { 47 | /** 角色列表 */ 48 | roles?: Role[]; 49 | /** 权限列表 */ 50 | permissions?: Permission

[]; 51 | }, 52 | T 53 | >; 54 | /** 55 | * 当前账户状态类型 56 | */ 57 | export type AuthStoreType = { 58 | /** 是否已经初始化Token */ 59 | inited: boolean; 60 | /** Token */ 61 | token: null | string; 62 | /** 用户信息 */ 63 | user: User | null; 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/Chart/_default.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TitleComponent, 3 | TooltipComponent, 4 | GridComponent, 5 | DatasetComponent, 6 | TransformComponent, 7 | } from 'echarts/components'; 8 | 9 | import type { ChartState } from './types'; 10 | 11 | export const getDefaultChartConfig = (): ChartState => ({ 12 | render: 'canvas', 13 | exts: [TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent], 14 | height: '300px', 15 | width: 'auto', 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/Chart/hooks.ts: -------------------------------------------------------------------------------- 1 | import { deepMerge, useStoreSetuped } from '@/utils'; 2 | 3 | import { ChartSetup, ChartStore } from './store'; 4 | 5 | import type { ChartConfig } from './types'; 6 | 7 | export const useSetupChart = (config?: ChartConfig) => { 8 | useStoreSetuped({ 9 | store: ChartSetup, 10 | callback: () => { 11 | ChartStore.setState((state) => 12 | deepMerge(state, { 13 | ...config, 14 | exts: config?.exts?.filter((e) => !state.exts.includes(e)) ?? [], 15 | }), 16 | ); 17 | }, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './hooks'; 3 | export * from './chart'; 4 | export * from './store'; 5 | export * from './components/PercentGaugeChart'; 6 | -------------------------------------------------------------------------------- /src/components/Chart/store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | 3 | import { createStore } from '@/utils'; 4 | 5 | import type { ChartState } from './types'; 6 | import { getDefaultChartConfig } from './_default.config'; 7 | 8 | export const ChartSetup = create<{ setuped?: true }>(() => ({})); 9 | 10 | export const ChartStore = createStore(() => getDefaultChartConfig()); 11 | -------------------------------------------------------------------------------- /src/components/Chart/types.ts: -------------------------------------------------------------------------------- 1 | import type * as echarts from 'echarts/core'; 2 | import type { ECBasicOption } from 'echarts/types/dist/shared'; 3 | import type { CSSProperties } from 'react'; 4 | import type { GaugeSeriesOption } from 'echarts/charts'; 5 | 6 | export type EChartExt = ArrayItem[0], Array>>; 7 | export interface ChartConfig { 8 | render?: 'svg' | 'canvas'; 9 | exts?: Array; 10 | height?: string; 11 | width?: string; 12 | } 13 | export interface ChartState extends Required {} 14 | export interface ChartLoading { 15 | type?: string; 16 | show?: boolean; 17 | text?: string; 18 | color?: string; 19 | textColor?: string; 20 | maskColor?: string; 21 | zlevel?: number; 22 | fontSize?: number; 23 | showSpinner?: true; 24 | spinnerRadius?: number; 25 | lineWidth?: number; 26 | fontWeight?: string; 27 | fontStyle?: string; 28 | fontFamily?: string; 29 | } 30 | export interface ChartProps extends ChartConfig { 31 | options: T; 32 | loading?: ChartLoading; 33 | className?: string; 34 | style?: CSSProperties; 35 | } 36 | export type GaugeChartProps = { 37 | config?: Omit & { 38 | click?: (chart: echarts.ECharts) => void; 39 | }; 40 | style?: CSSProperties; 41 | loading?: ChartLoading; 42 | data: NonNullable; 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Config/_default.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-29 11:55:02 +0800 6 | * @Updated_at : 2022-01-11 14:30:55 +0800 7 | * @Path : /src/components/Config/_default.config.ts 8 | * @Description : 默认配置 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | 14 | import { ConfigStoreType } from './types'; 15 | 16 | export const defaultConfig: ConfigStoreType['config'] = { 17 | timezone: 'UTC', 18 | isAntd: true, 19 | theme: { 20 | mode: 'light', 21 | depend: 'manual', 22 | range: { 23 | light: '07:30', 24 | dark: '18:30', 25 | }, 26 | darken: { 27 | theme: { 28 | brightness: 100, 29 | contrast: 90, 30 | sepia: 10, 31 | }, 32 | fixes: { 33 | invert: [], 34 | css: '', 35 | ignoreInlineStyle: [], 36 | ignoreImageAnalysis: [], 37 | disableStyleSheetsProxy: true, 38 | }, 39 | }, 40 | }, 41 | colors: { 42 | primary: '#1890ff', 43 | info: '#00adb5', 44 | success: '#52c41a', 45 | error: '#ff4d4f', 46 | warning: '#faad14', 47 | }, 48 | // layout: { 49 | // mode: 'side', 50 | // collapsed: false, 51 | // theme: { 52 | // header: 'light', 53 | // sidebar: 'dark', 54 | // embed: 'light', 55 | // }, 56 | // fixed: { 57 | // header: false, 58 | // sidebar: false, 59 | // embed: false, 60 | // }, 61 | // }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/Config/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2022-01-02 18:30:16 +0800 6 | * @Updated_at : 2022-01-11 14:19:47 +0800 7 | * @Path : /src/components/Config/constants.ts 8 | * @Description : 配置组件常量 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | /** 14 | * 主题切换依赖 15 | * 注意OS和TIME与手动切换并不冲突,但OS与TIME两者只能选择一个 16 | */ 17 | export enum ThemeDepend { 18 | /** 跟随操作系统 */ 19 | OS = 'os', 20 | /** 跟随时间范围 */ 21 | TIME = 'time', 22 | /** 只能手动切换 */ 23 | MANUAL = 'manual', 24 | } 25 | /** 26 | * 主题模式 27 | */ 28 | export enum ThemeMode { 29 | /** 明亮 */ 30 | LIGHT = 'light', 31 | /** 暗黑 */ 32 | DARK = 'dark', 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './setup'; 3 | export * from './hooks'; 4 | export * from './store'; 5 | export * from './constants'; 6 | -------------------------------------------------------------------------------- /src/components/Config/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-29 11:55:02 +0800 6 | * @Updated_at : 2022-01-16 00:29:58 +0800 7 | * @Path : /src/components/Config/store.ts 8 | * @Description : 配置组件状态池 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { createStore } from '@/utils'; 14 | 15 | import { ConfigStoreType } from './types'; 16 | import { defaultConfig } from './_default.config'; 17 | /** 18 | * 配置组件初始化状态池 19 | */ 20 | export const ConfigSetup = createStore<{ setuped?: true }>(() => ({})); 21 | /** 22 | * 配置组件状态池 23 | */ 24 | export const ConfigStore = createStore(() => ({ 25 | config: defaultConfig, 26 | watchers: {}, 27 | })); 28 | -------------------------------------------------------------------------------- /src/components/Config/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-29 11:55:02 +0800 6 | * @Updated_at : 2022-01-11 14:30:14 +0800 7 | * @Path : /src/components/Config/types.ts 8 | * @Description : 配置组件类型 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import DarkReader from 'darkreader'; 14 | 15 | import { ThemeDepend, ThemeMode } from './constants'; 16 | 17 | /** 18 | * 配置组件参数选项 19 | */ 20 | export interface ConfigProps { 21 | /** 默认时区 */ 22 | timezone?: string; 23 | /** 主题配置 */ 24 | theme?: ThemeConfig; 25 | /** 色系配置 */ 26 | colors?: ColorConfig; 27 | /** 28 | * 临时变量,是否使用antd组件 29 | * 由于antd下暂时不支持动态暗黑而采用dark-reader 30 | * 但是arco,tdesign等都支持 31 | * 有了这个变量在后面开发其它组件库的面板时切换暗黑模式时就可以做判断了 32 | */ 33 | isAntd?: boolean; 34 | } 35 | /** 36 | * 配置组件状态池 37 | */ 38 | export interface ConfigStoreType { 39 | /** 配置状态 */ 40 | config: ReRequired> & { 41 | /** 主题配置 */ 42 | theme: ReRequired> & { 43 | /** DarkReader配置 */ 44 | darken?: DarkReaderConfig; 45 | }; 46 | }; 47 | /** 监听器 */ 48 | watchers: { 49 | /** 主题切换依赖监听器 */ 50 | theme?: NodeJS.Timeout; 51 | }; 52 | } 53 | /** 54 | * 主题配置 55 | */ 56 | export interface ThemeConfig { 57 | /** 主题模式 */ 58 | mode?: `${ThemeMode}`; 59 | /** 主题依赖,注意OS和TIME与手动切换并不冲突,但OS与TIME两者只能选择一个 */ 60 | depend?: `${ThemeDepend}`; 61 | /** 切换时间 */ 62 | range?: Partial; 63 | /** DarkRender配置 */ 64 | darken?: DarkReaderConfig; 65 | } 66 | /** 67 | * 色系配置 68 | */ 69 | export interface ColorConfig { 70 | /** 主色 */ 71 | primary?: string; 72 | /** 信息色 */ 73 | info?: string; 74 | /** 成功色 */ 75 | success?: string; 76 | /** 错误色 */ 77 | error?: string; 78 | /** 警告色 */ 79 | warning?: string; 80 | } 81 | 82 | /** 83 | * 主题切换时间范围 84 | */ 85 | export type ThemeTimeRange = { [key in `${ThemeMode}`]: string }; 86 | /** 87 | * DarkReader配置 88 | */ 89 | export interface DarkReaderConfig { 90 | theme?: Partial; 91 | fixes?: Partial; 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Fetcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './store'; 3 | export * from './types'; 4 | export * from './provider'; 5 | -------------------------------------------------------------------------------- /src/components/Fetcher/provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Author : pincman 3 | * HomePage : https://pincman.com 4 | * Support : support@pincman.com 5 | * Created_at : 2021-12-25 07:26:27 +0800 6 | * Updated_at : 2022-01-10 10:15:56 +0800 7 | * Path : /src/components/Fetcher/provider.tsx 8 | * Description : SWR包装器 9 | * LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import type { AxiosRequestConfig } from 'axios'; 14 | import { useEffect } from 'react'; 15 | import { SWRConfig } from 'swr'; 16 | 17 | import { deepMerge } from '@/utils'; 18 | 19 | import { useFetcher } from './hooks'; 20 | 21 | import { FetcherStore } from './store'; 22 | /** 23 | * SWR包装器,如果要使用swr功能请使用此组件包裹根组件 24 | * @param props 25 | */ 26 | export const SWRFetcher: FC = ({ children }) => { 27 | const swr = FetcherStore((state) => state.swr); 28 | const fetcher = useFetcher(); 29 | useEffect(() => { 30 | FetcherStore.setState((state) => { 31 | state.swr = { 32 | ...(state.swr ?? {}), 33 | fetcher: async ( 34 | resource: string | AxiosRequestConfig, 35 | options?: AxiosRequestConfig, 36 | ) => { 37 | let config: AxiosRequestConfig = options ?? {}; 38 | if (typeof resource === 'string') config.url = resource; 39 | else config = deepMerge(config, resource, 'replace'); 40 | const res = await fetcher.request({ ...config, method: 'get' }); 41 | await new Promise((resolve) => { 42 | setTimeout(resolve, 3000); 43 | }); 44 | return res.data; 45 | }, 46 | }; 47 | }); 48 | }, [fetcher]); 49 | return {children}; 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/Fetcher/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-25 05:01:46 +0800 6 | * @Updated_at : 2022-01-16 00:29:34 +0800 7 | * @Path : /src/components/Fetcher/store.ts 8 | * @Description : Fetcher组件状态池 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import create from 'zustand'; 14 | 15 | import { createStore } from '@/utils'; 16 | 17 | import { FetcherStoreType } from './types'; 18 | /** 19 | * Fetcher组件初始化状态 20 | */ 21 | export const FetcherSetup = create<{ setuped?: true }>(() => ({})); 22 | /** 23 | * Fetcher组件状态池 24 | */ 25 | export const FetcherStore = createStore(() => ({ 26 | axios: {}, 27 | swr: {}, 28 | })); 29 | -------------------------------------------------------------------------------- /src/components/Fetcher/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2022-01-10 10:15:17 +0800 7 | * @Path : /src/components/Fetcher/types.ts 8 | * @Description : Fetcher组件类型 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import type { AxiosInterceptorManager, AxiosRequestConfig, AxiosResponse } from 'axios'; 14 | import type { BareFetcher, PublicConfiguration } from 'swr/dist/types'; 15 | /** 16 | * Fetcher配置 17 | */ 18 | export interface FetcherConfig extends AxiosRequestConfig, FetchOption {} 19 | /** 20 | * swrjs配置 21 | */ 22 | export interface SwrConfig 23 | extends Partial>>> {} 24 | 25 | /** 26 | * Fetcher组件状态池 27 | */ 28 | export interface FetcherStoreType { 29 | axios: FetcherConfig; 30 | swr: SwrConfig; 31 | } 32 | /** 33 | * 自定义选项参数 34 | */ 35 | export interface FetchOption { 36 | /** 当前账户验证token */ 37 | token?: string | null; 38 | /** 响应后设置token的函数 */ 39 | setToken?: (token: string) => Promise; 40 | /** 响应式清除token的函数 */ 41 | clearToken?: () => Promise; 42 | /** 是否禁止重复请求 */ 43 | cancel_repeat?: boolean; 44 | /** 自定义axios请求和响应函数 */ 45 | interceptors?: { 46 | request?: ( 47 | req: AxiosInterceptorManager, 48 | ) => AxiosInterceptorManager; 49 | response?: ( 50 | res: AxiosInterceptorManager, 51 | ) => AxiosInterceptorManager; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Icon/_default.config.ts: -------------------------------------------------------------------------------- 1 | import type { IconState } from './types'; 2 | 3 | export const getDefaultIconConfig = (): IconState => ({ 4 | size: 16, 5 | style: {}, 6 | classes: [], 7 | prefix: { svg: 'svg', iconfont: 'icon' }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/Icon/constants.ts: -------------------------------------------------------------------------------- 1 | export enum IconPrefixType { 2 | SVG = 'svg', 3 | ICONFONT = 'if', 4 | IONIFY = 'fy', 5 | } 6 | export enum IconType { 7 | SVG = 'svg', 8 | ICONFONT = 'iconfont', 9 | IONIFY = 'iconify', 10 | COMPONENT = 'component', 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Icon/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { createFromIconfontCN } from '@ant-design/icons'; 2 | import { omit } from 'lodash-es'; 3 | 4 | import { useStoreSetuped, deepMerge } from '@/utils'; 5 | 6 | import type { IconComputed, IconConfig, IconProps } from './types'; 7 | import type { IconType } from './constants'; 8 | import { IconSetup, IconStore } from './store'; 9 | 10 | export const useSetupIcon = (config?: IconConfig) => { 11 | useStoreSetuped({ 12 | store: IconSetup, 13 | callback: () => { 14 | const options: IconConfig = config ?? {}; 15 | IconStore.setState((state) => { 16 | const newState = deepMerge(state, omit(config, ['iconfont']) as any); 17 | if (options.iconfont_urls) { 18 | newState.iconfont = createFromIconfontCN({ 19 | scriptUrl: options.iconfont_urls, 20 | }); 21 | } 22 | return newState; 23 | }); 24 | }, 25 | }); 26 | }; 27 | export const useIcon = (args: IconProps) => { 28 | const config = IconStore((state) => ({ ...state })); 29 | const params = omit(config, ['size', 'prefix', 'classes', 'iconfont_urls']); 30 | const csize = typeof config.size === 'number' ? `${config.size}px` : config.size; 31 | const style = { fontSize: args.style?.fontSize ?? csize, ...(args.style ?? {}) }; 32 | const className = [...config.classes, args.className]; 33 | if ('component' in args) { 34 | const result = deepMerge(params, { 35 | ...args, 36 | type: 'component', 37 | style, 38 | className, 39 | }); 40 | return omit(result, ['iconfont']) as IconComputed; 41 | } 42 | let name: string; 43 | let type: `${IconType}` = 'svg'; 44 | const [prefix, ...names] = args.name.split(':'); 45 | if (prefix === 'if') { 46 | name = `${config.prefix.iconfont}-${names.join(':')}`; 47 | type = 'iconfont'; 48 | } else if (prefix === 'fy') { 49 | name = names.join(':'); 50 | type = 'iconify'; 51 | } else { 52 | name = `${config.prefix.svg}-${names.join(':')}`; 53 | type = 'svg'; 54 | } 55 | const result = deepMerge(config, { 56 | ...args, 57 | name, 58 | type, 59 | style, 60 | className, 61 | }); 62 | return (prefix !== 'if' ? omit(result, ['iconfont']) : result) as IconComputed; 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/Icon/icon.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import AntdIcon from '@ant-design/icons'; 3 | import { Icon as Iconify } from '@iconify/react'; 4 | 5 | import produce from 'immer'; 6 | 7 | import classNames from 'classnames'; 8 | 9 | import { IconType } from './constants'; 10 | import { useIcon } from './hooks'; 11 | import type { IconComputed, IconProps } from './types'; 12 | import { IconSetup } from './store'; 13 | 14 | const getAntdSvgIcon = ({ config }: { config: IconComputed }) => { 15 | if ('component' in config) { 16 | const { component, spin, rotate, className, ...rest } = config; 17 | return config.component({ className: classNames(className), ...rest }); 18 | } 19 | const { name, iconfont, className, inline, type, spin, rotate, ...rest } = config; 20 | return type === IconType.IONIFY ? ( 21 | 22 | ) : ( 23 | 26 | ); 27 | }; 28 | const Icon = (props: IconProps) => { 29 | const config = useIcon(props); 30 | const isSetuped = IconSetup((state) => state.setuped); 31 | const [setuped, setSetuped] = useState(isSetuped); 32 | useEffect(() => { 33 | setSetuped(isSetuped); 34 | }, [isSetuped]); 35 | if (!setuped) return null; 36 | if ('type' in config && config.iconfont && config.type === IconType.ICONFONT) { 37 | const { name, iconfont: FontIcon, inline, className, type, ...rest } = config; 38 | return ; 39 | } 40 | const options = produce(config, (draft) => { 41 | if (draft.spin) draft.className.push('anticon-spin'); 42 | if (draft.rotate) { 43 | draft.style.transform = draft.style.transform 44 | ? `${draft.style.transform} rotate(${draft.rotate}deg)` 45 | : `rotate(${draft.rotate}deg)`; 46 | } 47 | }); 48 | return ( 49 | getAntdSvgIcon({ config: options })} 52 | /> 53 | ); 54 | }; 55 | export default Icon; 56 | -------------------------------------------------------------------------------- /src/components/Icon/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Icon } from './icon'; 2 | export * from './hooks'; 3 | export * from './types'; 4 | export * from './constants'; 5 | export * from './store'; 6 | -------------------------------------------------------------------------------- /src/components/Icon/store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | 3 | import { createStore } from '@/utils'; 4 | 5 | import { getDefaultIconConfig } from './_default.config'; 6 | import type { IconState } from './types'; 7 | 8 | export const IconSetup = create<{ setuped?: true }>(() => ({})); 9 | 10 | export const IconStore = createStore(() => getDefaultIconConfig()); 11 | -------------------------------------------------------------------------------- /src/components/Icon/types.ts: -------------------------------------------------------------------------------- 1 | import type { IconFontProps as DefaultIconFontProps } from '@ant-design/icons/lib/components/IconFont'; 2 | // import type { IconProps as IconifyIconProps } from '@iconify/react'; 3 | import type { CSSProperties, FC, RefAttributes, SVGProps } from 'react'; 4 | 5 | import type { IconPrefixType, IconType } from './constants'; 6 | 7 | export type IconName = `${IconPrefixType}:${string}`; 8 | export type IconComponent = FC; 9 | export type IconConfig = RecordScalable< 10 | { 11 | size?: number | string; 12 | classes?: string[]; 13 | style?: CSSProperties; 14 | prefix?: { svg?: string; iconfont?: string }; 15 | iconfont_urls?: string | string[]; 16 | }, 17 | T 18 | >; 19 | export type IconState = RecordScalable< 20 | Required> & { 21 | iconfont?: FC>; 22 | }, 23 | T 24 | >; 25 | export type IconComputed = { 26 | spin?: boolean; 27 | rotate?: number; 28 | className: string[]; 29 | style: CSSProperties; 30 | } & ( 31 | | { 32 | name: string; 33 | type: `${IconType}`; 34 | inline?: boolean; 35 | iconfont?: FC>; 36 | } 37 | | { 38 | component: FC; 39 | } 40 | ); 41 | export interface BaseIconProps extends Omit { 42 | className?: string; 43 | spin?: boolean; 44 | rotate?: number; 45 | } 46 | export interface SvgProps extends BaseIconProps { 47 | name: IconName; 48 | component?: never; 49 | inline?: boolean; 50 | } 51 | export interface ComponentProps extends BaseIconProps { 52 | name?: never; 53 | component: IconComponent; 54 | } 55 | 56 | export type IconProps = SvgProps | ComponentProps; 57 | 58 | type BaseElementProps = RefAttributes & SVGProps; 59 | -------------------------------------------------------------------------------- /src/components/KeepAlive/constants.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch } from 'react'; 2 | 3 | import { KeepAliveAction } from './types'; 4 | 5 | export enum AliveActionType { 6 | REMOVE = 'remove', 7 | REMOVE_MULTI = 'remove_multi', 8 | ADD = 'add', 9 | CLEAR = 'clear', 10 | ACTIVE = 'active', 11 | CHANGE = 'change', 12 | RESET = 'reset', 13 | } 14 | export const KeepAliveIdContext = createContext(null); 15 | export const KeepAliveDispatchContext = createContext | null>(null); 16 | -------------------------------------------------------------------------------- /src/components/KeepAlive/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useUnmount } from 'react-use'; 2 | 3 | import { useCallback } from 'react'; 4 | 5 | import { isNil } from 'ramda'; 6 | 7 | import { deepMerge, useStoreSetuped } from '@/utils'; 8 | 9 | import { useNavigator } from '../Router'; 10 | 11 | import { KeepAliveSetup, KeepAliveStore } from './store'; 12 | 13 | import { KeepAliveConfig } from './types'; 14 | import { AliveActionType } from './constants'; 15 | 16 | export const useSetupKeepAlive = (config: KeepAliveConfig) => { 17 | useStoreSetuped({ 18 | store: KeepAliveSetup, 19 | callback: () => { 20 | KeepAliveStore.setState((state) => deepMerge(state, config, 'replace'), true); 21 | }, 22 | }); 23 | const listenLives = KeepAliveStore.subscribe( 24 | (state) => state.lives, 25 | (lives) => { 26 | KeepAliveStore.setState((state) => { 27 | state.include = lives; 28 | }); 29 | }, 30 | ); 31 | useUnmount(() => { 32 | listenLives(); 33 | }); 34 | }; 35 | export const useActivedAlive = () => KeepAliveStore(useCallback((state) => state.active, [])); 36 | export const useKeepAlives = () => KeepAliveStore(useCallback((state) => state.lives, [])); 37 | export const useKeepAliveDispath = () => { 38 | const navigate = useNavigator(); 39 | const removeAlive = useCallback( 40 | (id: string) => { 41 | KeepAliveStore.dispatch({ 42 | type: AliveActionType.REMOVE, 43 | params: { id, navigate }, 44 | }); 45 | }, 46 | [navigate], 47 | ); 48 | const removeAlives = useCallback( 49 | (ids: string[]) => { 50 | KeepAliveStore.dispatch({ 51 | type: AliveActionType.REMOVE_MULTI, 52 | params: { ids, navigate }, 53 | }); 54 | }, 55 | [navigate], 56 | ); 57 | 58 | const changeAlive = useCallback( 59 | (id: string) => { 60 | KeepAliveStore.dispatch({ 61 | type: AliveActionType.CHANGE, 62 | params: { id, navigate }, 63 | }); 64 | }, 65 | [navigate], 66 | ); 67 | const clearAlives = useCallback(() => { 68 | KeepAliveStore.dispatch({ 69 | type: AliveActionType.CLEAR, 70 | navigate, 71 | }); 72 | }, [navigate]); 73 | const refreshAlive = useCallback( 74 | (id: string | null) => { 75 | KeepAliveStore.dispatch({ 76 | type: AliveActionType.RESET, 77 | params: { id, navigate }, 78 | }); 79 | if (!isNil(id) && navigate) navigate({ id }); 80 | }, 81 | [navigate], 82 | ); 83 | return { changeAlive, removeAlive, removeAlives, clearAlives, refreshAlive }; 84 | }; 85 | -------------------------------------------------------------------------------- /src/components/KeepAlive/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './view'; 3 | export * from './hooks'; 4 | export * from './store'; 5 | -------------------------------------------------------------------------------- /src/components/KeepAlive/store.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | import { equals, filter, find, findIndex, includes, not } from 'ramda'; 3 | import { Reducer } from 'react'; 4 | 5 | import { createReduxStore, createStore } from '@/utils'; 6 | 7 | import { AliveActionType } from './constants'; 8 | 9 | import { KeepAliveAction, KeepAliveStoreType } from './types'; 10 | 11 | const keepAliveReducer: Reducer = produce((state, action) => { 12 | switch (action.type) { 13 | case AliveActionType.ADD: { 14 | const lives = [...state.lives]; 15 | if (lives.some((item) => item === action.id && state.active === action.id)) return; 16 | const isNew = lives.filter((item) => item === action.id).length < 1; 17 | if (isNew) { 18 | if (lives.length >= state.maxLen) state.lives.shift(); 19 | state.lives.push(action.id); 20 | state.active = action.id; 21 | } 22 | break; 23 | } 24 | case AliveActionType.REMOVE: { 25 | const { id, navigate } = action.params; 26 | const index = findIndex((item) => item === id, state.lives); 27 | if (equals(index, -1)) return; 28 | const toRemove = state.lives[index]; 29 | state.lives.splice(index, 1); 30 | if (state.active === toRemove) { 31 | if (state.lives.length < 1) { 32 | navigate(state.path); 33 | } else { 34 | const toActiveIndex = index > 0 ? index - 1 : index; 35 | state.active = state.lives[toActiveIndex]; 36 | navigate({ id: state.active }); 37 | } 38 | } 39 | break; 40 | } 41 | case AliveActionType.REMOVE_MULTI: { 42 | const { ids, navigate } = action.params; 43 | state.lives = filter((item) => not(includes(item, ids)), state.lives); 44 | if (state.lives.length < 1) navigate(state.path); 45 | break; 46 | } 47 | case AliveActionType.CLEAR: { 48 | state.lives = []; 49 | action.navigate(state.path); 50 | break; 51 | } 52 | case AliveActionType.ACTIVE: { 53 | const current = find((item) => item === action.id, state.lives); 54 | if (current && state.active !== current) state.active = current; 55 | break; 56 | } 57 | case AliveActionType.CHANGE: { 58 | const { id, navigate } = action.params; 59 | const current = find((item) => item === id, state.lives); 60 | if (!current || state.active === id) return; 61 | navigate({ id }); 62 | break; 63 | } 64 | case AliveActionType.RESET: { 65 | const { id, navigate } = action.params; 66 | state.reset = id; 67 | break; 68 | } 69 | default: 70 | break; 71 | } 72 | }); 73 | 74 | export const KeepAliveSetup = createStore<{ setuped?: true; generated?: true }>(() => ({})); 75 | 76 | export const KeepAliveStore = createReduxStore(keepAliveReducer, { 77 | path: '/', 78 | active: null, 79 | include: [], 80 | exclude: [], 81 | maxLen: 10, 82 | notFound: '/errors/404', 83 | lives: [], 84 | reset: null, 85 | }); 86 | -------------------------------------------------------------------------------- /src/components/KeepAlive/types.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | 3 | import { RouteNavigator, RouteOption } from '../Router'; 4 | 5 | import { AliveActionType } from './constants'; 6 | 7 | export type KeepAliveRouteOption = RouteOption<{ id: string }>; 8 | 9 | export interface KeepAliveConfig { 10 | path?: string; 11 | active?: string | null; 12 | exclude?: Array; 13 | maxLen?: number; 14 | notFound?: string; 15 | } 16 | 17 | export interface KeepAliveStoreType extends Required { 18 | include?: Array; // 是否异步添加 Include 如果不是又填写了 true 会导致重复渲染 19 | lives: string[]; 20 | reset: string | null; 21 | } 22 | export interface AlivePageProps { 23 | isActive: boolean; 24 | id: string; 25 | renderDiv: RefObject; 26 | } 27 | 28 | export type KeepAliveAction = 29 | | { 30 | type: AliveActionType.REMOVE; 31 | params: { 32 | id: string; 33 | navigate: RouteNavigator; 34 | }; 35 | } 36 | | { 37 | type: AliveActionType.REMOVE_MULTI; 38 | params: { 39 | ids: string[]; 40 | navigate: RouteNavigator; 41 | }; 42 | } 43 | | { 44 | type: AliveActionType.ADD; 45 | id: string; 46 | } 47 | | { 48 | type: AliveActionType.ACTIVE; 49 | id: string; 50 | } 51 | | { 52 | type: AliveActionType.CHANGE; 53 | params: { 54 | id: string; 55 | navigate: RouteNavigator; 56 | }; 57 | } 58 | | { 59 | type: AliveActionType.CLEAR; 60 | navigate: RouteNavigator; 61 | } 62 | | { 63 | type: AliveActionType.RESET; 64 | params: { 65 | id: string | null; 66 | navigate?: RouteNavigator; 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/Layout/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 布局模式 3 | */ 4 | export enum LayoutMode { 5 | /** 只有顶栏导航 */ 6 | TOP = 'top', 7 | /** 侧边导航,顶栏自定义 */ 8 | SIDE = 'side', 9 | /** 同side,但是LOGO在顶栏 */ 10 | CONTENT = 'content', 11 | /** 内嵌双导航,侧边折叠 */ 12 | EMBED = 'embed', 13 | } 14 | /** 15 | * 布局组件 16 | */ 17 | export enum LayoutComponent { 18 | /** 顶栏 */ 19 | HEADER = 'header', 20 | /** 侧边栏 */ 21 | SIDEBAR = 'sidebar', 22 | /** 内嵌导航,只在mode为embed时生效 */ 23 | EMBED = 'embed', 24 | } 25 | export enum LayoutActionType { 26 | /** 更改组件固定 */ 27 | CHANGE_FIXED = 'change_fixed', 28 | /** 更改CSS变量 */ 29 | CHANGE_VARS = 'change_vars', 30 | /** 更改布局模式 */ 31 | CHANGE_MODE = 'change_mode', 32 | /** 重置菜单 */ 33 | CHANGE_MENU = 'change_menu', 34 | /** 更改组件主题 */ 35 | CHANGE_THEME = 'change_theme', 36 | /** 更改侧边缩进 */ 37 | CHANGE_COLLAPSE = 'change_collapse', 38 | /** 反转侧边缩进 */ 39 | TOGGLE_COLLAPSE = 'toggle_collapse', 40 | /** 更改移动模式下的侧边缩进 */ 41 | CHANGE_MOBILE_SIDE = 'change_mobile_side', 42 | /** 反转移动模式下的侧边缩进 */ 43 | TOGGLE_MOBILE_SIDE = 'toggle_mobile_side', 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Layout/default.config.ts: -------------------------------------------------------------------------------- 1 | import { LayoutStorageStoreType } from './types'; 2 | 3 | export const defaultConfig: LayoutStorageStoreType = { 4 | mode: 'side', 5 | collapsed: false, 6 | theme: { 7 | header: 'light', 8 | sidebar: 'dark', 9 | embed: 'light', 10 | }, 11 | fixed: { 12 | header: false, 13 | sidebar: false, 14 | embed: false, 15 | }, 16 | vars: { 17 | sidebarWidth: '200px', 18 | sidebarCollapseWidth: '64px', 19 | headerHeight: '48px', 20 | headerLightColor: '#fff', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './utils'; 3 | export * from './hooks'; 4 | export * from './provider'; 5 | -------------------------------------------------------------------------------- /src/components/Layout/provider.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | import { useCallback, useReducer } from 'react'; 4 | 5 | import { useUpdateEffect } from 'react-use'; 6 | 7 | import { useAntdCheck, useTheme } from '@/components/Config'; 8 | 9 | import { useMenus } from '@/components/Menu'; 10 | 11 | import { LayoutConfig } from './types'; 12 | import { getMenuData, initLayoutConfig, layoutDarkTheme, layoutReducer } from './utils'; 13 | import { useChangeLayoutLocalData, useLayoutLocalData, useSetupLayout } from './hooks'; 14 | import { LayoutActionType } from './constants'; 15 | import { LayoutContext, LayoutDispatchContext, LayoutSetup } from './store'; 16 | 17 | export const LayoutStateProvider: FC = ({ children }) => { 18 | const isAntd = useAntdCheck(); 19 | const systemTheme = useTheme(); 20 | const config = useLayoutLocalData(); 21 | const changeConfig = useChangeLayoutLocalData(); 22 | const menus = useMenus(); 23 | const location = useLocation(); 24 | const [data, dispatch] = useReducer( 25 | layoutReducer, 26 | initLayoutConfig({ 27 | config, 28 | menu: getMenuData(menus, location, config.mode), 29 | systemTheme, 30 | }), 31 | ); 32 | useUpdateEffect(() => { 33 | changeConfig((state) => ({ ...state, ...data.config })); 34 | }, [data.config.vars, data.config.collapsed, data.config.mode, data.config.fixed]); 35 | useUpdateEffect(() => { 36 | if (!isAntd || systemTheme !== 'dark') { 37 | changeConfig((state) => ({ ...state, theme: data.config.theme })); 38 | } 39 | }, [data.config.theme]); 40 | useUpdateEffect(() => { 41 | if (!isAntd) return; 42 | if (systemTheme === 'dark') { 43 | dispatch({ 44 | type: LayoutActionType.CHANGE_THEME, 45 | value: layoutDarkTheme, 46 | }); 47 | } else { 48 | dispatch({ 49 | type: LayoutActionType.CHANGE_THEME, 50 | value: config.theme, 51 | }); 52 | } 53 | }, [systemTheme]); 54 | useUpdateEffect(() => { 55 | dispatch({ 56 | type: LayoutActionType.CHANGE_MENU, 57 | value: getMenuData(menus, location, data.config.mode), 58 | }); 59 | }, [data.config.mode, menus, location]); 60 | return ( 61 | 62 | 63 | {children} 64 | 65 | 66 | ); 67 | }; 68 | export const LayoutProvider: FC = ({ children, ...rest }) => { 69 | const setuped = LayoutSetup(useCallback((state) => state.setuped, [])); 70 | useSetupLayout(rest); 71 | return setuped ? {children} : null; 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/Layout/store.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch } from 'react'; 2 | 3 | import { createStore } from '@/utils'; 4 | 5 | import { RouteComponentProps } from '../Router'; 6 | 7 | import { defaultConfig } from './default.config'; 8 | 9 | import { LayoutAction, LayoutContextType, LayoutStorageStoreType } from './types'; 10 | 11 | /** 12 | * 布局组件初始化状态池 13 | */ 14 | export const LayoutSetup = createStore<{ setuped?: true }>(() => ({})); 15 | /** 16 | * 布局组件状态池 17 | */ 18 | export const LayoutStore = createStore(() => defaultConfig); 19 | export const LayoutContext = createContext(null); 20 | export const LayoutDispatchContext = createContext | null>(null); 21 | export const LayoutRouteInfo = createContext(null); 22 | -------------------------------------------------------------------------------- /src/components/Layout/types.ts: -------------------------------------------------------------------------------- 1 | import { ThemeMode } from '@/components/Config'; 2 | 3 | import { MenuOption } from '@/components/Menu'; 4 | 5 | import { LayoutActionType, LayoutComponent, LayoutMode } from './constants'; 6 | 7 | /** 8 | * 布局配置 9 | */ 10 | export interface LayoutConfig { 11 | /** 布局模式 */ 12 | mode?: `${LayoutMode}`; 13 | /** 是否折叠边栏,如果是embed模式则折叠子变量 */ 14 | collapsed?: boolean; 15 | /** 布局组件主题色 */ 16 | theme?: Partial; 17 | /** 布局组件固定设置 */ 18 | fixed?: Partial; 19 | /** 可用的CSS变量 */ 20 | vars?: LayoutVarsConfig; 21 | } 22 | /** 23 | * 布局配置本地储存状态池 24 | */ 25 | export interface LayoutStorageStoreType extends ReRequired {} 26 | /** 27 | * 布局组件状态 28 | */ 29 | export interface LayoutContextType { 30 | /** 配置状态 */ 31 | config: LayoutStorageStoreType; 32 | /** 是否展示移动设备下的菜单 */ 33 | mobileSide: boolean; 34 | /** 菜单状态 */ 35 | menu: LayoutMenuState; 36 | } 37 | 38 | /** 39 | * 布局组件主题色 40 | */ 41 | export type LayoutTheme = { [key in `${LayoutComponent}`]: `${ThemeMode}` }; 42 | /** 43 | * 布局组件是否固定 44 | */ 45 | export type LayoutFixed = { [key in `${LayoutComponent}`]: boolean }; 46 | /** 47 | * 布局组件CSS变量 48 | */ 49 | export interface LayoutVarsConfig { 50 | /** 侧边栏宽度 */ 51 | sidebarWidth?: string | number; 52 | /** 折叠时侧边栏宽度 */ 53 | sidebarCollapseWidth?: string | number; 54 | /** 顶栏高度 */ 55 | headerHeight?: string | number; 56 | /** 顶栏明亮模式下的颜色 */ 57 | headerLightColor?: string; 58 | } 59 | /** 60 | * 菜单状态 61 | */ 62 | export interface LayoutMenuState { 63 | /** 菜单列表 */ 64 | data: MenuOption[]; 65 | /** 展开的菜单 */ 66 | opens: string[]; 67 | /** 选中的菜单 */ 68 | selects: string[]; 69 | /** 拥有子菜单的顶级菜单,用于控制只有一个菜单打开 */ 70 | rootSubKeys: string[]; 71 | /** 分割菜单,用于top和embed模式的菜单 */ 72 | split: LayoutSplitMenuState; 73 | } 74 | /** 75 | * 分割菜单的顶级菜单列表,用于top和embed模式的菜单 76 | */ 77 | export interface LayoutSplitMenuState { 78 | /** 菜单数据 */ 79 | data: MenuOption[]; 80 | /** 选中的菜单 */ 81 | selects: string[]; 82 | } 83 | /** 84 | * 布局操作 85 | */ 86 | export type LayoutAction = 87 | | { 88 | type: LayoutActionType.CHANGE_FIXED; 89 | key: keyof LayoutFixed; 90 | value: boolean; 91 | } 92 | | { 93 | type: LayoutActionType.CHANGE_VARS; 94 | /** css值 */ 95 | vars: LayoutVarsConfig; 96 | } 97 | | { 98 | type: LayoutActionType.CHANGE_MODE; 99 | value: `${LayoutMode}`; 100 | } 101 | | { 102 | type: LayoutActionType.CHANGE_MENU; 103 | /** 菜单状态 */ 104 | value: RePartial; 105 | } 106 | | { 107 | type: LayoutActionType.CHANGE_THEME; 108 | value: Partial; 109 | } 110 | | { type: LayoutActionType.CHANGE_COLLAPSE; value: boolean } 111 | | { type: LayoutActionType.TOGGLE_COLLAPSE } 112 | | { type: LayoutActionType.CHANGE_MOBILE_SIDE; value: boolean } 113 | | { type: LayoutActionType.TOGGLE_MOBILE_SIDE }; 114 | -------------------------------------------------------------------------------- /src/components/Menu/_default.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2022-01-09 21:43:52 +0800 7 | * @Path : /src/components/Menu/_default.config.ts 8 | * @Description : 默认菜单配置 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { MenuStoreType } from './types'; 14 | 15 | export const getDefaultMenuStore = (): MenuStoreType => ({ 16 | config: { 17 | type: 'router', 18 | server: null, 19 | menus: [], 20 | }, 21 | data: [], 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/Menu/hooks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-16 05:55:08 +0800 6 | * @Updated_at : 2022-01-15 22:22:11 +0800 7 | * @Path : /src/components/Menu/hooks.ts 8 | * @Description : 可用的菜单组件钩子 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { useCallback } from 'react'; 14 | 15 | import { useUnmount } from 'react-use'; 16 | 17 | import { createStoreHooks, deepMerge, useStoreSetuped } from '@/utils'; 18 | 19 | import { AuthStore } from '../Auth'; 20 | 21 | import { useFetcherGetter } from '../Fetcher'; 22 | 23 | import { RouterStore } from '../Router/store'; 24 | 25 | import { MenuConfig, MenuOption, MenuStatusType } from './types'; 26 | import { changeMenus } from './utils'; 27 | import { MenuStatus, MenuStore } from './store'; 28 | /** 29 | * 动态菜单状态池 30 | */ 31 | export const useMenu = createStoreHooks(MenuStore); 32 | /** 33 | * 动态菜单数据 34 | */ 35 | export const useMenus = () => MenuStore(useCallback((state) => state.data, [])); 36 | // export const useAntdMenus = () => MenuStore(useCallback((state) => getAntdMenus(state.data), [])); 37 | 38 | /** 39 | * 初始化菜单 40 | * @param config 菜单配置 41 | */ 42 | export const useSetupMenu = >( 43 | config?: MenuConfig, 44 | ) => { 45 | // fech工具用于获取远程菜单 46 | const fecher = useFetcherGetter(); 47 | // 订阅next以刷新菜单数据 48 | const unMenuSub = MenuStatus.subscribe( 49 | (state) => state.next, 50 | (next) => changeMenus(next, fecher()), 51 | ); 52 | // 订阅user,如果获取菜单的方式为独立配置则在user改变时刷新菜单 53 | const unAuthSub = AuthStore.subscribe( 54 | (state) => state.user, 55 | () => { 56 | const { setuped } = MenuStatus.getState(); 57 | const { type } = MenuStore.getState().config; 58 | if (setuped && type !== 'router') { 59 | MenuStatus.setState((state) => { 60 | state.next = true; 61 | }); 62 | } 63 | }, 64 | ); 65 | // 订阅路由列表,如果获取菜单的方式为通过路由携带则在路由列表改变时刷新菜单 66 | const unRouterSub = RouterStore.subscribe( 67 | (state) => state.routes, 68 | () => { 69 | const { setuped } = MenuStatus.getState(); 70 | const { type } = MenuStore.getState().config; 71 | if (setuped && type === 'router') { 72 | MenuStatus.setState((state) => { 73 | state.next = true; 74 | }); 75 | } 76 | }, 77 | ); 78 | /** 合并传入配置生成菜单状态 */ 79 | useStoreSetuped({ 80 | store: MenuStatus, 81 | callback: () => { 82 | MenuStore.setState((state) => { 83 | state.config = deepMerge(state.config, config ?? {}); 84 | }); 85 | }, 86 | }); 87 | useUnmount(() => { 88 | unRouterSub(); 89 | unAuthSub(); 90 | unMenuSub(); 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /src/components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './hooks'; 3 | export * from './store'; 4 | -------------------------------------------------------------------------------- /src/components/Menu/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-24 06:16:29 +0800 6 | * @Updated_at : 2022-01-16 14:02:41 +0800 7 | * @Path : /src/components/Menu/store.ts 8 | * @Description : 菜单组件状态池 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | 14 | import { createStore } from '@/utils'; 15 | 16 | import { getDefaultMenuStore } from './_default.config'; 17 | import { MenuStoreType, MenuStatusType } from './types'; 18 | /** 19 | * 菜单信号状态管理池 20 | */ 21 | export const MenuStatus = createStore(() => ({ next: false })); 22 | /** 23 | * 菜单数据状态管理池 24 | */ 25 | export const MenuStore = createStore(() => getDefaultMenuStore()); 26 | -------------------------------------------------------------------------------- /src/components/Menu/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2022-01-12 21:02:47 +0800 7 | * @Path : /src/components/Menu/types.ts 8 | * @Description : 菜单组件类型 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { SetupedState } from '@/utils'; 14 | 15 | import { RouteMeta } from '../Router'; 16 | 17 | /** 18 | * 菜单配置 19 | */ 20 | export interface MenuConfig { 21 | /** 22 | * 获取菜单的方式 23 | * server: 通过服务器获取 24 | * router: 通过路由携带 25 | * configure: 独立配置 26 | */ 27 | type?: 'server' | 'router' | 'configure'; 28 | /** 通过服务器获取菜单的API地址 */ 29 | server?: string | null; 30 | /** 独立配置的菜单列表 */ 31 | menus?: MenuOption[]; 32 | } 33 | /** 34 | * 菜单数据状态 35 | */ 36 | export interface MenuState 37 | extends Omit>, 'menus'> { 38 | /** 菜单列表 */ 39 | menus: MenuOption[]; 40 | } 41 | /** 42 | * 菜单选项(继承自路由菜单元数据) 43 | */ 44 | export type MenuOption = RouteMeta & { 45 | /** 菜单ID */ 46 | id: string; 47 | /** 菜单文字 */ 48 | text: string; 49 | /** 菜单路径 */ 50 | path?: string; 51 | /** 子菜单 */ 52 | children?: MenuOption[]; 53 | }; 54 | /** 55 | * 菜单生成信号状态管理池 56 | */ 57 | export type MenuStatusType = SetupedState<{ 58 | /** 是否即将开始重新生成菜单 */ 59 | next: boolean; 60 | }>; 61 | /** 62 | * 菜单数据状态管理池 63 | */ 64 | export interface MenuStoreType { 65 | /** 菜单配置状态 */ 66 | config: MenuState; 67 | /** 最终菜单数据 */ 68 | data: MenuOption[]; 69 | } 70 | // export type AntdMenuOption = MenuOption< 71 | // AntdRouteMenuMeta 72 | // >; 73 | -------------------------------------------------------------------------------- /src/components/Router/_default.config.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Author : pincman 3 | * HomePage : https://pincman.com 4 | * Support : support@pincman.com 5 | * Created_at : 2021-12-14 00:07:50 +0800 6 | * Updated_at : 2022-01-13 22:51:52 +0800 7 | * Path : /src/components/Router/_default.config.tsx 8 | * Description : 路由组件默认配置 9 | * LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { Spinner } from '@/components/Spinner'; 14 | 15 | import { RouterStoreType } from './types'; 16 | 17 | export const getDefaultStore: () => RouterStoreType = () => ({ 18 | routes: [], 19 | items: [], 20 | flats: [], 21 | renders: [], 22 | maps: {}, 23 | config: { 24 | basePath: '/', 25 | hash: false, 26 | server: null, 27 | loading: () => , 28 | auth: { 29 | enabled: true, 30 | login_redirect: '/auth/login', 31 | white_list: [], 32 | role_column: 'name', 33 | permission_column: 'name', 34 | redirect: 'login', 35 | }, 36 | // permission: { 37 | // enabled: true, 38 | // column: 'name', 39 | // }, 40 | routes: { 41 | constants: [], 42 | dynamic: [], 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/Router/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './store'; 3 | export * from './provider'; 4 | export * from './hooks'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /src/components/Router/provider.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Author : pincman 3 | * HomePage : https://pincman.com 4 | * Support : support@pincman.com 5 | * Created_at : 2021-12-14 00:07:50 +0800 6 | * Updated_at : 2022-01-09 14:29:57 +0800 7 | * Path : /src/components/Router/provider.tsx 8 | * Description : 路由组件包装器 9 | * LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { 14 | BrowserRouter, 15 | HashRouter, 16 | matchRoutes, 17 | renderMatches, 18 | useLocation, 19 | RouteObject, 20 | } from 'react-router-dom'; 21 | 22 | import { useRouter, useRouterStatus } from './hooks'; 23 | 24 | /** 25 | * 根据路由渲染列表生成react router路由表 26 | * 也可以直接使用内置的`useRoutes`来替代 27 | * @param props 28 | */ 29 | const RoutesList: FC<{ routes: RouteObject[]; basename: string }> = ({ routes, basename }) => { 30 | const location = useLocation(); 31 | return renderMatches(matchRoutes(routes, location, basename)); 32 | }; 33 | /** 34 | * 路由渲染组件,用于渲染最终路由 35 | */ 36 | const RouterRender: FC = () => { 37 | const { basePath: basename, hash, window } = useRouter.useConfig(); 38 | const renders = useRouter.useRenders(); 39 | return hash ? ( 40 | 41 | 42 | 43 | ) : ( 44 | 45 | 46 | 47 | ); 48 | }; 49 | /** 50 | * 路由组件包装器,在路由渲染列表生成后立即渲染路由 51 | */ 52 | export const Router = () => { 53 | const success = useRouterStatus.useSuccess(); 54 | return success ? : null; 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/Router/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-16 17:08:42 +0800 6 | * @Updated_at : 2022-01-16 00:26:19 +0800 7 | * @Path : /src/components/Router/store.ts 8 | * @Description : 路由组件状态池 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | 14 | import { createStore } from '@/utils'; 15 | 16 | import { getDefaultStore } from './_default.config'; 17 | import { RouterStatusType, RouterStoreType } from './types'; 18 | 19 | /** 20 | * 路由初始化信号状态池 21 | */ 22 | export const RouterStatus = createStore(() => ({ 23 | next: false, 24 | success: false, 25 | })); 26 | /** 27 | * 路由状态池 28 | */ 29 | export const RouterStore = createStore(() => getDefaultStore()); 30 | -------------------------------------------------------------------------------- /src/components/Router/utils/factory/index.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | 3 | import { isArray } from 'lodash-es'; 4 | 5 | import { getUser } from '@/components/Auth'; 6 | 7 | import { RouterStatus, RouterStore } from '../../store'; 8 | 9 | import { RouteOption } from '../../types'; 10 | 11 | import { filteAccessRoutes, filteWhiteList } from './filter'; 12 | import { generateRoutes } from './generate'; 13 | 14 | /** 15 | * 构建用户生成路由渲染的路由列表 16 | * @param fetcher 远程Request对象 17 | */ 18 | export const factoryRoutes = async (fetcher: AxiosInstance) => { 19 | const user = getUser(); 20 | const { config } = RouterStore.getState(); 21 | RouterStatus.setState((state) => ({ ...state, next: false, success: false })); 22 | // 如果没有启用auth功能则使用配置中路由直接开始生成 23 | if (!config.auth.enabled) { 24 | RouterStore.setState((state) => { 25 | state.routes = [...state.config.routes.constants, ...state.config.routes.dynamic]; 26 | }); 27 | generateRoutes(); 28 | } else if (user) { 29 | // 如果用户已登录,首先过滤精通路由 30 | RouterStore.setState((state) => { 31 | state.routes = filteAccessRoutes(user, state.config.routes.constants, config.auth, { 32 | basePath: config.basePath, 33 | }); 34 | }); 35 | if (!config.server) { 36 | // 如果路由通过配置生成则直接过滤动态路由并合并已过滤的静态路由 37 | RouterStore.setState((state) => { 38 | state.routes = filteAccessRoutes( 39 | user, 40 | [...state.routes, ...state.config.routes.dynamic], 41 | config.auth, 42 | { 43 | basePath: config.basePath, 44 | }, 45 | ); 46 | }); 47 | generateRoutes(); 48 | } else { 49 | try { 50 | // 如果路由通过服务器生成则直接合并已过滤的动态路由(权限过滤由服务端搞定) 51 | const { data } = await fetcher.get(config.server); 52 | if (isArray(data)) { 53 | RouterStore.setState((state) => { 54 | state.routes = [...state.routes, ...data]; 55 | }); 56 | } 57 | } catch (error) { 58 | console.log(error); 59 | } 60 | } 61 | } else { 62 | // 如果没有登录用户则根据白名单和路由项中的access为false来生成路由 63 | RouterStore.setState((state) => { 64 | state.routes = [ 65 | ...filteWhiteList(state.config.routes.constants, config.auth, { 66 | basePath: config.basePath, 67 | }), 68 | ...filteWhiteList(state.config.routes.dynamic, config.auth, { 69 | basePath: config.basePath, 70 | }), 71 | ]; 72 | }); 73 | generateRoutes(); 74 | RouterStatus.setState((state) => ({ ...state, next: false })); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/Router/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author : pincman 3 | * @HomePage : https://pincman.com 4 | * @Support : support@pincman.com 5 | * @Created_at : 2021-12-14 00:07:50 +0800 6 | * @Updated_at : 2022-01-18 06:00:37 +0800 7 | * @Path : /src/components/Router/utils/helpers.ts 8 | * @Description : 工具函数 9 | * @LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import { trim } from 'lodash-es'; 14 | import { isNil } from 'ramda'; 15 | 16 | import { isUrl } from '@/utils'; 17 | 18 | import { IndexRouteOption, PathRouteOption, RouteOption } from '../types'; 19 | 20 | /** 21 | * 组装并格式化路由路径以获取完整路径 22 | * @param item 路由配置 23 | * @param basePath 基础路径 24 | * @param parentPath 父路径 25 | */ 26 | export const mergeRoutePath = ( 27 | item: RouteOption, 28 | basePath: string, 29 | parentPath?: string, 30 | ): string => { 31 | const currentPath = 'path' in item && typeof item.path === 'string' ? item.path : ''; 32 | // 如果没有传入父路径则使用basePath作为路由前缀 33 | let prefix = !parentPath ? basePath : `/${trim(parentPath, '/')}`; 34 | // 如果是父路径下的根路径则直接父路径 35 | if (trim(currentPath, '/') === '') return prefix; 36 | // 如果是顶级根路径并且当前路径以通配符"*"开头则直接返回当前路径 37 | if (prefix === '/' && currentPath.startsWith('*')) return currentPath; 38 | // 如果前缀不是"/",则为在前缀后添加"/"作为与当前路径的连接符 39 | if (prefix !== '/') prefix = `${prefix}/`; 40 | // 生成最终路径 41 | return `${prefix}${trim(currentPath, '/')}`; 42 | }; 43 | 44 | export const checkRoute = (option: RouteOption): option is PathRouteOption | IndexRouteOption => { 45 | if ('index' in option) return option.index; 46 | if ('path' in option) { 47 | return !isNil(option.path) && option.path.length > 0 && !isUrl(option.path); 48 | } 49 | return false; 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/Router/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './factory'; 2 | export * from './helpers'; 3 | -------------------------------------------------------------------------------- /src/components/Router/utils/views.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Author : pincman 3 | * HomePage : https://pincman.com 4 | * Support : support@pincman.com 5 | * Created_at : 2021-12-14 00:07:50 +0800 6 | * Updated_at : 2022-01-14 00:46:36 +0800 7 | * Path : /src/components/Router/utils/views.tsx 8 | * Description : 页面和视图组件 9 | * LastEditors : pincman 10 | * Copyright 2022 pincman, All Rights Reserved. 11 | * 12 | */ 13 | import loadable from '@loadable/component'; 14 | import pMinDelay from 'p-min-delay'; 15 | import { FC, FunctionComponent } from 'react'; 16 | import { Navigate, useLocation } from 'react-router-dom'; 17 | import { timeout } from 'promise-timeout'; 18 | import { has } from 'lodash-es'; 19 | 20 | import { RoutePage } from '../types'; 21 | 22 | /** 23 | * 根据正则和glob递归获取所有动态页面导入映射 24 | * [key:bar/foo]: () => import('{起始目录: 如page}/bar/foo.blade.tsx') 25 | * @param imports 需要遍历的路径规则,支持glob 26 | * @param reg 用于匹配出key的正则表达式 27 | */ 28 | const getAsyncImports = (imports: Record Promise>, reg: RegExp) => { 29 | return Object.keys(imports) 30 | .map((key) => { 31 | const names = reg.exec(key); 32 | return Array.isArray(names) && names.length >= 2 33 | ? { [names[1]]: imports[key] } 34 | : undefined; 35 | }) 36 | .filter((m) => !!m) 37 | .reduce((o, n) => ({ ...o, ...n }), []) as unknown as Record Promise>; 38 | }; 39 | /** 40 | * 所有动态页面映射 41 | */ 42 | export const pages = getAsyncImports( 43 | import.meta.glob('../../../views/**/*.blade.{tsx,jsx}'), 44 | /..\/..\/\..\/views\/([\w+.?/?]+)(.blade.tsx)|(.blade.jsx)/i, 45 | ); 46 | 47 | /** 48 | * 未登录跳转页面组件 49 | * @param props 50 | */ 51 | export const AuthRedirect: FC<{ 52 | /** 登录跳转地址 */ 53 | loginPath?: string; 54 | }> = ({ loginPath }) => { 55 | const location = useLocation(); 56 | let redirect = `?redirect=${location.pathname}`; 57 | if (location.search) redirect = `${redirect}${location.search}`; 58 | return ; 59 | }; 60 | /** 61 | * 异步页面组件 62 | * @param props 63 | */ 64 | export const getAsyncPage = (props: { 65 | /** 缓存key */ 66 | cacheKey: string; 67 | /** loading组件 */ 68 | loading: FunctionComponent | false; 69 | /** 页面路径 */ 70 | page: string; 71 | }) => { 72 | const { cacheKey, page } = props; 73 | const fallback: JSX.Element | undefined = props.loading ? : undefined; 74 | if (!has(pages, page)) throw new Error(`Page ${page} not exits in 'views' dir!`); 75 | return loadable(() => timeout(pMinDelay(pages[page](), 50), 220000), { 76 | cacheKey: () => cacheKey, 77 | fallback, 78 | }); 79 | }; 80 | 81 | export const IFramePage: RoutePage<{ to: string }> = ({ route, to }) => { 82 | return