dark 7 months ago
commit
8327fd09e8
  1. 24
      .eslintrc.cjs
  2. 24
      .gitignore
  3. 30
      README.md
  4. 13
      index.html
  5. 21
      mock/departments.ts
  6. 98
      mock/menus.ts
  7. 47
      package.json
  8. 1
      public/vite.svg
  9. 20
      src/App.css
  10. 11
      src/App.tsx
  11. 35
      src/Auth.tsx
  12. 1
      src/assets/react.svg
  13. 74
      src/components/breadcrumb/index.tsx
  14. 64
      src/components/error-boundary/index.tsx
  15. 52
      src/components/icon/index.tsx
  16. 23
      src/hooks/useFetch.ts
  17. 0
      src/index.css
  18. 22
      src/layout/FormPageLayout.tsx
  19. 21
      src/layout/ListPageLayout.tsx
  20. 144
      src/layout/RootLayout.tsx
  21. 20
      src/layout/TreePageLayout.tsx
  22. 69
      src/layout/_defaultProps.tsx
  23. 10
      src/main.tsx
  24. 25
      src/pages/dashboard/index.tsx
  25. 10
      src/pages/list/index.tsx
  26. 15
      src/pages/list/list.tsx
  27. 146
      src/pages/list/tree.tsx
  28. 11
      src/pages/system/departments/index.tsx
  29. 60
      src/pages/system/menus/index.tsx
  30. 11
      src/pages/system/roles/index.tsx
  31. 11
      src/pages/system/users/index.tsx
  32. 0
      src/patches/x-fetch.ts
  33. 17
      src/request.ts
  34. 149
      src/routes.tsx
  35. 25
      src/service/system.ts
  36. 44
      src/store/department.ts
  37. 46
      src/store/system.ts
  38. 9
      src/store/types/department.d.ts
  39. 8
      src/store/user.ts
  40. 66
      src/types.d.ts
  41. 33
      src/utils/uuid.ts
  42. 1
      src/vite-env.d.ts
  43. 34
      tsconfig.json
  44. 11
      tsconfig.node.json
  45. 27
      vite.config.ts
  46. 3276
      yarn.lock

24
.eslintrc.cjs

@ -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',
},
}

24
.gitignore

@ -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?

30
README.md

@ -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

13
index.html

@ -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>

21
mock/departments.ts

@ -0,0 +1,21 @@
export default [
{
url: '/api/departments',
method: 'get',
response: () => {
return {
code: 200,
data: [
{
id: '1',
name: '开发部'
},
{
id: '2',
name: '测试部'
}
]
}
}
}
]

98
mock/menus.ts

@ -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[]

47
package.json

@ -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"
}
}

1
public/vite.svg

@ -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>

20
src/App.css

@ -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;
}
}

11
src/App.tsx

@ -0,0 +1,11 @@
import './App.css'
import { RootProvider } from './routes.tsx'
function App() {
return (
<RootProvider/>
)
}
export default App

35
src/Auth.tsx

@ -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

1
src/assets/react.svg

@ -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>

74
src/components/breadcrumb/index.tsx

@ -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

64
src/components/error-boundary/index.tsx

@ -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
}
}

52
src/components/icon/index.tsx

@ -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

23
src/hooks/useFetch.ts

@ -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
src/index.css

22
src/layout/FormPageLayout.tsx

@ -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,
})

21
src/layout/ListPageLayout.tsx

@ -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,
})

144
src/layout/RootLayout.tsx

@ -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>
)
}

20
src/layout/TreePageLayout.tsx

@ -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,
})

69
src/layout/_defaultProps.tsx

@ -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',
},
],
}

10
src/main.tsx

@ -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>,
)

25
src/pages/dashboard/index.tsx

@ -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

10
src/pages/list/index.tsx

@ -0,0 +1,10 @@
const Index = () => {
return (
<div>
</div>
)
}
export default Index

15
src/pages/list/list.tsx

@ -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

146
src/pages/list/tree.tsx

@ -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

11
src/pages/system/departments/index.tsx

@ -0,0 +1,11 @@
import { PageContainer } from '@ant-design/pro-components'
const Departments = () => {
return (
<PageContainer breadcrumbRender={false}>
</PageContainer>
)
}
export default Departments

60
src/pages/system/menus/index.tsx

@ -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

11
src/pages/system/roles/index.tsx

@ -0,0 +1,11 @@
import { PageContainer } from '@ant-design/pro-components'
const Roles = () => {
return (
<PageContainer breadcrumbRender={false}>
</PageContainer>
)
}
export default Roles

11
src/pages/system/users/index.tsx

@ -0,0 +1,11 @@
import { PageContainer } from '@ant-design/pro-components'
const Users = () => {
return (
<PageContainer breadcrumbRender={false}>
</PageContainer>
)
}
export default Users

0
src/patches/x-fetch.ts

17
src/request.ts

@ -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

149
src/routes.tsx

@ -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>
)
}

25
src/service/system.ts

@ -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

44
src/store/department.ts

@ -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' ] })
}
}))

46
src/store/system.ts

@ -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,
}))

9
src/store/types/department.d.ts

@ -0,0 +1,9 @@
export interface IDepartment {
id: string
name: string
parentId: number
order: number
createAt: string
updateAt: string
}

8
src/store/user.ts

@ -0,0 +1,8 @@
import { atom } from 'jotai/index'
import { IAuth } from '../types'
export const authAtom = atom<IAuth>({
isLogin: false,
authKey: []
})

66
src/types.d.ts

@ -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
}
}
}

33
src/utils/uuid.ts

@ -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
}

1
src/vite-env.d.ts

@ -0,0 +1 @@
/// <reference types="vite/client" />

34
tsconfig.json

@ -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" }]
}

11
tsconfig.node.json

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

27
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

Loading…
Cancel
Save