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