dark
7 months ago
9 changed files with 488 additions and 65 deletions
-
93src/components/department-tree/DepartmentTree.tsx
-
0src/components/department-tree/index.ts
-
56src/components/department-tree/style.ts
-
17src/components/empty/EmptyWrap.tsx
-
1src/components/empty/index.ts
-
22src/components/user-picker/Item.tsx
-
81src/components/user-picker/List.tsx
-
163src/components/user-picker/UserPicker.tsx
-
72src/components/user-picker/style.ts
@ -0,0 +1,93 @@ |
|||
import { usePageStoreOptions } from '@/store' |
|||
import { Empty, Spin, Tree, TreeDataNode, TreeProps } from 'antd' |
|||
import { useStyle } from './style.ts' |
|||
import { useTranslation } from '@/i18n.ts' |
|||
import { departTreeAtom, } from '@/store/department.ts' |
|||
import { useAtomValue } from 'jotai' |
|||
import { useRef } from 'react' |
|||
import { flattenTree } from '@/utils' |
|||
import { useDeepCompareEffect } from 'react-use' |
|||
import { IDepartment } from '@/types/department' |
|||
|
|||
export interface DepartmentTreeProps extends TreeProps { |
|||
root?: TreeDataNode | boolean | string |
|||
} |
|||
|
|||
function getTopDataNode(root?: TreeDataNode | boolean | string, fieldNames?: TreeProps['fieldNames']) { |
|||
if (!root) { |
|||
return null |
|||
} |
|||
fieldNames = { key: 'key', title: 'title', ...fieldNames } |
|||
let data: TreeDataNode | null = {} as TreeDataNode |
|||
data[fieldNames.key!] = '' |
|||
|
|||
switch (typeof root) { |
|||
case 'boolean': { |
|||
if (!root) { |
|||
return null |
|||
} |
|||
data[fieldNames.title!] = '所有' |
|||
break |
|||
} |
|||
case 'string': { |
|||
data[fieldNames.title!] = root |
|||
break |
|||
} |
|||
case 'object': { |
|||
data = root |
|||
break |
|||
} |
|||
default: { |
|||
data = null |
|||
} |
|||
} |
|||
return data |
|||
} |
|||
|
|||
export const DepartmentTree = ({ root, ...props }: DepartmentTreeProps) => { |
|||
|
|||
const { styles } = useStyle() |
|||
const { t } = useTranslation() |
|||
const { data = [], isLoading } = useAtomValue(departTreeAtom, usePageStoreOptions()) |
|||
const flattenMenusRef = useRef<IDepartment[]>([]) |
|||
|
|||
useDeepCompareEffect(() => { |
|||
|
|||
if (isLoading) return |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|||
// @ts-ignore array
|
|||
if (data.length) { |
|||
// @ts-ignore flattenTree
|
|||
flattenMenusRef.current = flattenTree<IDepartment[]>(data as any) |
|||
} |
|||
|
|||
}, [ data, isLoading ]) |
|||
|
|||
|
|||
const renderEmpty = () => { |
|||
if ((data as any).length > 0 || isLoading) return null |
|||
return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/> |
|||
} |
|||
const topData = getTopDataNode(root, props.fieldNames) |
|||
const treeData = topData ? [ topData, ...data as Array<any> ] : data |
|||
|
|||
return (<> |
|||
<Spin spinning={isLoading} style={{ minHeight: 200 }}> |
|||
{ |
|||
renderEmpty() |
|||
} |
|||
<Tree.DirectoryTree |
|||
className={styles.tree} |
|||
treeData={treeData as any} |
|||
defaultExpandAll={true} |
|||
// checkable={true}
|
|||
showIcon={false} |
|||
{...props} |
|||
/> |
|||
</Spin> |
|||
</> |
|||
) |
|||
} |
|||
|
|||
export default DepartmentTree |
@ -0,0 +1,56 @@ |
|||
import { createStyles } from '@/theme' |
|||
|
|||
export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { |
|||
const prefix = `${prefixCls}-${token?.proPrefix}-department-tree` |
|||
|
|||
|
|||
const tree = css`
|
|||
.ant-tree { |
|||
overflow: auto; |
|||
height: 100%; |
|||
border-right: 1px solid ${token.colorBorder}; |
|||
background: ${token.colorBgContainer}; |
|||
|
|||
} |
|||
|
|||
.ant-tree-directory .ant-tree-treenode-selected::before { |
|||
background: ${token.colorBgTextHover}; |
|||
} |
|||
|
|||
.ant-tree-treenode:before { |
|||
border-radius: ${token.borderRadius}px; |
|||
} |
|||
`
|
|||
|
|||
const treeNode = css`
|
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
|
|||
.actions { |
|||
display: none; |
|||
padding: 0 10px; |
|||
} |
|||
|
|||
&:hover .actions { { |
|||
display: flex; |
|||
} |
|||
|
|||
`
|
|||
const treeActions = css`
|
|||
padding: 0 24px 16px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
position: sticky; |
|||
bottom: 0; |
|||
z-index: 10; |
|||
background: ${token.colorBgContainer}; |
|||
`
|
|||
|
|||
return { |
|||
container: cx(prefix), |
|||
tree, |
|||
treeNode, |
|||
treeActions |
|||
} |
|||
}) |
@ -0,0 +1,17 @@ |
|||
import { Empty, EmptyProps } from 'antd' |
|||
|
|||
export interface EmptyWrapProps extends EmptyProps { |
|||
//用于判断是否为空
|
|||
isEmpty: boolean |
|||
} |
|||
|
|||
const EmptyWrap = ({ isEmpty, children, ...props }: EmptyWrapProps) => { |
|||
if (!isEmpty) { |
|||
return children |
|||
} |
|||
return ( |
|||
<Empty {...props} style={{ marginTop: 20}} /> |
|||
) |
|||
} |
|||
|
|||
export default EmptyWrap |
@ -0,0 +1 @@ |
|||
export * from './EmptyWrap' |
@ -1,22 +0,0 @@ |
|||
import { IUser } from '@/types/user' |
|||
import { useAtom } from 'jotai' |
|||
import { useStyle } from './style.ts' |
|||
import { Checkbox } from 'antd' |
|||
|
|||
export const Item = ( props: { |
|||
value: IUser, |
|||
onChange:( value: IUser)=>void |
|||
} )=>{ |
|||
|
|||
const { styles } = useStyle() |
|||
|
|||
|
|||
return ( |
|||
<div className={styles.listItem}> |
|||
<Checkbox value={props.value} onChange={e=>}> |
|||
<span>{props.value.name}</span> |
|||
</Checkbox> |
|||
</div> |
|||
) |
|||
|
|||
} |
@ -0,0 +1,81 @@ |
|||
import { useStyle } from './style.ts' |
|||
import { Avatar, Button, Checkbox, CheckboxProps, Radio } from 'antd' |
|||
import { createContext, useContext } from 'react' |
|||
import { IUser } from '@/types' |
|||
import { DeleteOutlined } from '@ant-design/icons' |
|||
|
|||
export const ListItem = (props: CheckboxProps) => { |
|||
|
|||
const { styles, cx } = useStyle() |
|||
const ctx = useContext(ListContext) |
|||
|
|||
const Com = ctx?.multiple ? Checkbox : Radio |
|||
const selected = ctx?.multiple ? ctx?.value?.includes(props.value) : ctx?.value == props.value |
|||
return ( |
|||
<div className={cx(styles.listItem, { |
|||
['selected']: selected |
|||
})} |
|||
> |
|||
<Com {...props} |
|||
checked={selected}> |
|||
<span>{props?.name}</span> |
|||
</Com> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export const ListViewItem = ({ user, onDel }: { user: IUser, onDel: (user: IUser) => void }) => { |
|||
const { styles, cx, theme } = useStyle() |
|||
if (!user) { |
|||
return null |
|||
} |
|||
return ( |
|||
<div className={cx(styles.listItem, 'view')}> |
|||
<Avatar style={{ backgroundColor: `${theme.colorPrimary}`, verticalAlign: 'middle' }} size="small"> |
|||
{user.name?.charAt(0)} |
|||
</Avatar> |
|||
<span>{user.name}</span> |
|||
<span className={'del'}><Button onClick={() => onDel?.(user)} icon={<DeleteOutlined/>} type={'primary'} |
|||
size={'small'} danger={true} shape={'circle'}/> </span> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
|
|||
interface ListProps<T> { |
|||
dataSource: T[] |
|||
value?: any[] |
|||
onChange?: (value: any[]) => void |
|||
rowKey: string |
|||
multiple?: boolean |
|||
} |
|||
|
|||
const ListContext = createContext<ListProps<any> | undefined>(undefined) |
|||
|
|||
export const List = (props: ListProps<any>) => { |
|||
const { styles } = useStyle() |
|||
const { value, onChange, } = props |
|||
return ( |
|||
<ListContext.Provider value={props}> |
|||
<div className={styles.list}> |
|||
{ |
|||
props.dataSource?.map?.((item: any) => { |
|||
return <ListItem key={item.id} |
|||
{...item} |
|||
value={item.id} |
|||
onChange={e => { |
|||
const checked = e.target.checked |
|||
let val = value |
|||
if (props.multiple) { |
|||
val = checked ? [ ...(value || []), item.id ] : (value || []).filter(i => i !== item.id) |
|||
} else { |
|||
val = checked ? [ item.id ] : [] |
|||
} |
|||
onChange?.(val) |
|||
}}/> |
|||
}) |
|||
} |
|||
</div> |
|||
</ListContext.Provider> |
|||
) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue