react-toolkits 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +20 -0
- package/CHANGELOG.md +10 -0
- package/dist/512_orange_nobackground-L6MFCL6M.png +0 -0
- package/dist/index.css +230 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +191 -0
- package/dist/index.esm.js +3152 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +63 -0
- package/postcss.config.js +9 -0
- package/src/assets/512_orange_nobackground.png +0 -0
- package/src/components/DynamicTags/index.tsx +160 -0
- package/src/components/FilterForm/index.tsx +62 -0
- package/src/components/FormModal/hooks.tsx +48 -0
- package/src/components/FormModal/index.tsx +138 -0
- package/src/components/Highlight/index.tsx +51 -0
- package/src/components/PermissionButton/index.tsx +36 -0
- package/src/components/QueryList/index.tsx +158 -0
- package/src/components/index.ts +27 -0
- package/src/constants/index.ts +3 -0
- package/src/features/permission/components/PermissionList.tsx +129 -0
- package/src/features/permission/hooks/index.ts +140 -0
- package/src/features/permission/index.ts +5 -0
- package/src/features/permission/types/index.ts +33 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-fetcher.tsx +102 -0
- package/src/hooks/use-permission.tsx +58 -0
- package/src/index.ts +7 -0
- package/src/layouts/Layout.tsx +81 -0
- package/src/layouts/NavBar.tsx +176 -0
- package/src/layouts/index.ts +6 -0
- package/src/pages/Login/default.tsx +864 -0
- package/src/pages/Login/index.tsx +111 -0
- package/src/pages/index.ts +4 -0
- package/src/pages/permission/RoleDetail.tsx +40 -0
- package/src/pages/permission/RoleList.tsx +226 -0
- package/src/pages/permission/UserList.tsx +248 -0
- package/src/pages/permission/index.tsx +31 -0
- package/src/shims.d.ts +20 -0
- package/src/stores/index.ts +3 -0
- package/src/stores/menu.ts +27 -0
- package/src/stores/queryTrigger.ts +27 -0
- package/src/stores/token.ts +25 -0
- package/src/styles/index.css +5 -0
- package/src/types/index.ts +27 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +26 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Checkbox, Collapse, Skeleton, Typography } from 'antd'
|
|
2
|
+
import type { CheckboxChangeEvent } from 'antd/es/checkbox'
|
|
3
|
+
import type { CheckboxValueType } from 'antd/es/checkbox/Group'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { useAllPermissions } from '../hooks'
|
|
6
|
+
|
|
7
|
+
const { Text } = Typography
|
|
8
|
+
|
|
9
|
+
const PermissionList = ({
|
|
10
|
+
expand = true,
|
|
11
|
+
value,
|
|
12
|
+
readonly,
|
|
13
|
+
onChange,
|
|
14
|
+
}: {
|
|
15
|
+
expand?: boolean
|
|
16
|
+
value?: CheckboxValueType[]
|
|
17
|
+
readonly?: boolean
|
|
18
|
+
onChange?: (checkedValue: CheckboxValueType[]) => void
|
|
19
|
+
}) => {
|
|
20
|
+
const [activeKey, setActiveKey] = useState<string[]>([])
|
|
21
|
+
const [internalValue, setInternalValue] = useState<CheckboxValueType[]>(value ?? [])
|
|
22
|
+
const { data: permissions, isLoading, error } = useAllPermissions()
|
|
23
|
+
|
|
24
|
+
const [checkedMap, setCheckedMap] = useState<Record<string, boolean>>({})
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setInternalValue(value ?? [])
|
|
28
|
+
}, [value])
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (expand) {
|
|
32
|
+
setActiveKey((permissions ?? []).map(({ category }) => category))
|
|
33
|
+
}
|
|
34
|
+
}, [expand, permissions])
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const checkedValue = (permissions ?? []).reduce(
|
|
38
|
+
(acc, curr) => {
|
|
39
|
+
acc[curr.category] = curr.permissions.every(item => internalValue.includes(item.value))
|
|
40
|
+
return acc
|
|
41
|
+
},
|
|
42
|
+
{} as Record<string, boolean>,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
setCheckedMap(checkedValue)
|
|
46
|
+
}, [internalValue, permissions])
|
|
47
|
+
|
|
48
|
+
const onCollapseChange = (key: string | string[]) => {
|
|
49
|
+
setActiveKey(key as string[])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const getCheckedValue = (checkedValue: boolean, codes: string[]) => {
|
|
53
|
+
let tempValue: CheckboxValueType[] = []
|
|
54
|
+
|
|
55
|
+
if (checkedValue) {
|
|
56
|
+
tempValue = [...new Set(internalValue.concat(codes))]
|
|
57
|
+
} else {
|
|
58
|
+
tempValue = internalValue.slice()
|
|
59
|
+
|
|
60
|
+
codes.forEach(code => {
|
|
61
|
+
const index = tempValue.findIndex(item => item === code)
|
|
62
|
+
if (index > -1) {
|
|
63
|
+
tempValue.splice(index, 1)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return tempValue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const onCheckChange = (e: CheckboxChangeEvent, category: string, codes: string[]) => {
|
|
72
|
+
const checkedValue = getCheckedValue(e.target.checked, codes)
|
|
73
|
+
setInternalValue(checkedValue)
|
|
74
|
+
onChange?.(checkedValue)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex justify-center">
|
|
80
|
+
<Text type="danger">权限获取失败</Text>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Skeleton active loading={isLoading}>
|
|
87
|
+
<Collapse
|
|
88
|
+
style={{ width: '100%' }}
|
|
89
|
+
collapsible="header"
|
|
90
|
+
activeKey={activeKey}
|
|
91
|
+
items={(permissions ?? []).map(item => ({
|
|
92
|
+
key: item.category,
|
|
93
|
+
label: item.category,
|
|
94
|
+
extra: !readonly && (
|
|
95
|
+
<Checkbox
|
|
96
|
+
checked={checkedMap[item.category]}
|
|
97
|
+
onChange={e => {
|
|
98
|
+
onCheckChange(
|
|
99
|
+
e,
|
|
100
|
+
item.category,
|
|
101
|
+
item.permissions.map(permission => permission.value),
|
|
102
|
+
)
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
全选
|
|
106
|
+
</Checkbox>
|
|
107
|
+
),
|
|
108
|
+
children: (
|
|
109
|
+
<Checkbox.Group
|
|
110
|
+
style={{ width: '100%' }}
|
|
111
|
+
options={item.permissions.map(permission => ({
|
|
112
|
+
label: permission.label,
|
|
113
|
+
value: permission.value,
|
|
114
|
+
disabled: readonly,
|
|
115
|
+
onChange(e) {
|
|
116
|
+
onCheckChange(e, item.category, [permission.value])
|
|
117
|
+
},
|
|
118
|
+
}))}
|
|
119
|
+
value={internalValue}
|
|
120
|
+
/>
|
|
121
|
+
),
|
|
122
|
+
}))}
|
|
123
|
+
onChange={onCollapseChange}
|
|
124
|
+
/>
|
|
125
|
+
</Skeleton>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default PermissionList
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useFetcher, usePermission } from '@/hooks'
|
|
2
|
+
import useSWR from 'swr'
|
|
3
|
+
import useSWRMutation from 'swr/mutation'
|
|
4
|
+
import type { PermissionEnumItem, RoleEnumItem } from '../types'
|
|
5
|
+
|
|
6
|
+
export function useAllPermissions() {
|
|
7
|
+
return useSWR<PermissionEnumItem[]>({
|
|
8
|
+
url: '/api/usystem/user/allPermssions',
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useAllRoles() {
|
|
13
|
+
const { accessible } = usePermission('200005')
|
|
14
|
+
|
|
15
|
+
return useSWR<RoleEnumItem[]>(
|
|
16
|
+
accessible
|
|
17
|
+
? {
|
|
18
|
+
url: '/api/usystem/role/all',
|
|
19
|
+
}
|
|
20
|
+
: null,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useRole(name: string) {
|
|
25
|
+
return useSWR({
|
|
26
|
+
url: '/api/usystem/role/info',
|
|
27
|
+
params: { name },
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useCreateRole() {
|
|
32
|
+
const fetcher = useFetcher()
|
|
33
|
+
|
|
34
|
+
return useSWRMutation(
|
|
35
|
+
'/api/usystem/role/create',
|
|
36
|
+
(
|
|
37
|
+
url,
|
|
38
|
+
{
|
|
39
|
+
arg,
|
|
40
|
+
}: {
|
|
41
|
+
arg: { name: string; permissions: string[] }
|
|
42
|
+
},
|
|
43
|
+
) => fetcher({ method: 'POST', url, data: arg }),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useUpdateRole() {
|
|
48
|
+
const fetcher = useFetcher()
|
|
49
|
+
|
|
50
|
+
return useSWRMutation(
|
|
51
|
+
'/api/usystem/role/update',
|
|
52
|
+
(
|
|
53
|
+
url,
|
|
54
|
+
{
|
|
55
|
+
arg,
|
|
56
|
+
}: {
|
|
57
|
+
arg: { id: number; name: string; permissions: string[] }
|
|
58
|
+
},
|
|
59
|
+
) => fetcher({ method: 'POST', url, data: arg }),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useRemoveRole() {
|
|
64
|
+
const fetcher = useFetcher()
|
|
65
|
+
|
|
66
|
+
return useSWRMutation(
|
|
67
|
+
'/api/usystem/role/delete',
|
|
68
|
+
(
|
|
69
|
+
url,
|
|
70
|
+
{
|
|
71
|
+
arg,
|
|
72
|
+
}: {
|
|
73
|
+
arg: { id: number; name: string }
|
|
74
|
+
},
|
|
75
|
+
) => fetcher({ method: 'POST', url, data: arg }),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useCreateUser() {
|
|
80
|
+
const fetcher = useFetcher()
|
|
81
|
+
|
|
82
|
+
return useSWRMutation(
|
|
83
|
+
'/api/usystem/user/create',
|
|
84
|
+
(
|
|
85
|
+
url,
|
|
86
|
+
{
|
|
87
|
+
arg,
|
|
88
|
+
}: {
|
|
89
|
+
arg: { name: string; roles: string[] }
|
|
90
|
+
},
|
|
91
|
+
) =>
|
|
92
|
+
fetcher({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
url,
|
|
95
|
+
data: arg,
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function useUpdateUser() {
|
|
101
|
+
const fetcher = useFetcher()
|
|
102
|
+
|
|
103
|
+
return useSWRMutation(
|
|
104
|
+
'/api/usystem/user/update',
|
|
105
|
+
(
|
|
106
|
+
url,
|
|
107
|
+
{
|
|
108
|
+
arg,
|
|
109
|
+
}: {
|
|
110
|
+
arg: { id: string; name: string; roles: string[] }
|
|
111
|
+
},
|
|
112
|
+
) =>
|
|
113
|
+
fetcher({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
url,
|
|
116
|
+
data: arg,
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useRemoveUser() {
|
|
122
|
+
const fetcher = useFetcher()
|
|
123
|
+
|
|
124
|
+
return useSWRMutation(
|
|
125
|
+
'/api/usystem/user/delete',
|
|
126
|
+
(
|
|
127
|
+
url,
|
|
128
|
+
{
|
|
129
|
+
arg,
|
|
130
|
+
}: {
|
|
131
|
+
arg: { id: string; name: string }
|
|
132
|
+
},
|
|
133
|
+
) =>
|
|
134
|
+
fetcher({
|
|
135
|
+
method: 'POST',
|
|
136
|
+
url,
|
|
137
|
+
data: arg,
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface PermissionEnumItem {
|
|
2
|
+
category: string
|
|
3
|
+
permissions: {
|
|
4
|
+
label: string
|
|
5
|
+
value: string
|
|
6
|
+
route: string
|
|
7
|
+
}[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RoleEnumItem {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RoleListItem {
|
|
16
|
+
id: number
|
|
17
|
+
name: string
|
|
18
|
+
ctime: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Role {
|
|
22
|
+
id: number
|
|
23
|
+
name: string
|
|
24
|
+
ctime: string
|
|
25
|
+
permissions: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UserListItem {
|
|
29
|
+
id: string
|
|
30
|
+
name: string
|
|
31
|
+
ctime: string
|
|
32
|
+
roles: string[]
|
|
33
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useTokenStore } from '@/stores'
|
|
2
|
+
import type { BackendResponse } from '@/types'
|
|
3
|
+
import { App } from 'antd'
|
|
4
|
+
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
import { useNavigate } from 'react-router-dom'
|
|
7
|
+
import type { Merge } from 'ts-essentials'
|
|
8
|
+
|
|
9
|
+
// 覆盖 AxiosInstance 各种请求方法的返回值,为了方便我们在 interceptors.response 里把 AxiosResponse 直接打平成后端返回的数据,去掉了 axios 的封装。
|
|
10
|
+
type ShimmedAxiosInstance = Merge<
|
|
11
|
+
AxiosInstance,
|
|
12
|
+
{
|
|
13
|
+
request<T = unknown, D = unknown>(config: AxiosRequestConfig<D>): Promise<T>
|
|
14
|
+
get<T = unknown, D = unknown>(url: string, config?: AxiosRequestConfig<D>): Promise<T>
|
|
15
|
+
delete<T = unknown, D = unknown>(url: string, config?: AxiosRequestConfig<D>): Promise<T>
|
|
16
|
+
head<T = unknown, D = unknown>(url: string, config?: AxiosRequestConfig<D>): Promise<T>
|
|
17
|
+
post<T = unknown, D = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig<D>): Promise<T>
|
|
18
|
+
put<T = unknown, D = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig<D>): Promise<T>
|
|
19
|
+
patch<T = unknown, D = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig<D>): Promise<T>
|
|
20
|
+
}
|
|
21
|
+
>
|
|
22
|
+
|
|
23
|
+
export class FetcherError extends Error {
|
|
24
|
+
code: number
|
|
25
|
+
// 跳过错误提示
|
|
26
|
+
skip: boolean
|
|
27
|
+
|
|
28
|
+
constructor(message: string, code: number, skip = false) {
|
|
29
|
+
super(message)
|
|
30
|
+
this.code = code
|
|
31
|
+
this.skip = skip
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useFetcher() {
|
|
36
|
+
const { notification } = App.useApp()
|
|
37
|
+
const clearToken = useTokenStore(state => state.clearToken)
|
|
38
|
+
const navigate = useNavigate()
|
|
39
|
+
const token = useTokenStore(state => state.token)
|
|
40
|
+
|
|
41
|
+
const defaultOptions: AxiosRequestConfig = {
|
|
42
|
+
withCredentials: true,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const instance = axios.create(defaultOptions) as ShimmedAxiosInstance
|
|
46
|
+
|
|
47
|
+
instance.interceptors.request.use(config => {
|
|
48
|
+
const headers = config.headers
|
|
49
|
+
headers.set('Authorization', `Bearer ${token}`)
|
|
50
|
+
return config
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
instance.interceptors.response.use(
|
|
54
|
+
response => {
|
|
55
|
+
if (response.data.code === 0 || response.data.status === 0) {
|
|
56
|
+
return response.data.data
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new FetcherError(response.data.msg, 0)
|
|
60
|
+
},
|
|
61
|
+
(error: AxiosError<BackendResponse<unknown>>) => {
|
|
62
|
+
if (error.response) {
|
|
63
|
+
// 请求成功发出且服务器也响应了状态码,但状态码超出了 2xx 的范围
|
|
64
|
+
if (error.response.status === 401) {
|
|
65
|
+
throw new FetcherError('未登录或登录已过期', error.response.status)
|
|
66
|
+
} else if (error.response.status === 403) {
|
|
67
|
+
throw new FetcherError('无权限,请联系管理员进行授权', error.response.status)
|
|
68
|
+
} else if ([404, 405].includes(error.response.status)) {
|
|
69
|
+
throw new FetcherError('Not Found or Method not Allowed', error.response.status, true)
|
|
70
|
+
} else if (error.response.status === 412) {
|
|
71
|
+
throw new FetcherError('未注册用户', error.response.status)
|
|
72
|
+
} else {
|
|
73
|
+
throw new FetcherError(error.response.data?.msg, error.response.status)
|
|
74
|
+
}
|
|
75
|
+
} else if (error.request) {
|
|
76
|
+
// 请求已经成功发起,但没有收到响应
|
|
77
|
+
console.log(error.request)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return Promise.reject(error)
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return <T = unknown,>(config: AxiosRequestConfig) =>
|
|
85
|
+
instance.request<T>(config).catch((err: FetcherError) => {
|
|
86
|
+
switch (err.code) {
|
|
87
|
+
case 401:
|
|
88
|
+
case 412:
|
|
89
|
+
clearToken()
|
|
90
|
+
navigate(err.code === 401 ? '/login' : '/login?not_registered=1', { replace: true })
|
|
91
|
+
break
|
|
92
|
+
default:
|
|
93
|
+
if (!err.skip) {
|
|
94
|
+
notification.error({
|
|
95
|
+
message: '请求出错',
|
|
96
|
+
description: err.message,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw err
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import useSWRImmutable from 'swr/immutable'
|
|
2
|
+
import { useFetcher } from './use-fetcher'
|
|
3
|
+
|
|
4
|
+
export interface PermissionCheckResult {
|
|
5
|
+
[k: string]: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function usePermissions(codes: Record<string, string>) {
|
|
9
|
+
const fetcher = useFetcher()
|
|
10
|
+
|
|
11
|
+
const { data, isLoading } = useSWRImmutable(
|
|
12
|
+
Object.keys(codes).length > 0
|
|
13
|
+
? {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
url: '/api/usystem/user/check',
|
|
16
|
+
data: { permissions: Object.values(codes) },
|
|
17
|
+
}
|
|
18
|
+
: null,
|
|
19
|
+
config =>
|
|
20
|
+
fetcher<PermissionCheckResult>(config).then(res => {
|
|
21
|
+
if (res.has_all) {
|
|
22
|
+
return Object.keys(codes).reduce(
|
|
23
|
+
(acc, curr) => {
|
|
24
|
+
acc[curr] = true
|
|
25
|
+
return acc
|
|
26
|
+
},
|
|
27
|
+
{} as Record<string, boolean>,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Object.entries(codes).reduce(
|
|
32
|
+
(acc, curr) => {
|
|
33
|
+
acc[curr[0]] = (res as Record<string, boolean>)[curr[1] as string]
|
|
34
|
+
return acc
|
|
35
|
+
},
|
|
36
|
+
{} as Record<string, boolean>,
|
|
37
|
+
)
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return { data, isLoading }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function usePermission(code?: string) {
|
|
45
|
+
const { data, isLoading } = usePermissions(code ? { [code]: code } : {})
|
|
46
|
+
|
|
47
|
+
if (!code) {
|
|
48
|
+
return {
|
|
49
|
+
accessible: true,
|
|
50
|
+
isValidating: false,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
accessible: data?.[code] ?? false,
|
|
56
|
+
isValidating: isLoading,
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logo from '@/assets/512_orange_nobackground.png'
|
|
2
|
+
import { Layout as AntdLayout, Spin, theme } from 'antd'
|
|
3
|
+
import type { FC, ReactNode } from 'react'
|
|
4
|
+
import { Suspense } from 'react'
|
|
5
|
+
import { Link, Outlet } from 'react-router-dom'
|
|
6
|
+
import type { ItemType2 } from './NavBar'
|
|
7
|
+
import NavBar from './NavBar'
|
|
8
|
+
|
|
9
|
+
const { Header, Sider, Content } = AntdLayout
|
|
10
|
+
|
|
11
|
+
export interface LayoutProps {
|
|
12
|
+
title?: ReactNode
|
|
13
|
+
items: ItemType2[]
|
|
14
|
+
header?: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Layout: FC<LayoutProps> = props => {
|
|
18
|
+
const { title, items, header } = props
|
|
19
|
+
const {
|
|
20
|
+
token: { colorBgContainer, colorBorder },
|
|
21
|
+
} = theme.useToken()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<AntdLayout hasSider className="h-screen">
|
|
25
|
+
<Sider
|
|
26
|
+
width={256}
|
|
27
|
+
style={{
|
|
28
|
+
overflow: 'auto',
|
|
29
|
+
height: '100vh',
|
|
30
|
+
position: 'fixed',
|
|
31
|
+
left: 0,
|
|
32
|
+
top: 0,
|
|
33
|
+
bottom: 0,
|
|
34
|
+
borderRightWidth: 1,
|
|
35
|
+
borderRightStyle: 'solid',
|
|
36
|
+
borderRightColor: colorBorder,
|
|
37
|
+
}}
|
|
38
|
+
theme="light"
|
|
39
|
+
>
|
|
40
|
+
<div className="flex items-end px-6 py-4">
|
|
41
|
+
<img src={logo} alt="logo" className="w-8" />
|
|
42
|
+
<Link className="font-bold text-lg ml-2" to="/">
|
|
43
|
+
{title}
|
|
44
|
+
</Link>
|
|
45
|
+
</div>
|
|
46
|
+
<NavBar items={items} />
|
|
47
|
+
</Sider>
|
|
48
|
+
<AntdLayout className="ml-64">
|
|
49
|
+
<Header
|
|
50
|
+
style={{
|
|
51
|
+
padding: '0 24px',
|
|
52
|
+
background: colorBgContainer,
|
|
53
|
+
borderBottomWidth: 1,
|
|
54
|
+
borderBottomStyle: 'solid',
|
|
55
|
+
borderBottomColor: colorBorder,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{header}
|
|
59
|
+
</Header>
|
|
60
|
+
<Content className="p-6 overflow-auto bg-gray-50">
|
|
61
|
+
<Suspense
|
|
62
|
+
fallback={
|
|
63
|
+
<Spin
|
|
64
|
+
style={{
|
|
65
|
+
display: 'flex',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
height: '50vh',
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<Outlet />
|
|
74
|
+
</Suspense>
|
|
75
|
+
</Content>
|
|
76
|
+
</AntdLayout>
|
|
77
|
+
</AntdLayout>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default Layout
|