react-toolkits 0.0.7 → 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 -6
- package/CHANGELOG.md +12 -0
- package/README.md +41 -0
- package/dist/index.css +252 -1
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +55 -54
- package/dist/index.esm.js +3576 -15
- package/dist/index.esm.js.map +1 -0
- 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/{Login → base/Login}/index.tsx +3 -16
- 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/UserList/index.tsx +2 -2
- 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 +6 -6
- 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
|
@@ -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
|
|
@@ -1,129 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import type { RoleV1, RoleV2 } from '@/features/permission'
|
|
2
|
+
import { useReactToolkitsContext } from '@/components'
|
|
3
|
+
import type { FC } from 'react'
|
|
4
|
+
import PermissionListV1 from '../PermissionListV1'
|
|
5
|
+
import PermissionListV2 from '../PermissionListV2'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const PermissionList = ({
|
|
10
|
-
expand = true,
|
|
11
|
-
value,
|
|
12
|
-
readonly,
|
|
13
|
-
onChange,
|
|
14
|
-
}: {
|
|
7
|
+
export interface PermissionListPropsBase {
|
|
15
8
|
expand?: boolean
|
|
16
|
-
value?: CheckboxValueType[]
|
|
17
9
|
readonly?: boolean
|
|
18
|
-
|
|
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
|
-
}
|
|
10
|
+
}
|
|
67
11
|
|
|
68
|
-
|
|
69
|
-
|
|
12
|
+
interface PermissionListProps extends PermissionListPropsBase {
|
|
13
|
+
value?: RoleV1['permissions'] | RoleV2['permissions']
|
|
14
|
+
onChange?: (checkedValue: RoleV1['permissions'] | RoleV2['permissions']) => void
|
|
15
|
+
}
|
|
70
16
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onChange?.(checkedValue)
|
|
75
|
-
}
|
|
17
|
+
const PermissionList: FC<PermissionListProps> = (props: PermissionListProps) => {
|
|
18
|
+
const { value } = props
|
|
19
|
+
const isPermissionV2 = useReactToolkitsContext(state => state.isPermissionV2)
|
|
76
20
|
|
|
77
|
-
if (
|
|
78
|
-
return
|
|
79
|
-
<div className="flex justify-center">
|
|
80
|
-
<Text type="danger">权限获取失败</Text>
|
|
81
|
-
</div>
|
|
82
|
-
)
|
|
21
|
+
if (isPermissionV2) {
|
|
22
|
+
return <PermissionListV2 {...props} value={value as RoleV2['permissions']} />
|
|
83
23
|
}
|
|
84
24
|
|
|
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
|
-
)
|
|
25
|
+
return <PermissionListV1 {...props} value={value as RoleV1['permissions']} />
|
|
127
26
|
}
|
|
128
27
|
|
|
129
28
|
export default PermissionList
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Skeleton, Typography } from 'antd'
|
|
2
|
+
import { useAllPermissions } from '../../hooks'
|
|
3
|
+
import PermissionCollapse from '../PermissionCollapse'
|
|
4
|
+
import type { RoleV1 } from '../../types'
|
|
5
|
+
import type { PermissionListPropsBase } from '../PermissionList'
|
|
6
|
+
import type { FC } from 'react'
|
|
7
|
+
|
|
8
|
+
const { Text } = Typography
|
|
9
|
+
|
|
10
|
+
interface PermissionListV1Props extends PermissionListPropsBase {
|
|
11
|
+
value?: RoleV1['permissions']
|
|
12
|
+
onChange?: (checkedValue: RoleV1['permissions']) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PermissionListV1: FC<PermissionListV1Props> = props => {
|
|
16
|
+
const { expand = true, value, readonly, onChange } = props
|
|
17
|
+
const { data: permissions, isLoading, error } = useAllPermissions()
|
|
18
|
+
|
|
19
|
+
if (error) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex justify-center">
|
|
22
|
+
<Text type="danger">权限获取失败</Text>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Skeleton active loading={isLoading}>
|
|
29
|
+
<PermissionCollapse
|
|
30
|
+
value={value}
|
|
31
|
+
permissions={permissions}
|
|
32
|
+
readonly={readonly}
|
|
33
|
+
expand={expand}
|
|
34
|
+
onChange={newValue => {
|
|
35
|
+
onChange?.(newValue)
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
</Skeleton>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default PermissionListV1
|