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.
Files changed (50) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.js +4 -0
  3. package/.turbo/turbo-build.log +20 -0
  4. package/CHANGELOG.md +10 -0
  5. package/dist/512_orange_nobackground-L6MFCL6M.png +0 -0
  6. package/dist/index.css +230 -0
  7. package/dist/index.css.map +1 -0
  8. package/dist/index.d.mts +191 -0
  9. package/dist/index.esm.js +3152 -0
  10. package/dist/index.esm.js.map +1 -0
  11. package/package.json +63 -0
  12. package/postcss.config.js +9 -0
  13. package/src/assets/512_orange_nobackground.png +0 -0
  14. package/src/components/DynamicTags/index.tsx +160 -0
  15. package/src/components/FilterForm/index.tsx +62 -0
  16. package/src/components/FormModal/hooks.tsx +48 -0
  17. package/src/components/FormModal/index.tsx +138 -0
  18. package/src/components/Highlight/index.tsx +51 -0
  19. package/src/components/PermissionButton/index.tsx +36 -0
  20. package/src/components/QueryList/index.tsx +158 -0
  21. package/src/components/index.ts +27 -0
  22. package/src/constants/index.ts +3 -0
  23. package/src/features/permission/components/PermissionList.tsx +129 -0
  24. package/src/features/permission/hooks/index.ts +140 -0
  25. package/src/features/permission/index.ts +5 -0
  26. package/src/features/permission/types/index.ts +33 -0
  27. package/src/hooks/index.ts +2 -0
  28. package/src/hooks/use-fetcher.tsx +102 -0
  29. package/src/hooks/use-permission.tsx +58 -0
  30. package/src/index.ts +7 -0
  31. package/src/layouts/Layout.tsx +81 -0
  32. package/src/layouts/NavBar.tsx +176 -0
  33. package/src/layouts/index.ts +6 -0
  34. package/src/pages/Login/default.tsx +864 -0
  35. package/src/pages/Login/index.tsx +111 -0
  36. package/src/pages/index.ts +4 -0
  37. package/src/pages/permission/RoleDetail.tsx +40 -0
  38. package/src/pages/permission/RoleList.tsx +226 -0
  39. package/src/pages/permission/UserList.tsx +248 -0
  40. package/src/pages/permission/index.tsx +31 -0
  41. package/src/shims.d.ts +20 -0
  42. package/src/stores/index.ts +3 -0
  43. package/src/stores/menu.ts +27 -0
  44. package/src/stores/queryTrigger.ts +27 -0
  45. package/src/stores/token.ts +25 -0
  46. package/src/styles/index.css +5 -0
  47. package/src/types/index.ts +27 -0
  48. package/tailwind.config.js +5 -0
  49. package/tsconfig.json +11 -0
  50. 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,5 @@
1
+ import PermissionList from './components/PermissionList'
2
+
3
+ export * from './hooks'
4
+ export * from './types'
5
+ export { PermissionList }
@@ -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,2 @@
1
+ export * from './use-fetcher'
2
+ export * from './use-permission'
@@ -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,7 @@
1
+ import './styles/index.css'
2
+
3
+ export * from './components'
4
+ export * from './hooks'
5
+ export * from './stores'
6
+ export * from './pages'
7
+ export * from './layouts'
@@ -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