react-toolkits 0.0.8 → 0.1.0
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 +8 -8
- package/CHANGELOG.md +6 -0
- package/README.md +41 -0
- package/dist/index.css +251 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +55 -54
- package/dist/index.esm.js +3574 -15
- package/dist/index.esm.js.map +1 -1
- package/package.json +2 -1
- package/src/components/GameSelect/index.tsx +82 -0
- package/src/components/Layout/index.tsx +90 -0
- package/src/{layouts/NavBar.tsx → components/NavMenu/index.tsx} +8 -17
- package/src/components/ReactToolkitsProvider/context.ts +76 -0
- package/src/components/ReactToolkitsProvider/index.tsx +23 -0
- package/src/components/UserWidget/index.tsx +46 -0
- package/src/components/index.ts +25 -1
- package/src/features/permission/components/PermissionCollapse/index.tsx +121 -0
- package/src/features/permission/components/PermissionList/index.tsx +17 -118
- package/src/features/permission/components/PermissionListV1/index.tsx +42 -0
- package/src/features/permission/components/PermissionListV2/index.tsx +146 -0
- package/src/features/permission/hooks/index.ts +24 -9
- package/src/features/permission/types/index.ts +8 -1
- package/src/hooks/use-http-client.tsx +9 -0
- package/src/hooks/use-permission.tsx +10 -2
- package/src/index.ts +0 -1
- package/src/pages/{NoMatch → base/NotFound}/index.tsx +4 -4
- package/src/pages/base/index.tsx +20 -0
- package/src/pages/index.ts +3 -4
- package/src/pages/permission/RoleList/index.tsx +63 -36
- package/src/pages/permission/index.tsx +26 -1
- package/src/stores/index.ts +0 -1
- package/src/stores/token.ts +15 -1
- package/tsup.config.ts +1 -1
- package/src/layouts/Layout.tsx +0 -103
- package/src/layouts/index.ts +0 -6
- package/src/stores/menu.ts +0 -27
- /package/src/pages/{Login → base/Login}/default.tsx +0 -0
- /package/src/pages/{Login → base/Login}/index.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-toolkits",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"sideEffects": [
|
|
5
5
|
"**/*.css"
|
|
6
6
|
],
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"antd": "^5.7.3",
|
|
29
29
|
"axios": "^1.4.0",
|
|
30
30
|
"dayjs": "^1.11.9",
|
|
31
|
+
"jwt-decode": "^3.1.2",
|
|
31
32
|
"lodash-es": "^4.17.21",
|
|
32
33
|
"react-router-dom": "^6.14.2",
|
|
33
34
|
"swr": "^2.2.0",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Select, Space, Typography } from 'antd'
|
|
2
|
+
import { useCallback, useMemo } from 'react'
|
|
3
|
+
import { useTokenStore } from '@/stores'
|
|
4
|
+
import useSWRImmutable from 'swr/immutable'
|
|
5
|
+
import { useReactToolkitsContext } from '@/components'
|
|
6
|
+
|
|
7
|
+
const { Text } = Typography
|
|
8
|
+
|
|
9
|
+
export interface GameType {
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
area: 'cn' | 'global'
|
|
13
|
+
Ctime: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function useGames() {
|
|
17
|
+
const { isPermissionV2, isGlobalNS } = useReactToolkitsContext(state => state)
|
|
18
|
+
const user = useTokenStore(state => state.getUser())
|
|
19
|
+
|
|
20
|
+
const { data, isLoading } = useSWRImmutable<GameType[]>(
|
|
21
|
+
isPermissionV2 && !isGlobalNS && user
|
|
22
|
+
? {
|
|
23
|
+
method: 'GET',
|
|
24
|
+
url: '/api/usystem/game/all',
|
|
25
|
+
params: { user: user.authorityId },
|
|
26
|
+
headers: {
|
|
27
|
+
'App-ID': 'global',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: null,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
games: data,
|
|
35
|
+
isLoading,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GameSelect = () => {
|
|
40
|
+
const { game, setGame, isGlobalNS, isPermissionV2 } = useReactToolkitsContext(state => state)
|
|
41
|
+
const { games, isLoading } = useGames()
|
|
42
|
+
|
|
43
|
+
const options = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
games?.map(item => ({
|
|
46
|
+
label: item.name,
|
|
47
|
+
value: item.id,
|
|
48
|
+
})),
|
|
49
|
+
[games],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const onGameChange = useCallback(
|
|
53
|
+
(id: string) => {
|
|
54
|
+
const matchGame = (games ?? []).find(item => item.id === id) ?? null
|
|
55
|
+
setGame(matchGame)
|
|
56
|
+
// queryClient.clear()
|
|
57
|
+
},
|
|
58
|
+
[games, setGame],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (!isPermissionV2 || isGlobalNS) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Space>
|
|
67
|
+
<Text>当前游戏</Text>
|
|
68
|
+
<Select
|
|
69
|
+
showSearch
|
|
70
|
+
optionFilterProp="label"
|
|
71
|
+
value={game?.id}
|
|
72
|
+
placeholder="请选择游戏"
|
|
73
|
+
loading={isLoading}
|
|
74
|
+
style={{ width: '200px' }}
|
|
75
|
+
options={options}
|
|
76
|
+
onChange={onGameChange}
|
|
77
|
+
/>
|
|
78
|
+
</Space>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default GameSelect
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import logo from '@/assets/512_orange_nobackground.png'
|
|
2
|
+
import * as Antd from 'antd'
|
|
3
|
+
import { Divider, Space } from 'antd'
|
|
4
|
+
import type { FC, PropsWithChildren } from 'react'
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { Suspense } from 'react'
|
|
7
|
+
import { Link } from 'react-router-dom'
|
|
8
|
+
import { GameSelect, NavMenu, useReactToolkitsContext, UserWidget } from '@/components'
|
|
9
|
+
|
|
10
|
+
const { Spin, theme } = Antd
|
|
11
|
+
const { Header, Sider, Content } = Antd.Layout
|
|
12
|
+
|
|
13
|
+
export interface LayoutProps extends PropsWithChildren {
|
|
14
|
+
extra?: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Layout: FC<LayoutProps> = props => {
|
|
18
|
+
const { children, extra } = props
|
|
19
|
+
const {
|
|
20
|
+
token: { colorBgContainer, colorBorder },
|
|
21
|
+
} = theme.useToken()
|
|
22
|
+
const title = useReactToolkitsContext(state => state.title)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Antd.Layout hasSider className="h-screen">
|
|
26
|
+
<Sider
|
|
27
|
+
width={256}
|
|
28
|
+
style={{
|
|
29
|
+
overflow: 'auto',
|
|
30
|
+
height: '100vh',
|
|
31
|
+
position: 'fixed',
|
|
32
|
+
left: 0,
|
|
33
|
+
top: 0,
|
|
34
|
+
bottom: 0,
|
|
35
|
+
borderRightWidth: 1,
|
|
36
|
+
borderRightStyle: 'solid',
|
|
37
|
+
borderRightColor: colorBorder,
|
|
38
|
+
}}
|
|
39
|
+
theme="light"
|
|
40
|
+
>
|
|
41
|
+
<div className="flex items-end px-6 py-4">
|
|
42
|
+
<img src={logo} alt="logo" className="w-8 h-8" />
|
|
43
|
+
<Link className="font-bold text-lg ml-2" to="/">
|
|
44
|
+
{title}
|
|
45
|
+
</Link>
|
|
46
|
+
</div>
|
|
47
|
+
<NavMenu />
|
|
48
|
+
</Sider>
|
|
49
|
+
<Antd.Layout className="ml-64">
|
|
50
|
+
<Header
|
|
51
|
+
style={{
|
|
52
|
+
padding: '0 24px',
|
|
53
|
+
background: colorBgContainer,
|
|
54
|
+
borderBottomWidth: 1,
|
|
55
|
+
borderBottomStyle: 'solid',
|
|
56
|
+
borderBottomColor: colorBorder,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<div className="flex justify-between items-center h-full">
|
|
60
|
+
<div>
|
|
61
|
+
<GameSelect />
|
|
62
|
+
</div>
|
|
63
|
+
<Space size="small" split={<Divider type="vertical" />}>
|
|
64
|
+
{extra}
|
|
65
|
+
<UserWidget />
|
|
66
|
+
</Space>
|
|
67
|
+
</div>
|
|
68
|
+
</Header>
|
|
69
|
+
<Content className="p-6 overflow-auto bg-gray-50">
|
|
70
|
+
<Suspense
|
|
71
|
+
fallback={
|
|
72
|
+
<Spin
|
|
73
|
+
style={{
|
|
74
|
+
display: 'flex',
|
|
75
|
+
justifyContent: 'center',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
height: '50vh',
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</Suspense>
|
|
84
|
+
</Content>
|
|
85
|
+
</Antd.Layout>
|
|
86
|
+
</Antd.Layout>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default Layout
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {usePermissions} from '@/hooks'
|
|
2
|
-
import {useMenuStore} from '@/stores'
|
|
3
2
|
import {Menu} from 'antd'
|
|
4
3
|
import type {
|
|
5
4
|
ItemType,
|
|
@@ -8,10 +7,11 @@ import type {
|
|
|
8
7
|
MenuItemType,
|
|
9
8
|
SubMenuType,
|
|
10
9
|
} from 'antd/es/menu/hooks/useItems'
|
|
11
|
-
import type {
|
|
10
|
+
import type {ReactNode} from 'react'
|
|
12
11
|
import {useCallback, useEffect, useMemo} from 'react'
|
|
13
12
|
import {Link, useLocation} from 'react-router-dom'
|
|
14
13
|
import type {Merge} from 'ts-essentials'
|
|
14
|
+
import {useReactToolkitsContext} from '@/components'
|
|
15
15
|
|
|
16
16
|
// 扩展 antd Menu 的类型,使其支持一些我们想要的自定义字段。
|
|
17
17
|
type MenuItemType2 = Merge<
|
|
@@ -98,29 +98,20 @@ function flatItems(
|
|
|
98
98
|
return result
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
items: ItemType2[]
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const NavBar: FC<NavBarProps> = props => {
|
|
106
|
-
const { items } = props
|
|
101
|
+
const NavMenu = () => {
|
|
107
102
|
const location = useLocation()
|
|
103
|
+
const items = useReactToolkitsContext(state => state.menuItems)
|
|
108
104
|
const flattenItems = useMemo(() => flatItems(items ?? []), [items])
|
|
109
105
|
const codes = flattenItems.map(item => item.code).filter(Boolean) as string[]
|
|
110
|
-
const { data: permissions } = usePermissions(codes)
|
|
106
|
+
const { data: permissions } = usePermissions(codes, true)
|
|
111
107
|
const internalItems = useMemo(() => transformItems(items ?? [], permissions), [items, permissions])
|
|
112
|
-
|
|
113
|
-
const openKeys = useMenuStore(state => state.openKeys)
|
|
114
|
-
const selectedKeys = useMenuStore(state => state.selectedKeys)
|
|
115
|
-
const setOpenKeys = useMenuStore(state => state.setOpenKeys)
|
|
116
|
-
const setSelectedKeys = useMenuStore(state => state.setSelectedKeys)
|
|
108
|
+
const { openKeys, selectedKeys, setOpenKeys, setSelectedKeys } = useReactToolkitsContext(state => state)
|
|
117
109
|
|
|
118
110
|
const onOpenChange = useCallback(
|
|
119
111
|
(keys: string[]) => {
|
|
120
112
|
const latestOpenKey = keys?.find(key => openKeys?.indexOf(key) === -1)
|
|
121
113
|
const match = flattenItems.find(item => latestOpenKey === item.key)
|
|
122
|
-
|
|
123
|
-
setOpenKeys(_openKeys)
|
|
114
|
+
setOpenKeys((match?.keypath ?? [latestOpenKey]) as string[])
|
|
124
115
|
},
|
|
125
116
|
[flattenItems, openKeys, setOpenKeys],
|
|
126
117
|
)
|
|
@@ -148,4 +139,4 @@ const NavBar: FC<NavBarProps> = props => {
|
|
|
148
139
|
)
|
|
149
140
|
}
|
|
150
141
|
|
|
151
|
-
export default
|
|
142
|
+
export default NavMenu
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {create, useStore} from 'zustand'
|
|
2
|
+
import {createContext, useContext} from 'react'
|
|
3
|
+
import type {StateStorage} from 'zustand/middleware'
|
|
4
|
+
import {createJSONStorage, persist} from 'zustand/middleware'
|
|
5
|
+
import type {GameType} from '../GameSelect'
|
|
6
|
+
import type {ItemType2} from '../NavMenu'
|
|
7
|
+
|
|
8
|
+
// SessionStorage 在同一域下的不同页面间是隔离的,用于防止多开页面时的数据冲突
|
|
9
|
+
const mixedStorage: StateStorage = {
|
|
10
|
+
getItem: (name: string): string | null => {
|
|
11
|
+
return sessionStorage.getItem(name) || localStorage.getItem(name)
|
|
12
|
+
},
|
|
13
|
+
setItem: (name: string, value: string) => {
|
|
14
|
+
localStorage.setItem(name, value)
|
|
15
|
+
sessionStorage.setItem(name, value)
|
|
16
|
+
},
|
|
17
|
+
removeItem: async (name: string) => {
|
|
18
|
+
localStorage.removeItem(name)
|
|
19
|
+
sessionStorage.removeItem(name)
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReactToolkitsState {
|
|
24
|
+
title: string
|
|
25
|
+
isPermissionV2: boolean
|
|
26
|
+
isGlobalNS: boolean
|
|
27
|
+
game: GameType | null
|
|
28
|
+
setGame: (game: GameType | null) => void
|
|
29
|
+
openKeys: string[]
|
|
30
|
+
selectedKeys: string[]
|
|
31
|
+
setOpenKeys: (keys: string[]) => void
|
|
32
|
+
setSelectedKeys: (keys: string[]) => void
|
|
33
|
+
menuItems: ItemType2[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ReactToolkitsStore = ReturnType<typeof createReactToolkitsStore>
|
|
37
|
+
|
|
38
|
+
export const createReactToolkitsStore = () => {
|
|
39
|
+
return create<ReactToolkitsState>()(
|
|
40
|
+
persist(
|
|
41
|
+
set => ({
|
|
42
|
+
title: '',
|
|
43
|
+
isPermissionV2: false,
|
|
44
|
+
isGlobalNS: false,
|
|
45
|
+
game: null,
|
|
46
|
+
setGame: game => set({ game }),
|
|
47
|
+
openKeys: [],
|
|
48
|
+
setOpenKeys: keys => set({ openKeys: keys }),
|
|
49
|
+
selectedKeys: [],
|
|
50
|
+
setSelectedKeys: keys => set({ selectedKeys: keys }),
|
|
51
|
+
menuItems: [],
|
|
52
|
+
}),
|
|
53
|
+
{
|
|
54
|
+
name: 'ReactToolkits',
|
|
55
|
+
storage: createJSONStorage(() => mixedStorage),
|
|
56
|
+
partialize: state => ({
|
|
57
|
+
title: state.title,
|
|
58
|
+
game: state.game,
|
|
59
|
+
openKeys: state.openKeys,
|
|
60
|
+
selectedKeys: state.selectedKeys,
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const ReactToolkitsContext = createContext<ReactToolkitsStore | null>(null)
|
|
68
|
+
|
|
69
|
+
export function useReactToolkitsContext<T>(
|
|
70
|
+
selector: (state: ReactToolkitsState) => T,
|
|
71
|
+
equalityFn?: (left: T, right: T) => boolean,
|
|
72
|
+
): T {
|
|
73
|
+
const store = useContext(ReactToolkitsContext)
|
|
74
|
+
if (!store) throw new Error('Missing ReactToolkitsContext.Provider in the tree')
|
|
75
|
+
return useStore(store, selector, equalityFn)
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FC, PropsWithChildren } from 'react'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
import type { ReactToolkitsState, ReactToolkitsStore } from './context'
|
|
4
|
+
import { createReactToolkitsStore, ReactToolkitsContext } from './context'
|
|
5
|
+
|
|
6
|
+
const ReactToolkitsProvider: FC<
|
|
7
|
+
PropsWithChildren<Partial<Pick<ReactToolkitsState, 'isPermissionV2' | 'isGlobalNS' | 'menuItems' | 'title'>>>
|
|
8
|
+
> = props => {
|
|
9
|
+
const { children, ...restProps } = props
|
|
10
|
+
const storeRef = useRef<ReactToolkitsStore>()
|
|
11
|
+
|
|
12
|
+
if (!storeRef.current) {
|
|
13
|
+
storeRef.current = createReactToolkitsStore()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
storeRef.current?.setState(restProps)
|
|
18
|
+
}, [restProps])
|
|
19
|
+
|
|
20
|
+
return <ReactToolkitsContext.Provider value={storeRef.current}>{children}</ReactToolkitsContext.Provider>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default ReactToolkitsProvider
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { Dropdown, Space } from 'antd'
|
|
4
|
+
import Link from 'antd/es/typography/Link'
|
|
5
|
+
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'
|
|
6
|
+
import { useTokenStore } from '@/stores'
|
|
7
|
+
|
|
8
|
+
const UserWidget: FC = props => {
|
|
9
|
+
const navigate = useNavigate()
|
|
10
|
+
const clearToken = useTokenStore(state => state.clearToken)
|
|
11
|
+
const user = useTokenStore(state => state.getUser())
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Dropdown
|
|
15
|
+
menu={{
|
|
16
|
+
selectable: true,
|
|
17
|
+
items: [
|
|
18
|
+
{
|
|
19
|
+
key: '1',
|
|
20
|
+
label: (
|
|
21
|
+
<Link
|
|
22
|
+
onClick={() => {
|
|
23
|
+
clearToken()
|
|
24
|
+
navigate('/login')
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
登出
|
|
28
|
+
</Link>
|
|
29
|
+
),
|
|
30
|
+
icon: <LogoutOutlined />,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}}
|
|
34
|
+
placement="bottomRight"
|
|
35
|
+
>
|
|
36
|
+
<Link>
|
|
37
|
+
<Space align="center">
|
|
38
|
+
<span>{user?.authorityId}</span>
|
|
39
|
+
<UserOutlined style={{ fontSize: '16px' }} />
|
|
40
|
+
</Space>
|
|
41
|
+
</Link>
|
|
42
|
+
</Dropdown>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default UserWidget
|
package/src/components/index.ts
CHANGED
|
@@ -12,8 +12,30 @@ import type { PermissionButtonProps } from './PermissionButton'
|
|
|
12
12
|
import PermissionButton from './PermissionButton'
|
|
13
13
|
import type { QueryListKey, QueryListProps } from './QueryList'
|
|
14
14
|
import QueryList from './QueryList'
|
|
15
|
+
import { useReactToolkitsContext } from './ReactToolkitsProvider/context'
|
|
16
|
+
import ReactToolkitsProvider from './ReactToolkitsProvider'
|
|
17
|
+
import GameSelect from './GameSelect'
|
|
18
|
+
import UserWidget from './UserWidget'
|
|
19
|
+
import type { ItemType2 } from './NavMenu'
|
|
20
|
+
import NavMenu from './NavMenu'
|
|
21
|
+
import type { LayoutProps } from './Layout'
|
|
22
|
+
import Layout from './Layout'
|
|
15
23
|
|
|
16
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
FormModal,
|
|
26
|
+
PermissionButton,
|
|
27
|
+
DynamicTags,
|
|
28
|
+
QueryList,
|
|
29
|
+
FilterForm,
|
|
30
|
+
Highlight,
|
|
31
|
+
useFormModal,
|
|
32
|
+
useReactToolkitsContext,
|
|
33
|
+
ReactToolkitsProvider,
|
|
34
|
+
GameSelect,
|
|
35
|
+
UserWidget,
|
|
36
|
+
NavMenu,
|
|
37
|
+
Layout,
|
|
38
|
+
}
|
|
17
39
|
export type {
|
|
18
40
|
DynamicTagsProps,
|
|
19
41
|
FilterFormProps,
|
|
@@ -24,4 +46,6 @@ export type {
|
|
|
24
46
|
QueryListKey,
|
|
25
47
|
HighlightTextsProps,
|
|
26
48
|
PermissionButtonProps,
|
|
49
|
+
ItemType2,
|
|
50
|
+
LayoutProps,
|
|
27
51
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { PermissionEnumItem } from '@/features/permission'
|
|
2
|
+
import type { FC } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import type { CheckboxChangeEvent } from 'antd/es/checkbox'
|
|
5
|
+
import { Checkbox, Col, Collapse, Row } from 'antd'
|
|
6
|
+
|
|
7
|
+
const { Panel } = Collapse
|
|
8
|
+
|
|
9
|
+
interface PermissionCollapseProps {
|
|
10
|
+
expand?: boolean
|
|
11
|
+
permissions?: PermissionEnumItem[]
|
|
12
|
+
readonly?: boolean
|
|
13
|
+
value?: string[]
|
|
14
|
+
onChange?: (value: string[]) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PermissionCollapse: FC<PermissionCollapseProps> = props => {
|
|
18
|
+
const { permissions, readonly, expand, value, onChange } = props
|
|
19
|
+
const [activeKey, setActiveKey] = useState<string[]>([])
|
|
20
|
+
const [checkedMap, setCheckedMap] = useState<Record<string, boolean>>({})
|
|
21
|
+
const [internalValue, setInternalValue] = useState<string[]>(value ?? [])
|
|
22
|
+
|
|
23
|
+
const onCollapseChange = useCallback((key: string | string[]) => {
|
|
24
|
+
setActiveKey(key as string[])
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
const getCheckedValue = (checkedValue: boolean, codes: string[]) => {
|
|
28
|
+
let tempValue: string[] = []
|
|
29
|
+
|
|
30
|
+
if (checkedValue) {
|
|
31
|
+
tempValue = [...new Set(internalValue.concat(codes))]
|
|
32
|
+
} else {
|
|
33
|
+
tempValue = internalValue.slice()
|
|
34
|
+
|
|
35
|
+
codes.forEach(code => {
|
|
36
|
+
const index = tempValue.findIndex(item => item === code)
|
|
37
|
+
if (index > -1) {
|
|
38
|
+
tempValue.splice(index, 1)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return tempValue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const onCheckChange = (e: CheckboxChangeEvent, category: string, codes: string[]) => {
|
|
47
|
+
const checkedValue = getCheckedValue(e.target.checked, codes)
|
|
48
|
+
setInternalValue(checkedValue)
|
|
49
|
+
onChange?.(checkedValue)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setInternalValue(value ?? [])
|
|
54
|
+
}, [value])
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (expand) {
|
|
58
|
+
setActiveKey((permissions ?? []).map(({ category }) => category))
|
|
59
|
+
}
|
|
60
|
+
}, [expand, permissions])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const checkedValue = (permissions ?? []).reduce(
|
|
64
|
+
(acc, curr) => {
|
|
65
|
+
acc[curr.category] = curr.permissions.every(item => internalValue.includes(item.value))
|
|
66
|
+
return acc
|
|
67
|
+
},
|
|
68
|
+
{} as Record<string, boolean>,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
setCheckedMap(checkedValue)
|
|
72
|
+
}, [internalValue, permissions])
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Collapse
|
|
76
|
+
style={{ width: '100%' }}
|
|
77
|
+
collapsible="header"
|
|
78
|
+
activeKey={activeKey}
|
|
79
|
+
items={(permissions ?? []).map(item => ({
|
|
80
|
+
key: item.category,
|
|
81
|
+
label: item.category,
|
|
82
|
+
extra: !readonly && (
|
|
83
|
+
<Checkbox
|
|
84
|
+
checked={checkedMap[item.category]}
|
|
85
|
+
onChange={e => {
|
|
86
|
+
onCheckChange(
|
|
87
|
+
e,
|
|
88
|
+
item.category,
|
|
89
|
+
item.permissions.map(permission => permission.value),
|
|
90
|
+
)
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
全选
|
|
94
|
+
</Checkbox>
|
|
95
|
+
),
|
|
96
|
+
children: (
|
|
97
|
+
<Checkbox.Group style={{ width: '100%' }} value={internalValue}>
|
|
98
|
+
<Row gutter={[10, 10]} style={{ width: '100%' }}>
|
|
99
|
+
{item.permissions.map(permission => (
|
|
100
|
+
<Col key={permission.value} span={6}>
|
|
101
|
+
<Checkbox
|
|
102
|
+
disabled={readonly}
|
|
103
|
+
value={permission.value}
|
|
104
|
+
onChange={e => {
|
|
105
|
+
onCheckChange(e, item.category, [permission.value])
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{permission.label}
|
|
109
|
+
</Checkbox>
|
|
110
|
+
</Col>
|
|
111
|
+
))}
|
|
112
|
+
</Row>
|
|
113
|
+
</Checkbox.Group>
|
|
114
|
+
),
|
|
115
|
+
}))}
|
|
116
|
+
onChange={onCollapseChange}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default PermissionCollapse
|