From c2925431824956bf1b74af3af656de4a7e72bcbe Mon Sep 17 00:00:00 2001 From: dark Date: Wed, 1 May 2024 15:34:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=EF=BC=9A=E6=88=90=E5=91=98?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E7=BB=84=E4=BB=B6,=20=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E6=A0=91=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=A9=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=8C=85=E8=A3=B9=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/department-tree/DepartmentTree.tsx | 93 ++++++++++ src/components/department-tree/index.ts | 0 src/components/department-tree/style.ts | 56 ++++++ src/components/empty/EmptyWrap.tsx | 17 ++ src/components/empty/index.ts | 1 + src/components/user-picker/Item.tsx | 22 --- src/components/user-picker/List.tsx | 81 +++++++++ src/components/user-picker/UserPicker.tsx | 205 ++++++++++++++++++---- src/components/user-picker/style.ts | 78 +++++++- 9 files changed, 488 insertions(+), 65 deletions(-) create mode 100644 src/components/department-tree/DepartmentTree.tsx create mode 100644 src/components/department-tree/index.ts create mode 100644 src/components/department-tree/style.ts create mode 100644 src/components/empty/EmptyWrap.tsx create mode 100644 src/components/empty/index.ts delete mode 100644 src/components/user-picker/Item.tsx create mode 100644 src/components/user-picker/List.tsx diff --git a/src/components/department-tree/DepartmentTree.tsx b/src/components/department-tree/DepartmentTree.tsx new file mode 100644 index 0000000..06f53d5 --- /dev/null +++ b/src/components/department-tree/DepartmentTree.tsx @@ -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([]) + + 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(data as any) + } + + }, [ data, isLoading ]) + + + const renderEmpty = () => { + if ((data as any).length > 0 || isLoading) return null + return + } + const topData = getTopDataNode(root, props.fieldNames) + const treeData = topData ? [ topData, ...data as Array ] : data + + return (<> + + { + renderEmpty() + } + + + + ) +} + +export default DepartmentTree \ No newline at end of file diff --git a/src/components/department-tree/index.ts b/src/components/department-tree/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/components/department-tree/style.ts b/src/components/department-tree/style.ts new file mode 100644 index 0000000..fa4843e --- /dev/null +++ b/src/components/department-tree/style.ts @@ -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 + } +}) \ No newline at end of file diff --git a/src/components/empty/EmptyWrap.tsx b/src/components/empty/EmptyWrap.tsx new file mode 100644 index 0000000..e76642e --- /dev/null +++ b/src/components/empty/EmptyWrap.tsx @@ -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 ( + + ) +} + +export default EmptyWrap \ No newline at end of file diff --git a/src/components/empty/index.ts b/src/components/empty/index.ts new file mode 100644 index 0000000..90e7fdc --- /dev/null +++ b/src/components/empty/index.ts @@ -0,0 +1 @@ + export * from './EmptyWrap' \ No newline at end of file diff --git a/src/components/user-picker/Item.tsx b/src/components/user-picker/Item.tsx deleted file mode 100644 index 8bd15b5..0000000 --- a/src/components/user-picker/Item.tsx +++ /dev/null @@ -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 ( -
- }> - {props.value.name} - -
- ) - -} \ No newline at end of file diff --git a/src/components/user-picker/List.tsx b/src/components/user-picker/List.tsx new file mode 100644 index 0000000..429a58a --- /dev/null +++ b/src/components/user-picker/List.tsx @@ -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 ( +
+ + {props?.name} + +
+ ) +} + +export const ListViewItem = ({ user, onDel }: { user: IUser, onDel: (user: IUser) => void }) => { + const { styles, cx, theme } = useStyle() + if (!user) { + return null + } + return ( +
+ + {user.name?.charAt(0)} + + {user.name} +
+ ) +} + + +interface ListProps { + dataSource: T[] + value?: any[] + onChange?: (value: any[]) => void + rowKey: string + multiple?: boolean +} + +const ListContext = createContext | undefined>(undefined) + +export const List = (props: ListProps) => { + const { styles } = useStyle() + const { value, onChange, } = props + return ( + +
+ { + props.dataSource?.map?.((item: any) => { + return { + 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) + }}/> + }) + } +
+
+ ) +} \ No newline at end of file diff --git a/src/components/user-picker/UserPicker.tsx b/src/components/user-picker/UserPicker.tsx index d2c015a..ce492ae 100644 --- a/src/components/user-picker/UserPicker.tsx +++ b/src/components/user-picker/UserPicker.tsx @@ -1,61 +1,190 @@ -import { Modal, ModalProps, Select, SelectProps } from 'antd' -import { memo } from 'react' +import { Button, Input, Modal, ModalProps, Select, SelectProps, Spin } from 'antd' +import { memo, ReactNode, useEffect, useRef, useState } from 'react' import { Flexbox } from 'react-layout-kit' import { useStyle } from './style.ts' +import { List, ListViewItem } from './List.tsx' +import { useTranslation } from '@/i18n.ts' +import DepartmentTree from '@/components/department-tree/DepartmentTree.tsx' +import { DraggablePanel } from '@/components/draggable-panel' +import { useAtom, useAtomValue } from 'jotai' +import { userListAtom, userSearchAtom, } from '@/store/user.ts' +import { IUser } from '@/types' +import EmptyWrap from '@/components/empty/EmptyWrap.tsx' export interface UserSelectProps extends SelectProps { - + value?: any[] + onChange?: (value: any[]) => void + //多选 + multiple?: boolean } -export interface UserModelProps extends ModalProps { +export interface UserModelProps extends Pick { + value?: any[] + onChange?: (value?: any[]) => void + children?: ReactNode + //多选 + multiple?: boolean } export type UserPickerProps = - | { - type?: 'modal'; - /** Props for the modal component */ + | { + type?: 'modal'; + /** Props for the modal component */ } & UserModelProps - | { - type: 'select'; - /** Props for the select component */ + | { + type: 'select'; + /** Props for the select component */ } & UserSelectProps const UserSelect = memo((props: UserSelectProps) => { - console.log(props) - return ( - - ) -}) - -const UserModel = memo(({ open, ...props }: UserModelProps) => { - const { styles } = useStyle() + return ( + + ) }) +const UserModel = memo(({ multiple, children, value, onChange, ...props }: UserModelProps) => { + const { styles, cx } = useStyle() + const { t } = useTranslation() + const [ innerValue, setValue ] = useState(() => { + + if (!multiple && !Array.isArray(value)) { + return [ value ] + } + }) + const [ , setSearch ] = useAtom(userSearchAtom) + const { data: users, isPending } = useAtomValue(userListAtom) + const [ open, setOpen ] = useState(false) + const selectUserRef = useRef([]) + const [, update ] = useState({}) + + useEffect(() => { + if (value === undefined) return + + setValue(Array.isArray(value) ? value : [ value ]) + }, [ value ]) + + useEffect(() => { + + selectUserRef.current = [] + innerValue?.forEach(id => { + const item = users?.rows?.find(user => user.id === id) + if (item) { + selectUserRef.current = [ ...selectUserRef.current, item ] + } + }) + update({}) + + }, [ innerValue ]) + + useEffect(() => { + return () => { + selectUserRef.current = [] + } + + }, []) + + const renderTarget = () => { + return setOpen(true)}> + {children ?? } + + } + + return ( + <> + {renderTarget()} + { + onChange?.(multiple ? innerValue : innerValue?.[0]) + setOpen(false) + }} + onCancel={() => { + setOpen(false) + }} + > + + + { + setSearch({ + key: value, + }) + }} + placeholder={t('component.UserPicker.placeholder', '输入成员姓名,回车查询')}/> + + + { + setSearch({ + dept_id: keys?.[0] + }) + }} + /> + + + + + { + setValue(val) + }} + dataSource={users?.rows ?? []} + /> + + + + + + + +
{t('component.UserPicker.selected', '已选({{count}})', { count: selectUserRef.current?.length ?? 0 })}
+
+ { + selectUserRef.current?.map(user => { + return ( { + setValue(innerValue?.filter(id => id !== user.id)) + }} + key={user.id} + user={user!}/>) + }) + } +
+
+
+ +
+
+ + + ) +}) -const UserPicker = memo(({ type, ...props }: UserPickerProps) => { - return type === 'modal' ? : +const UserPicker = memo(({ type = 'modal', ...props }: UserPickerProps) => { + return type === 'modal' ? : }) export default UserPicker \ No newline at end of file diff --git a/src/components/user-picker/style.ts b/src/components/user-picker/style.ts index fdc2942..aa9d8e1 100644 --- a/src/components/user-picker/style.ts +++ b/src/components/user-picker/style.ts @@ -7,19 +7,87 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { const listItem = css` - padding: 5px; - height: 40px; - background-color: ${token.colorBgContainer}; + height: 40px; + background-color: ${token.colorBgContainer}; + display: flex; + align-items: center; + cursor: pointer; - :hover { - background-color: ${token.controlItemBgActiveHover}; + :first-child { + border-radius: 0 ${token.borderRadius}px 0 0; + } + + :last-child && :not(:first-child) { + border-radius: 0 0 ${token.borderRadius}px 0; + } + + :hover { + background-color: ${token.controlItemBgHover}; + } + + &.selected { + background-color: ${token.controlItemBgActive}; + } + + .ant-radio-wrapper, + .ant-checkbox-wrapper { + flex: 1; + padding: 5px 10px; + } + + &.view { + padding: 0 15px; + gap: 5px; + position: relative; + + .del { + position: absolute; + right: 15px; + display: none; + + } + + :hover .del { + display: block; + } + } + + ` + const modal = css` + .ant-modal-body { + max-height: 600px; + overflow: auto; } + ` + + const usersPanel = css` + border-left: 1px solid ${token.colorBorder}; + ` + const bordered = css` + + border: 1px solid ${token.colorBorder}; + border-radius: ${token.borderRadius}px; + ` + + const draggablePanel = css` + .ant-pro-draggable-panel-fixed { + background: transparent; + } + ` + const selected = css` + border-bottom: 1px solid ${token.colorBorder}; + padding: 8px; ` return { container: cx(prefix), + modal, + usersPanel, + draggablePanel, + selected, list, listItem, + bordered, } }) \ No newline at end of file