dark
8 months ago
commit
8327fd09e8
46 changed files with 4859 additions and 0 deletions
-
24.eslintrc.cjs
-
24.gitignore
-
30README.md
-
13index.html
-
21mock/departments.ts
-
98mock/menus.ts
-
47package.json
-
1public/vite.svg
-
20src/App.css
-
11src/App.tsx
-
35src/Auth.tsx
-
1src/assets/react.svg
-
74src/components/breadcrumb/index.tsx
-
64src/components/error-boundary/index.tsx
-
52src/components/icon/index.tsx
-
23src/hooks/useFetch.ts
-
0src/index.css
-
22src/layout/FormPageLayout.tsx
-
21src/layout/ListPageLayout.tsx
-
144src/layout/RootLayout.tsx
-
20src/layout/TreePageLayout.tsx
-
69src/layout/_defaultProps.tsx
-
10src/main.tsx
-
25src/pages/dashboard/index.tsx
-
10src/pages/list/index.tsx
-
15src/pages/list/list.tsx
-
146src/pages/list/tree.tsx
-
11src/pages/system/departments/index.tsx
-
60src/pages/system/menus/index.tsx
-
11src/pages/system/roles/index.tsx
-
11src/pages/system/users/index.tsx
-
0src/patches/x-fetch.ts
-
17src/request.ts
-
149src/routes.tsx
-
25src/service/system.ts
-
44src/store/department.ts
-
46src/store/system.ts
-
9src/store/types/department.d.ts
-
8src/store/user.ts
-
66src/types.d.ts
-
33src/utils/uuid.ts
-
1src/vite-env.d.ts
-
34tsconfig.json
-
11tsconfig.node.json
-
27vite.config.ts
-
3276yarn.lock
@ -0,0 +1,24 @@ |
|||||
|
module.exports = { |
||||
|
root: true, |
||||
|
env: { browser: true, es2020: true }, |
||||
|
extends: [ |
||||
|
'eslint:recommended', |
||||
|
'plugin:@typescript-eslint/recommended', |
||||
|
'plugin:react-hooks/recommended', |
||||
|
], |
||||
|
ignorePatterns: [ 'dist', '.eslintrc.cjs' ], |
||||
|
parser: '@typescript-eslint/parser', |
||||
|
plugins: [ 'react-refresh' ], |
||||
|
rules: { |
||||
|
'react-refresh/only-export-components': [ |
||||
|
'warn', |
||||
|
{ allowConstantExport: true }, |
||||
|
], |
||||
|
'@typescript-eslint/ban-ts-comment': [ 'error', { |
||||
|
'ts-expect-error': 'allow-with-description', |
||||
|
'ts-ignore': 'allow-with-description', |
||||
|
'minimumDescriptionLength': 10 |
||||
|
} ], |
||||
|
'@typescript-eslint/no-explicit-any': 'off', |
||||
|
}, |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
pnpm-debug.log* |
||||
|
lerna-debug.log* |
||||
|
|
||||
|
node_modules |
||||
|
dist |
||||
|
dist-ssr |
||||
|
*.local |
||||
|
|
||||
|
# Editor directories and files |
||||
|
.vscode/* |
||||
|
!.vscode/extensions.json |
||||
|
.idea |
||||
|
.DS_Store |
||||
|
*.suo |
||||
|
*.ntvs* |
||||
|
*.njsproj |
||||
|
*.sln |
||||
|
*.sw? |
@ -0,0 +1,30 @@ |
|||||
|
# React + TypeScript + Vite |
||||
|
|
||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. |
||||
|
|
||||
|
Currently, two official plugins are available: |
||||
|
|
||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh |
||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh |
||||
|
|
||||
|
## Expanding the ESLint configuration |
||||
|
|
||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: |
||||
|
|
||||
|
- Configure the top-level `parserOptions` property like this: |
||||
|
|
||||
|
```js |
||||
|
export default { |
||||
|
// other rules... |
||||
|
parserOptions: { |
||||
|
ecmaVersion: 'latest', |
||||
|
sourceType: 'module', |
||||
|
project: ['./tsconfig.json', './tsconfig.node.json'], |
||||
|
tsconfigRootDir: __dirname, |
||||
|
}, |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` |
||||
|
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` |
||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list |
@ -0,0 +1,13 @@ |
|||||
|
<!doctype html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>Crazy Pro</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div id="root"></div> |
||||
|
<script type="module" src="/src/main.tsx"></script> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,21 @@ |
|||||
|
export default [ |
||||
|
{ |
||||
|
url: '/api/departments', |
||||
|
method: 'get', |
||||
|
response: () => { |
||||
|
return { |
||||
|
code: 200, |
||||
|
data: [ |
||||
|
{ |
||||
|
id: '1', |
||||
|
name: '开发部' |
||||
|
}, |
||||
|
{ |
||||
|
id: '2', |
||||
|
name: '测试部' |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
@ -0,0 +1,98 @@ |
|||||
|
import { MockMethod } from 'vite-plugin-mock' |
||||
|
|
||||
|
export default [ |
||||
|
{ |
||||
|
url: '/api/menus', |
||||
|
method: 'get', |
||||
|
response: () => { |
||||
|
return { |
||||
|
code: 200, |
||||
|
message: 'Success', |
||||
|
data: [ |
||||
|
{ |
||||
|
id:1, |
||||
|
path: '/welcome', |
||||
|
name: '欢迎', |
||||
|
icon: 'ApplicationMenu', |
||||
|
component: './pages/dashboard', |
||||
|
type: 1, |
||||
|
order: 1, |
||||
|
}, |
||||
|
{ |
||||
|
id:2, |
||||
|
path: '/admin', |
||||
|
name: '系统管理', |
||||
|
icon: 'SettingTwo', |
||||
|
access: 'canAdmin', |
||||
|
type: 1, |
||||
|
order: 2, |
||||
|
children: [ |
||||
|
{ |
||||
|
id:3, |
||||
|
path: '/admin/menus', |
||||
|
name: '导航管理', |
||||
|
icon: 'AllApplication', |
||||
|
component: './pages/system/menus', |
||||
|
|
||||
|
type: 1, |
||||
|
}, |
||||
|
{ |
||||
|
id:4, |
||||
|
path: '/admin/users', |
||||
|
name: '用户管理', |
||||
|
icon: 'PeoplesTwo', |
||||
|
component: './pages/system/users', |
||||
|
type: 1, |
||||
|
}, |
||||
|
{ |
||||
|
id:5, |
||||
|
path: '/admin/departments', |
||||
|
name: '部门管理', |
||||
|
icon: 'Browser', |
||||
|
component: './pages/system/departments', |
||||
|
type: 1, |
||||
|
}, |
||||
|
{ |
||||
|
id:6, |
||||
|
path: '/admin/roles', |
||||
|
name: '角色管理', |
||||
|
icon: 'Permissions', |
||||
|
component: './pages/system/roles', |
||||
|
type: 1, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
id:7, |
||||
|
name: '列表页', |
||||
|
icon: 'ListView', |
||||
|
path: '/list', |
||||
|
type: 1, |
||||
|
children: [ |
||||
|
{ |
||||
|
id:8, |
||||
|
path: '/list/index', |
||||
|
name: '列表页面', |
||||
|
component: './pages/list/list', |
||||
|
type: 1, |
||||
|
}, |
||||
|
{ |
||||
|
id:9, |
||||
|
path: '/list/tree', |
||||
|
name: '树形列表页面', |
||||
|
component: './pages/list/tree', |
||||
|
type: 1, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
id:10, |
||||
|
path: 'https://ant.design', |
||||
|
name: 'Ant Design 官网外链', |
||||
|
type: 3, |
||||
|
}, |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] as MockMethod[] |
@ -0,0 +1,47 @@ |
|||||
|
{ |
||||
|
"name": "pro", |
||||
|
"private": true, |
||||
|
"version": "0.0.0", |
||||
|
"type": "module", |
||||
|
"scripts": { |
||||
|
"dev": "vite --host --port 3000", |
||||
|
"build": "tsc && vite build", |
||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", |
||||
|
"preview": "vite preview" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@ant-design/icons": "^5.3.6", |
||||
|
"@ant-design/pro-components": "^2.7.0", |
||||
|
"@formily/antd-v5": "^1.2.0", |
||||
|
"@formily/core": "^2.3.1", |
||||
|
"@formily/react": "^2.3.1", |
||||
|
"@icon-park/react": "^1.4.2", |
||||
|
"@tanstack/query-core": "^5.29.0", |
||||
|
"@tanstack/react-query": "^5.29.2", |
||||
|
"@tanstack/react-router": "^1.26.20", |
||||
|
"antd": "^5.16.1", |
||||
|
"axios": "^1.6.8", |
||||
|
"dayjs": "^1.11.10", |
||||
|
"jotai": "^2.8.0", |
||||
|
"jotai-tanstack-query": "^0.8.5", |
||||
|
"react": "^18.2.0", |
||||
|
"react-dom": "^18.2.0", |
||||
|
"wonka": "^6.3.4" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@tanstack/router-devtools": "^1.26.20", |
||||
|
"@tanstack/router-vite-plugin": "^1.26.16", |
||||
|
"@types/react": "^18.2.66", |
||||
|
"@types/react-dom": "^18.2.22", |
||||
|
"@typescript-eslint/eslint-plugin": "^7.2.0", |
||||
|
"@typescript-eslint/parser": "^7.2.0", |
||||
|
"@vitejs/plugin-react": "^4.2.1", |
||||
|
"eslint": "^8.57.0", |
||||
|
"eslint-plugin-react-hooks": "^4.6.0", |
||||
|
"eslint-plugin-react-refresh": "^0.4.6", |
||||
|
"mockjs": "^1.1.0", |
||||
|
"typescript": "^5.2.2", |
||||
|
"vite": "^5.2.0", |
||||
|
"vite-plugin-mock": "^3.0.1" |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> |
@ -0,0 +1,20 @@ |
|||||
|
.i-icon { |
||||
|
display: flex; |
||||
|
} |
||||
|
.ant-tree-iconEle{ |
||||
|
.i-icon { |
||||
|
display: inherit; |
||||
|
line-height: 28px; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.top-breadcrumb { |
||||
|
.item { |
||||
|
|
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 5px; |
||||
|
} |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
import './App.css' |
||||
|
import { RootProvider } from './routes.tsx' |
||||
|
|
||||
|
function App() { |
||||
|
|
||||
|
return ( |
||||
|
<RootProvider/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default App |
@ -0,0 +1,35 @@ |
|||||
|
import React from 'react' |
||||
|
import { Props } from './types' |
||||
|
import { useAtomValue } from 'jotai' |
||||
|
import { authAtom } from './store/user.ts' |
||||
|
|
||||
|
|
||||
|
export type AuthProps = Props & { |
||||
|
//是否渲染没有权限的组件
|
||||
|
noAuth?: React.ReactNode |
||||
|
//权限key
|
||||
|
authKey?: string[] |
||||
|
} |
||||
|
|
||||
|
export const Auth: React.FC<AuthProps> = (props) => { |
||||
|
|
||||
|
const auth = useAtomValue(authAtom) |
||||
|
|
||||
|
if (!auth.isLogin) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
if (props.authKey && props.authKey.length > 0) { |
||||
|
if (props.authKey.some(key => !auth.authKey?.includes(key))) { |
||||
|
return props.noAuth || null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
{props!.children} |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Auth |
@ -0,0 +1 @@ |
|||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> |
@ -0,0 +1,74 @@ |
|||||
|
import { Breadcrumb, BreadcrumbProps, Dropdown } from 'antd' |
||||
|
import { Link, useNavigate } from '@tanstack/react-router' |
||||
|
import { DownOutlined } from '@ant-design/icons' |
||||
|
import { getIcon } from '@/components/icon' |
||||
|
import { useCallback } from 'react' |
||||
|
|
||||
|
export const PageBreadcrumb = (props: BreadcrumbProps & { |
||||
|
showIcon?: boolean; |
||||
|
}) => { |
||||
|
|
||||
|
const nav = useNavigate() |
||||
|
const { items = [], showIcon = true, ...other } = props |
||||
|
|
||||
|
const renderIcon = useCallback((icon: any) => { |
||||
|
if (icon && showIcon) { |
||||
|
return getIcon(icon) |
||||
|
} |
||||
|
return null |
||||
|
}, []) |
||||
|
|
||||
|
const itemRender = (route) => { |
||||
|
|
||||
|
const isLast = route?.path === items[items.length - 1]?.path |
||||
|
|
||||
|
if (route.children) { |
||||
|
const items = route.children.map((item) => { |
||||
|
return { |
||||
|
...item, |
||||
|
key: item.path, |
||||
|
label: item.name, |
||||
|
} |
||||
|
}) |
||||
|
return ( |
||||
|
<Dropdown menu={{ |
||||
|
items, onClick: (e) => { |
||||
|
nav({ |
||||
|
to: e.key |
||||
|
}) |
||||
|
} |
||||
|
}} |
||||
|
trigger={[ 'hover' ]}> |
||||
|
{ |
||||
|
!route.component ? <a className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span> |
||||
|
<DownOutlined/></a> |
||||
|
: <Link to={`/${route.path}`} |
||||
|
className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span> |
||||
|
<DownOutlined/> |
||||
|
</Link> |
||||
|
} |
||||
|
|
||||
|
|
||||
|
</Dropdown> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return isLast ? ( |
||||
|
<span className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span></span> |
||||
|
) : ( |
||||
|
<Link to={`/${route.path}`} |
||||
|
className={'item'}>{renderIcon(route.icon)}<span>{route.name}</span></Link> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Breadcrumb {...other} |
||||
|
items={items} |
||||
|
itemRender={itemRender} |
||||
|
/> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default PageBreadcrumb |
@ -0,0 +1,64 @@ |
|||||
|
import React, { ErrorInfo } from 'react' |
||||
|
import { Button, Result } from 'antd' |
||||
|
|
||||
|
export class ErrorBoundary extends React.Component< |
||||
|
Record<string, any>, |
||||
|
{ hasError: boolean; errorInfo: string } |
||||
|
> { |
||||
|
state = { hasError: false, errorInfo: '' } |
||||
|
|
||||
|
static getDerivedStateFromError(error: Error) { |
||||
|
return { hasError: true, errorInfo: error.message } |
||||
|
} |
||||
|
|
||||
|
componentDidCatch(error: any, errorInfo: ErrorInfo) { |
||||
|
// You can also log the error to an error reporting service
|
||||
|
// eslint-disable-next-line no-console
|
||||
|
console.log(error, errorInfo) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
if (this.state.hasError) { |
||||
|
// You can render any custom fallback UI
|
||||
|
return ( |
||||
|
<Result |
||||
|
style={{ |
||||
|
height: '100%', |
||||
|
background: '#fff', |
||||
|
}} |
||||
|
title="错误信息" |
||||
|
extra={ |
||||
|
<> |
||||
|
<div |
||||
|
style={{ |
||||
|
maxWidth: 620, |
||||
|
textAlign: 'start', |
||||
|
backgroundColor: 'rgba(255,229,100,0.3)', |
||||
|
borderInlineStartColor: '#ffe564', |
||||
|
borderInlineStartWidth: '9px', |
||||
|
borderInlineStartStyle: 'solid', |
||||
|
padding: '20px 45px 20px 26px', |
||||
|
margin: 'auto', |
||||
|
marginBlockEnd: '30px', |
||||
|
marginBlockStart: '20px', |
||||
|
}} |
||||
|
> |
||||
|
<p>{this.state.errorInfo}</p> |
||||
|
</div> |
||||
|
<Button |
||||
|
danger |
||||
|
type="primary" |
||||
|
onClick={() => { |
||||
|
window.location.reload() |
||||
|
}} |
||||
|
> |
||||
|
刷新页面 |
||||
|
</Button> |
||||
|
</> |
||||
|
} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
return this.props.children |
||||
|
} |
||||
|
} |
@ -0,0 +1,52 @@ |
|||||
|
import IconAll, { ALL_ICON_KEYS, IconType, IIconAllProps } from '@icon-park/react/es/all' |
||||
|
import React, { Fragment } from 'react' |
||||
|
import * as AntIcons from '@ant-design/icons/es/icons' |
||||
|
import { IconComponentProps } from '@ant-design/icons/es/components/Icon' |
||||
|
|
||||
|
export function Icon(props: Partial<IIconAllProps | IconComponentProps>) { |
||||
|
|
||||
|
const { type, ...other } = props |
||||
|
const AntIcon = AntIcons[type as keyof typeof AntIcons] |
||||
|
if (AntIcon) { |
||||
|
return <AntIcon {...other}/> |
||||
|
} |
||||
|
|
||||
|
//如果是http或https链接,直接返回图片
|
||||
|
if (type && (type.startsWith('http') || type.startsWith('https') || type.startsWith('data:image'))) { |
||||
|
// @ts-ignore 没有办法把所有的属性都传递给img
|
||||
|
return <img src={type} alt="icon" width={16} height={16} {...other}/> |
||||
|
} |
||||
|
|
||||
|
if (ALL_ICON_KEYS.indexOf(type as IconType) < 0) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Fragment> |
||||
|
<IconAll type={type as IconType} |
||||
|
theme="outline" size="20" fill="#868686" strokeWidth={3} |
||||
|
{...other}/> |
||||
|
</Fragment> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||
|
export const getIcon = (type: string, props?: Partial<IIconAllProps>) => { |
||||
|
|
||||
|
if(React.isValidElement(type)){ |
||||
|
return type |
||||
|
} |
||||
|
//判断是否为json格式
|
||||
|
if (type && type.startsWith('{') && type.endsWith('}')) { |
||||
|
try { |
||||
|
const obj = JSON.parse(type) |
||||
|
type = obj.type |
||||
|
props = obj |
||||
|
} catch (e) { /* empty */ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return <Icon type={type} {...props}/> |
||||
|
} |
||||
|
|
||||
|
export default Icon |
@ -0,0 +1,23 @@ |
|||||
|
import { atomFamily } from 'jotai/utils' |
||||
|
import { IDataProps } from '@/types' |
||||
|
import { useMemo, useRef } from 'react' |
||||
|
import { generateUUID } from '@/utils/uuid' |
||||
|
|
||||
|
|
||||
|
export type FetchProps<T> = IDataProps<T> & { |
||||
|
key?: string |
||||
|
initialValue?: T |
||||
|
} |
||||
|
|
||||
|
const factoryAtom = atomFamily((param: FetchProps<any>) => param.initialValue) |
||||
|
export const useInnerAtom = <T>(props: FetchProps<T>) => { |
||||
|
const { initialValue, key } = props |
||||
|
// 生成唯一的key
|
||||
|
const _key = useRef(key) |
||||
|
if (!_key.current) { |
||||
|
_key.current = generateUUID() |
||||
|
} |
||||
|
|
||||
|
return useMemo(() => factoryAtom({ initialValue }), [ _key.current ]) |
||||
|
|
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import React from 'react' |
||||
|
import { createLazyRoute, Outlet } from '@tanstack/react-router' |
||||
|
|
||||
|
interface IFormPageLayoutProps { |
||||
|
children: React.ReactNode |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const FormPageLayout: React.FC<IFormPageLayoutProps> = (props) => { |
||||
|
return ( |
||||
|
<> |
||||
|
{props.children} |
||||
|
<Outlet/> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default FormPageLayout |
||||
|
|
||||
|
export const GenRoute = (id: string) => createLazyRoute(id)({ |
||||
|
component: FormPageLayout, |
||||
|
}) |
@ -0,0 +1,21 @@ |
|||||
|
import React from 'react' |
||||
|
import { createLazyRoute, Outlet } from '@tanstack/react-router' |
||||
|
|
||||
|
interface IListPageLayoutProps { |
||||
|
children: React.ReactNode |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const ListPageLayout: React.FC<IListPageLayoutProps> = (props) => { |
||||
|
return ( |
||||
|
<>{props.children} |
||||
|
<Outlet/> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default ListPageLayout |
||||
|
|
||||
|
export const GenRoute = (id: string) => createLazyRoute(id)({ |
||||
|
component: ListPageLayout, |
||||
|
}) |
@ -0,0 +1,144 @@ |
|||||
|
import { |
||||
|
ProConfigProvider, |
||||
|
ProLayout, |
||||
|
} from '@ant-design/pro-components' |
||||
|
import { ConfigProvider, Dropdown } from 'antd' |
||||
|
import { useState } from 'react' |
||||
|
import defaultProps from './_defaultProps' |
||||
|
import { Link, Outlet, useRouteContext } from '@tanstack/react-router' |
||||
|
import Icon from '../components/icon' |
||||
|
import { MenuItem } from '@/types' |
||||
|
import PageBreadcrumb from '@/components/breadcrumb' |
||||
|
import { ErrorBoundary } from '@/components/error-boundary' |
||||
|
|
||||
|
|
||||
|
//根据menuData生成Breadcrumb所需的数据
|
||||
|
const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => { |
||||
|
const breadcrumbData: any[] = [] |
||||
|
const findItem = (menuData: any[], pathname: string) => { |
||||
|
for (let i = 0; i < menuData.length; i++) { |
||||
|
if (menuData[i].path === pathname) { |
||||
|
breadcrumbData.push(menuData[i]) |
||||
|
return true |
||||
|
} |
||||
|
if (menuData[i].children) { |
||||
|
if (findItem(menuData[i].children, pathname)) { |
||||
|
breadcrumbData.push(menuData[i]) |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
findItem(menuData, pathname) |
||||
|
return breadcrumbData.reverse() |
||||
|
} |
||||
|
|
||||
|
export default () => { |
||||
|
|
||||
|
const { menuData } = useRouteContext({ |
||||
|
from: undefined, |
||||
|
strict: false, |
||||
|
select: (state) => state |
||||
|
}) |
||||
|
|
||||
|
const items = getBreadcrumbData(menuData, location.pathname) |
||||
|
|
||||
|
const [ pathname, setPathname ] = useState(location.pathname) |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
id="crazy-pro-layout" |
||||
|
style={{ |
||||
|
height: '100vh', |
||||
|
// overflow: 'auto',
|
||||
|
}} |
||||
|
> |
||||
|
<ProConfigProvider hashed={false}> |
||||
|
<ConfigProvider |
||||
|
getTargetContainer={() => { |
||||
|
return document.getElementById('crazy-pro-layout') || document.body |
||||
|
}} |
||||
|
> |
||||
|
<ProLayout |
||||
|
headerContentRender={() => <PageBreadcrumb |
||||
|
className={'top-breadcrumb'} |
||||
|
showIcon={false} |
||||
|
items={items}/>} |
||||
|
title="Crazy Pro" |
||||
|
{...defaultProps} |
||||
|
route={{ |
||||
|
path: '/', |
||||
|
routes: menuData |
||||
|
}} |
||||
|
location={{ |
||||
|
pathname, |
||||
|
}} |
||||
|
token={{ |
||||
|
header: { |
||||
|
colorBgMenuItemSelected: 'rgba(0,0,0,0.04)', |
||||
|
}, |
||||
|
}} |
||||
|
menu={{ |
||||
|
collapsedShowGroupTitle: true, |
||||
|
}} |
||||
|
avatarProps={{ |
||||
|
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg', |
||||
|
size: 'small', |
||||
|
title: '管理员', |
||||
|
render: (_, dom) => { |
||||
|
return ( |
||||
|
<Dropdown |
||||
|
menu={{ |
||||
|
items: [ |
||||
|
{ |
||||
|
key: 'logout', |
||||
|
icon: <Icon type={'Logout'}/>, |
||||
|
label: '退出登录', |
||||
|
}, |
||||
|
], |
||||
|
}} |
||||
|
> |
||||
|
{dom} |
||||
|
</Dropdown> |
||||
|
) |
||||
|
}, |
||||
|
}} |
||||
|
actionsRender={(props) => { |
||||
|
if (props.isMobile) return [] |
||||
|
if (typeof window === 'undefined') return [] |
||||
|
return [] |
||||
|
}} |
||||
|
menuRender={(_, defaultDom) => ( |
||||
|
<> |
||||
|
{defaultDom} |
||||
|
</> |
||||
|
)} |
||||
|
menuItemRender={(item, dom) => { |
||||
|
return <div onClick={() => { |
||||
|
setPathname(item.path || '/welcome') |
||||
|
}} |
||||
|
> |
||||
|
<Link to={item.path} target={item.type === 3 ? '_blank': '_self' }> |
||||
|
{dom} |
||||
|
</Link> |
||||
|
</div> |
||||
|
}} |
||||
|
{...{ |
||||
|
'layout': 'mix', |
||||
|
'navTheme': 'light', |
||||
|
'contentWidth': 'Fluid', |
||||
|
'fixSiderbar': true, |
||||
|
'colorPrimary': '#1677FF', |
||||
|
'siderMenuType': 'group', |
||||
|
// layout: 'side',
|
||||
|
}} |
||||
|
ErrorBoundary={ErrorBoundary} |
||||
|
> |
||||
|
<Outlet/> |
||||
|
</ProLayout> |
||||
|
</ConfigProvider> |
||||
|
</ProConfigProvider> |
||||
|
</div> |
||||
|
) |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
import React from 'react' |
||||
|
import { createLazyRoute, Outlet } from '@tanstack/react-router' |
||||
|
|
||||
|
interface ITreePageLayoutProps { |
||||
|
children: React.ReactNode |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const TreePageLayout: React.FC<ITreePageLayoutProps> = (props) => { |
||||
|
return ( |
||||
|
<> |
||||
|
{props.children} |
||||
|
<Outlet/> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const GenRoute = (id: string) => createLazyRoute(id)({ |
||||
|
component: TreePageLayout, |
||||
|
}) |
@ -0,0 +1,69 @@ |
|||||
|
export default { |
||||
|
route: { |
||||
|
path: '/', |
||||
|
routes: [], |
||||
|
}, |
||||
|
location: { |
||||
|
// pathname: '/',
|
||||
|
}, |
||||
|
bgLayoutImgList: [ |
||||
|
{ |
||||
|
src: 'https://img.alicdn.com/imgextra/i2/O1CN01O4etvp1DvpFLKfuWq_!!6000000000279-2-tps-609-606.png', |
||||
|
left: 85, |
||||
|
bottom: 100, |
||||
|
height: '303px', |
||||
|
}, |
||||
|
{ |
||||
|
src: 'https://img.alicdn.com/imgextra/i2/O1CN01O4etvp1DvpFLKfuWq_!!6000000000279-2-tps-609-606.png', |
||||
|
bottom: -68, |
||||
|
right: -45, |
||||
|
height: '303px', |
||||
|
}, |
||||
|
{ |
||||
|
src: 'https://img.alicdn.com/imgextra/i3/O1CN018NxReL1shX85Yz6Cx_!!6000000005798-2-tps-884-496.png', |
||||
|
bottom: 0, |
||||
|
left: 0, |
||||
|
width: '331px', |
||||
|
}, |
||||
|
], |
||||
|
appList: [ |
||||
|
{ |
||||
|
icon: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', |
||||
|
title: 'Ant Design', |
||||
|
desc: '杭州市较知名的 UI 设计语言', |
||||
|
url: 'https://ant.design', |
||||
|
}, |
||||
|
{ |
||||
|
icon: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', |
||||
|
title: 'AntV', |
||||
|
desc: '蚂蚁集团全新一代数据可视化解决方案', |
||||
|
url: 'https://antv.vision/', |
||||
|
target: '_blank', |
||||
|
}, |
||||
|
{ |
||||
|
icon: 'https://img.alicdn.com/tfs/TB1zomHwxv1gK0jSZFFXXb0sXXa-200-200.png', |
||||
|
title: 'umi', |
||||
|
desc: '插件化的企业级前端应用框架。', |
||||
|
url: 'https://umijs.org/zh-CN/docs', |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
icon: 'https://gw.alipayobjects.com/zos/rmsportal/XuVpGqBFxXplzvLjJBZB.svg', |
||||
|
title: '语雀', |
||||
|
desc: '知识创作与分享工具', |
||||
|
url: 'https://www.yuque.com/', |
||||
|
}, |
||||
|
{ |
||||
|
icon: 'https://gw.alipayobjects.com/zos/rmsportal/LFooOLwmxGLsltmUjTAP.svg', |
||||
|
title: 'Kitchen ', |
||||
|
desc: 'Sketch 工具集', |
||||
|
url: 'https://kitchen.alipay.com/', |
||||
|
}, |
||||
|
{ |
||||
|
icon: 'https://gw.alipayobjects.com/zos/bmw-prod/d3e3eb39-1cd7-4aa5-827c-877deced6b7e/lalxt4g3_w256_h256.png', |
||||
|
title: 'dumi', |
||||
|
desc: '为组件开发场景而生的文档工具', |
||||
|
url: 'https://d.umijs.org/zh-CN', |
||||
|
}, |
||||
|
], |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
import ReactDOM from 'react-dom/client' |
||||
|
import App from './App.tsx' |
||||
|
import './index.css' |
||||
|
|
||||
|
|
||||
|
ReactDOM.createRoot(document.getElementById('root')!).render( |
||||
|
// <React.StrictMode>
|
||||
|
<App/> |
||||
|
// </React.StrictMode>,
|
||||
|
) |
@ -0,0 +1,25 @@ |
|||||
|
import { ProCard } from '@ant-design/pro-components' |
||||
|
import { createLazyRoute } from '@tanstack/react-router' |
||||
|
|
||||
|
|
||||
|
const Index = () => { |
||||
|
return ( |
||||
|
<> |
||||
|
<ProCard |
||||
|
style={{ |
||||
|
height: '100vh', |
||||
|
minHeight: 800, |
||||
|
}} |
||||
|
> |
||||
|
|
||||
|
<h1>Dashboard</h1> |
||||
|
|
||||
|
</ProCard> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const Route = createLazyRoute('/welcome')({ |
||||
|
component: Index, |
||||
|
}) |
||||
|
export default Index |
@ -0,0 +1,10 @@ |
|||||
|
|
||||
|
const Index = () => { |
||||
|
return ( |
||||
|
<div> |
||||
|
普通页面 |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Index |
@ -0,0 +1,15 @@ |
|||||
|
import { createLazyRoute } from '@tanstack/react-router' |
||||
|
import { ProCard } from '@ant-design/pro-components' |
||||
|
const List = () => { |
||||
|
return ( |
||||
|
<ProCard> |
||||
|
列表页面 |
||||
|
</ProCard> |
||||
|
) |
||||
|
} |
||||
|
export const Route = createLazyRoute('/list/index')({ |
||||
|
component: List, |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
export default List |
@ -0,0 +1,146 @@ |
|||||
|
import { LightFilter, PageContainer, ProCard, ProColumns, ProTable } from '@ant-design/pro-components' |
||||
|
import { Tree, Input, Space, Button } from 'antd' |
||||
|
import { createLazyRoute } from '@tanstack/react-router' |
||||
|
import { departmentAtom } from '../../store/department.ts' |
||||
|
import { useAtomValue } from 'jotai' |
||||
|
import { getIcon } from '../../components/icon' |
||||
|
import dayjs from 'dayjs' |
||||
|
|
||||
|
//递归渲染树形结构,将name->title, id->key
|
||||
|
const renderTree = (data: any[]) => { |
||||
|
return data?.map((item) => { |
||||
|
if (item.children) { |
||||
|
return { |
||||
|
title: item.name, |
||||
|
key: item.id, |
||||
|
children: renderTree(item.children), |
||||
|
} |
||||
|
} |
||||
|
return { |
||||
|
title: item.name, |
||||
|
key: item.id, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const columns: ProColumns[] = [ |
||||
|
{ |
||||
|
title: '姓名', |
||||
|
dataIndex: 'name', |
||||
|
render: (_) => <a>{_}</a>, |
||||
|
formItemProps: { |
||||
|
lightProps: { |
||||
|
labelFormatter: (value) => `app-${value}`, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
title: '帐号', |
||||
|
dataIndex: 'account', |
||||
|
}, |
||||
|
{ |
||||
|
title: '创建者', |
||||
|
dataIndex: 'creator', |
||||
|
valueType: 'select', |
||||
|
search: false, |
||||
|
valueEnum: { |
||||
|
all: { text: '全部' }, |
||||
|
付小小: { text: '付小小' }, |
||||
|
曲丽丽: { text: '曲丽丽' }, |
||||
|
林东东: { text: '林东东' }, |
||||
|
陈帅帅: { text: '陈帅帅' }, |
||||
|
兼某某: { text: '兼某某' }, |
||||
|
}, |
||||
|
}, |
||||
|
//操作
|
||||
|
{ |
||||
|
title: '操作', |
||||
|
valueType: 'option', |
||||
|
render: (_, record) => { |
||||
|
return [ |
||||
|
<a key="editable" onClick={() => { |
||||
|
alert('edit') |
||||
|
}}>编辑</a>, |
||||
|
<a key="delete" onClick={() => { |
||||
|
alert('delete') |
||||
|
}}>删除</a>, |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
const TreePage = () => { |
||||
|
|
||||
|
const { data, isError, isPending } = useAtomValue(departmentAtom) |
||||
|
|
||||
|
if (isError) { |
||||
|
return <div>Error</div> |
||||
|
} |
||||
|
// if (isPending){
|
||||
|
// return <div>Loading</div>
|
||||
|
// }
|
||||
|
|
||||
|
return ( |
||||
|
<PageContainer breadcrumbRender={false}> |
||||
|
<ProCard split="vertical"> |
||||
|
<ProCard title="部门" |
||||
|
colSpan="25%" |
||||
|
loading={isPending} |
||||
|
extra={<> |
||||
|
<Button size={'small'} icon={getIcon('Plus')} shape={'circle'}/> |
||||
|
</>} |
||||
|
> |
||||
|
<Tree showLine={true} treeData={renderTree(data)}/> |
||||
|
</ProCard> |
||||
|
<ProCard headerBordered> |
||||
|
<div style={{ height: 360 }}> |
||||
|
<ProTable |
||||
|
rowKey="account" |
||||
|
headerTitle={'帐号列表'} |
||||
|
columns={columns} |
||||
|
dataSource={[ |
||||
|
{ |
||||
|
name: '张三', |
||||
|
account: 'zhangsan', |
||||
|
}, |
||||
|
{ |
||||
|
name: '李四', |
||||
|
account: 'lisi', |
||||
|
}, |
||||
|
]} |
||||
|
// pagination={false}
|
||||
|
options={{ |
||||
|
search: true, |
||||
|
}} |
||||
|
search={false} |
||||
|
toolbar={{ |
||||
|
search: { |
||||
|
onSearch: (value: string) => { |
||||
|
alert(value) |
||||
|
}, |
||||
|
}, |
||||
|
actions: [ |
||||
|
<Button |
||||
|
key="primary" |
||||
|
type="primary" |
||||
|
onClick={() => { |
||||
|
alert('add') |
||||
|
}} |
||||
|
> |
||||
|
添加 |
||||
|
</Button>, |
||||
|
], |
||||
|
}} |
||||
|
|
||||
|
/> |
||||
|
|
||||
|
</div> |
||||
|
</ProCard> |
||||
|
</ProCard> |
||||
|
</PageContainer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export default TreePage |
@ -0,0 +1,11 @@ |
|||||
|
import { PageContainer } from '@ant-design/pro-components' |
||||
|
|
||||
|
const Departments = () => { |
||||
|
return ( |
||||
|
<PageContainer breadcrumbRender={false}> |
||||
|
|
||||
|
</PageContainer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Departments |
@ -0,0 +1,60 @@ |
|||||
|
import { PageContainer, ProCard } from '@ant-design/pro-components' |
||||
|
import { Button, Space, Tree } from 'antd' |
||||
|
import { useAtom, useAtomValue } from 'jotai' |
||||
|
import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from '@/store/system.ts' |
||||
|
import { formatterMenuData } from '@/utils/uuid.ts' |
||||
|
import { CloseOutlined, PlusOutlined } from '@ant-design/icons' |
||||
|
|
||||
|
const Menus = () => { |
||||
|
|
||||
|
const { data, isLoading } = useAtomValue(menuDataAtom) |
||||
|
const [ currentMenu, setCurrentMenu ] = useAtom(selectedMenuAtom) |
||||
|
const [ selectedKey, setSelectedKey ] = useAtom(selectedMenuIdAtom) |
||||
|
|
||||
|
const treeData = formatterMenuData(data!) |
||||
|
|
||||
|
return ( |
||||
|
<PageContainer |
||||
|
breadcrumbRender={false} title={false}> |
||||
|
<ProCard split={'vertical'} |
||||
|
style={{ |
||||
|
height: 'calc(100vh - 164px)', |
||||
|
overflow: 'auto', |
||||
|
}} |
||||
|
> |
||||
|
<ProCard title={'导航'} |
||||
|
colSpan={'350px'} |
||||
|
extra={ |
||||
|
<Space> |
||||
|
<Button type="primary" size={'small'} icon={<PlusOutlined/>} shape={'circle'}/> |
||||
|
<Button type="default" danger={true} size={'small'} icon={<CloseOutlined/>} |
||||
|
shape={'circle'}/> |
||||
|
|
||||
|
</Space> |
||||
|
} |
||||
|
loading={isLoading}> |
||||
|
<Tree treeData={treeData} |
||||
|
|
||||
|
onSelect={(item) => { |
||||
|
setSelectedKey(item[0]) |
||||
|
setCurrentMenu( data?.find((menu) => menu.id === item[0])!) |
||||
|
}} |
||||
|
checkable={true} showIcon={true}/> |
||||
|
</ProCard> |
||||
|
<ProCard title={'配置'}> |
||||
|
|
||||
|
{selectedKey} |
||||
|
{ |
||||
|
JSON.stringify(currentMenu) |
||||
|
} |
||||
|
|
||||
|
</ProCard> |
||||
|
<ProCard title={'按钮'} colSpan={7}> |
||||
|
|
||||
|
</ProCard> |
||||
|
</ProCard> |
||||
|
</PageContainer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Menus |
@ -0,0 +1,11 @@ |
|||||
|
import { PageContainer } from '@ant-design/pro-components' |
||||
|
|
||||
|
const Roles = () => { |
||||
|
return ( |
||||
|
<PageContainer breadcrumbRender={false}> |
||||
|
|
||||
|
</PageContainer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Roles |
@ -0,0 +1,11 @@ |
|||||
|
import { PageContainer } from '@ant-design/pro-components' |
||||
|
|
||||
|
const Users = () => { |
||||
|
return ( |
||||
|
<PageContainer breadcrumbRender={false}> |
||||
|
|
||||
|
</PageContainer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Users |
@ -0,0 +1,17 @@ |
|||||
|
import axios from 'axios' |
||||
|
|
||||
|
export const request = axios.create({ |
||||
|
baseURL: '/api', |
||||
|
timeout: 1000, |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
//拦截response,返回data
|
||||
|
request.interceptors.response.use((response) => { |
||||
|
// console.log('response', response.data)
|
||||
|
return response.data |
||||
|
}) |
||||
|
|
||||
|
export default request |
@ -0,0 +1,149 @@ |
|||||
|
import { |
||||
|
createRouter, |
||||
|
createRoute, |
||||
|
RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute, |
||||
|
} from '@tanstack/react-router' |
||||
|
import { TanStackRouterDevtools } from '@tanstack/router-devtools' |
||||
|
import RootLayout from './layout/RootLayout' |
||||
|
import ListPageLayout from '@/layout/ListPageLayout.tsx' |
||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' |
||||
|
import { useAtomValue } from 'jotai' |
||||
|
|
||||
|
import { IRootContext, MenuItem } from './types' |
||||
|
import { menuDataAtom } from './store/system.ts' |
||||
|
|
||||
|
export const queryClient = new QueryClient({ |
||||
|
defaultOptions: { |
||||
|
queries: { |
||||
|
retry: false, |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
const rootRoute = createRootRouteWithContext<IRootContext>()({ |
||||
|
component: () => ( |
||||
|
<> |
||||
|
<RootLayout/> |
||||
|
<TanStackRouterDevtools position={'bottom-right'}/> |
||||
|
</> |
||||
|
|
||||
|
|
||||
|
), |
||||
|
beforeLoad: ({ location }) => { |
||||
|
if (location.pathname === '/') { |
||||
|
return redirect({ to: '/welcome' }) |
||||
|
} |
||||
|
}, |
||||
|
notFoundComponent: () => <div>404 Not Found</div>, |
||||
|
}) |
||||
|
|
||||
|
const generateDynamicRoutes = (menuData: MenuItem[]) => { |
||||
|
// 递归生成路由,如果有routes则递归生成子路由
|
||||
|
|
||||
|
const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { |
||||
|
|
||||
|
const path = menu.path?.replace(parentRoute.options?.path, '') |
||||
|
const isLayout = menu.children && menu.children.length > 0 && menu.type === 1 |
||||
|
|
||||
|
if (isLayout && !menu.component) { |
||||
|
//没有component的layout,直接返回
|
||||
|
return createRoute({ |
||||
|
getParentRoute: () => parentRoute, |
||||
|
id: path!, |
||||
|
component: ListPageLayout, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// @ts-ignore 添加menu属性,方便后面获取
|
||||
|
const options = { |
||||
|
getParentRoute: () => parentRoute, |
||||
|
menu, |
||||
|
} as any |
||||
|
|
||||
|
if (isLayout) { |
||||
|
options.id = path! |
||||
|
} else { |
||||
|
options.path = path! |
||||
|
} |
||||
|
|
||||
|
//删除掉parentRoute的path,避免重复
|
||||
|
const route = createRoute(options).lazy(async () => { |
||||
|
|
||||
|
// @ts-ignore 获取route中的menu属性
|
||||
|
const menu = route.options.menu as MenuItem |
||||
|
let component = menu.component |
||||
|
|
||||
|
// menu.type
|
||||
|
// 1,组件(页面),2,IFrame,3,外链接,4,按钮
|
||||
|
if (menu.type === 2) { |
||||
|
component = '@/components/Iframe' |
||||
|
} |
||||
|
|
||||
|
if (!component) { |
||||
|
return createLazyRoute(menu.path)({ |
||||
|
component: () => <div>404 Not Found</div> |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const d = await import(`${component}`) |
||||
|
if (d.Route) { |
||||
|
return d.Route |
||||
|
} |
||||
|
if (d.GenRoute) { |
||||
|
return d.GenRoute(menu.path) |
||||
|
} |
||||
|
return createLazyRoute(menu.path)({ |
||||
|
component: d.default || d |
||||
|
}) |
||||
|
}) |
||||
|
return route |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// 对menuData递归生成路由,只处理type =1 的菜单
|
||||
|
const did = (menus: MenuItem[], parentRoute: AnyRoute) => { |
||||
|
return menus.filter((item) => item.type === 1).map((item) => { |
||||
|
// 如果有children则递归生成子路由,同样只处理type =1 的菜单
|
||||
|
const route = generateRoutes(item, parentRoute) |
||||
|
if (item.children && item.children.length > 0) { |
||||
|
const children = did(item.children, route) |
||||
|
if (children.length > 0) { |
||||
|
route.addChildren(children) |
||||
|
} |
||||
|
} |
||||
|
return route |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return did(menuData, rootRoute) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export const RootProvider = () => { |
||||
|
|
||||
|
const { data, isError, isPending } = useAtomValue(menuDataAtom) |
||||
|
|
||||
|
if (isError) { |
||||
|
return <div>Error</div> |
||||
|
} |
||||
|
|
||||
|
if (isPending) { |
||||
|
return <div>Loading...</div> |
||||
|
} |
||||
|
|
||||
|
const dynamicRoutes = generateDynamicRoutes(data!) |
||||
|
const routeTree = rootRoute.addChildren(dynamicRoutes) |
||||
|
|
||||
|
const router = createRouter({ |
||||
|
routeTree, |
||||
|
context: { queryClient, menuData: data }, |
||||
|
defaultPreload: 'intent' |
||||
|
}) |
||||
|
|
||||
|
return ( |
||||
|
<QueryClientProvider client={queryClient}> |
||||
|
<RouterProvider router={router}/> |
||||
|
</QueryClientProvider> |
||||
|
) |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
import request from '../request.ts' |
||||
|
|
||||
|
const systemServ = { |
||||
|
|
||||
|
menus: { |
||||
|
list: () => { |
||||
|
return request.get('/menus') |
||||
|
}, |
||||
|
add: (data: any) => { |
||||
|
return request.post('/menus', data) |
||||
|
}, |
||||
|
update: (id: number| string, data: any) => { |
||||
|
return request.put(`/menus/${id}`, data) |
||||
|
}, |
||||
|
delete: (id: number| string) => { |
||||
|
return request.delete(`/menus/${id}`) |
||||
|
}, |
||||
|
info: (id: number| string) => { |
||||
|
return request.get(`/menus/${id}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export default systemServ |
@ -0,0 +1,44 @@ |
|||||
|
import { atom } from 'jotai' |
||||
|
import { IDepartment } from './types/department' |
||||
|
import { QueryClient } from '@tanstack/query-core' |
||||
|
import { atomWithMutation, atomWithQuery } from 'jotai-tanstack-query' |
||||
|
import { IApiResult } from '../types' |
||||
|
import request from '../request.ts' |
||||
|
|
||||
|
|
||||
|
export const departmentSearchAtom = atom<Partial<IDepartment>>({}) |
||||
|
|
||||
|
export const departmentAtom = atomWithQuery<IApiResult<IDepartment[]>, any, IDepartment[]>((get) => ({ |
||||
|
queryKey: [ 'departments', get(departmentSearchAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , departSearch ] }) => { |
||||
|
await new Promise(resolve => setTimeout(resolve, 5000)) |
||||
|
|
||||
|
return await request('/departments', { |
||||
|
data: departSearch, |
||||
|
}) |
||||
|
}, |
||||
|
select: data => data.data, |
||||
|
})) |
||||
|
|
||||
|
export const departmentDetailAtom = atomWithQuery<IApiResult<IDepartment>>((get) => ({ |
||||
|
queryKey: [ 'department', get(departmentSearchAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , departSearch ] }) => { |
||||
|
return await request(`/departments/${(departSearch as IDepartment).id}`) |
||||
|
}, |
||||
|
select: data => data, |
||||
|
})) |
||||
|
|
||||
|
//add use atomWithMutation
|
||||
|
export const departmentAddAtom = (client: QueryClient) => atomWithMutation<IApiResult<IDepartment>, any, IDepartment>(() => ({ |
||||
|
mutationKey: [ 'addDepartment', ], |
||||
|
mutationFn: async (depart: IDepartment) => { |
||||
|
return await request.post('/departments', depart) |
||||
|
}, |
||||
|
onSuccess: (result) => { |
||||
|
console.log(result) |
||||
|
}, |
||||
|
onSettled: () => { |
||||
|
//清空列表的缓存
|
||||
|
void client.invalidateQueries({ queryKey: [ 'departments' ] }) |
||||
|
} |
||||
|
})) |
@ -0,0 +1,46 @@ |
|||||
|
import { atomWithQuery } from 'jotai-tanstack-query' |
||||
|
import systemServ from '../service/system.ts' |
||||
|
import { MenuItem } from '../types' |
||||
|
import { getIcon } from '../components/icon' |
||||
|
import { atom } from 'jotai/index' |
||||
|
|
||||
|
// 格式化菜单数据, 把children转换成routes
|
||||
|
export const formatMenuData = (data: MenuItem[]) => { |
||||
|
const result: MenuItem[] = [] |
||||
|
for (const item of data) { |
||||
|
if (item.icon && typeof item.icon === 'string') { |
||||
|
item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' }) |
||||
|
} |
||||
|
if (!item.children || !item.children.length) { |
||||
|
result.push(item) |
||||
|
} else { |
||||
|
const { children, ...other } = item |
||||
|
result.push({ |
||||
|
...other, |
||||
|
children, |
||||
|
routes: formatMenuData(children), |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
export const menuDataAtom = atomWithQuery(() => ({ |
||||
|
queryKey: [ 'menus' ], |
||||
|
queryFn: async () => { |
||||
|
return await systemServ.menus.list() |
||||
|
}, |
||||
|
select: data => formatMenuData(data.data ?? []), |
||||
|
})) |
||||
|
|
||||
|
export const selectedMenuIdAtom = atom<string | number>(0) |
||||
|
export const selectedMenuAtom = atom<MenuItem>({}) |
||||
|
export const byIdMenuAtom = atomWithQuery((get) => ({ |
||||
|
queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , id ] }) => { |
||||
|
|
||||
|
return await systemServ.menus.info(id) |
||||
|
}, |
||||
|
select: data => data.data, |
||||
|
})) |
@ -0,0 +1,9 @@ |
|||||
|
|
||||
|
export interface IDepartment { |
||||
|
id: string |
||||
|
name: string |
||||
|
parentId: number |
||||
|
order: number |
||||
|
createAt: string |
||||
|
updateAt: string |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
import { atom } from 'jotai/index' |
||||
|
import { IAuth } from '../types' |
||||
|
|
||||
|
export const authAtom = atom<IAuth>({ |
||||
|
isLogin: false, |
||||
|
authKey: [] |
||||
|
}) |
||||
|
|
@ -0,0 +1,66 @@ |
|||||
|
import { Attributes, ReactNode } from 'react' |
||||
|
import { QueryClient } from '@tanstack/react-query' |
||||
|
import { Router } from '@tanstack/react-router' |
||||
|
import { RouteOptions } from '@tanstack/react-router/src/route.ts' |
||||
|
|
||||
|
export type LayoutType = 'list' | 'form' | 'tree' | 'normal' |
||||
|
|
||||
|
export type TRouter = { |
||||
|
router: Router & { |
||||
|
context: IRootContext |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export type IApiResult<T = any> = { |
||||
|
code: number; |
||||
|
data: T; |
||||
|
message: string; |
||||
|
} |
||||
|
|
||||
|
export type IDataProps<T = any> = { |
||||
|
value?: T; |
||||
|
onChange?: (value: T) => void; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export type Props = Attributes & { |
||||
|
children?: ReactNode |
||||
|
}; |
||||
|
|
||||
|
export interface IRootContext { |
||||
|
menuData: MenuItem[]; |
||||
|
queryClient: QueryClient; |
||||
|
} |
||||
|
|
||||
|
interface MenuItem { |
||||
|
id?: number | string; |
||||
|
key: string; |
||||
|
title: string; |
||||
|
name: string; |
||||
|
path?: string; |
||||
|
icon?: string | ReactNode; |
||||
|
component?: string; |
||||
|
type: number | string; |
||||
|
order: number; |
||||
|
hideInMenu?: boolean; |
||||
|
children?: MenuItem[]; |
||||
|
routes?: MenuItem[]; |
||||
|
} |
||||
|
|
||||
|
interface IAuth { |
||||
|
isLogin: boolean; |
||||
|
authKey?: string[]; |
||||
|
} |
||||
|
|
||||
|
declare module '@tanstack/react-router' { |
||||
|
interface Register { |
||||
|
router: TRouter |
||||
|
} |
||||
|
|
||||
|
interface AnyRoute { |
||||
|
options: RouteOptions & { |
||||
|
menu: MenuItem |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,33 @@ |
|||||
|
import { MenuItem } from '@/types' |
||||
|
import { TreeDataNode } from 'antd' |
||||
|
|
||||
|
export function generateUUID() { |
||||
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { |
||||
|
return crypto.randomUUID(); |
||||
|
} else { |
||||
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//把MenuItem[]转换成antd树形结构
|
||||
|
export const formatterMenuData = (data: MenuItem[]): TreeDataNode[] => { |
||||
|
const result: TreeDataNode[] = [] |
||||
|
for (const item of data) { |
||||
|
if (item.children && item.children.length) { |
||||
|
const { children, ...other } = item |
||||
|
result.push({ |
||||
|
...other, |
||||
|
key: item.id!, |
||||
|
title: item.name!, |
||||
|
children: formatterMenuData(children), |
||||
|
}) |
||||
|
} else { |
||||
|
result.push({ |
||||
|
...item, |
||||
|
key: item.id!, |
||||
|
title: item.name!, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
return result |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
/// <reference types="vite/client" />
|
@ -0,0 +1,34 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"target": "ES2020", |
||||
|
"useDefineForClassFields": true, |
||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"], |
||||
|
"module": "ESNext", |
||||
|
"skipLibCheck": true, |
||||
|
|
||||
|
/* Bundler mode */ |
||||
|
"moduleResolution": "bundler", |
||||
|
"allowImportingTsExtensions": true, |
||||
|
"resolveJsonModule": true, |
||||
|
"isolatedModules": true, |
||||
|
"noEmit": true, |
||||
|
"jsx": "react-jsx", |
||||
|
|
||||
|
/* Linting */ |
||||
|
"noImplicitAny": false, |
||||
|
"strict": true, |
||||
|
"noUnusedLocals": true, |
||||
|
"noUnusedParameters": true, |
||||
|
"noFallthroughCasesInSwitch": true, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
|
||||
|
/* Type checking */ |
||||
|
"strictNullChecks": true, |
||||
|
"files": [ "src/**/*.d.ts"], |
||||
|
"paths": { |
||||
|
"@/*": ["src/*"] |
||||
|
} |
||||
|
}, |
||||
|
"include": ["src"], |
||||
|
"references": [{ "path": "./tsconfig.node.json" }] |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"composite": true, |
||||
|
"skipLibCheck": true, |
||||
|
"module": "ESNext", |
||||
|
"moduleResolution": "bundler", |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
"strict": true |
||||
|
}, |
||||
|
"include": ["vite.config.ts"] |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
import { defineConfig } from 'vite' |
||||
|
import react from '@vitejs/plugin-react' |
||||
|
import { viteMockServe } from 'vite-plugin-mock' |
||||
|
|
||||
|
//import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
|
||||
|
|
||||
|
// https://vitejs.dev/config/
|
||||
|
export default defineConfig({ |
||||
|
//定义别名的路径
|
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': '/src', |
||||
|
}, |
||||
|
}, |
||||
|
plugins: [ |
||||
|
react(), |
||||
|
viteMockServe({ |
||||
|
// 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production')
|
||||
|
enable: true, |
||||
|
|
||||
|
// mock 文件的根路径,默认值:'mocks'
|
||||
|
mockPath: 'mock', |
||||
|
logger: true, |
||||
|
}), |
||||
|
//TanStackRouterVite(),
|
||||
|
], |
||||
|
}) |
3276
yarn.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue