react-toolkits 0.0.3 → 0.0.5
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +44 -32
- package/dist/index.esm.js +159 -146
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FilterForm/index.tsx +1 -0
- package/src/components/FormModal/hooks.tsx +2 -3
- package/src/components/FormModal/index.tsx +11 -12
- package/src/components/QueryList/index.tsx +8 -21
- package/src/constants/index.ts +0 -2
- package/src/features/permission/hooks/index.ts +13 -28
- package/src/hooks/index.ts +1 -1
- package/src/hooks/{use-fetcher.tsx → use-http-client.tsx} +16 -42
- package/src/hooks/use-permission.tsx +13 -4
- package/src/index.ts +1 -0
- package/src/layouts/Layout.tsx +75 -37
- package/src/layouts/NavBar.tsx +8 -21
- package/src/pages/Login/index.tsx +3 -0
- package/src/pages/NoMatch/index.tsx +27 -0
- package/src/pages/index.ts +2 -1
- package/src/pages/permission/RoleList.tsx +11 -11
- package/src/types/index.ts +0 -17
package/package.json
CHANGED
|
@@ -5,10 +5,9 @@ import type { Merge } from 'ts-essentials'
|
|
|
5
5
|
import { createPortal } from 'react-dom'
|
|
6
6
|
|
|
7
7
|
export type UseFormModalProps<T> = Merge<
|
|
8
|
-
Omit<FormModalProps<T>, 'open' | 'onCancel' | 'closeFn' | 'children'
|
|
8
|
+
Omit<FormModalProps<T>, 'open' | 'onCancel' | 'closeFn' | 'children'>,
|
|
9
9
|
{
|
|
10
10
|
content: FormModalProps<T>['children']
|
|
11
|
-
onConfirm?: (values: T) => void
|
|
12
11
|
}
|
|
13
12
|
>
|
|
14
13
|
|
|
@@ -34,7 +33,7 @@ export function useFormModal<T extends object>(props: UseFormModalProps<T>) {
|
|
|
34
33
|
|
|
35
34
|
const Modal = useMemo(() => {
|
|
36
35
|
return (
|
|
37
|
-
<FormModal {...restProps} open={open} closeFn={closeModal} title={title} onConfirm={onConfirm}>
|
|
36
|
+
<FormModal ref={formRef} {...restProps} open={open} closeFn={closeModal} title={title} onConfirm={onConfirm}>
|
|
38
37
|
{content}
|
|
39
38
|
</FormModal>
|
|
40
39
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/* eslint-disable react/jsx-indent */
|
|
2
1
|
import type { FormInstance, FormProps, ModalProps } from 'antd'
|
|
3
2
|
import { Button, Form, Modal } from 'antd'
|
|
4
|
-
import type { ForwardedRef } from 'react'
|
|
3
|
+
import type { ForwardedRef, ReactElement } from 'react'
|
|
5
4
|
import { forwardRef, useId, useImperativeHandle, useRef, useState } from 'react'
|
|
6
5
|
|
|
7
6
|
type RenderChildren<T> = (props: {
|
|
@@ -20,7 +19,7 @@ export type RecursivePartial<T> = T extends object
|
|
|
20
19
|
? RecursivePartial<T[P]>
|
|
21
20
|
: T[P]
|
|
22
21
|
}
|
|
23
|
-
:
|
|
22
|
+
: unknown
|
|
24
23
|
|
|
25
24
|
export interface FormModalProps<T>
|
|
26
25
|
extends Pick<ModalProps, 'width' | 'title' | 'open' | 'afterClose' | 'bodyStyle' | 'maskClosable'>,
|
|
@@ -29,7 +28,7 @@ export interface FormModalProps<T>
|
|
|
29
28
|
footer?: ModalProps['footer']
|
|
30
29
|
closeFn?: VoidFunction
|
|
31
30
|
initialValues?: RecursivePartial<T>
|
|
32
|
-
onConfirm?: (values: T) => void
|
|
31
|
+
onConfirm?: (values: T) => Promise<void>
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export interface FormModalRef<T = object> {
|
|
@@ -81,17 +80,17 @@ const InternalFormModal = <T extends object>(props: FormModalProps<T>, ref: Forw
|
|
|
81
80
|
typeof footer === 'object'
|
|
82
81
|
? footer
|
|
83
82
|
: [
|
|
84
|
-
|
|
83
|
+
<Button
|
|
85
84
|
key="cancel"
|
|
86
85
|
onClick={() => {
|
|
87
86
|
closeFn?.()
|
|
88
87
|
}}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
>
|
|
89
|
+
取消
|
|
90
|
+
</Button>,
|
|
91
|
+
<Button form={id} key="submit" type="primary" htmlType="submit" loading={confirmLoading}>
|
|
92
|
+
确定
|
|
93
|
+
</Button>,
|
|
95
94
|
]
|
|
96
95
|
}
|
|
97
96
|
afterClose={() => {
|
|
@@ -133,6 +132,6 @@ const InternalFormModal = <T extends object>(props: FormModalProps<T>, ref: Forw
|
|
|
133
132
|
|
|
134
133
|
const FormModal = forwardRef(InternalFormModal) as <T extends object>(
|
|
135
134
|
props: FormModalProps<T> & { ref?: ForwardedRef<FormModalRef<T>> },
|
|
136
|
-
) =>
|
|
135
|
+
) => ReactElement
|
|
137
136
|
|
|
138
137
|
export default FormModal
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useHttpClient, usePermission } from '@/hooks'
|
|
2
2
|
import { useQueryTriggerStore } from '@/stores'
|
|
3
3
|
import type { ListResponse, PaginationParams } from '@/types'
|
|
4
4
|
import type { FormInstance, FormProps } from 'antd'
|
|
5
|
-
import { Form, Result,
|
|
5
|
+
import { Form, Result, Table } from 'antd'
|
|
6
6
|
import type { TableProps } from 'antd/es/table'
|
|
7
7
|
import type { AxiosRequestConfig } from 'axios'
|
|
8
8
|
import type { ReactNode } from 'react'
|
|
@@ -27,7 +27,7 @@ export interface QueryListProps<Item, Values>
|
|
|
27
27
|
|
|
28
28
|
const QueryList = <Item extends object, Values = NonNullable<unknown>>(props: QueryListProps<Item, Values>) => {
|
|
29
29
|
const { code, confirmText, labelCol, swrKey, renderForm, transformArg, initialValues, ...tableProps } = props
|
|
30
|
-
const { accessible
|
|
30
|
+
const { accessible } = usePermission(code ?? '')
|
|
31
31
|
const [form] = Form.useForm<Values>()
|
|
32
32
|
const setTrigger = useQueryTriggerStore(state => state.setTrigger)
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ const QueryList = <Item extends object, Values = NonNullable<unknown>>(props: Qu
|
|
|
36
36
|
perPage: 10,
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const httpClient = useHttpClient()
|
|
40
40
|
|
|
41
41
|
const { data, isMutating, trigger } = useSWRMutation(
|
|
42
42
|
swrKey,
|
|
@@ -62,10 +62,10 @@ const QueryList = <Item extends object, Values = NonNullable<unknown>>(props: Qu
|
|
|
62
62
|
...newPaginationData,
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
return
|
|
65
|
+
return httpClient.request<ListResponse<Item>>({
|
|
66
66
|
...key,
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
[key.method === 'POST' ? 'data' : 'params']:
|
|
68
|
+
typeof transformArg === 'function' ? transformArg(fetcherArg) : fetcherArg,
|
|
69
69
|
})
|
|
70
70
|
},
|
|
71
71
|
)
|
|
@@ -96,7 +96,7 @@ const QueryList = <Item extends object, Values = NonNullable<unknown>>(props: Qu
|
|
|
96
96
|
|
|
97
97
|
useEffect(() => {
|
|
98
98
|
setTrigger(swrKey, trigger)
|
|
99
|
-
}, [swrKey,
|
|
99
|
+
}, [swrKey, trigger, setTrigger])
|
|
100
100
|
|
|
101
101
|
useEffect(() => {
|
|
102
102
|
;(async () => {
|
|
@@ -109,19 +109,6 @@ const QueryList = <Item extends object, Values = NonNullable<unknown>>(props: Qu
|
|
|
109
109
|
})()
|
|
110
110
|
}, [form, trigger])
|
|
111
111
|
|
|
112
|
-
if (isValidating) {
|
|
113
|
-
return (
|
|
114
|
-
<Spin
|
|
115
|
-
style={{
|
|
116
|
-
display: 'flex',
|
|
117
|
-
justifyContent: 'center',
|
|
118
|
-
alignItems: 'center',
|
|
119
|
-
height: 200,
|
|
120
|
-
}}
|
|
121
|
-
/>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
112
|
if (!accessible) {
|
|
126
113
|
return <Result status={403} subTitle="无权限,请联系管理员进行授权" />
|
|
127
114
|
}
|
package/src/constants/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useHttpClient, usePermission } from '@/hooks'
|
|
2
2
|
import useSWR from 'swr'
|
|
3
3
|
import useSWRMutation from 'swr/mutation'
|
|
4
4
|
import type { PermissionEnumItem, RoleEnumItem } from '../types'
|
|
@@ -29,7 +29,7 @@ export function useRole(name: string) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function useCreateRole() {
|
|
32
|
-
const
|
|
32
|
+
const httpClient = useHttpClient()
|
|
33
33
|
|
|
34
34
|
return useSWRMutation(
|
|
35
35
|
'/api/usystem/role/create',
|
|
@@ -40,12 +40,12 @@ export function useCreateRole() {
|
|
|
40
40
|
}: {
|
|
41
41
|
arg: { name: string; permissions: string[] }
|
|
42
42
|
},
|
|
43
|
-
) =>
|
|
43
|
+
) => httpClient.post(url, arg),
|
|
44
44
|
)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function useUpdateRole() {
|
|
48
|
-
const
|
|
48
|
+
const httpClient = useHttpClient()
|
|
49
49
|
|
|
50
50
|
return useSWRMutation(
|
|
51
51
|
'/api/usystem/role/update',
|
|
@@ -56,12 +56,12 @@ export function useUpdateRole() {
|
|
|
56
56
|
}: {
|
|
57
57
|
arg: { id: number; name: string; permissions: string[] }
|
|
58
58
|
},
|
|
59
|
-
) =>
|
|
59
|
+
) => httpClient.post(url, arg),
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export function useRemoveRole() {
|
|
64
|
-
const
|
|
64
|
+
const httpClient = useHttpClient()
|
|
65
65
|
|
|
66
66
|
return useSWRMutation(
|
|
67
67
|
'/api/usystem/role/delete',
|
|
@@ -72,12 +72,12 @@ export function useRemoveRole() {
|
|
|
72
72
|
}: {
|
|
73
73
|
arg: { id: number; name: string }
|
|
74
74
|
},
|
|
75
|
-
) =>
|
|
75
|
+
) => httpClient.post(url, arg),
|
|
76
76
|
)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export function useCreateUser() {
|
|
80
|
-
const
|
|
80
|
+
const httpClient = useHttpClient()
|
|
81
81
|
|
|
82
82
|
return useSWRMutation(
|
|
83
83
|
'/api/usystem/user/create',
|
|
@@ -88,17 +88,12 @@ export function useCreateUser() {
|
|
|
88
88
|
}: {
|
|
89
89
|
arg: { name: string; roles: string[] }
|
|
90
90
|
},
|
|
91
|
-
) =>
|
|
92
|
-
fetcher({
|
|
93
|
-
method: 'POST',
|
|
94
|
-
url,
|
|
95
|
-
data: arg,
|
|
96
|
-
}),
|
|
91
|
+
) => httpClient.post(url, arg),
|
|
97
92
|
)
|
|
98
93
|
}
|
|
99
94
|
|
|
100
95
|
export function useUpdateUser() {
|
|
101
|
-
const
|
|
96
|
+
const httpClient = useHttpClient()
|
|
102
97
|
|
|
103
98
|
return useSWRMutation(
|
|
104
99
|
'/api/usystem/user/update',
|
|
@@ -109,17 +104,12 @@ export function useUpdateUser() {
|
|
|
109
104
|
}: {
|
|
110
105
|
arg: { id: string; name: string; roles: string[] }
|
|
111
106
|
},
|
|
112
|
-
) =>
|
|
113
|
-
fetcher({
|
|
114
|
-
method: 'POST',
|
|
115
|
-
url,
|
|
116
|
-
data: arg,
|
|
117
|
-
}),
|
|
107
|
+
) => httpClient.post(url, arg),
|
|
118
108
|
)
|
|
119
109
|
}
|
|
120
110
|
|
|
121
111
|
export function useRemoveUser() {
|
|
122
|
-
const
|
|
112
|
+
const httpClient = useHttpClient()
|
|
123
113
|
|
|
124
114
|
return useSWRMutation(
|
|
125
115
|
'/api/usystem/user/delete',
|
|
@@ -130,11 +120,6 @@ export function useRemoveUser() {
|
|
|
130
120
|
}: {
|
|
131
121
|
arg: { id: string; name: string }
|
|
132
122
|
},
|
|
133
|
-
) =>
|
|
134
|
-
fetcher({
|
|
135
|
-
method: 'POST',
|
|
136
|
-
url,
|
|
137
|
-
data: arg,
|
|
138
|
-
}),
|
|
123
|
+
) => httpClient.post(url, arg),
|
|
139
124
|
)
|
|
140
125
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './use-
|
|
1
|
+
export * from './use-http-client'
|
|
2
2
|
export * from './use-permission'
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import { App } from 'antd'
|
|
4
|
-
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
|
|
1
|
+
import {useTokenStore} from '@/stores'
|
|
2
|
+
import type {AxiosInstance, AxiosRequestConfig} from 'axios'
|
|
5
3
|
import axios from 'axios'
|
|
6
|
-
import {
|
|
7
|
-
import type { Merge } from 'ts-essentials'
|
|
4
|
+
import type {Merge} from 'ts-essentials'
|
|
8
5
|
|
|
9
6
|
// 覆盖 AxiosInstance 各种请求方法的返回值,为了方便我们在 interceptors.response 里把 AxiosResponse 直接打平成后端返回的数据,去掉了 axios 的封装。
|
|
10
7
|
type ShimmedAxiosInstance = Merge<
|
|
@@ -20,22 +17,19 @@ type ShimmedAxiosInstance = Merge<
|
|
|
20
17
|
}
|
|
21
18
|
>
|
|
22
19
|
|
|
23
|
-
export class
|
|
24
|
-
code
|
|
20
|
+
export class HttpClientError extends Error {
|
|
21
|
+
code?: number
|
|
25
22
|
// 跳过错误提示
|
|
26
23
|
skip: boolean
|
|
27
24
|
|
|
28
|
-
constructor(message: string, code
|
|
25
|
+
constructor(message: string, code?: number, skip = false) {
|
|
29
26
|
super(message)
|
|
30
27
|
this.code = code
|
|
31
28
|
this.skip = skip
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
export function
|
|
36
|
-
const { notification } = App.useApp()
|
|
37
|
-
const clearToken = useTokenStore(state => state.clearToken)
|
|
38
|
-
const navigate = useNavigate()
|
|
32
|
+
export function useHttpClient() {
|
|
39
33
|
const token = useTokenStore(state => state.token)
|
|
40
34
|
|
|
41
35
|
const defaultOptions: AxiosRequestConfig = {
|
|
@@ -56,47 +50,27 @@ export function useFetcher() {
|
|
|
56
50
|
return response.data.data
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
throw new
|
|
53
|
+
throw new HttpClientError(response.data.msg, 0)
|
|
60
54
|
},
|
|
61
|
-
|
|
55
|
+
error => {
|
|
62
56
|
if (error.response) {
|
|
63
57
|
// 请求成功发出且服务器也响应了状态码,但状态码超出了 2xx 的范围
|
|
64
58
|
if (error.response.status === 401) {
|
|
65
|
-
throw new
|
|
59
|
+
throw new HttpClientError('未登录或登录已过期', error.response.status)
|
|
66
60
|
} else if (error.response.status === 403) {
|
|
67
|
-
throw new
|
|
61
|
+
throw new HttpClientError('无权限,请联系管理员进行授权', error.response.status)
|
|
68
62
|
} else if ([404, 405].includes(error.response.status)) {
|
|
69
|
-
throw new
|
|
63
|
+
throw new HttpClientError('Not Found or Method not Allowed', error.response.status, true)
|
|
70
64
|
} else if (error.response.status === 412) {
|
|
71
|
-
throw new
|
|
65
|
+
throw new HttpClientError('未注册用户', error.response.status)
|
|
72
66
|
} else {
|
|
73
|
-
throw new
|
|
67
|
+
throw new HttpClientError(error.response.data?.msg, error.response.status)
|
|
74
68
|
}
|
|
75
|
-
} else if (error.request) {
|
|
76
|
-
// 请求已经成功发起,但没有收到响应
|
|
77
|
-
console.log(error.request)
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
throw new HttpClientError('无响应')
|
|
81
72
|
},
|
|
82
73
|
)
|
|
83
74
|
|
|
84
|
-
return
|
|
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
|
-
})
|
|
75
|
+
return instance
|
|
102
76
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import useSWRImmutable from 'swr/immutable'
|
|
2
|
-
import {
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { useHttpClient } from './use-http-client'
|
|
3
4
|
|
|
4
5
|
export interface PermissionCheckResult {
|
|
5
6
|
[k: string]: boolean
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function usePermissions(codes: string[]) {
|
|
9
|
-
const
|
|
10
|
+
const httpClient = useHttpClient()
|
|
11
|
+
const navigate = useNavigate()
|
|
10
12
|
|
|
11
13
|
const { data, isLoading } = useSWRImmutable(
|
|
12
14
|
codes.length > 0
|
|
@@ -17,7 +19,7 @@ export function usePermissions(codes: string[]) {
|
|
|
17
19
|
}
|
|
18
20
|
: null,
|
|
19
21
|
config =>
|
|
20
|
-
|
|
22
|
+
httpClient.request<PermissionCheckResult>(config).then(res => {
|
|
21
23
|
if (res.has_all) {
|
|
22
24
|
return codes.reduce(
|
|
23
25
|
(acc, curr) => {
|
|
@@ -36,12 +38,19 @@ export function usePermissions(codes: string[]) {
|
|
|
36
38
|
{} as Record<string, boolean>,
|
|
37
39
|
)
|
|
38
40
|
}),
|
|
41
|
+
{
|
|
42
|
+
suspense: true,
|
|
43
|
+
shouldRetryOnError: false,
|
|
44
|
+
onError() {
|
|
45
|
+
navigate('/login')
|
|
46
|
+
},
|
|
47
|
+
},
|
|
39
48
|
)
|
|
40
49
|
|
|
41
50
|
return { data, isLoading }
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
export function usePermission(code
|
|
53
|
+
export function usePermission(code: string) {
|
|
45
54
|
const { data, isLoading } = usePermissions(code ? [code] : [])
|
|
46
55
|
|
|
47
56
|
if (!code) {
|
package/src/index.ts
CHANGED
package/src/layouts/Layout.tsx
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import logo from '@/assets/512_orange_nobackground.png'
|
|
2
|
-
import { Layout as AntdLayout, theme } from 'antd'
|
|
2
|
+
import { Alert, Layout as AntdLayout, Spin, theme } from 'antd'
|
|
3
3
|
import type { FC, PropsWithChildren, ReactNode } from 'react'
|
|
4
|
+
import { Suspense } from 'react'
|
|
4
5
|
import { Link } from 'react-router-dom'
|
|
5
6
|
import type { ItemType2 } from './NavBar'
|
|
6
7
|
import NavBar from './NavBar'
|
|
8
|
+
import { usePermission } from '@/hooks'
|
|
7
9
|
|
|
8
10
|
const { Header, Sider, Content } = AntdLayout
|
|
11
|
+
const { ErrorBoundary } = Alert
|
|
9
12
|
|
|
10
13
|
export interface LayoutProps {
|
|
11
14
|
title?: ReactNode
|
|
@@ -19,45 +22,80 @@ const Layout: FC<PropsWithChildren<LayoutProps>> = props => {
|
|
|
19
22
|
token: { colorBgContainer, colorBorder },
|
|
20
23
|
} = theme.useToken()
|
|
21
24
|
|
|
25
|
+
// 为了验证 token 是否过期的请求,此处无其他用处。
|
|
26
|
+
usePermission('100001')
|
|
27
|
+
|
|
22
28
|
return (
|
|
23
29
|
<AntdLayout hasSider className="h-screen">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
theme="light"
|
|
38
|
-
>
|
|
39
|
-
<div className="flex items-end px-6 py-4">
|
|
40
|
-
<img src={logo} alt="logo" className="w-8 h-8" />
|
|
41
|
-
<Link className="font-bold text-lg ml-2" to="/">
|
|
42
|
-
{title}
|
|
43
|
-
</Link>
|
|
44
|
-
</div>
|
|
45
|
-
<NavBar items={items} />
|
|
46
|
-
</Sider>
|
|
47
|
-
<AntdLayout className="ml-64">
|
|
48
|
-
<Header
|
|
49
|
-
style={{
|
|
50
|
-
padding: '0 24px',
|
|
51
|
-
background: colorBgContainer,
|
|
52
|
-
borderBottomWidth: 1,
|
|
53
|
-
borderBottomStyle: 'solid',
|
|
54
|
-
borderBottomColor: colorBorder,
|
|
55
|
-
}}
|
|
30
|
+
<ErrorBoundary>
|
|
31
|
+
<Suspense
|
|
32
|
+
fallback={
|
|
33
|
+
<Spin
|
|
34
|
+
style={{
|
|
35
|
+
display: 'flex',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
width: '100vw',
|
|
39
|
+
height: '100vh',
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
}
|
|
56
43
|
>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
<Sider
|
|
45
|
+
width={256}
|
|
46
|
+
style={{
|
|
47
|
+
overflow: 'auto',
|
|
48
|
+
height: '100vh',
|
|
49
|
+
position: 'fixed',
|
|
50
|
+
left: 0,
|
|
51
|
+
top: 0,
|
|
52
|
+
bottom: 0,
|
|
53
|
+
borderRightWidth: 1,
|
|
54
|
+
borderRightStyle: 'solid',
|
|
55
|
+
borderRightColor: colorBorder,
|
|
56
|
+
}}
|
|
57
|
+
theme="light"
|
|
58
|
+
>
|
|
59
|
+
<div className="flex items-end px-6 py-4">
|
|
60
|
+
<img src={logo} alt="logo" className="w-8 h-8" />
|
|
61
|
+
<Link className="font-bold text-lg ml-2" to="/">
|
|
62
|
+
{title}
|
|
63
|
+
</Link>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<NavBar items={items} />
|
|
67
|
+
</Sider>
|
|
68
|
+
<AntdLayout className="ml-64">
|
|
69
|
+
<Header
|
|
70
|
+
style={{
|
|
71
|
+
padding: '0 24px',
|
|
72
|
+
background: colorBgContainer,
|
|
73
|
+
borderBottomWidth: 1,
|
|
74
|
+
borderBottomStyle: 'solid',
|
|
75
|
+
borderBottomColor: colorBorder,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{header}
|
|
79
|
+
</Header>
|
|
80
|
+
<Content className="p-6 overflow-auto bg-gray-50">
|
|
81
|
+
<Suspense
|
|
82
|
+
fallback={
|
|
83
|
+
<Spin
|
|
84
|
+
style={{
|
|
85
|
+
display: 'flex',
|
|
86
|
+
justifyContent: 'center',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
height: '50vh',
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</Suspense>
|
|
95
|
+
</Content>
|
|
96
|
+
</AntdLayout>
|
|
97
|
+
</Suspense>
|
|
98
|
+
</ErrorBoundary>
|
|
61
99
|
</AntdLayout>
|
|
62
100
|
)
|
|
63
101
|
}
|
package/src/layouts/NavBar.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import {usePermissions} from '@/hooks'
|
|
2
|
+
import {useMenuStore} from '@/stores'
|
|
3
|
+
import {Menu} from 'antd'
|
|
4
4
|
import type {
|
|
5
5
|
ItemType,
|
|
6
6
|
MenuDividerType,
|
|
@@ -8,10 +8,10 @@ import type {
|
|
|
8
8
|
MenuItemType,
|
|
9
9
|
SubMenuType,
|
|
10
10
|
} from 'antd/es/menu/hooks/useItems'
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
11
|
+
import type {FC, ReactNode} from 'react'
|
|
12
|
+
import {useCallback, useEffect, useMemo} from 'react'
|
|
13
|
+
import {Link, useLocation} from 'react-router-dom'
|
|
14
|
+
import type {Merge} from 'ts-essentials'
|
|
15
15
|
|
|
16
16
|
// 扩展 antd Menu 的类型,使其支持一些我们想要的自定义字段。
|
|
17
17
|
type MenuItemType2 = Merge<
|
|
@@ -107,7 +107,7 @@ const NavBar: FC<NavBarProps> = props => {
|
|
|
107
107
|
const location = useLocation()
|
|
108
108
|
const flattenItems = useMemo(() => flatItems(items ?? []), [items])
|
|
109
109
|
const codes = flattenItems.map(item => item.code).filter(Boolean) as string[]
|
|
110
|
-
const { data: permissions
|
|
110
|
+
const { data: permissions } = usePermissions(codes)
|
|
111
111
|
const internalItems = useMemo(() => transformItems(items ?? [], permissions), [items, permissions])
|
|
112
112
|
|
|
113
113
|
const openKeys = useMenuStore(state => state.openKeys)
|
|
@@ -136,19 +136,6 @@ const NavBar: FC<NavBarProps> = props => {
|
|
|
136
136
|
}
|
|
137
137
|
}, [flattenItems, location, setOpenKeys, setSelectedKeys])
|
|
138
138
|
|
|
139
|
-
if (isLoading) {
|
|
140
|
-
return (
|
|
141
|
-
<Spin
|
|
142
|
-
style={{
|
|
143
|
-
display: 'flex',
|
|
144
|
-
justifyContent: 'center',
|
|
145
|
-
alignItems: 'center',
|
|
146
|
-
height: 'calc(100vh - 64px)',
|
|
147
|
-
}}
|
|
148
|
-
/>
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
139
|
return (
|
|
153
140
|
<Menu
|
|
154
141
|
style={{ borderRight: 'none' }}
|
|
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'
|
|
|
7
7
|
import { Navigate, useSearchParams } from 'react-router-dom'
|
|
8
8
|
import useSWRImmutable from 'swr/immutable'
|
|
9
9
|
import Default from './default'
|
|
10
|
+
import { useHttpClient } from '@/hooks'
|
|
10
11
|
|
|
11
12
|
const { Title } = Typography
|
|
12
13
|
|
|
@@ -16,6 +17,7 @@ const Login: FC<PropsWithChildren> = props => {
|
|
|
16
17
|
const token = useTokenStore(state => state.token)
|
|
17
18
|
const setToken = useTokenStore(state => state.setToken)
|
|
18
19
|
const [showAlert, setShowAlert] = useState(false)
|
|
20
|
+
const httpClient = useHttpClient()
|
|
19
21
|
|
|
20
22
|
const { isLoading } = useSWRImmutable<{ token: string }>(
|
|
21
23
|
searchParams.has('ticket')
|
|
@@ -26,6 +28,7 @@ const Login: FC<PropsWithChildren> = props => {
|
|
|
26
28
|
},
|
|
27
29
|
}
|
|
28
30
|
: null,
|
|
31
|
+
config => httpClient.request(config),
|
|
29
32
|
{
|
|
30
33
|
onSuccess: data => {
|
|
31
34
|
setToken(data.token)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button, Result } from 'antd'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
|
|
4
|
+
const NoMatch = () => {
|
|
5
|
+
const navigate = useNavigate()
|
|
6
|
+
return (
|
|
7
|
+
<div className="h-screen flex justify-center items-center">
|
|
8
|
+
<Result
|
|
9
|
+
status="404"
|
|
10
|
+
title="404"
|
|
11
|
+
subTitle="Sorry, the page you visited does not exist."
|
|
12
|
+
extra={
|
|
13
|
+
<Button
|
|
14
|
+
type="primary"
|
|
15
|
+
onClick={() => {
|
|
16
|
+
navigate('/')
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
Back Home
|
|
20
|
+
</Button>
|
|
21
|
+
}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default NoMatch
|