From 3247f7558ed181b99178bf2d7e87ce4a0a380e05 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Sun, 17 Nov 2024 20:26:05 +0800 Subject: [PATCH 1/9] feat(catalogue): add catalogue --- src/catalogue/components/catalogue.tsx | 239 ++++++++ src/catalogue/components/icon.tsx | 290 +++++++++ src/catalogue/components/index.ts | 3 - src/catalogue/components/tree.tsx | 32 + .../tree/components/header/index.tsx | 50 -- .../tree/components/header/style.scss | 27 - .../components/tree/components/index.ts | 1 - .../components/tree/helpers/index.tsx | 88 --- src/catalogue/components/tree/index.tsx | 330 ----------- src/catalogue/components/tree/style.scss | 128 ---- src/catalogue/components/treeSelect/index.tsx | 7 - .../components/treeSelect/style.scss | 0 src/catalogue/consts.scss | 93 --- .../demos/tree/DefaultTree/index.tsx | 126 ---- .../demos/tree/NoHeaderTree/index.tsx | 55 -- src/catalogue/demos/tree/SmallTree/index.tsx | 132 ----- .../demos/tree/WithBtnSlotTree/index.tsx | 62 -- .../demos/tree/WithCheckboxTree/index.tsx | 87 --- .../demos/tree/WithTabsTree/index.tsx | 102 ---- src/catalogue/demos/tree/data.ts | 554 ------------------ .../treeSelect/NormalTreeSelect/index.tsx | 42 -- src/catalogue/demos/treeSelect/data.ts | 186 ------ src/catalogue/index.md | 65 -- src/catalogue/index.scss | 143 +++++ src/catalogue/index.tsx | 22 +- src/catalogue/useTreeData.tsx | 95 +++ src/catalogue/utils.tsx | 34 ++ 27 files changed, 842 insertions(+), 2151 deletions(-) create mode 100644 src/catalogue/components/catalogue.tsx create mode 100644 src/catalogue/components/icon.tsx delete mode 100644 src/catalogue/components/index.ts create mode 100644 src/catalogue/components/tree.tsx delete mode 100644 src/catalogue/components/tree/components/header/index.tsx delete mode 100644 src/catalogue/components/tree/components/header/style.scss delete mode 100644 src/catalogue/components/tree/components/index.ts delete mode 100644 src/catalogue/components/tree/helpers/index.tsx delete mode 100644 src/catalogue/components/tree/index.tsx delete mode 100644 src/catalogue/components/tree/style.scss delete mode 100644 src/catalogue/components/treeSelect/index.tsx delete mode 100644 src/catalogue/components/treeSelect/style.scss delete mode 100644 src/catalogue/consts.scss delete mode 100644 src/catalogue/demos/tree/DefaultTree/index.tsx delete mode 100644 src/catalogue/demos/tree/NoHeaderTree/index.tsx delete mode 100644 src/catalogue/demos/tree/SmallTree/index.tsx delete mode 100644 src/catalogue/demos/tree/WithBtnSlotTree/index.tsx delete mode 100644 src/catalogue/demos/tree/WithCheckboxTree/index.tsx delete mode 100644 src/catalogue/demos/tree/WithTabsTree/index.tsx delete mode 100644 src/catalogue/demos/tree/data.ts delete mode 100644 src/catalogue/demos/treeSelect/NormalTreeSelect/index.tsx delete mode 100644 src/catalogue/demos/treeSelect/data.ts delete mode 100644 src/catalogue/index.md create mode 100644 src/catalogue/index.scss create mode 100644 src/catalogue/useTreeData.tsx create mode 100644 src/catalogue/utils.tsx diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx new file mode 100644 index 000000000..7eb4ed15d --- /dev/null +++ b/src/catalogue/components/catalogue.tsx @@ -0,0 +1,239 @@ +import React, { useMemo } from 'react'; +import { Dropdown, Form, Input, Menu, Spin } from 'antd'; +import { DataNode } from 'antd/lib/tree'; +import { EllipsisText, Empty } from 'dt-react-component'; +import BlockHeader, { IBlockHeaderProps } from 'dt-react-component/blockHeader'; + +import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import { CatalogIcon, DeleteIcon, DragIcon, EditIcon, EllipsisIcon, PlusCircleIcon } from './icon'; +import CatalogueTree, { ICatalogueTree } from './tree'; + +interface ICatalogue + extends Pick, + Partial, 'onChange'>>, + ICatalogueTree { + icon?: React.ReactNode; + title?: string; + showSearch?: boolean; + edit?: boolean; + placeholder?: string; + loading?: boolean; + onSearch?: (value: string) => void; + onSave?: (data: ITreeNode, value: string) => Promise; + onDelete?: (data: ITreeNode) => Promise; +} + +const Catalogue = ({ + title, + icon = , + tooltip = false, + showSearch = false, + placeholder = '搜索目录名称', + addonAfter, + edit = true, + loading = false, + treeData, + draggable, + onChange, + onSearch, + onExpand, + onSave, + onDelete, + ...rest +}: ICatalogue) => { + const [form] = Form.useForm(); + + const loopTree = (data: DataNode[]): DataNode[] => { + return data?.map((item) => { + if (item.children) { + return { + ...item, + title: renderTitle(item), + children: loopTree(item.children), + }; + } + return { + ...item, + title: renderTitle(item), + children: undefined, + }; + }); + }; + + const renderHeader = () => { + if (!title) return null; + return ( + + ); + }; + + const renderSearch = () => { + if (!showSearch) return null; + return ( + + ); + }; + + const renderTree = () => { + if (!treeDataWithTitle.length) return ; + return ( +
+ + + +
+ ); + }; + + const handleInputSubmit = (item: ITreeNode, value: string) => { + if (!value) { + return onChange?.(undefined, undefined); + } + // item 为当前编辑的数据,对于 Append 的情况需要传入父级的 key + if (item.type === InputStatus.Append) { + const findAppendParents = (data: ITreeNode[], item: ITreeNode): ITreeNode | null => { + let result: ITreeNode | null = null; + function traverse(node: ITreeNode, parent: ITreeNode | null): void { + if (node.type === 'append' && node.key === item.key && parent) { + result = parent; + } + if (Array.isArray(node.children)) { + node.children.forEach((child) => traverse(child, node)); + } + } + data.forEach((item) => traverse(item, null)); + return result; + }; + const parentItem = findAppendParents(treeData, item); + return parentItem && onSave?.({ ...parentItem, type: InputStatus.Append }, value); + } + onSave?.(item, value); + }; + + const renderInput = (item: DataNode) => { + return ( +
+
+ + e.stopPropagation()} + onBlur={({ target }) => handleInputSubmit(item, target.value)} + onPressEnter={({ target }) => + handleInputSubmit(item, (target as any).value) + } + /> + +
+
+ ); + }; + + const renderTitle = (item: ITreeNode) => { + if (item.type) { + return renderInput(item); + } + return ( +
+ + {edit && renderNodeHover(item)} +
+ ); + }; + + const renderNodeHover = (item: DataNode) => { + const menu = ( + { + domEvent.stopPropagation(); + }} + > + onChange?.(item, InputStatus.Append)} + > + + 新建目录 + + onChange?.(item, InputStatus.Edit)} + > + + 编辑 + + onDelete?.(item)} + > + + 删除 + + + ); + return ( +
{ + e.stopPropagation(); + // this.setState({ isSelected: false, draggable: false }); + }} + > + triggerNode.parentElement} + > + e.stopPropagation()} /> + + +
+ ); + }; + + const treeDataWithTitle = useMemo(() => { + return loopTree(treeData); + }, [treeData]); + + return ( +
+
+ {renderHeader()} + {renderSearch()} +
+ {renderTree()} +
+ ); +}; + +export default Catalogue; diff --git a/src/catalogue/components/icon.tsx b/src/catalogue/components/icon.tsx new file mode 100644 index 000000000..a82ab9ef0 --- /dev/null +++ b/src/catalogue/components/icon.tsx @@ -0,0 +1,290 @@ +import React from 'react'; +import classNames from 'classnames'; + +interface IIcon { + className?: string; + style?: React.CSSProperties; + onClick?: (e) => void; +} + +const FolderIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + + + ); +}; + +const FolderOpenedIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + + + ); +}; + +const DownTriangleIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +const CatalogIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +const PlusSquareIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + ); +}; + +const MenuFoldIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + ); +}; + +const MenuUnFoldIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +const DragIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +const EllipsisIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +const EditIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +const DeleteIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; +const PlusCircleIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + + + ); +}; + +export { + CatalogIcon, + DeleteIcon, + DownTriangleIcon, + DragIcon, + EditIcon, + EllipsisIcon, + FolderIcon, + FolderOpenedIcon, + MenuFoldIcon, + MenuUnFoldIcon, + PlusCircleIcon, + PlusSquareIcon, +}; diff --git a/src/catalogue/components/index.ts b/src/catalogue/components/index.ts deleted file mode 100644 index 8a3022552..000000000 --- a/src/catalogue/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as Tree } from './tree'; -export { type ITabsStatus, type ITreeDataItem } from './tree'; -export { default as TreeSelect } from './treeSelect'; diff --git a/src/catalogue/components/tree.tsx b/src/catalogue/components/tree.tsx new file mode 100644 index 000000000..cb90c4b6b --- /dev/null +++ b/src/catalogue/components/tree.tsx @@ -0,0 +1,32 @@ +import React, { Key, useMemo } from 'react'; +import { Tree, TreeProps } from 'antd'; + +import { ITreeNode } from '../useTreeData'; +import { loopTree } from '../utils'; +import { DownTriangleIcon } from './icon'; + +export interface ICatalogueTree + extends Omit { + treeData: ITreeNode[]; +} + +const CatalogueTree = ({ treeData, onExpand, ...rest }: ICatalogueTree) => { + const renderTreeData = useMemo(() => loopTree(treeData), [treeData]); + + const handleExpand = (expandedKeys: Key[], info) => { + onExpand?.(expandedKeys, info); + }; + + return ( + } + showIcon + treeData={renderTreeData} + onExpand={handleExpand} + {...rest} + /> + ); +}; + +export default CatalogueTree; diff --git a/src/catalogue/components/tree/components/header/index.tsx b/src/catalogue/components/tree/components/header/index.tsx deleted file mode 100644 index 54890695e..000000000 --- a/src/catalogue/components/tree/components/header/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import { MenuFoldOutlined, MenuUnfoldOutlined, ProfileFilled } from '@ant-design/icons'; - -import { ITreeProps } from '../../'; -import './style.scss'; - -export const prefixCls = 'dtTreeHeaderWrapper'; - -export const btnSlotProp = 'btnSlot'; - -export interface ITreeHeaderProps { - /** 目录标题 */ - title?: React.ReactNode; - /** 是否收起 */ - collapsed?: boolean; - /** 展开、收起的回调 */ - onCollapsed?: (collapsed: boolean) => void; - /** Header 右侧按扭组插槽【建议 icon 不超过3个,超出使用更多icon,下拉显示】,showHeader 为 true 时生效 */ - btnSlot?: React.ReactElement; - size?: ITreeProps['size']; -} - -export default (props: ITreeHeaderProps) => { - const { onCollapsed, btnSlot } = props; - const handleCollapsed = (flag: boolean) => (_: any) => { - onCollapsed?.(flag); - }; - return ( -
-
- - {props?.title ?? ''} -
-
- {btnSlot} - {props.collapsed ? ( - - ) : ( - - )} -
-
- ); -}; diff --git a/src/catalogue/components/tree/components/header/style.scss b/src/catalogue/components/tree/components/header/style.scss deleted file mode 100644 index b11ae6ed4..000000000 --- a/src/catalogue/components/tree/components/header/style.scss +++ /dev/null @@ -1,27 +0,0 @@ -.dtTreeHeaderWrapper { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 12px 0; - margin-bottom: 8px; - &-left { - display: flex; - align-items: center; - &__catalogueIcon { - font-size: 20px; - margin-right: 4px; - color: #1D78FF; - } - &__title { - font-size: 14px; - } - } - &-right { - display: flex; - align-items: center; - } - &-menuFold { - font-size: 20px; - color: #AAA; - } -} diff --git a/src/catalogue/components/tree/components/index.ts b/src/catalogue/components/tree/components/index.ts deleted file mode 100644 index 7899000d7..000000000 --- a/src/catalogue/components/tree/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Header } from './header'; diff --git a/src/catalogue/components/tree/helpers/index.tsx b/src/catalogue/components/tree/helpers/index.tsx deleted file mode 100644 index dfc12767b..000000000 --- a/src/catalogue/components/tree/helpers/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import * as React from 'react'; -import { FolderFilled, FolderOpenFilled } from '@ant-design/icons'; -import type { TreeProps } from 'antd'; -import type { DataNode } from 'antd/es/tree'; -import { ContextMenu } from 'dt-react-component'; - -import type { ITreeDataItem } from '..'; - -/** - * @description 根据 query 计算应该展开的 expendKeys - */ -export const getExpendKeysByQuery = ({ - tree, - searchStr, -}: { - /** 树数据 */ - tree: DataNode[]; - /** 搜索字符串 */ - searchStr: string; -}): React.Key[] => { - const keys: React.Key[] = []; - if (!searchStr) return []; - tree?.forEach?.((item) => { - if (item?.title?.toString()?.includes?.(searchStr)) { - keys.push(item.key); - } - if (item?.children && item?.children?.length) { - keys.push(...getExpendKeysByQuery({ tree: item?.children, searchStr })); - } - }); - return keys; -}; - -/** - * @description 文件 展开/收起样式 - */ -export const getIcon: DataNode['icon'] = ({ expanded, data }) => { - if (!data) return null; - if (!data?.hasOwnProperty('children')) return null; - const styles: React.CSSProperties = { - fontSize: 14, - color: '#1D78FF', - }; - if (expanded) return ; - return ; -}; - -/** - * @description 轮询 Tree 数据,赋值 搜索标识和leafIcon - */ -export const loopTree = ( - data: ITreeDataItem[] | undefined, - searchValue: string -): TreeProps['treeData'] => { - return data?.map((item: ITreeDataItem) => { - const strTitle = item?.title as string; - const index = strTitle?.indexOf?.(searchValue); - const beforeStr = strTitle?.substring?.(0, index); - const afterStr = strTitle?.slice?.(index + searchValue?.length); - const contextMenuData = - item?.contextMenuConfig?.data?.map?.((e: any) => ({ - ...e, - cb: () => { - item?.contextMenuConfig?.onClick(e, item); - }, - })) || []; - const title = - index > -1 ? ( - - {beforeStr} - {searchValue} - {afterStr} - - ) : ( - {strTitle} - ); - if (item.children) { - return { - icon: getIcon, - ...item, - title, - key: item.key, - children: loopTree(item.children, searchValue), - }; - } - return { ...item, title, key: item.key, children: undefined }; - }); -}; diff --git a/src/catalogue/components/tree/index.tsx b/src/catalogue/components/tree/index.tsx deleted file mode 100644 index d2f23c4bb..000000000 --- a/src/catalogue/components/tree/index.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import * as React from 'react'; -import { useCallback, useEffect, useMemo, useReducer } from 'react'; -import { - CaretDownOutlined, - CloseOutlined, - MenuUnfoldOutlined, - SearchOutlined, -} from '@ant-design/icons'; -import type { TreeProps } from 'antd'; -import { Input, Spin, Tabs, Tree } from 'antd'; -import type { TabsProps } from 'antd/es/tabs'; -import type { DataNode } from 'antd/es/tree'; - -import type { ITreeHeaderProps } from './components/header'; -import { Header } from './components'; -import { getExpendKeysByQuery, loopTree } from './helpers'; -import './style.scss'; - -export const prefixCls = 'dtTreeWrapper'; - -/** ContextMenu item props */ -type ContextItemProps = { text: React.ReactNode; key: React.Key }; - -export enum ITabsStatus { - /** 搜索态 */ - search = 'search', - /** tab 切换态 */ - tabs = 'tabs', -} - -export interface ITreeDataItem extends DataNode { - /** Item ContextMenu 配置 */ - contextMenuConfig?: { - data: Array; - onClick: (e: ContextItemProps, item: ITreeDataItem) => void; - }; - children?: ITreeDataItem[]; -} - -export interface ITabsItem { - /** 选项卡头显示文字 */ - label: React.ReactNode; - /** 对应 activeKey */ - key: string; - /** 禁用某一项 */ - disabled?: boolean; - // [key: string]: any; -} - -export interface ITreeProps extends TreeProps, Pick { - /** 是否加载中 */ - loading?: boolean; - /** 是否展示头部组件 */ - showHeader?: boolean; - /** 头部文案 */ - treeTitle?: React.ReactNode; - /** 容器类名 */ - wrapperClassName?: string; - /** 容器行内样式 */ - wrapperStyle?: React.CSSProperties; - /** 尺寸大小,small 每一个 item 高度是 28px,middle 每一个 item 高度是 32px, large 未实现 ,默认为 small */ - size?: 'small' | 'middle'; - /** 点击搜索按钮回调 */ - onSearch?: ( - value: string, - e: - | React.ChangeEvent - | React.MouseEvent - | React.KeyboardEvent - | undefined - ) => void; - /** tabs 配置 */ - tabsProps?: TabsProps & { - /** 相当于 antd >=4.23.0 时的 items,目前只实现了 label、key、disabled > */ - items: ITabsItem[]; - }; - /** 与 TreeProps['treeData'] 类型相似,只是增加了 ContextMenu 配置 */ - treeData?: ITreeDataItem[]; - /** 默认展示 tabs 还是 search,仅 items 有值时生效 */ - defaultStatus?: ITabsStatus; - /** tabs or status 变化时的回调 */ - onStatusChange?: ( - status: ITabsStatus, - e: - | React.ChangeEvent - | React.MouseEvent - | React.KeyboardEvent - | undefined - ) => void; -} - -interface IState { - searchStr: string | undefined; - collapsed: boolean; - _expandedKeys: React.Key[]; - /** 搜索状态,默认 search 状态,若 tabsItems 有值则可以改动 */ - status: ITabsStatus; -} - -const initState: IState = { - searchStr: undefined, - collapsed: false, - _expandedKeys: [], - status: ITabsStatus.search, -}; - -enum Action { - UPDATE = 'update', - RESET = 'reset', -} - -interface IAction { - type: Action; - payload: Partial; -} - -const reducer = (state: IState, action: IAction) => { - switch (action.type) { - case Action.RESET: - return initState; - case Action.UPDATE: - return { ...state, ...action.payload }; - default: - return state; - } -}; - -const DtTree = (props: ITreeProps) => { - const { - showHeader, - treeTitle, - wrapperClassName, - wrapperStyle, - onSearch, - treeData, - tabsProps, - loading, - size, - btnSlot, - defaultStatus, - onStatusChange, - ...restProps - } = props; - const [{ searchStr, collapsed, _expandedKeys, status }, dispatch] = useReducer(reducer, { - ...initState, - status: defaultStatus || initState.status, - }); - - useEffect(() => { - dispatch({ type: Action.UPDATE, payload: { _expandedKeys: restProps.expandedKeys } }); - }, [restProps.expandedKeys]); - - const data: TreeProps['treeData'] = useMemo( - () => loopTree(treeData, searchStr as string), - [treeData, searchStr] - ); - - const handleSearch = useCallback( - ( - val: string, - e?: - | React.ChangeEvent - | React.MouseEvent - | React.KeyboardEvent - | undefined - ) => { - const newExpandedKeys = getExpendKeysByQuery({ - tree: treeData as DataNode[], - searchStr: val, - }); - dispatch({ - type: Action.UPDATE, - payload: { - _expandedKeys: newExpandedKeys, - searchStr: val, - }, - }); - onSearch?.(val, e); - }, - [treeData, onSearch] - ); - - const handleChange: React.ChangeEventHandler = useCallback( - (e) => { - const val = e?.target?.value; - const newKeys = getExpendKeysByQuery({ tree: treeData as DataNode[], searchStr: val }); - dispatch({ - type: Action.UPDATE, - payload: { - _expandedKeys: newKeys, - searchStr: val, - }, - }); - }, - [treeData] - ); - - const toggleCollapsed = useCallback((flag: boolean) => { - dispatch({ type: Action.UPDATE, payload: { collapsed: flag } }); - }, []); - - const onExpand = useCallback((newExpandedKeys: React.Key[]) => { - dispatch({ type: Action.UPDATE, payload: { _expandedKeys: newExpandedKeys } }); - }, []); - const handleChangeToSearch = useCallback( - (e: any) => { - dispatch({ - type: Action.UPDATE, - payload: { status: ITabsStatus.search }, - }); - onStatusChange?.(ITabsStatus.search, e); - }, - [onStatusChange] - ); - - const handleChangeToTabs = useCallback( - (e: any) => { - dispatch({ - type: Action.UPDATE, - payload: { status: ITabsStatus.tabs, searchStr: undefined }, - }); - onStatusChange?.(ITabsStatus.tabs, e); - }, - [onStatusChange] - ); - - const renderHeader = useCallback(() => { - if (!showHeader) return null; - return ( -
- ); - }, [showHeader, treeTitle, collapsed, size, btnSlot, toggleCollapsed]); - const renderTabsAndSearch = useCallback(() => { - const { items, ...restTabsProps } = tabsProps || {}; - if (status === ITabsStatus.tabs && items?.length) { - return ( - - } - size={size} - {...restTabsProps} - > - {items?.map((item) => ( - - ))} - - ); - } else if (status === ITabsStatus.search || !items?.length) { - return ( -
- { - handleSearch(e?.target?.value, e); - }} - /> - {items?.length ? ( - - ) : null} -
- ); - } - return null; - }, [ - handleSearch, - handleChange, - tabsProps, - status, - size, - handleChangeToSearch, - handleChangeToTabs, - ]); - if (collapsed) { - return ( - { - toggleCollapsed(false); - }} - /> - ); - } - return ( - -
- {renderHeader()} - {renderTabsAndSearch()} - -
-
- ); -}; - -DtTree.defaultProps = { - loading: false, - size: 'middle', - showHeader: true, - treeTitle: '标签目录', - collapsed: true, - showIcon: true, - showLine: { showLeafIcon: false }, - switcherIcon: , - defaultStatus: ITabsStatus.tabs, -} as ITreeProps; - -export default DtTree; diff --git a/src/catalogue/components/tree/style.scss b/src/catalogue/components/tree/style.scss deleted file mode 100644 index 3f32d63e9..000000000 --- a/src/catalogue/components/tree/style.scss +++ /dev/null @@ -1,128 +0,0 @@ -@import "../../consts.scss"; - -$smallSize: 24px; -$middleSize: 36px; - -.dtTreeWrapper { - background-color: $white; - &-icon { - &__unfold { - font-size: $font20; - color: $navy4; - } - &__search { - margin-left: 12px; - font-size: $font14; - color: $navy4; - } - &__close { - margin-left: 12px; - font-size: $font14; - color: $navy4; - } - } - &__search { - display: flex; - flex-direction: row; - align-items: center; - padding: 0 12px; - } - .ant-tree { - .ant-tree-node-content-wrapper.ant-tree-node-selected { - background-color: transparent; - } - &-list { - margin-top: 8px; - } - &-treenode { - width: 100%; - padding: 0 12px; - align-items: center; - .ant-tree-node-content-wrapper { - width: 100%; - padding: 0; - // TODO 会导致拖拽时遮挡 - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - &:hover { - background-color: transparent; - .ant-tree-switcher { - background-color: $navy7; - } - } - } - .ant-tree-switcher { - background: none; - } - &-selected { - position: relative; - background-color: $bg1; - &::before { - content: ""; - position: absolute; - left: 0; - width: 4px; - height: $smallSize; - background-color: $primary; - } - } - &:hover { - background-color: $navy7; - } - } - } - .ant-tabs { - padding: 0 12px; - &-nav { - margin-bottom: 0; - } - } - &-small { - font-size: $font14; - .ant-tree { - &-treenode { - .ant-tree-switcher { - width: $smallSize; - line-height: $smallSize; - } - &-selected { - &::before { - height: $smallSize; - } - } - } - } - } - &-middle { - font-size: $font16; - .ant-tree { - &-treenode { - .ant-tree-switcher { - // width: $middleSize; - line-height: $middleSize; - } - .ant-tree-node-content-wrapper { - min-height: $middleSize; - line-height: $middleSize; - .ant-tree-iconEle { - // width: $middleSize; - height: $middleSize; - line-height: $middleSize; - .anticon { - vertical-align: middle; - svg { - font-size: 18px; - } - } - } - } - &-selected { - &::before { - height: $middleSize; - } - } - } - } - } -} diff --git a/src/catalogue/components/treeSelect/index.tsx b/src/catalogue/components/treeSelect/index.tsx deleted file mode 100644 index fdfd9507c..000000000 --- a/src/catalogue/components/treeSelect/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import type { TreeSelectProps } from 'antd'; -import { TreeSelect } from 'antd'; - -export interface IProps extends TreeSelectProps {} - -export default (props: IProps) => ; diff --git a/src/catalogue/components/treeSelect/style.scss b/src/catalogue/components/treeSelect/style.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/catalogue/consts.scss b/src/catalogue/consts.scss deleted file mode 100644 index f0190104c..000000000 --- a/src/catalogue/consts.scss +++ /dev/null @@ -1,93 +0,0 @@ -// ==== 颜色 ==== -$bgColor: #F2F3F8; // 背景色 -$color1: #2E3943; // 主色 -$color2: #2491F7; // 第二色 - -// ==== 字体 ==== -$font1: 14px; -$font2: 16px; -$fontColor: #9FA9BA; -// ==== 页面大小 ==== -$minWidth:1260px; - -// ====== UI 5.0 颜色规范 ====== - -// DTinsight Blue - -/** 用于主按钮底色,描边按钮,可操作项,tab选中状态的底部线条,运行中的状态以及文字图标颜色 */ -$primary: #1D78FF; - -/** 用于主按钮底色,描边按钮,可操作项,tab选中状态的hover状态填充色 */ -$hover: #0A67F2; - -/** 用于主按钮底色,描边按钮,可操作项,tab选中状态的点击状态填充色 */ -$click: #005CE6; - -/** 用于主按钮底色,描边按钮,可操作项的不可用状态填充色 */ -$disable: #BBD6FF; - -/** 用于小模块切换背景,提示背景 */ -$bg1: #E8F1FF; - -/** 用于整个页面的底色 */ -$bg2: #F2F7FA; - -// DTinsight Navy - -/** 用于标题,主文字颜色 */ -$navy1: #3D446E; - -/** 用于表单标题备注说明文字颜色 */ -$navy2: #64698B; - -/** 用于分页器提示文字颜色 */ -$navy3: #8B8FA8; - -/** 用于文本框内的提示文字颜色 */ -$navy4: #B1B4C5; - -/** 用于表单边框颜色,图表线条颜色 */ -$navy5: #D8DAE2; - -/** 用于分割线、失效按钮状态颜色 */ -$navy6: #EBECF0; - -/** 用于模块标题背景色 */ -$navy7: #F5F5F8; - -/** 用于二级导航底色 */ -$navy8: #F9F9FA; - -/** 一级按钮文字颜色 */ -$white: #FFF; - -// Functional Color - -/** 用于成功状态颜色 */ -$green: #11D7B2; - -/** 用于失败状态、信息文字、必填项图标颜色 */ -$red: #F96C5B; - -/** 用于等待状态、警告提示颜色 */ -$yellow: #FBB310; - -/** 用于取消、冻结状态、未开始、未完成、未进行、未配置提示颜色 */ -$purple: #AC9DFF; - -// ====== UI 5.0 字体规范 ====== - -/** 主字体大小,应用于列表内容,下拉、选择、输入框,弹窗等控件文字 */ -$font12: 12px; - -/** 应用于左侧菜单栏字体,主标题字体,弹窗标题字体大小 */ -$font14: 14px; - -/** 应用于顶部导航字体和部分需要强调的标题 */ -$font16: 16px; - -/** 较少使用,应用于部分大标题 */ -$font20: 20px; - -/** 应用于数据统计概览页面等需突出数字 */ -$font40: 40px; diff --git a/src/catalogue/demos/tree/DefaultTree/index.tsx b/src/catalogue/demos/tree/DefaultTree/index.tsx deleted file mode 100644 index 610e9ae89..000000000 --- a/src/catalogue/demos/tree/DefaultTree/index.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState } from 'react'; -import type { DataNode, TreeProps } from 'antd/es/tree'; -import { Catalogue } from 'dt-react-component'; -import { ITreeDataItem } from 'dt-react-component/catalogue/components/tree'; - -import { initTreeData } from '../data'; - -export const DefaultTree = () => { - const [dataSource, setDataSource] = useState(initTreeData); - const [selectedItems, setSelectedItems] = useState([]); - const handleDrop: TreeProps['onDrop'] = (info) => { - console.log(info); - const dropKey = info.node.key; - const dragKey = info.dragNode.key; - const dropPos = info.node.pos.split('-'); - const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); - - const loop = ( - data: DataNode[], - key: React.Key, - callback: (node: DataNode, i: number, data: DataNode[]) => void - ) => { - for (let i = 0; i < data.length; i++) { - if (data[i].key === key) { - return callback(data[i], i, data); - } - if (data[i].children) { - loop(data[i].children!, key, callback); - } - } - }; - const data = [...dataSource]; - - // Find dragObject - let dragObj: DataNode; - loop(data, dragKey, (item, index, arr) => { - arr.splice(index, 1); - dragObj = item; - }); - - if (!info.dropToGap) { - // Drop on the content - loop(data, dropKey, (item) => { - item.children = item.children || []; - // where to insert 示例添加到头部,可以是随意位置 - item.children.unshift(dragObj); - }); - } else if ( - ((info.node as any).props.children || []).length > 0 && // Has children - (info.node as any).props.expanded && // Is expanded - dropPosition === 1 // On the bottom gap - ) { - loop(data, dropKey, (item) => { - item.children = item.children || []; - // where to insert 示例添加到头部,可以是随意位置 - item.children.unshift(dragObj); - // in previous version, we use item.children.push(dragObj) to insert the - // item to the tail of the children - }); - } else { - let ar: DataNode[] = []; - let i: number; - loop(data, dropKey, (_item, index, arr) => { - ar = arr; - i = index; - }); - if (dropPosition === -1) { - ar.splice(i!, 0, dragObj!); - } else { - ar.splice(i! + 1, 0, dragObj!); - } - } - setDataSource(data); - }; - const handleSelect: TreeProps['onSelect'] = (selectedKeys) => { - // const selectedKey = - const selectedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (selectedKeys.includes(item.key)) { - selectedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setSelectedItems(selectedItems); - console.log(selectedKeys, '--selectedKeys'); - }; - console.log(selectedItems, '--selectedItems'); - return ( -
- true }} - height={500} - wrapperStyle={{ width: 300 }} - onSearch={(v, e) => { - console.log(v, e, '--onSearch'); - }} - onDrop={handleDrop} - onSelect={handleSelect} - multiple - /> -

- {selectedItems?.length - ? `选中了 ${selectedItems?.map?.((item) => item?.title)?.join?.('、')}` - : 'Content'} -

-
- ); -}; - -export default DefaultTree; diff --git a/src/catalogue/demos/tree/NoHeaderTree/index.tsx b/src/catalogue/demos/tree/NoHeaderTree/index.tsx deleted file mode 100644 index 42ca783a5..000000000 --- a/src/catalogue/demos/tree/NoHeaderTree/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { useState } from 'react'; -import type { TreeProps } from 'antd/es'; -import { Catalogue } from 'dt-react-component'; -import { ITreeDataItem } from 'dt-react-component/catalogue/components/tree'; - -import { initTreeData } from '../data'; - -export const NoHeaderTree = () => { - const [dataSource] = useState(initTreeData); - const [selectedItems, setSelectedItems] = useState([]); - const handleSelect: TreeProps['onSelect'] = (selectedKeys) => { - // const selectedKey = - const selectedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (selectedKeys.includes(item.key)) { - selectedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setSelectedItems(selectedItems); - console.log(selectedKeys, '--selectedKeys'); - }; - console.log(selectedItems, '--selectedItems'); - return ( -
- -

- {selectedItems?.length - ? `选中了 ${selectedItems?.map?.((item) => item?.title)?.join?.('、')}` - : 'Content'} -

-
- ); -}; - -export default NoHeaderTree; diff --git a/src/catalogue/demos/tree/SmallTree/index.tsx b/src/catalogue/demos/tree/SmallTree/index.tsx deleted file mode 100644 index c986cbfd8..000000000 --- a/src/catalogue/demos/tree/SmallTree/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useState } from 'react'; -import type { DataNode, TreeProps } from 'antd/es/tree'; -import { Catalogue } from 'dt-react-component'; -import { ITreeDataItem } from 'dt-react-component/catalogue/components/tree'; - -import { initTreeData, sleep } from '../data'; - -export const SmallTree = () => { - const [dataSource, setDataSource] = useState(initTreeData); - const [selectedItems, setSelectedItems] = useState([]); - const handleDrop: TreeProps['onDrop'] = (info) => { - console.log(info); - const dropKey = info.node.key; - const dragKey = info.dragNode.key; - const dropPos = info.node.pos.split('-'); - const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); - - const loop = ( - data: DataNode[], - key: React.Key, - callback: (node: DataNode, i: number, data: DataNode[]) => void - ) => { - for (let i = 0; i < data.length; i++) { - if (data[i].key === key) { - return callback(data[i], i, data); - } - if (data[i].children) { - loop(data[i].children!, key, callback); - } - } - }; - const data = [...dataSource]; - - // Find dragObject - let dragObj: DataNode; - loop(data, dragKey, (item, index, arr) => { - arr.splice(index, 1); - dragObj = item; - }); - - if (!info.dropToGap) { - // Drop on the content - loop(data, dropKey, (item) => { - item.children = item.children || []; - // where to insert 示例添加到头部,可以是随意位置 - item.children.unshift(dragObj); - }); - } else if ( - ((info.node as any).props.children || []).length > 0 && // Has children - (info.node as any).props.expanded && // Is expanded - dropPosition === 1 // On the bottom gap - ) { - loop(data, dropKey, (item) => { - item.children = item.children || []; - // where to insert 示例添加到头部,可以是随意位置 - item.children.unshift(dragObj); - // in previous version, we use item.children.push(dragObj) to insert the - // item to the tail of the children - }); - } else { - let ar: DataNode[] = []; - let i: number; - loop(data, dropKey, (_item, index, arr) => { - ar = arr; - i = index; - }); - if (dropPosition === -1) { - ar.splice(i!, 0, dragObj!); - } else { - ar.splice(i! + 1, 0, dragObj!); - } - } - setDataSource(data); - }; - const handleLoadData: TreeProps['loadData'] = (node) => { - console.log(node, '--node'); - // node.children = [{ }]; - return sleep(1500, []); - }; - const handleSelect: TreeProps['onSelect'] = (selectedKeys) => { - // const selectedKey = - const selectedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (selectedKeys.includes(item.key)) { - selectedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setSelectedItems(selectedItems); - console.log(selectedKeys, '--selectedKeys'); - }; - console.log(selectedItems, '--selectedItems'); - return ( -
- true }} - height={500} - loadData={handleLoadData} - // wrapperStyle={{ width: 500 }} - onSearch={(v, e) => { - console.log(v, e, '--onSearch'); - }} - onDrop={handleDrop} - onSelect={handleSelect} - /> -

- {selectedItems?.length - ? `选中了 ${selectedItems?.map?.((item) => item?.title)?.join?.('、')}` - : 'Content'} -

-
- ); -}; - -export default SmallTree; diff --git a/src/catalogue/demos/tree/WithBtnSlotTree/index.tsx b/src/catalogue/demos/tree/WithBtnSlotTree/index.tsx deleted file mode 100644 index faed6ebc1..000000000 --- a/src/catalogue/demos/tree/WithBtnSlotTree/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useState } from 'react'; -import { DeleteOutlined, FormOutlined, PlusSquareOutlined } from '@ant-design/icons'; -import { Space, TreeProps } from 'antd'; -import { Catalogue } from 'dt-react-component'; -import { ITreeDataItem } from 'dt-react-component/catalogue/components/tree'; - -import { initTreeData } from '../data'; - -export const WithBtnSlotTree = () => { - const [dataSource] = useState(initTreeData); - const [selectedItems, setSelectedItems] = useState([]); - const handleSelect: TreeProps['onSelect'] = (selectedKeys) => { - // const selectedKey = - const selectedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (selectedKeys.includes(item.key)) { - selectedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setSelectedItems(selectedItems); - console.log(selectedKeys, '--selectedKeys'); - }; - console.log(selectedItems, '--selectedItems'); - return ( -
- - - - - - } - /> -

- {selectedItems?.length - ? `选中了 ${selectedItems?.map?.((item) => item?.title)?.join?.('、')}` - : 'Content'} -

-
- ); -}; - -export default WithBtnSlotTree; diff --git a/src/catalogue/demos/tree/WithCheckboxTree/index.tsx b/src/catalogue/demos/tree/WithCheckboxTree/index.tsx deleted file mode 100644 index fe4d9cd10..000000000 --- a/src/catalogue/demos/tree/WithCheckboxTree/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from 'react'; -import { TreeProps } from 'antd'; -import { Catalogue } from 'dt-react-component'; -import { ITreeDataItem } from 'dt-react-component/catalogue/components/tree'; - -import { initTreeData } from '../data'; - -export const WithCheckboxTree = () => { - const [dataSource] = useState(initTreeData); - const [selectedItems, setSelectedItems] = useState([]); - const [checkedItems, setCheckedItems] = useState([]); - const handleSelect: TreeProps['onSelect'] = (selectedKeys) => { - console.log(selectedKeys, '--selectedKeys'); - const selectedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (selectedKeys.includes(item.key)) { - selectedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setSelectedItems(selectedItems); - }; - const handleCheck: TreeProps['onCheck'] = (checkedKeys, info) => { - console.log(info, '--info'); - console.log(checkedKeys, '--checkedKeys'); - const checkedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (checkedKeys?.includes?.(item.key)) { - checkedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setCheckedItems(checkedItems); - }; - console.log(selectedItems, '--selectedItems'); - console.log(checkedItems, '--checkedItems'); - return ( -
- true }} - height={500} - // wrapperStyle={{ width: 500 }} - onSearch={(v, e) => { - console.log(v, e, '--onSearch'); - }} - checkable - onSelect={handleSelect} - onCheck={handleCheck} - /> -

-

- {selectedItems?.length - ? `选中了 ${selectedItems?.map?.((item) => item?.title)?.join?.('、')}` - : '未选中'} -

-

- {checkedItems?.length - ? `勾选了 ${checkedItems?.map((item) => item?.title)?.join('、')}` - : null} -

-

-
- ); -}; - -export default WithCheckboxTree; diff --git a/src/catalogue/demos/tree/WithTabsTree/index.tsx b/src/catalogue/demos/tree/WithTabsTree/index.tsx deleted file mode 100644 index 5aa6457f7..000000000 --- a/src/catalogue/demos/tree/WithTabsTree/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { useState } from 'react'; -import { DeleteOutlined, FormOutlined, PlusSquareOutlined } from '@ant-design/icons'; -import { Space, TreeProps } from 'antd'; -import { Catalogue } from 'dt-react-component'; -import { ITabsStatus, ITreeDataItem } from 'dt-react-component/catalogue/components/tree'; - -import { initTreeData } from '../data'; - -export const WithTabsTree = () => { - const [dataSource, setDataSource] = useState(initTreeData); - const [selectedItems, setSelectedItems] = useState([]); - - const handleSelect: TreeProps['onSelect'] = (selectedKeys) => { - console.log(selectedKeys, '--selectedKeys'); - const selectedItems: ITreeDataItem[] = []; - const loopTree = (tree: ITreeDataItem[]) => { - tree.forEach((item) => { - if (selectedKeys.includes(item.key)) { - selectedItems.push(item); - } - if (item?.children?.length) { - loopTree(item?.children); - } - }); - }; - loopTree(dataSource); - setSelectedItems(selectedItems); - }; - console.log(selectedItems, '--selectedItems'); - return ( -
- { - console.log(activeKey, e, 'onTabClick'); - if (activeKey === 'project_1') { - setDataSource(initTreeData.slice(0, 2)); - } else if (activeKey === 'project_2') { - setDataSource(initTreeData.slice(3)); - } else if (activeKey === 'all') { - setDataSource(initTreeData); - } - }, - }} - onStatusChange={(status) => { - if (status === ITabsStatus.search) { - setDataSource(initTreeData); - } - }} - btnSlot={ - - - - - - } - /> -

-

- {selectedItems?.length - ? `选中了 ${selectedItems?.map?.((item) => item?.title)?.join?.('、')}` - : '未选中'} -

-

-
- ); -}; - -export default WithTabsTree; diff --git a/src/catalogue/demos/tree/data.ts b/src/catalogue/demos/tree/data.ts deleted file mode 100644 index a2693a7e4..000000000 --- a/src/catalogue/demos/tree/data.ts +++ /dev/null @@ -1,554 +0,0 @@ -import type { ITreeDataItem } from '../../components/tree'; - -export const initTreeData: ITreeDataItem[] = [ - { - key: '1', - title: '文件夹1', - contextMenuConfig: { - data: [ - { text: '日志', key: 'log' }, - { text: '自定义操作', key: 'customOperation' }, - ], - onClick: (a, b) => { - console.log(a, b, '--8'); - }, - }, - children: [ - { - key: '1.1', - title: '很长很长很长很长很长很长很长很长很长很长很长的文件夹名称', - contextMenuConfig: { - data: [ - { text: '新建文件夹', key: 'newFolder' }, - { text: '删除', key: 'deleteFolder' }, - ], - onClick: (a, b) => { - console.log(a, b, '--13'); - }, - }, - children: [ - { - key: '1.1.1', - title: '文件2', - contextMenuConfig: { - data: [ - { text: '编辑', key: 'editFile' }, - { text: '删除', key: 'deleteFile' }, - { text: '克隆', key: 'cloneFile' }, - ], - onClick: (a, b) => { - console.log(a, b, '--23'); - }, - }, - }, - { - key: '1.1.2', - title: '文件3', - }, - { - key: '1.1.3', - title: '文件4', - }, - { - key: '1.1.4', - title: '文件5', - }, - { - key: '1.1.5', - title: '文件6', - }, - { - key: '1.1.6', - title: '文件7', - }, - ], - }, - { - key: '1.2', - title: '文件夹8', - children: [ - { - key: '1.2.1', - title: '文件9', - }, - ], - }, - ], - }, - { - key: '2', - title: '文件夹10', - children: [ - { - key: '2.1', - title: '文件夹11', - children: [ - { - key: '2.1.1', - title: '文件12', - }, - { - key: '2.1.2', - title: '文件13', - }, - ], - }, - { - key: '2.2', - title: '文件夹14', - children: [ - { - key: '2.2.1', - title: '文件15', - }, - { - key: '2.2.2', - title: '文件16', - }, - { - key: '2.2.3', - title: '文件17', - }, - { - key: '2.2.4', - title: '文件18', - }, - { - key: '2.2.5', - title: '文件19', - }, - { - key: '2.2.6', - title: '文件20', - }, - { - key: '2.2.7', - title: '文件21', - }, - ], - }, - ], - }, - { - key: '3', - title: '文件夹22', - children: [ - { - key: '3.1', - title: '文件夹23', - children: [ - { - key: '3.1.1', - title: '文件24', - }, - { - key: '3.1.2', - title: '文件25', - }, - ], - }, - { - key: '3.2', - title: '文件夹26', - children: [ - { - key: '3.2.1', - title: '文件27', - }, - ], - }, - ], - }, - { - key: '4', - title: '4', - children: [ - { - key: '4.1', - title: '文件夹28', - children: [ - { - key: '4.1.1', - title: '文件29', - }, - { - key: '4.1.2', - title: '文件30', - }, - ], - }, - { - key: '4.2', - title: '文件夹31', - children: [ - { - key: '4.2.1', - title: '文件32', - }, - ], - }, - ], - }, - { - key: '521751', - title: '文件夹33', - children: [ - { - key: '5.1', - title: '文件夹34', - children: [ - { - key: '5.1.1', - title: '文件35', - }, - { - key: '5.1.2', - title: '文件36', - }, - { - key: '5.1.3', - title: '文件37', - }, - { - key: '5.1.4', - title: '文件38', - }, - ], - }, - { - key: '5.2', - title: '文件夹39', - children: [ - { - key: '5.2.12', - title: '文件40', - }, - ], - }, - { - key: '5.3', - title: '文件夹41', - children: [ - { - key: '5.2.131', - title: '文件42', - }, - { - key: '5.2.2', - title: '文件43', - }, - { - key: '5.2.3', - title: '文件44', - }, - ], - }, - ], - }, - { - key: '6123123', - title: '文件夹45', - children: [ - { - key: '6.1', - title: '文件夹46', - children: [ - { - key: '6.1.1', - title: '文件47', - }, - { - key: '6.1.2', - title: '文件48', - }, - { - key: '6.1.3', - title: '文件49', - }, - { - key: '6.1.4', - title: '文件50', - }, - ], - }, - { - key: '6.2', - title: '文件夹51', - children: [ - { - key: '6.2.1123', - title: '文件52', - }, - ], - }, - { - key: '6.3', - title: '文件夹53', - children: [ - { - key: '6.2.1867234', - title: '文件54', - }, - { - key: '6.2.2', - title: '文件55', - }, - { - key: '6.2.3', - title: '文件56', - }, - ], - }, - ], - }, - { - key: '721751', - title: '文件夹57', - children: [ - { - key: '7.1', - title: '文件夹58', - children: [ - { - key: '7.1.1', - title: '文件59', - }, - { - key: '7.1.2', - title: '文件60', - }, - { - key: '7.1.3', - title: '文件61', - }, - { - key: '7.1.4', - title: '文件62', - }, - ], - }, - { - key: '7.2', - title: '文件夹63', - children: [ - { - key: '7.2.31878', - title: '文件64', - }, - ], - }, - { - key: '7.3', - title: '文件夹65', - children: [ - { - key: '7.2.12451', - title: '文件66', - }, - { - key: '7.2.2', - title: '文件67', - }, - { - key: '7.2.3', - title: '文件68', - }, - ], - }, - ], - }, - { - key: '821751', - title: '文件夹69', - children: [ - { - key: '8.1', - title: '文件夹70', - children: [ - { - key: '8.1.1', - title: '文件71', - }, - { - key: '8.1.2', - title: '文件72', - }, - { - key: '8.1.3', - title: '文件73', - }, - { - key: '8.1.4', - title: '文件74', - }, - ], - }, - { - key: '8.2', - title: '文件夹75', - children: [ - { - key: '8.2.1', - title: '文件76', - }, - ], - }, - { - key: '8.3', - title: '文件夹77', - children: [ - { - key: '8.2.878231', - title: '文件78', - }, - { - key: '8.2.2', - title: '文件79', - }, - { - key: '8.2.3', - title: '文件80', - }, - ], - }, - ], - }, - { - key: '99921266', - title: '文件夹81', - // type: 'x', - children: [ - { - key: '9.1', - title: '测试很长很长很长很长很长很长很长很长的文件夹名称', - children: [ - { - key: '9.1.1', - title: '文件82', - }, - { - key: '9.1.2', - title: '文件83', - }, - { - key: '9.1.3', - title: '文件84', - }, - { - key: '9.1.4', - title: '文件85', - }, - { - key: '9.1.5', - title: '文件86', - }, - { - key: '9.1.6', - title: '文件87', - }, - ], - }, - { - key: '9.2', - title: '文件夹88', - children: [ - { - key: '9.2.1', - title: '文件89', - }, - ], - }, - ], - }, - { - key: '10', - title: '文件夹90', - children: [ - { - key: '10.1', - title: '文件夹91', - children: [ - { - key: '10.1.1', - title: '文件92', - }, - { - key: '10.1.2', - title: '文件93', - }, - ], - }, - { - key: '10.2', - title: '文件夹94', - children: [ - { - key: '10.2.1', - title: '文件95', - }, - { - key: '10.2.2', - title: '文件96', - }, - { - key: '10.2.3', - title: '文件97', - }, - { - key: '10.2.4', - title: '文件98', - }, - { - key: '10.2.5', - title: '文件99', - }, - { - key: '10.2.6', - title: '文件100', - }, - { - key: '10.2.7', - title: '文件101', - }, - ], - }, - ], - }, - { - key: '118326832101dd', - title: '文件夹102', - children: [ - { - key: '11.13', - title: '文件夹103', - children: [ - { - key: '11.41.1', - title: '文件104', - }, - { - key: '11.71.2', - title: '文件105', - }, - ], - }, - { - key: '11.2', - title: '文件夹106', - children: [ - { - key: '11.82.1', - title: '文件107', - }, - ], - }, - ], - }, -]; - -export type ISleep = (t: number, d?: any) => Promise; - -export const sleep: ISleep = (t = 1500, data) => - new Promise((resolve) => { - setTimeout(() => resolve(data), t); - }); diff --git a/src/catalogue/demos/treeSelect/NormalTreeSelect/index.tsx b/src/catalogue/demos/treeSelect/NormalTreeSelect/index.tsx deleted file mode 100644 index e97dc7ce8..000000000 --- a/src/catalogue/demos/treeSelect/NormalTreeSelect/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { ReactNode, useState } from 'react'; -import { TreeSelectProps } from 'antd'; -import { Catalogue } from 'dt-react-component'; - -import { initTreeSelectData } from '../data'; - -export const NormalTreeSelect = () => { - const [dataSource] = useState(initTreeSelectData); - const [selectedLabelList, setSelectedLabelList] = useState([]); - const handleChange: TreeSelectProps['onChange'] = (value, labelList, _) => { - setSelectedLabelList(labelList); - }; - console.log(selectedLabelList, '--selectedLabelList'); - return ( -
- -

-

- {selectedLabelList?.length - ? `选中了 ${selectedLabelList?.join?.('、')}` - : '未选中'} -

-

-
- ); -}; - -export default NormalTreeSelect; diff --git a/src/catalogue/demos/treeSelect/data.ts b/src/catalogue/demos/treeSelect/data.ts deleted file mode 100644 index 60cb7c307..000000000 --- a/src/catalogue/demos/treeSelect/data.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { TreeSelectProps } from 'antd'; - -export const initTreeSelectData: TreeSelectProps['treeData'] = [ - { - value: '1', - title: '1', - children: [ - { - value: '1.1', - title: '1.1', - children: [ - { - value: '1.1.1', - title: '1.1.1', - isLeaf: true, - }, - { - value: '1.1.2', - title: '1.1.2', - isLeaf: true, - }, - { - value: '1.1.3', - title: '1.1.3', - isLeaf: true, - }, - { - value: '1.1.4', - title: '1.1.4', - isLeaf: true, - }, - { - value: '1.1.5', - title: '1.1.5', - isLeaf: true, - }, - { - value: '1.1.6', - title: '1.1.6', - isLeaf: true, - }, - ], - }, - { - value: '1.2', - title: '1.2', - children: [ - { - value: '1.2.1', - title: '1.2.1', - isLeaf: true, - }, - ], - }, - ], - }, - { - value: '2', - title: '2', - children: [ - { - value: '2.1', - title: '2.1', - children: [ - { - value: '2.1.1', - title: '2.1.1', - isLeaf: true, - }, - { - value: '2.1.2', - title: '2.1.2', - isLeaf: true, - }, - ], - }, - { - value: '2.2', - title: '2.2', - children: [ - { - value: '2.2.1', - title: '2.2.1', - isLeaf: true, - }, - { - value: '2.2.2', - title: '2.2.2', - isLeaf: true, - }, - { - value: '2.2.3', - title: '2.2.3', - isLeaf: true, - }, - { - value: '2.2.4', - title: '2.2.4', - isLeaf: true, - }, - { - value: '2.2.5', - title: '2.2.5', - isLeaf: true, - }, - { - value: '2.2.6', - title: '2.2.6', - isLeaf: true, - }, - { - value: '2.2.7', - title: '2.2.7', - isLeaf: true, - }, - ], - }, - ], - }, - { - value: '3', - title: '3', - children: [ - { - value: '3.1', - title: '3.1', - children: [ - { - value: '3.1.1', - title: '3.1.1', - isLeaf: true, - }, - { - value: '3.1.2', - title: '3.1.2', - isLeaf: true, - }, - ], - }, - { - value: '3.2', - title: '3.2', - children: [ - { - value: '3.2.1', - title: '3.2.1', - isLeaf: true, - }, - ], - }, - ], - }, - { - value: '4', - title: '4', - children: [ - { - value: '4.1', - title: '4.1', - children: [ - { - value: '4.1.1', - title: '4.1.1', - isLeaf: true, - }, - { - value: '4.1.2', - title: '4.1.2', - isLeaf: true, - }, - ], - }, - { - value: '4.2', - title: '4.2', - children: [ - { - value: '4.2.1', - title: '4.2.1', - isLeaf: true, - }, - ], - }, - ], - }, -]; diff --git a/src/catalogue/index.md b/src/catalogue/index.md deleted file mode 100644 index 0550cc1f5..000000000 --- a/src/catalogue/index.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Catalogue 目录 -group: 组件 -toc: content -demo: - cols: 2 ---- - -# Catalogue 目录 - -## 何时使用 - -目录树 - -## 示例 - -### Catalogue.Tree - - - - - - - - - - - - - -### Catalogue.TreeSelect - - - -## API - -### Catalog.Tree - -| 参数 | 说明 | 类型 | 默认值 | -| ---------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | ------------------ | -| loading | 是否加载中 | `boolean` | `false` | -| showHeader | 是否展示头部组件 | `boolean` | `true` | -| treeTitle | 头部文案 | `React.ReactNode` | - | -| wrapperClassName | 容器类名 | `string` | - | -| wrapperStyle | 容器行内样式 | `React.CSSProperties` | - | -| size | 尺寸大小,small 每一个 item 高度是 24px,middle 每一个 item 高度是 36px,默认为 middle | `small \| middle` | `middle` | -| onSearch | 点击搜索按钮回调 | `(value: string, e) => void` | - | -| tabsProps | tabs 配置 | `TabsProps & { items: ITabsItem[]; }` | - | -| treeData | `数据源,与 TreeProps['treeData'] 类型相似,只是增加了 ContextMenu 配置` | `ITreeDataItem[]` | - | -| defaultStatus | `默认展示 tabs 还是 search,仅 items 有值时生效` | `ITabsStatus` | `ITabsStatus.tabs` | -| onStatusChange | `tabs or status 变化时的回调` | `(status: ITabsStatus, e) => void` | - | - -其余属性均继承自 `Tree` 组件,参考 Tree API - -### Catalog.TreeSelect - -即 `TreeSelect` 组件,参考 TreeSelect API - -
-
-
- -:::info -其余属性参考 Ant Design 的 Tree、TreeSelect 组件 -::: diff --git a/src/catalogue/index.scss b/src/catalogue/index.scss new file mode 100644 index 000000000..217acc02d --- /dev/null +++ b/src/catalogue/index.scss @@ -0,0 +1,143 @@ +.dt-catalogue { + position: relative; + height: 100%; + width: 320px; + padding-top: 12px; + display: flex; + flex-direction: column; + background-color: #FFF; + &--show { + opacity: 1; + transition: 0.2s all ease-in; + } + &--hide { + opacity: 0; + width: 0; + transition: 0.2s all ease-in; + } + &__header { + padding: 0 12px; + } + &__tree { + width: 100%; + flex: 1; + min-height: 0; + padding-bottom: 24px; + overflow-y: auto; + .ant-spin-nested-loading, + .ant-spin-container { + height: 100%; + } + .ant-tree-treenode { + width: 100%; + display: flex; + align-items: center; + &:hover { + background-color: #F5F5F8; + .tree__title--operation { + display: flex; + align-items: center; + gap: 4px; + color: #B1B4C5; + } + } + .ant-tree-node-content-wrapper { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + &:hover { + background: none; + } + .ant-tree-iconEle { + display: flex; + align-items: center; + width: 20px; + &:empty { + display: none; + } + } + .ant-tree-title { + flex: 1; + min-width: 0; + } + } + &.ant-tree-treenode-selected { + background-color: #E8F1FF; + position: relative; + &::before { + position: absolute; + content: ""; + height: 100%; + width: 4px; + background-color: #1D78FF; + } + } + .ant-tree-switcher { + background: none; + } + .ant-tree-checkbox { + margin: 0; + } + .ant-tree-indent-unit::before { + height: 32px; + border-right: 1px solid #D8DAE2; + } + .ant-tree-switcher-leaf-line { + &::before { + height: 32px; + } + &::after { + height: 16px; + } + } + } + .ant-tree { + height: 100%; + .ant-tree-treenode-leaf-last .ant-tree-switcher-leaf-line::before { + height: 16px !important; + } + } + .tree { + &__title { + display: flex; + align-items: center; + &--text { + flex: 1; + min-width: 0; + } + &--operation { + display: none; + .ant-dropdown-menu-title-content { + display: flex; + gap: 8px; + align-items: center; + .dt-catalogue__icon { + color: #B1B4C5; + } + } + } + &--input { + margin-right: 12px; + .ant-form-item { + margin-bottom: 0; + } + } + } + } + } + &__icon { + display: inline-flex; + align-items: center; + color: inherit; + font-style: normal; + font-size: 16px; + line-height: 0; + text-align: center; + text-transform: none; + vertical-align: -0.125em; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} diff --git a/src/catalogue/index.tsx b/src/catalogue/index.tsx index 36b8cb614..6852b635a 100644 --- a/src/catalogue/index.tsx +++ b/src/catalogue/index.tsx @@ -1,17 +1,13 @@ -import React from 'react'; +import Catalogue from './components/catalogue'; +import CatalogueTree from './components/tree'; +import './index.scss'; -import type { ITreeProps } from './components/tree'; -import { Tree, TreeSelect } from './components'; - -export type { ITreeDataItem, ITreeProps } from './components/tree'; -export type { ITreeHeaderProps } from './components/tree/components/header'; -export { getExpendKeysByQuery, getIcon, loopTree } from './components/tree/helpers'; - -function Catalogue(props: ITreeProps) { - return ; +type OriginInputType = typeof Catalogue; +interface CatalogueInterface extends OriginInputType { + Tree: typeof CatalogueTree; } -Catalogue.Tree = Tree; -Catalogue.TreeSelect = TreeSelect; +const WrapperCatalogue = Catalogue; +(WrapperCatalogue as CatalogueInterface).Tree = CatalogueTree; -export default Catalogue; +export default WrapperCatalogue as CatalogueInterface; diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx new file mode 100644 index 000000000..560814b5b --- /dev/null +++ b/src/catalogue/useTreeData.tsx @@ -0,0 +1,95 @@ +import { useState } from 'react'; +import { DataNode, TreeProps } from 'antd/lib/tree'; +import { cloneDeep } from 'lodash'; + +export interface ITreeNode extends Omit { + type?: InputStatus; + editable?: boolean; + deletable?: boolean; + addable?: boolean; + children?: ITreeNode[]; +} + +export enum InputStatus { + Add = 'add', + Edit = 'edit', + Append = 'append', +} + +export const useTreeData = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [expandedKeys, setExpandedKeys] = useState([]); + + const initData = (treeData: ITreeNode[]) => { + setData(treeData); + }; + + const processData = (data: ITreeNode[]): ITreeNode[] => { + function traverse(item: ITreeNode): ITreeNode | null { + if (item.type === InputStatus.Edit) { + item.type = undefined; + } + if (item.type === InputStatus.Add || item.type === InputStatus.Append) { + return null; + } + if (Array.isArray(item.children)) { + item.children = item.children.map(traverse).filter(Boolean) as ITreeNode[]; + } + return item; + } + return data.map(traverse).filter(Boolean) as ITreeNode[]; + }; + + const onChange = (node?: ITreeNode, type?: InputStatus) => { + const newData = cloneDeep(data); + // 做 onBlur 清除数据 + if (!node && !type) { + return setData(processData(newData)); + } + if (!node && type === InputStatus.Add) + return setData([{ key: '', type: InputStatus.Add }, ...data]); + if (node && type === InputStatus.Append) { + const newExpandedKeys = expandedKeys ? [...expandedKeys] : []; + loopTree(newData, node.key, (item: ITreeNode) => { + const { children } = item; + item['children'] = [ + ...(children || []), + { key: node.key + 'new', type: InputStatus.Append }, + ]; + }); + setData(newData); + if (!expandedKeys?.includes(node.key)) { + newExpandedKeys.push(node.key); + } + return setExpandedKeys(newExpandedKeys); + } + if (node && type === InputStatus.Edit) { + loopTree(newData, node.key, (item: ITreeNode) => { + item.type = InputStatus.Edit; + }); + setData(newData); + } + }; + + const loopTree = (data: ITreeNode[], key: ITreeNode['key'], callback: Function) => { + data.forEach((item, index, arr) => { + if (item.key === key) { + return callback(item, index, arr); + } + if (item.children) { + return loopTree(item.children, key, callback); + } + }); + }; + + return { + data, + loading, + expandedKeys, + onChange, + initData, + setLoading, + setExpandedKeys, + }; +}; diff --git a/src/catalogue/utils.tsx b/src/catalogue/utils.tsx new file mode 100644 index 000000000..aedc1518e --- /dev/null +++ b/src/catalogue/utils.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { DataNode, TreeProps } from 'antd/lib/tree'; + +import { FolderIcon, FolderOpenedIcon } from './components/icon'; + +export const getIcon: DataNode['icon'] = ({ selected, expanded }) => { + const styles: React.CSSProperties = { + fontSize: 16, + color: '#1D78FF', + }; + + if (expanded || selected) return ; + return ; +}; + +/** + * @description 轮询 Tree 数据,赋值搜索标识和leafIcon + */ +export const loopTree = (data: TreeProps['treeData']): TreeProps['treeData'] => { + return data?.map((item) => { + if (item.children) { + return { + ...item, + icon: getIcon, + children: loopTree(item.children), + }; + } + return { + icon: getIcon, + ...item, + children: undefined, + }; + }); +}; From 7584b49c2093cb1136b64ba81e451848e1d0bef6 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Mon, 18 Nov 2024 20:42:41 +0800 Subject: [PATCH 2/9] fix: add useTreeData return type --- src/catalogue/useTreeData.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx index 560814b5b..d26d53748 100644 --- a/src/catalogue/useTreeData.tsx +++ b/src/catalogue/useTreeData.tsx @@ -16,7 +16,15 @@ export enum InputStatus { Append = 'append', } -export const useTreeData = () => { +export const useTreeData = (): { + data: ITreeNode[]; + loading: boolean; + expandedKeys: TreeProps['expandedKeys']; + initData: (treeData: ITreeNode[]) => void; + onChange: (node?: ITreeNode, type?: InputStatus) => void; + setLoading: React.Dispatch>; + setExpandedKeys: React.Dispatch>; +} => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [expandedKeys, setExpandedKeys] = useState([]); From 99e2000629d449ec788dcbbbb1f152fc1112b575 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Wed, 4 Dec 2024 21:25:52 +0800 Subject: [PATCH 3/9] feat(catalogue): change catalogue and add examples --- src/catalogue/components/catalogue.tsx | 47 +++--- src/catalogue/components/icon.tsx | 2 +- src/catalogue/components/tree.tsx | 6 +- src/catalogue/demos/basic.tsx | 70 ++++++++ src/catalogue/demos/config.tsx | 215 +++++++++++++++++++++++++ src/catalogue/demos/drag.tsx | 212 ++++++++++++++++++++++++ src/catalogue/demos/operator.tsx | 155 ++++++++++++++++++ src/catalogue/index.md | 54 +++++++ src/catalogue/index.scss | 5 +- src/catalogue/useTreeData.tsx | 3 + 10 files changed, 744 insertions(+), 25 deletions(-) create mode 100644 src/catalogue/demos/basic.tsx create mode 100644 src/catalogue/demos/config.tsx create mode 100644 src/catalogue/demos/drag.tsx create mode 100644 src/catalogue/demos/operator.tsx create mode 100644 src/catalogue/index.md diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index 7eb4ed15d..94820c991 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -9,23 +9,21 @@ import { CatalogIcon, DeleteIcon, DragIcon, EditIcon, EllipsisIcon, PlusCircleIc import CatalogueTree, { ICatalogueTree } from './tree'; interface ICatalogue - extends Pick, - Partial, 'onChange'>>, + extends Pick, + Pick, 'onChange'>, ICatalogueTree { - icon?: React.ReactNode; - title?: string; showSearch?: boolean; edit?: boolean; placeholder?: string; loading?: boolean; onSearch?: (value: string) => void; - onSave?: (data: ITreeNode, value: string) => Promise; - onDelete?: (data: ITreeNode) => Promise; + onSave?: (data: ITreeNode, value: string) => void; + onDelete?: (data: ITreeNode) => void; } const Catalogue = ({ title, - icon = , + addonBefore = , tooltip = false, showSearch = false, placeholder = '搜索目录名称', @@ -43,18 +41,24 @@ const Catalogue = ({ }: ICatalogue) => { const [form] = Form.useForm(); - const loopTree = (data: DataNode[]): DataNode[] => { + const loopTree = (data: ITreeNode[]): ITreeNode[] => { return data?.map((item) => { + const reset: ITreeNode = { + ...item, + editable: item?.editable === undefined ? true : item?.editable, + addable: item?.addable === undefined ? true : item?.addable, + deletable: item?.deletable === undefined ? true : item?.deletable, + }; if (item.children) { return { - ...item, - title: renderTitle(item), + ...reset, + title: renderTitle(reset), children: loopTree(item.children), }; } return { - ...item, - title: renderTitle(item), + ...reset, + title: renderTitle(reset), children: undefined, }; }); @@ -67,7 +71,7 @@ const Catalogue = ({ title={title} tooltip={tooltip} background={false} - addonBefore={icon} + addonBefore={addonBefore} addonAfter={addonAfter} spaceBottom={12} /> @@ -165,7 +169,7 @@ const Catalogue = ({ ); }; - const renderNodeHover = (item: DataNode) => { + const renderNodeHover = (item: ITreeNode) => { const menu = ( onChange?.(item, InputStatus.Append)} + disabled={!item.addable} + onClick={() => item.addable && onChange?.(item, InputStatus.Append)} > 新建目录 @@ -184,7 +189,8 @@ const Catalogue = ({ onChange?.(item, InputStatus.Edit)} + disabled={!item.editable} + onClick={() => item.editable && onChange?.(item, InputStatus.Edit)} > 编辑 @@ -192,7 +198,8 @@ const Catalogue = ({ onDelete?.(item)} + disabled={!item.deletable} + onClick={() => item.deletable && onDelete?.(item)} > 删除 @@ -204,7 +211,6 @@ const Catalogue = ({ className="tree__title--operation" onMouseDown={(e) => { e.stopPropagation(); - // this.setState({ isSelected: false, draggable: false }); }} > triggerNode.parentElement} + destroyPopupOnHide + getPopupContainer={(triggerNode) => triggerNode.parentElement as HTMLElement} > e.stopPropagation()} /> - + {draggable && } ); }; diff --git a/src/catalogue/components/icon.tsx b/src/catalogue/components/icon.tsx index a82ab9ef0..f3e7dabb9 100644 --- a/src/catalogue/components/icon.tsx +++ b/src/catalogue/components/icon.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; interface IIcon { className?: string; style?: React.CSSProperties; - onClick?: (e) => void; + onClick?: React.MouseEventHandler; } const FolderIcon = function ({ className, ...rest }: IIcon) { diff --git a/src/catalogue/components/tree.tsx b/src/catalogue/components/tree.tsx index cb90c4b6b..71f86f6dc 100644 --- a/src/catalogue/components/tree.tsx +++ b/src/catalogue/components/tree.tsx @@ -1,4 +1,4 @@ -import React, { Key, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { Tree, TreeProps } from 'antd'; import { ITreeNode } from '../useTreeData'; @@ -10,10 +10,10 @@ export interface ICatalogueTree treeData: ITreeNode[]; } -const CatalogueTree = ({ treeData, onExpand, ...rest }: ICatalogueTree) => { +const CatalogueTree = ({ treeData = [], onExpand, ...rest }: ICatalogueTree) => { const renderTreeData = useMemo(() => loopTree(treeData), [treeData]); - const handleExpand = (expandedKeys: Key[], info) => { + const handleExpand: TreeProps['onExpand'] = (expandedKeys, info) => { onExpand?.(expandedKeys, info); }; diff --git a/src/catalogue/demos/basic.tsx b/src/catalogue/demos/basic.tsx new file mode 100644 index 000000000..6ab011da0 --- /dev/null +++ b/src/catalogue/demos/basic.tsx @@ -0,0 +1,70 @@ +import React, { useEffect } from 'react'; + +import { useTreeData } from '../useTreeData'; +import Catalogue from '..'; + +const DEFAULT_DATA = [ + { + title: '0-0', + key: '0-0', + children: [ + { + title: '0-0-0', + key: '0-0-0', + children: [ + { title: '0-0-0-0', key: '0-0-0-0' }, + { title: '0-0-0-1', key: '0-0-0-1' }, + { title: '0-0-0-2', key: '0-0-0-2' }, + ], + }, + { + title: '0-0-1', + key: '0-0-1', + children: [ + { title: '0-0-1-0', key: '0-0-1-0' }, + { title: '0-0-1-1', key: '0-0-1-1' }, + { title: '0-0-1-2', key: '0-0-1-2' }, + ], + }, + { + title: '0-0-2', + key: '0-0-2', + }, + ], + }, + { + title: '0-1', + key: '0-1', + children: [ + { title: '0-1-0-0', key: '0-1-0-0' }, + { title: '0-1-0-1', key: '0-1-0-1' }, + { title: '0-1-0-2', key: '0-1-0-2' }, + ], + }, + { + title: '0-2', + key: '0-2', + }, +]; + +export default () => { + const treeData = useTreeData(); + + useEffect(() => { + treeData.initData(DEFAULT_DATA); + }, []); + + return ( +
+ +
+ ); +}; diff --git a/src/catalogue/demos/config.tsx b/src/catalogue/demos/config.tsx new file mode 100644 index 000000000..030a1df62 --- /dev/null +++ b/src/catalogue/demos/config.tsx @@ -0,0 +1,215 @@ +import React, { useEffect } from 'react'; +import { TreeProps } from 'antd/lib/tree'; +import { cloneDeep } from 'lodash'; +import shortid from 'shortid'; + +import { PlusSquareIcon } from '../components/icon'; +import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import Catalogue from '..'; + +const DEFAULT_DATA: ITreeNode[] = [ + { + title: '不可编辑的节点', + editable: false, + key: '0-0', + children: [ + { + title: '0-0-0', + key: '0-0-0', + children: [ + { title: '0-0-0-0', key: '0-0-0-0' }, + { title: '0-0-0-1', key: '0-0-0-1' }, + { title: '0-0-0-2', key: '0-0-0-2' }, + ], + }, + { + title: '0-0-1', + key: '0-0-1', + children: [ + { title: '0-0-1-0', key: '0-0-1-0' }, + { title: '0-0-1-1', key: '0-0-1-1' }, + { title: '0-0-1-2', key: '0-0-1-2' }, + ], + }, + { + title: '0-0-2', + key: '0-0-2', + }, + ], + }, + { + title: '不可新增的节点', + addable: false, + key: '0-1', + children: [ + { title: '0-1-0-0', key: '0-1-0-0' }, + { title: '0-1-0-1', key: '0-1-0-1' }, + { title: '0-1-0-2', key: '0-1-0-2' }, + ], + }, + { + title: '不可删除节点', + key: '0-2', + deletable: false, + }, +]; + +export default () => { + const treeData = useTreeData(); + + const findNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode | null => { + for (const node of data) { + if (node.key === targetKey) { + return node; + } + if (node.children) { + const result = findNodeByKey(node.children, targetKey); + if (result) return result; + } + } + return null; + }; + + const handleSave = (data: ITreeNode, value: string) => { + const newData = cloneDeep(treeData.data); + if (data.type === InputStatus.Add) { + newData.push({ + title: value, + key: shortid(), + }); + newData.shift(); + treeData.initData(newData); + return; + } + if (data.type === InputStatus.Append) { + let node = findNodeByKey(newData, data.key); + if (!node) return; + if (node.children) { + const newChildren = node.children + .filter((item) => !item.type) + .concat({ title: value, key: shortid() }); + node.children = newChildren; + } else { + node = { + ...node, + children: [ + { + title: value, + key: shortid(), + }, + ], + }; + } + treeData.initData(newData); + return; + } + const node = findNodeByKey(newData, data.key); + if (node) { + node.title = value; + node.type = undefined; + } + treeData.initData(newData); + }; + + const removeNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode[] => { + return data + .filter((node) => node.key !== targetKey) + .map((node) => { + if (node.children) { + return { + ...node, + children: removeNodeByKey(node.children, targetKey), + }; + } + return node; + }); + }; + + const handleDelete = (data: ITreeNode) => { + let newData = cloneDeep(treeData.data); + newData = removeNodeByKey(newData, data.key); + treeData.initData(newData); + }; + + const handleDrop: TreeProps['onDrop'] = (info) => { + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split('-'); + const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1 + + const loop = ( + data: ITreeNode[], + key: React.Key, + callback: (node: ITreeNode, i: number, data: ITreeNode[]) => void + ) => { + for (let i = 0; i < data.length; i++) { + if (data[i].key === key) { + return callback(data[i], i, data); + } + if (data[i].children) { + loop(data[i].children!, key, callback); + } + } + }; + const data = [...treeData.data]; + + // Find dragObject + let dragObj: ITreeNode; + loop(data, dragKey, (item, index, arr) => { + arr.splice(index, 1); + dragObj = item; + }); + if (!info.dropToGap) { + // Drop on the content + loop(data, dropKey, (item) => { + item.children = item.children || []; + // where to insert. New item was inserted to the start of the array in this example, but can be anywhere + item.children.unshift(dragObj); + }); + } else { + let ar: ITreeNode[] = []; + let i: number; + loop(data, dropKey, (_item, index, arr) => { + ar = arr; + i = index; + }); + if (dropPosition === -1) { + // Drop on the top of the drop node + ar.splice(i!, 0, dragObj!); + } else { + // Drop on the bottom of the drop node + ar.splice(i! + 1, 0, dragObj!); + } + } + treeData.initData(data); + }; + + useEffect(() => { + treeData.initData(DEFAULT_DATA); + }, []); + + return ( +
+ treeData.onChange(undefined, InputStatus.Add)} + /> + } + title="标签目录" + showSearch + draggable + treeData={treeData.data} + expandedKeys={treeData.expandedKeys} + onExpand={treeData.setExpandedKeys} + onDragEnter={({ expandedKeys }) => treeData.setExpandedKeys(expandedKeys)} + onChange={treeData.onChange} + onSave={handleSave} + onDelete={handleDelete} + onDrop={handleDrop} + /> +
+ ); +}; diff --git a/src/catalogue/demos/drag.tsx b/src/catalogue/demos/drag.tsx new file mode 100644 index 000000000..c01401560 --- /dev/null +++ b/src/catalogue/demos/drag.tsx @@ -0,0 +1,212 @@ +import React, { useEffect } from 'react'; +import { TreeProps } from 'antd/lib/tree'; +import { cloneDeep } from 'lodash'; +import shortid from 'shortid'; + +import { PlusSquareIcon } from '../components/icon'; +import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import Catalogue from '..'; + +const DEFAULT_DATA = [ + { + title: '0-0', + key: '0-0', + children: [ + { + title: '0-0-0', + key: '0-0-0', + children: [ + { title: '0-0-0-0', key: '0-0-0-0' }, + { title: '0-0-0-1', key: '0-0-0-1' }, + { title: '0-0-0-2', key: '0-0-0-2' }, + ], + }, + { + title: '0-0-1', + key: '0-0-1', + children: [ + { title: '0-0-1-0', key: '0-0-1-0' }, + { title: '0-0-1-1', key: '0-0-1-1' }, + { title: '0-0-1-2', key: '0-0-1-2' }, + ], + }, + { + title: '0-0-2', + key: '0-0-2', + }, + ], + }, + { + title: '0-1', + key: '0-1', + children: [ + { title: '0-1-0-0', key: '0-1-0-0' }, + { title: '0-1-0-1', key: '0-1-0-1' }, + { title: '0-1-0-2', key: '0-1-0-2' }, + ], + }, + { + title: '0-2', + key: '0-2', + }, +]; + +export default () => { + const treeData = useTreeData(); + + const findNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode | null => { + for (const node of data) { + if (node.key === targetKey) { + return node; + } + if (node.children) { + const result = findNodeByKey(node.children, targetKey); + if (result) return result; + } + } + return null; + }; + + const handleSave = (data: ITreeNode, value: string) => { + const newData = cloneDeep(treeData.data); + if (data.type === InputStatus.Add) { + newData.push({ + title: value, + key: shortid(), + }); + newData.shift(); + treeData.initData(newData); + return; + } + if (data.type === InputStatus.Append) { + let node = findNodeByKey(newData, data.key); + if (!node) return; + if (node.children) { + const newChildren = node.children + .filter((item) => !item.type) + .concat({ title: value, key: shortid() }); + node.children = newChildren; + } else { + node = { + ...node, + children: [ + { + title: value, + key: shortid(), + }, + ], + }; + } + treeData.initData(newData); + return; + } + const node = findNodeByKey(newData, data.key); + if (node) { + node.title = value; + node.type = undefined; + } + treeData.initData(newData); + }; + + const removeNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode[] => { + return data + .filter((node) => node.key !== targetKey) + .map((node) => { + if (node.children) { + return { + ...node, + children: removeNodeByKey(node.children, targetKey), + }; + } + return node; + }); + }; + + const handleDelete = (data: ITreeNode) => { + let newData = cloneDeep(treeData.data); + newData = removeNodeByKey(newData, data.key); + treeData.initData(newData); + }; + + const handleDrop: TreeProps['onDrop'] = (info) => { + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split('-'); + const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1 + + const loop = ( + data: ITreeNode[], + key: React.Key, + callback: (node: ITreeNode, i: number, data: ITreeNode[]) => void + ) => { + for (let i = 0; i < data.length; i++) { + if (data[i].key === key) { + return callback(data[i], i, data); + } + if (data[i].children) { + loop(data[i].children!, key, callback); + } + } + }; + const data = [...treeData.data]; + + // Find dragObject + let dragObj: ITreeNode; + loop(data, dragKey, (item, index, arr) => { + arr.splice(index, 1); + dragObj = item; + }); + if (!info.dropToGap) { + // Drop on the content + loop(data, dropKey, (item) => { + item.children = item.children || []; + // where to insert. New item was inserted to the start of the array in this example, but can be anywhere + item.children.unshift(dragObj); + }); + } else { + let ar: ITreeNode[] = []; + let i: number; + loop(data, dropKey, (_item, index, arr) => { + ar = arr; + i = index; + }); + if (dropPosition === -1) { + // Drop on the top of the drop node + ar.splice(i!, 0, dragObj!); + } else { + // Drop on the bottom of the drop node + ar.splice(i! + 1, 0, dragObj!); + } + } + treeData.initData(data); + }; + + useEffect(() => { + treeData.initData(DEFAULT_DATA); + }, []); + + return ( +
+ treeData.onChange(undefined, InputStatus.Add)} + /> + } + title="标签目录" + showSearch + draggable + treeData={treeData.data} + expandedKeys={treeData.expandedKeys} + onExpand={treeData.setExpandedKeys} + onDragEnter={({ expandedKeys }) => treeData.setExpandedKeys(expandedKeys)} + onChange={treeData.onChange} + onSave={handleSave} + onDelete={handleDelete} + onDrop={handleDrop} + /> +
+ ); +}; diff --git a/src/catalogue/demos/operator.tsx b/src/catalogue/demos/operator.tsx new file mode 100644 index 000000000..360fb9ac2 --- /dev/null +++ b/src/catalogue/demos/operator.tsx @@ -0,0 +1,155 @@ +import React, { useEffect } from 'react'; +import { cloneDeep } from 'lodash'; +import shortid from 'shortid'; + +import { PlusSquareIcon } from '../components/icon'; +import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import Catalogue from '..'; + +const DEFAULT_DATA = [ + { + title: '0-0', + key: '0-0', + children: [ + { + title: '0-0-0', + key: '0-0-0', + children: [ + { title: '0-0-0-0', key: '0-0-0-0' }, + { title: '0-0-0-1', key: '0-0-0-1' }, + { title: '0-0-0-2', key: '0-0-0-2' }, + ], + }, + { + title: '0-0-1', + key: '0-0-1', + children: [ + { title: '0-0-1-0', key: '0-0-1-0' }, + { title: '0-0-1-1', key: '0-0-1-1' }, + { title: '0-0-1-2', key: '0-0-1-2' }, + ], + }, + { + title: '0-0-2', + key: '0-0-2', + }, + ], + }, + { + title: '0-1', + key: '0-1', + children: [ + { title: '0-1-0-0', key: '0-1-0-0' }, + { title: '0-1-0-1', key: '0-1-0-1' }, + { title: '0-1-0-2', key: '0-1-0-2' }, + ], + }, + { + title: '0-2', + key: '0-2', + }, +]; + +export default () => { + const treeData = useTreeData(); + + const findNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode | null => { + for (const node of data) { + if (node.key === targetKey) { + return node; + } + if (node.children) { + const result = findNodeByKey(node.children, targetKey); + if (result) return result; + } + } + return null; + }; + + const handleSave = (data: ITreeNode, value: string) => { + const newData = cloneDeep(treeData.data); + if (data.type === InputStatus.Add) { + newData.push({ + title: value, + key: shortid(), + }); + newData.shift(); + treeData.initData(newData); + return; + } + if (data.type === InputStatus.Append) { + let node = findNodeByKey(newData, data.key); + if (!node) return; + if (node.children) { + const newChildren = node.children + .filter((item) => !item.type) + .concat({ title: value, key: shortid() }); + node.children = newChildren; + } else { + node = { + ...node, + children: [ + { + title: value, + key: shortid(), + }, + ], + }; + } + treeData.initData(newData); + return; + } + const node = findNodeByKey(newData, data.key); + if (node) { + node.title = value; + node.type = undefined; + } + treeData.initData(newData); + }; + + const removeNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode[] => { + return data + .filter((node) => node.key !== targetKey) + .map((node) => { + if (node.children) { + return { + ...node, + children: removeNodeByKey(node.children, targetKey), + }; + } + return node; + }); + }; + + const handleDelete = (data: ITreeNode) => { + let newData = cloneDeep(treeData.data); + newData = removeNodeByKey(newData, data.key); + treeData.initData(newData); + }; + + useEffect(() => { + treeData.initData(DEFAULT_DATA); + }, []); + + return ( +
+ treeData.onChange(undefined, InputStatus.Add)} + /> + } + title="标签目录" + showSearch + treeData={treeData.data} + expandedKeys={treeData.expandedKeys} + onExpand={treeData.setExpandedKeys} + onChange={treeData.onChange} + onSave={handleSave} + onDelete={handleDelete} + /> +
+ ); +}; diff --git a/src/catalogue/index.md b/src/catalogue/index.md new file mode 100644 index 000000000..90a023c83 --- /dev/null +++ b/src/catalogue/index.md @@ -0,0 +1,54 @@ +--- +title: Catalogue 目录树 +group: 组件 +toc: content +demo: + cols: 2 +--- + +# Catalogue 目录树 + +用于展示目录树 + +## 何时使用 + +适合使用在需要展示目录树的地方 + +## 示例 + +纯展示的目录树 +可操作的目录树 +可拖拽的目录树 +配置操作项的目录树 + +## API + +### Catalogue + +| 参数 | 说明 | 类型 | 默认值 | +| ----------- | ------------------------------ | ------------------------------------------------ | -------------- | +| loading | 目录树是否加载中 | `boolean` | `false` | +| treeData | 目录树是否加载中 | `ITreeNode[]` | `[]` | +| showSearch | 是否展示搜索框 | `boolean` | `false` | +| edit | 目录是否可以操作 | `boolean` | `true` | +| placeholder | 搜索框的默认展示内容 | `string` | `搜索目录名称` | +| onSearch | 点击搜索图标、清除图标时的回调 | `(value: string) => void` | - | +| onSave | 新增/编辑时的回调 | `(data: ITreeNode, value: string) => void` | - | +| onDelete | 删除时的回调 | `(data: ITreeNode) => void` | - | +| onChange | 触发新增/编辑的回调 | `(node?: ITreeNode, type?: InputStatus) => void` | - | + +:::info +其余参数继承 antd4.x 的 `Omit`
+继承 dt-react-component 的 `Pick`
+[TreeProps](https://ant.design/components/tree-cn#tree-props),[IBlockHeaderProps](https://dtstack.github.io/dt-react-component/components/block-header#blockheader) +::: + +### ITreeNode + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | -------------- | ----------------------- | ------ | +| type | 节点输入框内容 | `add \| edit \| append` | - | +| editable | 是否可编辑 | `boolean` | `true` | +| deletable | 是否可删除 | `boolean` | `true` | +| addable | 是否可增加 | `boolean` | `true` | +| children | 子节点 | `ITreeNode[]` | - | diff --git a/src/catalogue/index.scss b/src/catalogue/index.scss index 217acc02d..67fe3c8e0 100644 --- a/src/catalogue/index.scss +++ b/src/catalogue/index.scss @@ -1,7 +1,7 @@ .dt-catalogue { position: relative; height: 100%; - width: 320px; + width: 100%; padding-top: 12px; display: flex; flex-direction: column; @@ -97,6 +97,9 @@ .ant-tree-treenode-leaf-last .ant-tree-switcher-leaf-line::before { height: 16px !important; } + .ant-tree-node-content-wrapper.ant-tree-node-selected { + background: none; + } } .tree { &__title { diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx index d26d53748..35843e091 100644 --- a/src/catalogue/useTreeData.tsx +++ b/src/catalogue/useTreeData.tsx @@ -4,8 +4,11 @@ import { cloneDeep } from 'lodash'; export interface ITreeNode extends Omit { type?: InputStatus; + /** 是否可编辑 */ editable?: boolean; + /** 是否可删除 */ deletable?: boolean; + /** 是否可增加 */ addable?: boolean; children?: ITreeNode[]; } From 97b42590dc5f0d9952ff2fea1224d57f32a4ddf8 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Tue, 10 Dec 2024 17:50:42 +0800 Subject: [PATCH 4/9] feat(catalogue): add custom overlay, form.error msg and drag style --- src/catalogue/components/catalogue.tsx | 102 +++++++++---------------- src/catalogue/demos/basic.tsx | 2 +- src/catalogue/demos/config.tsx | 46 ++++++++++- src/catalogue/demos/drag.tsx | 46 ++++++++++- src/catalogue/demos/operator.tsx | 46 ++++++++++- src/catalogue/index.md | 22 +++--- src/catalogue/index.scss | 16 +++- 7 files changed, 187 insertions(+), 93 deletions(-) diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index 94820c991..d84154d9d 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -1,24 +1,24 @@ -import React, { useMemo } from 'react'; -import { Dropdown, Form, Input, Menu, Spin } from 'antd'; +import React from 'react'; +import { Dropdown, DropdownProps, Form, Input, Spin } from 'antd'; import { DataNode } from 'antd/lib/tree'; -import { EllipsisText, Empty } from 'dt-react-component'; -import BlockHeader, { IBlockHeaderProps } from 'dt-react-component/blockHeader'; +import { BlockHeader, EllipsisText, Empty } from 'dt-react-component'; +import { IBlockHeaderProps } from 'dt-react-component/blockHeader'; import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; -import { CatalogIcon, DeleteIcon, DragIcon, EditIcon, EllipsisIcon, PlusCircleIcon } from './icon'; +import { CatalogIcon, DragIcon, EllipsisIcon } from './icon'; import CatalogueTree, { ICatalogueTree } from './tree'; -interface ICatalogue +export interface ICatalogue extends Pick, - Pick, 'onChange'>, ICatalogueTree { showSearch?: boolean; edit?: boolean; placeholder?: string; loading?: boolean; + onChange?: ReturnType['onChange']; + overlay?: (item: ITreeNode) => DropdownProps['overlay']; onSearch?: (value: string) => void; - onSave?: (data: ITreeNode, value: string) => void; - onDelete?: (data: ITreeNode) => void; + onSave?: (data: ITreeNode, value: string) => Promise; } const Catalogue = ({ @@ -32,11 +32,11 @@ const Catalogue = ({ loading = false, treeData, draggable, + overlay, onChange, onSearch, onExpand, onSave, - onDelete, ...rest }: ICatalogue) => { const [form] = Form.useForm(); @@ -90,12 +90,13 @@ const Catalogue = ({ }; const renderTree = () => { + const treeDataWithTitle = loopTree(treeData); if (!treeDataWithTitle.length) return ; return (
{ + form.setFields([{ name: 'catalog_input', errors: msg ? [msg] : [] }]); + }) + ); } - onSave?.(item, value); + onSave?.(item, value).then((msg) => { + form.setFields([{ name: 'catalog_input', errors: msg ? [msg] : [] }]); + }); }; const renderInput = (item: DataNode) => { return (
-
- + + form.setFields([{ name: 'catalog_input', errors: [] }])} onClick={(e) => e.stopPropagation()} onBlur={({ target }) => handleInputSubmit(item, target.value)} onPressEnter={({ target }) => @@ -170,42 +179,6 @@ const Catalogue = ({ }; const renderNodeHover = (item: ITreeNode) => { - const menu = ( - { - domEvent.stopPropagation(); - }} - > - item.addable && onChange?.(item, InputStatus.Append)} - > - - 新建目录 - - item.editable && onChange?.(item, InputStatus.Edit)} - > - - 编辑 - - item.deletable && onDelete?.(item)} - > - - 删除 - - - ); return (
- triggerNode.parentElement as HTMLElement} - > - e.stopPropagation()} /> - + {overlay && ( + + triggerNode.parentElement as HTMLElement + } + > + e.stopPropagation()} /> + + )} {draggable && }
); }; - const treeDataWithTitle = useMemo(() => { - return loopTree(treeData); - }, [treeData]); - return (
diff --git a/src/catalogue/demos/basic.tsx b/src/catalogue/demos/basic.tsx index 6ab011da0..30144f542 100644 --- a/src/catalogue/demos/basic.tsx +++ b/src/catalogue/demos/basic.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; +import { Catalogue } from 'dt-react-component'; import { useTreeData } from '../useTreeData'; -import Catalogue from '..'; const DEFAULT_DATA = [ { diff --git a/src/catalogue/demos/config.tsx b/src/catalogue/demos/config.tsx index 030a1df62..0186b4c68 100644 --- a/src/catalogue/demos/config.tsx +++ b/src/catalogue/demos/config.tsx @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; +import { Menu } from 'antd'; import { TreeProps } from 'antd/lib/tree'; +import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; -import { PlusSquareIcon } from '../components/icon'; +import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; -import Catalogue from '..'; const DEFAULT_DATA: ITreeNode[] = [ { @@ -70,7 +71,7 @@ export default () => { return null; }; - const handleSave = (data: ITreeNode, value: string) => { + const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); if (data.type === InputStatus.Add) { newData.push({ @@ -201,13 +202,50 @@ export default () => { title="标签目录" showSearch draggable + overlay={(item) => ( + { + domEvent.stopPropagation(); + }} + > + + item.addable && treeData.onChange(item, InputStatus.Append) + } + > + + 新建目录 + + + item.editable && treeData.onChange(item, InputStatus.Edit) + } + > + + 编辑 + + item.deletable && handleDelete(item)} + > + + 删除 + + + )} treeData={treeData.data} expandedKeys={treeData.expandedKeys} onExpand={treeData.setExpandedKeys} onDragEnter={({ expandedKeys }) => treeData.setExpandedKeys(expandedKeys)} onChange={treeData.onChange} onSave={handleSave} - onDelete={handleDelete} onDrop={handleDrop} />
diff --git a/src/catalogue/demos/drag.tsx b/src/catalogue/demos/drag.tsx index c01401560..5898d2f04 100644 --- a/src/catalogue/demos/drag.tsx +++ b/src/catalogue/demos/drag.tsx @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; +import { Menu } from 'antd'; import { TreeProps } from 'antd/lib/tree'; +import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; -import { PlusSquareIcon } from '../components/icon'; +import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; -import Catalogue from '..'; const DEFAULT_DATA = [ { @@ -67,7 +68,7 @@ export default () => { return null; }; - const handleSave = (data: ITreeNode, value: string) => { + const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); if (data.type === InputStatus.Add) { newData.push({ @@ -198,13 +199,50 @@ export default () => { title="标签目录" showSearch draggable + overlay={(item) => ( + { + domEvent.stopPropagation(); + }} + > + + item.addable && treeData.onChange(item, InputStatus.Append) + } + > + + 新建目录 + + + item.editable && treeData.onChange(item, InputStatus.Edit) + } + > + + 编辑 + + item.deletable && handleDelete(item)} + > + + 删除 + + + )} treeData={treeData.data} expandedKeys={treeData.expandedKeys} onExpand={treeData.setExpandedKeys} onDragEnter={({ expandedKeys }) => treeData.setExpandedKeys(expandedKeys)} onChange={treeData.onChange} onSave={handleSave} - onDelete={handleDelete} onDrop={handleDrop} />
diff --git a/src/catalogue/demos/operator.tsx b/src/catalogue/demos/operator.tsx index 360fb9ac2..b24a24af9 100644 --- a/src/catalogue/demos/operator.tsx +++ b/src/catalogue/demos/operator.tsx @@ -1,10 +1,11 @@ import React, { useEffect } from 'react'; +import { Menu } from 'antd'; +import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; -import { PlusSquareIcon } from '../components/icon'; +import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; -import Catalogue from '..'; const DEFAULT_DATA = [ { @@ -66,7 +67,7 @@ export default () => { return null; }; - const handleSave = (data: ITreeNode, value: string) => { + const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); if (data.type === InputStatus.Add) { newData.push({ @@ -142,13 +143,50 @@ export default () => { /> } title="标签目录" + overlay={(item) => ( + { + domEvent.stopPropagation(); + }} + > + + item.addable && treeData.onChange(item, InputStatus.Append) + } + > + + 新建目录 + + + item.editable && treeData.onChange(item, InputStatus.Edit) + } + > + + 编辑 + + item.deletable && handleDelete(item)} + > + + 删除 + + + )} showSearch treeData={treeData.data} expandedKeys={treeData.expandedKeys} onExpand={treeData.setExpandedKeys} onChange={treeData.onChange} onSave={handleSave} - onDelete={handleDelete} />
); diff --git a/src/catalogue/index.md b/src/catalogue/index.md index 90a023c83..218a8c463 100644 --- a/src/catalogue/index.md +++ b/src/catalogue/index.md @@ -25,17 +25,17 @@ demo: ### Catalogue -| 参数 | 说明 | 类型 | 默认值 | -| ----------- | ------------------------------ | ------------------------------------------------ | -------------- | -| loading | 目录树是否加载中 | `boolean` | `false` | -| treeData | 目录树是否加载中 | `ITreeNode[]` | `[]` | -| showSearch | 是否展示搜索框 | `boolean` | `false` | -| edit | 目录是否可以操作 | `boolean` | `true` | -| placeholder | 搜索框的默认展示内容 | `string` | `搜索目录名称` | -| onSearch | 点击搜索图标、清除图标时的回调 | `(value: string) => void` | - | -| onSave | 新增/编辑时的回调 | `(data: ITreeNode, value: string) => void` | - | -| onDelete | 删除时的回调 | `(data: ITreeNode) => void` | - | -| onChange | 触发新增/编辑的回调 | `(node?: ITreeNode, type?: InputStatus) => void` | - | +| 参数 | 说明 | 类型 | 默认值 | +| ----------- | ------------------------------ | ------------------------------------------------------------- | -------------- | +| loading | 目录树是否加载中 | `boolean` | `false` | +| overlay | 目录树的操作配置 | `(item: ITreeNode) => DropdownProps['overlay']` | - | +| treeData | 目录树是否加载中 | `ITreeNode[]` | `[]` | +| showSearch | 是否展示搜索框 | `boolean` | `false` | +| edit | 目录是否可以操作 | `boolean` | `true` | +| placeholder | 搜索框的默认展示内容 | `string` | `搜索目录名称` | +| onSearch | 点击搜索图标、清除图标时的回调 | `(value: string) => void` | - | +| onSave | 新增/编辑时的回调 | `(data: ITreeNode, value: string) => Promise` | - | +| onChange | 触发新增/编辑的回调 | `(node?: ITreeNode, type?: InputStatus) => void` | - | :::info 其余参数继承 antd4.x 的 `Omit`
diff --git a/src/catalogue/index.scss b/src/catalogue/index.scss index 67fe3c8e0..ea141ed0f 100644 --- a/src/catalogue/index.scss +++ b/src/catalogue/index.scss @@ -22,12 +22,20 @@ width: 100%; flex: 1; min-height: 0; - padding-bottom: 24px; + padding: 2px 0 24px; overflow-y: auto; .ant-spin-nested-loading, .ant-spin-container { height: 100%; } + .ant-tree-drop-indicator { + background: none; + border-bottom: 1px dashed #1D78FF; + bottom: 0 !important; + &::after { + border: none; + } + } .ant-tree-treenode { width: 100%; display: flex; @@ -80,15 +88,15 @@ margin: 0; } .ant-tree-indent-unit::before { - height: 32px; + height: 100%; border-right: 1px solid #D8DAE2; } .ant-tree-switcher-leaf-line { &::before { - height: 32px; + height: 100%; } &::after { - height: 16px; + height: 50%; } } } From 9545f76b04ca00b741fef8a3e7d3530dc06ce7d1 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Wed, 11 Dec 2024 14:32:56 +0800 Subject: [PATCH 5/9] feat(catalogue): support title and icon, change CatalogueTree --- src/catalogue/components/catalogue.tsx | 58 ++++++++-------------- src/catalogue/components/tree.tsx | 31 ++++++------ src/catalogue/demos/basic.tsx | 9 +++- src/catalogue/demos/config.tsx | 36 ++++++++------ src/catalogue/demos/drag.tsx | 22 ++++----- src/catalogue/demos/operator.tsx | 18 +++---- src/catalogue/demos/simple.tsx | 67 ++++++++++++++++++++++++++ src/catalogue/index.md | 7 +-- src/catalogue/index.scss | 33 +++---------- src/catalogue/index.tsx | 3 ++ src/catalogue/useTreeData.tsx | 41 ++++++++-------- src/catalogue/utils.tsx | 9 ++-- 12 files changed, 189 insertions(+), 145 deletions(-) create mode 100644 src/catalogue/demos/simple.tsx diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index d84154d9d..a1783b5f7 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { Dropdown, DropdownProps, Form, Input, Spin } from 'antd'; -import { DataNode } from 'antd/lib/tree'; -import { BlockHeader, EllipsisText, Empty } from 'dt-react-component'; +import { Dropdown, DropdownProps, Form, Input } from 'antd'; +import { BlockHeader } from 'dt-react-component'; import { IBlockHeaderProps } from 'dt-react-component/blockHeader'; -import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; import { CatalogIcon, DragIcon, EllipsisIcon } from './icon'; import CatalogueTree, { ICatalogueTree } from './tree'; @@ -29,13 +28,11 @@ const Catalogue = ({ placeholder = '搜索目录名称', addonAfter, edit = true, - loading = false, treeData, draggable, overlay, onChange, onSearch, - onExpand, onSave, ...rest }: ICatalogue) => { @@ -43,7 +40,7 @@ const Catalogue = ({ const loopTree = (data: ITreeNode[]): ITreeNode[] => { return data?.map((item) => { - const reset: ITreeNode = { + const newItem = { ...item, editable: item?.editable === undefined ? true : item?.editable, addable: item?.addable === undefined ? true : item?.addable, @@ -51,14 +48,14 @@ const Catalogue = ({ }; if (item.children) { return { - ...reset, - title: renderTitle(reset), + ...newItem, + title: renderTitle(newItem), children: loopTree(item.children), }; } return { - ...reset, - title: renderTitle(reset), + ...newItem, + title: renderTitle(newItem), children: undefined, }; }); @@ -89,33 +86,16 @@ const Catalogue = ({ ); }; - const renderTree = () => { - const treeDataWithTitle = loopTree(treeData); - if (!treeDataWithTitle.length) return ; - return ( -
- - - -
- ); - }; - const handleInputSubmit = (item: ITreeNode, value: string) => { if (!value) { return onChange?.(undefined, undefined); } // item 为当前编辑的数据,对于 Append 的情况需要传入父级的 key - if (item.type === InputStatus.Append) { + if (item.inputMode === InputMode.Append) { const findAppendParents = (data: ITreeNode[], item: ITreeNode): ITreeNode | null => { let result: ITreeNode | null = null; function traverse(node: ITreeNode, parent: ITreeNode | null): void { - if (node.type === 'append' && node.key === item.key && parent) { + if (node.inputMode === 'append' && node.key === item.key && parent) { result = parent; } if (Array.isArray(node.children)) { @@ -128,7 +108,7 @@ const Catalogue = ({ const parentItem = findAppendParents(treeData, item); return ( parentItem && - onSave?.({ ...parentItem, type: InputStatus.Append }, value).then((msg) => { + onSave?.({ ...parentItem, inputMode: InputMode.Append }, value).then((msg) => { form.setFields([{ name: 'catalog_input', errors: msg ? [msg] : [] }]); }) ); @@ -138,7 +118,7 @@ const Catalogue = ({ }); }; - const renderInput = (item: DataNode) => { + const renderInput = (item: ITreeNode) => { return (
@@ -163,16 +143,12 @@ const Catalogue = ({ }; const renderTitle = (item: ITreeNode) => { - if (item.type) { + if (item.inputMode) { return renderInput(item); } return (
- +
{item.title}
{edit && renderNodeHover(item)}
); @@ -210,7 +186,11 @@ const Catalogue = ({ {renderHeader()} {renderSearch()}
- {renderTree()} +
); }; diff --git a/src/catalogue/components/tree.tsx b/src/catalogue/components/tree.tsx index 71f86f6dc..f03a30a50 100644 --- a/src/catalogue/components/tree.tsx +++ b/src/catalogue/components/tree.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; -import { Tree, TreeProps } from 'antd'; +import { Spin, Tree, TreeProps } from 'antd'; +import { Empty } from 'dt-react-component'; import { ITreeNode } from '../useTreeData'; import { loopTree } from '../utils'; @@ -7,25 +8,27 @@ import { DownTriangleIcon } from './icon'; export interface ICatalogueTree extends Omit { + loading?: boolean; treeData: ITreeNode[]; } -const CatalogueTree = ({ treeData = [], onExpand, ...rest }: ICatalogueTree) => { - const renderTreeData = useMemo(() => loopTree(treeData), [treeData]); +const CatalogueTree = ({ treeData = [], loading = false, ...rest }: ICatalogueTree) => { + const renderTreeData = useMemo(() => loopTree(treeData) || [], [treeData]); - const handleExpand: TreeProps['onExpand'] = (expandedKeys, info) => { - onExpand?.(expandedKeys, info); - }; + if (!renderTreeData.length) return ; return ( - } - showIcon - treeData={renderTreeData} - onExpand={handleExpand} - {...rest} - /> +
+ + } + showIcon + treeData={renderTreeData} + {...rest} + /> + +
); }; diff --git a/src/catalogue/demos/basic.tsx b/src/catalogue/demos/basic.tsx index 30144f542..b1150d783 100644 --- a/src/catalogue/demos/basic.tsx +++ b/src/catalogue/demos/basic.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { Catalogue } from 'dt-react-component'; +import { Catalogue, EllipsisText } from 'dt-react-component'; import { useTreeData } from '../useTreeData'; @@ -9,7 +9,12 @@ const DEFAULT_DATA = [ key: '0-0', children: [ { - title: '0-0-0', + title: ( + + ), key: '0-0-0', children: [ { title: '0-0-0-0', key: '0-0-0-0' }, diff --git a/src/catalogue/demos/config.tsx b/src/catalogue/demos/config.tsx index 0186b4c68..c564f73cd 100644 --- a/src/catalogue/demos/config.tsx +++ b/src/catalogue/demos/config.tsx @@ -1,12 +1,12 @@ import React, { useEffect } from 'react'; import { Menu } from 'antd'; -import { TreeProps } from 'antd/lib/tree'; -import { Catalogue } from 'dt-react-component'; +import { Catalogue, EllipsisText } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; +import { ICatalogue } from '../components/catalogue'; import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; -import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; const DEFAULT_DATA: ITreeNode[] = [ { @@ -15,10 +15,18 @@ const DEFAULT_DATA: ITreeNode[] = [ key: '0-0', children: [ { - title: '0-0-0', + title: ( + + ), key: '0-0-0', children: [ - { title: '0-0-0-0', key: '0-0-0-0' }, + { + title: '0-0-0-0', + key: '0-0-0-0', + }, { title: '0-0-0-1', key: '0-0-0-1' }, { title: '0-0-0-2', key: '0-0-0-2' }, ], @@ -73,7 +81,7 @@ export default () => { const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); - if (data.type === InputStatus.Add) { + if (data.inputMode === InputMode.Add) { newData.push({ title: value, key: shortid(), @@ -82,12 +90,12 @@ export default () => { treeData.initData(newData); return; } - if (data.type === InputStatus.Append) { + if (data.inputMode === InputMode.Append) { let node = findNodeByKey(newData, data.key); if (!node) return; if (node.children) { const newChildren = node.children - .filter((item) => !item.type) + .filter((item) => !item.inputMode) .concat({ title: value, key: shortid() }); node.children = newChildren; } else { @@ -107,7 +115,7 @@ export default () => { const node = findNodeByKey(newData, data.key); if (node) { node.title = value; - node.type = undefined; + node.inputMode = undefined; } treeData.initData(newData); }; @@ -132,7 +140,7 @@ export default () => { treeData.initData(newData); }; - const handleDrop: TreeProps['onDrop'] = (info) => { + const handleDrop: ICatalogue['onDrop'] = (info) => { const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); @@ -196,7 +204,7 @@ export default () => { addonAfter={ treeData.onChange(undefined, InputStatus.Add)} + onClick={() => treeData.onChange(undefined, InputMode.Add)} /> } title="标签目录" @@ -212,7 +220,7 @@ export default () => { key="add" disabled={!item.addable} onClick={() => - item.addable && treeData.onChange(item, InputStatus.Append) + item.addable && treeData.onChange(item, InputMode.Append) } > @@ -222,9 +230,7 @@ export default () => { key="edit" className="title__menu--item" disabled={!item.editable} - onClick={() => - item.editable && treeData.onChange(item, InputStatus.Edit) - } + onClick={() => item.editable && treeData.onChange(item, InputMode.Edit)} > 编辑 diff --git a/src/catalogue/demos/drag.tsx b/src/catalogue/demos/drag.tsx index 5898d2f04..25a14b34e 100644 --- a/src/catalogue/demos/drag.tsx +++ b/src/catalogue/demos/drag.tsx @@ -1,12 +1,12 @@ import React, { useEffect } from 'react'; import { Menu } from 'antd'; -import { TreeProps } from 'antd/lib/tree'; import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; +import { ICatalogue } from '../components/catalogue'; import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; -import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; const DEFAULT_DATA = [ { @@ -70,7 +70,7 @@ export default () => { const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); - if (data.type === InputStatus.Add) { + if (data.inputMode === InputMode.Add) { newData.push({ title: value, key: shortid(), @@ -79,12 +79,12 @@ export default () => { treeData.initData(newData); return; } - if (data.type === InputStatus.Append) { + if (data.inputMode === InputMode.Append) { let node = findNodeByKey(newData, data.key); if (!node) return; if (node.children) { const newChildren = node.children - .filter((item) => !item.type) + .filter((item) => !item.inputMode) .concat({ title: value, key: shortid() }); node.children = newChildren; } else { @@ -104,7 +104,7 @@ export default () => { const node = findNodeByKey(newData, data.key); if (node) { node.title = value; - node.type = undefined; + node.inputMode = undefined; } treeData.initData(newData); }; @@ -129,7 +129,7 @@ export default () => { treeData.initData(newData); }; - const handleDrop: TreeProps['onDrop'] = (info) => { + const handleDrop: ICatalogue['onDrop'] = (info) => { const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); @@ -193,7 +193,7 @@ export default () => { addonAfter={ treeData.onChange(undefined, InputStatus.Add)} + onClick={() => treeData.onChange(undefined, InputMode.Add)} /> } title="标签目录" @@ -209,7 +209,7 @@ export default () => { key="add" disabled={!item.addable} onClick={() => - item.addable && treeData.onChange(item, InputStatus.Append) + item.addable && treeData.onChange(item, InputMode.Append) } > @@ -219,9 +219,7 @@ export default () => { key="edit" className="title__menu--item" disabled={!item.editable} - onClick={() => - item.editable && treeData.onChange(item, InputStatus.Edit) - } + onClick={() => item.editable && treeData.onChange(item, InputMode.Edit)} > 编辑 diff --git a/src/catalogue/demos/operator.tsx b/src/catalogue/demos/operator.tsx index b24a24af9..09e769f59 100644 --- a/src/catalogue/demos/operator.tsx +++ b/src/catalogue/demos/operator.tsx @@ -5,7 +5,7 @@ import { cloneDeep } from 'lodash'; import shortid from 'shortid'; import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; -import { InputStatus, ITreeNode, useTreeData } from '../useTreeData'; +import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; const DEFAULT_DATA = [ { @@ -69,7 +69,7 @@ export default () => { const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); - if (data.type === InputStatus.Add) { + if (data.inputMode === InputMode.Add) { newData.push({ title: value, key: shortid(), @@ -78,12 +78,12 @@ export default () => { treeData.initData(newData); return; } - if (data.type === InputStatus.Append) { + if (data.inputMode === InputMode.Append) { let node = findNodeByKey(newData, data.key); if (!node) return; if (node.children) { const newChildren = node.children - .filter((item) => !item.type) + .filter((item) => !item.inputMode) .concat({ title: value, key: shortid() }); node.children = newChildren; } else { @@ -103,7 +103,7 @@ export default () => { const node = findNodeByKey(newData, data.key); if (node) { node.title = value; - node.type = undefined; + node.inputMode = undefined; } treeData.initData(newData); }; @@ -139,7 +139,7 @@ export default () => { addonAfter={ treeData.onChange(undefined, InputStatus.Add)} + onClick={() => treeData.onChange(undefined, InputMode.Add)} /> } title="标签目录" @@ -153,7 +153,7 @@ export default () => { key="add" disabled={!item.addable} onClick={() => - item.addable && treeData.onChange(item, InputStatus.Append) + item.addable && treeData.onChange(item, InputMode.Append) } > @@ -163,9 +163,7 @@ export default () => { key="edit" className="title__menu--item" disabled={!item.editable} - onClick={() => - item.editable && treeData.onChange(item, InputStatus.Edit) - } + onClick={() => item.editable && treeData.onChange(item, InputMode.Edit)} > 编辑 diff --git a/src/catalogue/demos/simple.tsx b/src/catalogue/demos/simple.tsx new file mode 100644 index 000000000..fee452a70 --- /dev/null +++ b/src/catalogue/demos/simple.tsx @@ -0,0 +1,67 @@ +import React, { useEffect } from 'react'; +import { Catalogue, EllipsisText } from 'dt-react-component'; + +import { useTreeData } from '../useTreeData'; + +const DEFAULT_DATA = [ + { + title: '0-0', + key: '0-0', + children: [ + { + title: ( + + ), + key: '0-0-0', + children: [ + { title: '0-0-0-0', key: '0-0-0-0' }, + { title: '0-0-0-1', key: '0-0-0-1' }, + { title: '0-0-0-2', key: '0-0-0-2' }, + ], + }, + { + title: '0-0-1', + key: '0-0-1', + children: [ + { title: '0-0-1-0', key: '0-0-1-0' }, + { title: '0-0-1-1', key: '0-0-1-1' }, + { title: '0-0-1-2', key: '0-0-1-2' }, + ], + }, + { + title: '0-0-2', + key: '0-0-2', + }, + ], + }, + { + title: '0-1', + key: '0-1', + children: [ + { title: '0-1-0-0', key: '0-1-0-0' }, + { title: '0-1-0-1', key: '0-1-0-1' }, + { title: '0-1-0-2', key: '0-1-0-2' }, + ], + }, + { + title: '0-2', + key: '0-2', + }, +]; + +export default () => { + const treeData = useTreeData(); + + useEffect(() => { + treeData.initData(DEFAULT_DATA); + }, []); + + return ( +
+ +
+ ); +}; diff --git a/src/catalogue/index.md b/src/catalogue/index.md index 218a8c463..d5f7fc639 100644 --- a/src/catalogue/index.md +++ b/src/catalogue/index.md @@ -16,7 +16,8 @@ demo: ## 示例 -纯展示的目录树 +带有 Title 纯展示的目录树 +无 Title 纯展示操作项的目录树 可操作的目录树 可拖拽的目录树 配置操作项的目录树 @@ -35,7 +36,7 @@ demo: | placeholder | 搜索框的默认展示内容 | `string` | `搜索目录名称` | | onSearch | 点击搜索图标、清除图标时的回调 | `(value: string) => void` | - | | onSave | 新增/编辑时的回调 | `(data: ITreeNode, value: string) => Promise` | - | -| onChange | 触发新增/编辑的回调 | `(node?: ITreeNode, type?: InputStatus) => void` | - | +| onChange | 触发新增/编辑的回调 | `(node?: ITreeNode, inputMode?: InputMode) => void` | - | :::info 其余参数继承 antd4.x 的 `Omit`
@@ -47,7 +48,7 @@ demo: | 参数 | 说明 | 类型 | 默认值 | | --------- | -------------- | ----------------------- | ------ | -| type | 节点输入框内容 | `add \| edit \| append` | - | +| inputMode | 节点输入框类型 | `add \| edit \| append` | - | | editable | 是否可编辑 | `boolean` | `true` | | deletable | 是否可删除 | `boolean` | `true` | | addable | 是否可增加 | `boolean` | `true` | diff --git a/src/catalogue/index.scss b/src/catalogue/index.scss index ea141ed0f..62174e4c0 100644 --- a/src/catalogue/index.scss +++ b/src/catalogue/index.scss @@ -2,27 +2,20 @@ position: relative; height: 100%; width: 100%; - padding-top: 12px; display: flex; flex-direction: column; background-color: #FFF; - &--show { - opacity: 1; - transition: 0.2s all ease-in; - } - &--hide { - opacity: 0; - width: 0; - transition: 0.2s all ease-in; - } &__header { - padding: 0 12px; + padding: 12px 12px 0; + } + .dt-catalogue__tree { + padding: 2px 0 24px; } &__tree { + height: 100%; width: 100%; flex: 1; min-height: 0; - padding: 2px 0 24px; overflow-y: auto; .ant-spin-nested-loading, .ant-spin-container { @@ -103,7 +96,7 @@ .ant-tree { height: 100%; .ant-tree-treenode-leaf-last .ant-tree-switcher-leaf-line::before { - height: 16px !important; + height: 100% !important; } .ant-tree-node-content-wrapper.ant-tree-node-selected { background: none; @@ -137,18 +130,4 @@ } } } - &__icon { - display: inline-flex; - align-items: center; - color: inherit; - font-style: normal; - font-size: 16px; - line-height: 0; - text-align: center; - text-transform: none; - vertical-align: -0.125em; - text-rendering: optimizelegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } } diff --git a/src/catalogue/index.tsx b/src/catalogue/index.tsx index 6852b635a..dcaf017b9 100644 --- a/src/catalogue/index.tsx +++ b/src/catalogue/index.tsx @@ -10,4 +10,7 @@ interface CatalogueInterface extends OriginInputType { const WrapperCatalogue = Catalogue; (WrapperCatalogue as CatalogueInterface).Tree = CatalogueTree; +export * from './components/catalogue'; +export * from './components/tree'; + export default WrapperCatalogue as CatalogueInterface; diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx index 35843e091..4b1da8903 100644 --- a/src/catalogue/useTreeData.tsx +++ b/src/catalogue/useTreeData.tsx @@ -1,19 +1,22 @@ import { useState } from 'react'; -import { DataNode, TreeProps } from 'antd/lib/tree'; +import { DataNode } from 'antd/lib/tree'; import { cloneDeep } from 'lodash'; -export interface ITreeNode extends Omit { - type?: InputStatus; +import { ICatalogue } from './components/catalogue'; + +export interface ITreeNode extends Omit { + inputMode?: InputMode; /** 是否可编辑 */ editable?: boolean; /** 是否可删除 */ deletable?: boolean; /** 是否可增加 */ addable?: boolean; + title?: React.ReactNode; children?: ITreeNode[]; } -export enum InputStatus { +export enum InputMode { Add = 'add', Edit = 'edit', Append = 'append', @@ -22,15 +25,15 @@ export enum InputStatus { export const useTreeData = (): { data: ITreeNode[]; loading: boolean; - expandedKeys: TreeProps['expandedKeys']; + expandedKeys: ICatalogue['expandedKeys']; initData: (treeData: ITreeNode[]) => void; - onChange: (node?: ITreeNode, type?: InputStatus) => void; + onChange: (node?: ITreeNode, inputMode?: InputMode) => void; setLoading: React.Dispatch>; - setExpandedKeys: React.Dispatch>; + setExpandedKeys: React.Dispatch>; } => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); - const [expandedKeys, setExpandedKeys] = useState([]); + const [expandedKeys, setExpandedKeys] = useState([]); const initData = (treeData: ITreeNode[]) => { setData(treeData); @@ -38,10 +41,10 @@ export const useTreeData = (): { const processData = (data: ITreeNode[]): ITreeNode[] => { function traverse(item: ITreeNode): ITreeNode | null { - if (item.type === InputStatus.Edit) { - item.type = undefined; + if (item.inputMode === InputMode.Edit) { + item.inputMode = undefined; } - if (item.type === InputStatus.Add || item.type === InputStatus.Append) { + if (item.inputMode === InputMode.Add || item.inputMode === InputMode.Append) { return null; } if (Array.isArray(item.children)) { @@ -52,21 +55,21 @@ export const useTreeData = (): { return data.map(traverse).filter(Boolean) as ITreeNode[]; }; - const onChange = (node?: ITreeNode, type?: InputStatus) => { + const onChange = (node?: ITreeNode, inputMode?: InputMode) => { const newData = cloneDeep(data); // 做 onBlur 清除数据 - if (!node && !type) { + if (!node && !inputMode) { return setData(processData(newData)); } - if (!node && type === InputStatus.Add) - return setData([{ key: '', type: InputStatus.Add }, ...data]); - if (node && type === InputStatus.Append) { + if (!node && inputMode === InputMode.Add) + return setData([{ key: '', inputMode: InputMode.Add }, ...data]); + if (node && inputMode === InputMode.Append) { const newExpandedKeys = expandedKeys ? [...expandedKeys] : []; loopTree(newData, node.key, (item: ITreeNode) => { const { children } = item; item['children'] = [ ...(children || []), - { key: node.key + 'new', type: InputStatus.Append }, + { key: node.key + 'new', inputMode: InputMode.Append }, ]; }); setData(newData); @@ -75,9 +78,9 @@ export const useTreeData = (): { } return setExpandedKeys(newExpandedKeys); } - if (node && type === InputStatus.Edit) { + if (node && inputMode === InputMode.Edit) { loopTree(newData, node.key, (item: ITreeNode) => { - item.type = InputStatus.Edit; + item.inputMode = InputMode.Edit; }); setData(newData); } diff --git a/src/catalogue/utils.tsx b/src/catalogue/utils.tsx index aedc1518e..23c8f2133 100644 --- a/src/catalogue/utils.tsx +++ b/src/catalogue/utils.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { DataNode, TreeProps } from 'antd/lib/tree'; +import { ICatalogue } from './components/catalogue'; import { FolderIcon, FolderOpenedIcon } from './components/icon'; +import { ITreeNode } from './useTreeData'; -export const getIcon: DataNode['icon'] = ({ selected, expanded }) => { +export const getIcon: ITreeNode['icon'] = ({ selected, expanded }) => { const styles: React.CSSProperties = { fontSize: 16, color: '#1D78FF', @@ -16,12 +17,12 @@ export const getIcon: DataNode['icon'] = ({ selected, expanded }) => { /** * @description 轮询 Tree 数据,赋值搜索标识和leafIcon */ -export const loopTree = (data: TreeProps['treeData']): TreeProps['treeData'] => { +export const loopTree = (data: ICatalogue['treeData']): ICatalogue['treeData'] => { return data?.map((item) => { if (item.children) { return { - ...item, icon: getIcon, + ...item, children: loopTree(item.children), }; } From 2e730f56a57472630b3c95dbc9f08ecaf0fd6011 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Sat, 14 Dec 2024 22:03:33 +0800 Subject: [PATCH 6/9] feat(catalogue): support tabs in cataligue --- src/catalogue/components/catalogue.tsx | 127 +++++++++++++++++-------- src/catalogue/components/icon.tsx | 44 +++++++++ src/catalogue/demos/tabs.tsx | 89 +++++++++++++++++ src/catalogue/index.md | 1 + src/catalogue/index.scss | 30 ++++++ src/catalogue/useTreeData.tsx | 8 +- 6 files changed, 257 insertions(+), 42 deletions(-) create mode 100644 src/catalogue/demos/tabs.tsx diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index a1783b5f7..cb4ddeacb 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -1,13 +1,22 @@ -import React from 'react'; -import { Dropdown, DropdownProps, Form, Input } from 'antd'; +import React, { useState } from 'react'; +import { Dropdown, DropdownProps, Form, Input, Tabs } from 'antd'; import { BlockHeader } from 'dt-react-component'; import { IBlockHeaderProps } from 'dt-react-component/blockHeader'; import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; -import { CatalogIcon, DragIcon, EllipsisIcon } from './icon'; +import { CatalogIcon, CloseIcon, DragIcon, EllipsisIcon, SearchIcon } from './icon'; import CatalogueTree, { ICatalogueTree } from './tree'; -export interface ICatalogue +interface Tab { + readonly key: string; + readonly title: React.ReactNode; +} + +type readOnlyTab = readonly Tab[]; + +type TabKey = T[number]['key']; + +interface NormalCatalogueProps extends Pick, ICatalogueTree { showSearch?: boolean; @@ -19,23 +28,43 @@ export interface ICatalogue onSearch?: (value: string) => void; onSave?: (data: ITreeNode, value: string) => Promise; } +interface TabsCatalogueProps extends NormalCatalogueProps { + tabList?: T; + activeTabKey?: TabKey; + defaultTabKey?: TabKey; + onTabChange?: (key: TabKey) => void; +} + +export type CatalogueProps = + | TabsCatalogueProps + | NormalCatalogueProps; + +function isTabMode( + props: CatalogueProps +): props is TabsCatalogueProps { + return 'tabList' in props; +} + +const Catalogue = (props: CatalogueProps) => { + const { + title, + addonBefore = , + tooltip = false, + showSearch = false, + placeholder = '搜索目录名称', + addonAfter, + edit = true, + treeData, + draggable, + overlay, + onChange, + onSearch, + onSave, + ...rest + } = props; + + const [tabSearch, setTabSearch] = useState(false); -const Catalogue = ({ - title, - addonBefore = , - tooltip = false, - showSearch = false, - placeholder = '搜索目录名称', - addonAfter, - edit = true, - treeData, - draggable, - overlay, - onChange, - onSearch, - onSave, - ...rest -}: ICatalogue) => { const [form] = Form.useForm(); const loopTree = (data: ITreeNode[]): ITreeNode[] => { @@ -64,25 +93,48 @@ const Catalogue = ({ const renderHeader = () => { if (!title) return null; return ( - +
+ +
); }; const renderSearch = () => { - if (!showSearch) return null; + if (!showSearch || (isTabMode(props) && !tabSearch)) return null; return ( - +
+ + {isTabMode(props) && ( + setTabSearch(false)} /> + )} +
+ ); + }; + + const renderTab = () => { + if (!isTabMode(props) || tabSearch) return null; + const { activeTabKey, tabList, onTabChange } = props; + return ( + setTabSearch(true)} /> + } + activeKey={activeTabKey} + onChange={onTabChange} + > + {tabList?.map((tab: { key: string; title: React.ReactNode }) => ( + + ))} + ); }; @@ -182,10 +234,9 @@ const Catalogue = ({ return (
-
- {renderHeader()} - {renderSearch()} -
+ {renderHeader()} + {renderSearch()} + {renderTab()} + + + + + ); +}; + +const CloseIcon = function ({ className, ...rest }: IIcon) { + return ( + + + + + + ); +}; + export { CatalogIcon, + CloseIcon, DeleteIcon, DownTriangleIcon, DragIcon, @@ -287,4 +330,5 @@ export { MenuUnFoldIcon, PlusCircleIcon, PlusSquareIcon, + SearchIcon, }; diff --git a/src/catalogue/demos/tabs.tsx b/src/catalogue/demos/tabs.tsx new file mode 100644 index 000000000..ce1e1e6aa --- /dev/null +++ b/src/catalogue/demos/tabs.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from 'react'; +import { Catalogue, EllipsisText } from 'dt-react-component'; + +import { useTreeData } from '../useTreeData'; + +enum TreeType { + Api = 'api', + Index = 'index', +} + +const DEFAULT_DATA = (key: TreeType) => [ + { + title: `0-0-${key}`, + key: '0-0', + children: [ + { + title: ( + + ), + key: '0-0-0', + children: [ + { title: '0-0-0-0', key: '0-0-0-0' }, + { title: '0-0-0-1', key: '0-0-0-1' }, + { title: '0-0-0-2', key: '0-0-0-2' }, + ], + }, + { + title: '0-0-1', + key: '0-0-1', + children: [ + { title: '0-0-1-0', key: '0-0-1-0' }, + { title: '0-0-1-1', key: '0-0-1-1' }, + { title: '0-0-1-2', key: '0-0-1-2' }, + ], + }, + { + title: '0-0-2', + key: '0-0-2', + }, + ], + }, + { + title: `0-1-${key}`, + key: '0-1', + children: [ + { title: '0-1-0-0', key: '0-1-0-0' }, + { title: '0-1-0-1', key: '0-1-0-1' }, + { title: '0-1-0-2', key: '0-1-0-2' }, + ], + }, + { + title: `0-2-${key}`, + key: '0-2', + }, +]; + +export default () => { + const treeData = useTreeData(); + const [activeKey, setActiveKey] = useState(TreeType.Index); + + useEffect(() => { + treeData.initData(DEFAULT_DATA(activeKey)); + }, [activeKey]); + + return ( +
+ setActiveKey(key)} + expandedKeys={treeData.expandedKeys} + onExpand={treeData.setExpandedKeys} + /> +
+ ); +}; diff --git a/src/catalogue/index.md b/src/catalogue/index.md index d5f7fc639..3399eccb7 100644 --- a/src/catalogue/index.md +++ b/src/catalogue/index.md @@ -21,6 +21,7 @@ demo: 可操作的目录树 可拖拽的目录树 配置操作项的目录树 +配置操作项的目录树 ## API diff --git a/src/catalogue/index.scss b/src/catalogue/index.scss index 62174e4c0..d7a923336 100644 --- a/src/catalogue/index.scss +++ b/src/catalogue/index.scss @@ -8,6 +8,17 @@ &__header { padding: 12px 12px 0; } + &__search { + display: flex; + padding: 0 12px 6px; + align-items: center; + .close { + font-size: 16px; + color: #B1B4C5; + cursor: pointer; + margin-left: 46px; + } + } .dt-catalogue__tree { padding: 2px 0 24px; } @@ -130,4 +141,23 @@ } } } + &__tabs { + margin-bottom: 6px; + height: fit-content; + &.ant-tabs-top > .ant-tabs-nav { + margin: 0; + padding: 0 16px; + } + .ant-tabs-extra-content { + .search { + font-size: 16px; + color: #B1B4C5; + cursor: pointer; + } + } + } + &__icon { + display: flex; + align-items: center; + } } diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx index 4b1da8903..a752e43f9 100644 --- a/src/catalogue/useTreeData.tsx +++ b/src/catalogue/useTreeData.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { DataNode } from 'antd/lib/tree'; import { cloneDeep } from 'lodash'; -import { ICatalogue } from './components/catalogue'; +import { CatalogueProps } from './components/catalogue'; export interface ITreeNode extends Omit { inputMode?: InputMode; @@ -25,15 +25,15 @@ export enum InputMode { export const useTreeData = (): { data: ITreeNode[]; loading: boolean; - expandedKeys: ICatalogue['expandedKeys']; + expandedKeys: CatalogueProps['expandedKeys']; initData: (treeData: ITreeNode[]) => void; onChange: (node?: ITreeNode, inputMode?: InputMode) => void; setLoading: React.Dispatch>; - setExpandedKeys: React.Dispatch>; + setExpandedKeys: React.Dispatch>; } => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); - const [expandedKeys, setExpandedKeys] = useState([]); + const [expandedKeys, setExpandedKeys] = useState([]); const initData = (treeData: ITreeNode[]) => { setData(treeData); From 98ac9d1a3f88bc032452f930c3333ccee8450088 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Sun, 22 Dec 2024 18:07:12 +0800 Subject: [PATCH 7/9] feat(catalogue): change catalogue and add some utils to CRUD treedata --- src/catalogue/components/catalogue.tsx | 150 ++++++---------- src/catalogue/components/tree.tsx | 19 +- src/catalogue/demos/basic.tsx | 4 +- src/catalogue/demos/config.tsx | 169 ++++++++++-------- src/catalogue/demos/drag.tsx | 167 ++++++++++-------- src/catalogue/demos/operator.tsx | 233 ++++++++++++++++--------- src/catalogue/demos/simple.tsx | 2 +- src/catalogue/demos/tabs.tsx | 4 +- src/catalogue/useTreeData.tsx | 220 ++++++++++++++--------- src/catalogue/utils.tsx | 6 +- 10 files changed, 544 insertions(+), 430 deletions(-) diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index cb4ddeacb..d113f7e9c 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -3,7 +3,7 @@ import { Dropdown, DropdownProps, Form, Input, Tabs } from 'antd'; import { BlockHeader } from 'dt-react-component'; import { IBlockHeaderProps } from 'dt-react-component/blockHeader'; -import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; +import { ITreeNode } from '../useTreeData'; import { CatalogIcon, CloseIcon, DragIcon, EllipsisIcon, SearchIcon } from './icon'; import CatalogueTree, { ICatalogueTree } from './tree'; @@ -16,36 +16,39 @@ type readOnlyTab = readonly Tab[]; type TabKey = T[number]['key']; -interface NormalCatalogueProps +interface NormalCatalogueProps = {}> extends Pick, - ICatalogueTree { + ICatalogueTree { showSearch?: boolean; edit?: boolean; placeholder?: string; loading?: boolean; - onChange?: ReturnType['onChange']; - overlay?: (item: ITreeNode) => DropdownProps['overlay']; + onCancelSave?: (item: ITreeNode) => void; + overlay?: (item: ITreeNode) => DropdownProps['overlay']; onSearch?: (value: string) => void; - onSave?: (data: ITreeNode, value: string) => Promise; + onSave?: (data: ITreeNode, value: string) => Promise; } -interface TabsCatalogueProps extends NormalCatalogueProps { +interface TabsCatalogueProps, T extends readOnlyTab> + extends NormalCatalogueProps { tabList?: T; activeTabKey?: TabKey; defaultTabKey?: TabKey; onTabChange?: (key: TabKey) => void; } -export type CatalogueProps = - | TabsCatalogueProps - | NormalCatalogueProps; +export type CatalogueProps = {}, T extends readOnlyTab = any> = + | TabsCatalogueProps + | NormalCatalogueProps; -function isTabMode( - props: CatalogueProps -): props is TabsCatalogueProps { +function isTabMode = {}, T extends readOnlyTab = any>( + props: CatalogueProps +): props is TabsCatalogueProps { return 'tabList' in props; } -const Catalogue = (props: CatalogueProps) => { +const Catalogue = = {}, T extends readOnlyTab = any>( + props: CatalogueProps +) => { const { title, addonBefore = , @@ -56,10 +59,11 @@ const Catalogue = (props: CatalogueProps) => { edit = true, treeData, draggable, + titleRender, overlay, - onChange, onSearch, onSave, + onCancelSave, ...rest } = props; @@ -67,27 +71,33 @@ const Catalogue = (props: CatalogueProps) => { const [form] = Form.useForm(); - const loopTree = (data: ITreeNode[]): ITreeNode[] => { - return data?.map((item) => { - const newItem = { - ...item, - editable: item?.editable === undefined ? true : item?.editable, - addable: item?.addable === undefined ? true : item?.addable, - deletable: item?.deletable === undefined ? true : item?.deletable, - }; - if (item.children) { - return { - ...newItem, - title: renderTitle(newItem), - children: loopTree(item.children), - }; - } - return { - ...newItem, - title: renderTitle(newItem), - children: undefined, - }; - }); + const defaultTitleRender = (item: ITreeNode) => { + if (item.edit) { + return ( + + + form.setFields([{ name: 'catalog_input', errors: [] }])} + onClick={(e) => e.stopPropagation()} + onBlur={({ target }) => handleInputSubmit(item, target.value)} + onPressEnter={({ target }) => + handleInputSubmit(item, (target as any).value) + } + /> + + + ); + } + return ( +
+
{item.title}
+ {edit && renderNodeHover(item)} +
+ ); }; const renderHeader = () => { @@ -138,75 +148,16 @@ const Catalogue = (props: CatalogueProps) => { ); }; - const handleInputSubmit = (item: ITreeNode, value: string) => { + const handleInputSubmit = (item: ITreeNode, value: string) => { if (!value) { - return onChange?.(undefined, undefined); - } - // item 为当前编辑的数据,对于 Append 的情况需要传入父级的 key - if (item.inputMode === InputMode.Append) { - const findAppendParents = (data: ITreeNode[], item: ITreeNode): ITreeNode | null => { - let result: ITreeNode | null = null; - function traverse(node: ITreeNode, parent: ITreeNode | null): void { - if (node.inputMode === 'append' && node.key === item.key && parent) { - result = parent; - } - if (Array.isArray(node.children)) { - node.children.forEach((child) => traverse(child, node)); - } - } - data.forEach((item) => traverse(item, null)); - return result; - }; - const parentItem = findAppendParents(treeData, item); - return ( - parentItem && - onSave?.({ ...parentItem, inputMode: InputMode.Append }, value).then((msg) => { - form.setFields([{ name: 'catalog_input', errors: msg ? [msg] : [] }]); - }) - ); + return onCancelSave?.(item); } onSave?.(item, value).then((msg) => { form.setFields([{ name: 'catalog_input', errors: msg ? [msg] : [] }]); }); }; - const renderInput = (item: ITreeNode) => { - return ( -
-
- - form.setFields([{ name: 'catalog_input', errors: [] }])} - onClick={(e) => e.stopPropagation()} - onBlur={({ target }) => handleInputSubmit(item, target.value)} - onPressEnter={({ target }) => - handleInputSubmit(item, (target as any).value) - } - /> - -
-
- ); - }; - - const renderTitle = (item: ITreeNode) => { - if (item.inputMode) { - return renderInput(item); - } - return ( -
-
{item.title}
- {edit && renderNodeHover(item)} -
- ); - }; - - const renderNodeHover = (item: ITreeNode) => { + const renderNodeHover = (item: ITreeNode) => { return (
(props: CatalogueProps) => { {renderSearch()} {renderTab()}
diff --git a/src/catalogue/components/tree.tsx b/src/catalogue/components/tree.tsx index f03a30a50..cd2a2beec 100644 --- a/src/catalogue/components/tree.tsx +++ b/src/catalogue/components/tree.tsx @@ -6,25 +6,32 @@ import { ITreeNode } from '../useTreeData'; import { loopTree } from '../utils'; import { DownTriangleIcon } from './icon'; -export interface ICatalogueTree - extends Omit { +export interface ICatalogueTree = {}> + extends Omit { loading?: boolean; - treeData: ITreeNode[]; + treeData: ITreeNode[]; + titleRender?: TreeProps>['titleRender']; } -const CatalogueTree = ({ treeData = [], loading = false, ...rest }: ICatalogueTree) => { - const renderTreeData = useMemo(() => loopTree(treeData) || [], [treeData]); +const CatalogueTree = = {}>({ + treeData = [], + loading = false, + titleRender, + ...rest +}: ICatalogueTree) => { + const renderTreeData = useMemo(() => loopTree(treeData) || [], [treeData]); if (!renderTreeData.length) return ; return (
- > showLine={{ showLeafIcon: false }} switcherIcon={} showIcon treeData={renderTreeData} + titleRender={titleRender} {...rest} /> diff --git a/src/catalogue/demos/basic.tsx b/src/catalogue/demos/basic.tsx index b1150d783..e09058c0a 100644 --- a/src/catalogue/demos/basic.tsx +++ b/src/catalogue/demos/basic.tsx @@ -56,7 +56,7 @@ export default () => { const treeData = useTreeData(); useEffect(() => { - treeData.initData(DEFAULT_DATA); + treeData.onChange(DEFAULT_DATA); }, []); return ( @@ -68,7 +68,7 @@ export default () => { treeData={treeData.data} edit={false} expandedKeys={treeData.expandedKeys} - onExpand={treeData.setExpandedKeys} + onExpand={treeData.onExpand} />
); diff --git a/src/catalogue/demos/config.tsx b/src/catalogue/demos/config.tsx index c564f73cd..97ab083b7 100644 --- a/src/catalogue/demos/config.tsx +++ b/src/catalogue/demos/config.tsx @@ -4,11 +4,20 @@ import { Catalogue, EllipsisText } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; -import { ICatalogue } from '../components/catalogue'; +import { CatalogueProps } from '../components/catalogue'; import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; -import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; +import { + appendNodeByKey, + findNodeByKey, + findParentNodeByKey, + ITreeNode, + removeEditNode, + removeNodeByKey, + updateTreeNodeEdit, + useTreeData, +} from '../useTreeData'; -const DEFAULT_DATA: ITreeNode[] = [ +const DEFAULT_DATA: ITreeNode[] = [ { title: '不可编辑的节点', editable: false, @@ -63,39 +72,48 @@ const DEFAULT_DATA: ITreeNode[] = [ }, ]; +interface IData { + edit?: boolean; + addable?: boolean; + deletable?: boolean; + editable?: boolean; +} + export default () => { - const treeData = useTreeData(); + const treeData = useTreeData(); - const findNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode | null => { - for (const node of data) { - if (node.key === targetKey) { - return node; - } - if (node.children) { - const result = findNodeByKey(node.children, targetKey); - if (result) return result; - } + const handleEdit = (key: ITreeNode['key']) => { + const data = updateTreeNodeEdit(treeData.data, key); + treeData.onChange(data); + }; + + const handleAdd = (key: ITreeNode['key']) => { + const newExpandedKeys = treeData.expandedKeys ? [...treeData.expandedKeys] : []; + const data = appendNodeByKey(treeData.data, key); + if (!newExpandedKeys?.includes(key)) { + newExpandedKeys.push(key); } - return null; + treeData.onExpand(newExpandedKeys); + treeData.onChange(data); }; const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); - if (data.inputMode === InputMode.Add) { + if (!data.key) { newData.push({ title: value, key: shortid(), }); newData.shift(); - treeData.initData(newData); + treeData.onChange(newData); return; } - if (data.inputMode === InputMode.Append) { - let node = findNodeByKey(newData, data.key); + if ((data.key as string).startsWith('new')) { + let node = findParentNodeByKey(newData, data.key); if (!node) return; if (node.children) { const newChildren = node.children - .filter((item) => !item.inputMode) + .filter((item) => !item.edit) .concat({ title: value, key: shortid() }); node.children = newChildren; } else { @@ -109,38 +127,29 @@ export default () => { ], }; } - treeData.initData(newData); + treeData.onChange(newData); return; } const node = findNodeByKey(newData, data.key); if (node) { node.title = value; - node.inputMode = undefined; + node.edit = false; } - treeData.initData(newData); + treeData.onChange(newData); }; - const removeNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode[] => { - return data - .filter((node) => node.key !== targetKey) - .map((node) => { - if (node.children) { - return { - ...node, - children: removeNodeByKey(node.children, targetKey), - }; - } - return node; - }); + const handleCancelSave = () => { + const newData = removeEditNode(treeData.data); + treeData.onChange(newData); }; const handleDelete = (data: ITreeNode) => { let newData = cloneDeep(treeData.data); newData = removeNodeByKey(newData, data.key); - treeData.initData(newData); + treeData.onChange(newData); }; - const handleDrop: ICatalogue['onDrop'] = (info) => { + const handleDrop: CatalogueProps['onDrop'] = (info) => { const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); @@ -190,11 +199,11 @@ export default () => { ar.splice(i! + 1, 0, dragObj!); } } - treeData.initData(data); + treeData.onChange(data); }; useEffect(() => { - treeData.initData(DEFAULT_DATA); + treeData.onChange(DEFAULT_DATA); }, []); return ( @@ -204,54 +213,60 @@ export default () => { addonAfter={ treeData.onChange(undefined, InputMode.Add)} + onClick={() => + treeData.onChange([ + ...treeData.data, + { edit: true, title: '', key: '' }, + ]) + } /> } title="标签目录" showSearch draggable - overlay={(item) => ( - { - domEvent.stopPropagation(); - }} - > - - item.addable && treeData.onChange(item, InputMode.Append) - } - > - - 新建目录 - - item.editable && treeData.onChange(item, InputMode.Edit)} - > - - 编辑 - - item.deletable && handleDelete(item)} + overlay={(item) => { + const { addable = true, editable = true, deletable = true } = item; + return ( + { + domEvent.stopPropagation(); + }} > - - 删除 - - - )} + addable && handleAdd(item.key)} + > + + 新建目录 + + editable && handleEdit(item.key)} + > + + 编辑 + + deletable && handleDelete(item)} + > + + 删除 + + + ); + }} treeData={treeData.data} expandedKeys={treeData.expandedKeys} - onExpand={treeData.setExpandedKeys} - onDragEnter={({ expandedKeys }) => treeData.setExpandedKeys(expandedKeys)} - onChange={treeData.onChange} + onExpand={treeData.onExpand} + onDragEnter={({ expandedKeys }) => treeData.onExpand(expandedKeys)} onSave={handleSave} + onCancelSave={handleCancelSave} onDrop={handleDrop} />
diff --git a/src/catalogue/demos/drag.tsx b/src/catalogue/demos/drag.tsx index 25a14b34e..2828237f2 100644 --- a/src/catalogue/demos/drag.tsx +++ b/src/catalogue/demos/drag.tsx @@ -4,9 +4,25 @@ import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; -import { ICatalogue } from '../components/catalogue'; +import { CatalogueProps } from '../components/catalogue'; import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; -import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; +import { + appendNodeByKey, + findNodeByKey, + findParentNodeByKey, + ITreeNode, + removeEditNode, + removeNodeByKey, + updateTreeNodeEdit, + useTreeData, +} from '../useTreeData'; + +interface IData { + edit?: boolean; + addable?: boolean; + deletable?: boolean; + editable?: boolean; +} const DEFAULT_DATA = [ { @@ -53,38 +69,40 @@ const DEFAULT_DATA = [ ]; export default () => { - const treeData = useTreeData(); + const treeData = useTreeData(); - const findNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode | null => { - for (const node of data) { - if (node.key === targetKey) { - return node; - } - if (node.children) { - const result = findNodeByKey(node.children, targetKey); - if (result) return result; - } + const handleEdit = (key: ITreeNode['key']) => { + const data = updateTreeNodeEdit(treeData.data, key); + treeData.onChange(data); + }; + + const handleAdd = (key: ITreeNode['key']) => { + const newExpandedKeys = treeData.expandedKeys ? [...treeData.expandedKeys] : []; + const data = appendNodeByKey(treeData.data, key); + if (!newExpandedKeys?.includes(key)) { + newExpandedKeys.push(key); } - return null; + treeData.onExpand(newExpandedKeys); + treeData.onChange(data); }; const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); - if (data.inputMode === InputMode.Add) { + if (!data.key) { newData.push({ title: value, key: shortid(), }); newData.shift(); - treeData.initData(newData); + treeData.onChange(newData); return; } - if (data.inputMode === InputMode.Append) { - let node = findNodeByKey(newData, data.key); + if ((data.key as string).startsWith('new')) { + let node = findParentNodeByKey(newData, data.key); if (!node) return; if (node.children) { const newChildren = node.children - .filter((item) => !item.inputMode) + .filter((item) => !item.edit) .concat({ title: value, key: shortid() }); node.children = newChildren; } else { @@ -98,38 +116,29 @@ export default () => { ], }; } - treeData.initData(newData); + treeData.onChange(newData); return; } const node = findNodeByKey(newData, data.key); if (node) { node.title = value; - node.inputMode = undefined; + node.edit = false; } - treeData.initData(newData); + treeData.onChange(newData); }; - const removeNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode[] => { - return data - .filter((node) => node.key !== targetKey) - .map((node) => { - if (node.children) { - return { - ...node, - children: removeNodeByKey(node.children, targetKey), - }; - } - return node; - }); + const handleCancelSave = () => { + const newData = removeEditNode(treeData.data); + treeData.onChange(newData); }; const handleDelete = (data: ITreeNode) => { let newData = cloneDeep(treeData.data); newData = removeNodeByKey(newData, data.key); - treeData.initData(newData); + treeData.onChange(newData); }; - const handleDrop: ICatalogue['onDrop'] = (info) => { + const handleDrop: CatalogueProps['onDrop'] = (info) => { const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); @@ -179,11 +188,11 @@ export default () => { ar.splice(i! + 1, 0, dragObj!); } } - treeData.initData(data); + treeData.onChange(data); }; useEffect(() => { - treeData.initData(DEFAULT_DATA); + treeData.onChange(DEFAULT_DATA); }, []); return ( @@ -193,54 +202,60 @@ export default () => { addonAfter={ treeData.onChange(undefined, InputMode.Add)} + onClick={() => + treeData.onChange([ + ...treeData.data, + { edit: true, title: '', key: '' }, + ]) + } /> } title="标签目录" showSearch draggable - overlay={(item) => ( - { - domEvent.stopPropagation(); - }} - > - - item.addable && treeData.onChange(item, InputMode.Append) - } - > - - 新建目录 - - item.editable && treeData.onChange(item, InputMode.Edit)} - > - - 编辑 - - item.deletable && handleDelete(item)} + overlay={(item) => { + const { addable = true, editable = true, deletable = true } = item; + return ( + { + domEvent.stopPropagation(); + }} > - - 删除 - - - )} + addable && handleAdd(item.key)} + > + + 新建目录 + + editable && handleEdit(item.key)} + > + + 编辑 + + deletable && handleDelete(item)} + > + + 删除 + + + ); + }} treeData={treeData.data} expandedKeys={treeData.expandedKeys} - onExpand={treeData.setExpandedKeys} - onDragEnter={({ expandedKeys }) => treeData.setExpandedKeys(expandedKeys)} - onChange={treeData.onChange} + onExpand={treeData.onExpand} + onDragEnter={({ expandedKeys }) => treeData.onExpand(expandedKeys)} onSave={handleSave} + onCancelSave={handleCancelSave} onDrop={handleDrop} /> diff --git a/src/catalogue/demos/operator.tsx b/src/catalogue/demos/operator.tsx index 09e769f59..096d35cdf 100644 --- a/src/catalogue/demos/operator.tsx +++ b/src/catalogue/demos/operator.tsx @@ -5,85 +5,157 @@ import { cloneDeep } from 'lodash'; import shortid from 'shortid'; import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; -import { InputMode, ITreeNode, useTreeData } from '../useTreeData'; +import { + appendNodeByKey, + findNodeByKey, + findParentNodeByKey, + ITreeNode, + removeEditNode, + removeNodeByKey, + updateTreeNodeEdit, + useTreeData, +} from '../useTreeData'; const DEFAULT_DATA = [ { title: '0-0', key: '0-0', + addable: true, + deletable: true, + editable: true, children: [ { title: '0-0-0', key: '0-0-0', + addable: true, + deletable: true, + editable: true, children: [ - { title: '0-0-0-0', key: '0-0-0-0' }, - { title: '0-0-0-1', key: '0-0-0-1' }, - { title: '0-0-0-2', key: '0-0-0-2' }, + { + title: '0-0-0-0', + key: '0-0-0-0', + addable: true, + deletable: true, + editable: true, + }, + { + title: '0-0-0-1', + key: '0-0-0-1', + addable: true, + deletable: true, + editable: true, + }, + { + title: '0-0-0-2', + key: '0-0-0-2', + addable: true, + deletable: true, + editable: true, + }, ], }, { title: '0-0-1', key: '0-0-1', children: [ - { title: '0-0-1-0', key: '0-0-1-0' }, - { title: '0-0-1-1', key: '0-0-1-1' }, - { title: '0-0-1-2', key: '0-0-1-2' }, + { + title: '0-0-1-0', + key: '0-0-1-0', + addable: true, + deletable: true, + editable: true, + }, + { + title: '0-0-1-1', + key: '0-0-1-1', + addable: true, + deletable: true, + editable: true, + }, + { + title: '0-0-1-2', + key: '0-0-1-2', + addable: true, + deletable: true, + editable: true, + }, ], + addable: true, + deletable: true, + editable: true, }, { title: '0-0-2', key: '0-0-2', + addable: true, + deletable: true, + editable: true, }, ], }, { title: '0-1', key: '0-1', + addable: true, + deletable: true, + editable: true, children: [ - { title: '0-1-0-0', key: '0-1-0-0' }, - { title: '0-1-0-1', key: '0-1-0-1' }, - { title: '0-1-0-2', key: '0-1-0-2' }, + { title: '0-1-0-0', key: '0-1-0-0', addable: true, deletable: true, editable: true }, + { title: '0-1-0-1', key: '0-1-0-1', addable: true, deletable: true, editable: true }, + { title: '0-1-0-2', key: '0-1-0-2', addable: true, deletable: true, editable: true }, ], }, { title: '0-2', key: '0-2', + addable: true, + deletable: true, + editable: true, }, ]; +interface IData { + edit?: boolean; + addable?: boolean; + deletable?: boolean; + editable?: boolean; +} + export default () => { - const treeData = useTreeData(); + const treeData = useTreeData(); - const findNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode | null => { - for (const node of data) { - if (node.key === targetKey) { - return node; - } - if (node.children) { - const result = findNodeByKey(node.children, targetKey); - if (result) return result; - } + const handleEdit = (key: ITreeNode['key']) => { + const data = updateTreeNodeEdit(treeData.data, key); + treeData.onChange(data); + }; + + const handleAdd = (key: ITreeNode['key']) => { + const newExpandedKeys = treeData.expandedKeys ? [...treeData.expandedKeys] : []; + const data = appendNodeByKey(treeData.data, key); + if (!newExpandedKeys?.includes(key)) { + newExpandedKeys.push(key); } - return null; + treeData.onExpand(newExpandedKeys); + treeData.onChange(data); }; const handleSave = async (data: ITreeNode, value: string) => { const newData = cloneDeep(treeData.data); - if (data.inputMode === InputMode.Add) { + if (!data.key) { newData.push({ title: value, key: shortid(), }); newData.shift(); - treeData.initData(newData); + treeData.onChange(newData); return; } - if (data.inputMode === InputMode.Append) { - let node = findNodeByKey(newData, data.key); + if ((data.key as string).startsWith('new')) { + let node = findParentNodeByKey(newData, data.key); if (!node) return; if (node.children) { const newChildren = node.children - .filter((item) => !item.inputMode) + .filter((item) => !item.edit) .concat({ title: value, key: shortid() }); node.children = newChildren; } else { @@ -97,94 +169,91 @@ export default () => { ], }; } - treeData.initData(newData); + treeData.onChange(newData); return; } const node = findNodeByKey(newData, data.key); if (node) { node.title = value; - node.inputMode = undefined; + node.edit = false; } - treeData.initData(newData); + treeData.onChange(newData); }; - const removeNodeByKey = (data: ITreeNode[], targetKey: ITreeNode['key']): ITreeNode[] => { - return data - .filter((node) => node.key !== targetKey) - .map((node) => { - if (node.children) { - return { - ...node, - children: removeNodeByKey(node.children, targetKey), - }; - } - return node; - }); + const handleCancelSave = () => { + const newData = removeEditNode(treeData.data); + treeData.onChange(newData); }; const handleDelete = (data: ITreeNode) => { let newData = cloneDeep(treeData.data); newData = removeNodeByKey(newData, data.key); - treeData.initData(newData); + treeData.onChange(newData); }; useEffect(() => { - treeData.initData(DEFAULT_DATA); + treeData.onChange(DEFAULT_DATA); }, []); return (
- tooltip="嘿嘿,这是tooltip" addonAfter={ treeData.onChange(undefined, InputMode.Add)} + onClick={() => + treeData.onChange([ + ...treeData.data, + { edit: true, title: '', key: '' }, + ]) + } /> } title="标签目录" - overlay={(item) => ( - { - domEvent.stopPropagation(); - }} - > - - item.addable && treeData.onChange(item, InputMode.Append) - } - > - - 新建目录 - - item.editable && treeData.onChange(item, InputMode.Edit)} - > - - 编辑 - - item.deletable && handleDelete(item)} + overlay={(item) => { + const { addable = true, editable = true, deletable = true } = item; + return ( + { + domEvent.stopPropagation(); + }} > - - 删除 - - - )} + addable && handleAdd(item.key)} + > + + 新建目录 + + editable && handleEdit(item.key)} + > + + 编辑 + + deletable && handleDelete(item)} + > + + 删除 + + + ); + }} showSearch treeData={treeData.data} expandedKeys={treeData.expandedKeys} - onExpand={treeData.setExpandedKeys} - onChange={treeData.onChange} + onExpand={treeData.onExpand} onSave={handleSave} + onCancelSave={handleCancelSave} />
); diff --git a/src/catalogue/demos/simple.tsx b/src/catalogue/demos/simple.tsx index fee452a70..b239ca95a 100644 --- a/src/catalogue/demos/simple.tsx +++ b/src/catalogue/demos/simple.tsx @@ -56,7 +56,7 @@ export default () => { const treeData = useTreeData(); useEffect(() => { - treeData.initData(DEFAULT_DATA); + treeData.onChange(DEFAULT_DATA); }, []); return ( diff --git a/src/catalogue/demos/tabs.tsx b/src/catalogue/demos/tabs.tsx index ce1e1e6aa..ab3163ca8 100644 --- a/src/catalogue/demos/tabs.tsx +++ b/src/catalogue/demos/tabs.tsx @@ -62,7 +62,7 @@ export default () => { const [activeKey, setActiveKey] = useState(TreeType.Index); useEffect(() => { - treeData.initData(DEFAULT_DATA(activeKey)); + treeData.onChange(DEFAULT_DATA(activeKey)); }, [activeKey]); return ( @@ -82,7 +82,7 @@ export default () => { } onTabChange={(key) => setActiveKey(key)} expandedKeys={treeData.expandedKeys} - onExpand={treeData.setExpandedKeys} + onExpand={treeData.onExpand} /> ); diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx index a752e43f9..dc4d72f97 100644 --- a/src/catalogue/useTreeData.tsx +++ b/src/catalogue/useTreeData.tsx @@ -1,109 +1,163 @@ import { useState } from 'react'; import { DataNode } from 'antd/lib/tree'; -import { cloneDeep } from 'lodash'; import { CatalogueProps } from './components/catalogue'; -export interface ITreeNode extends Omit { - inputMode?: InputMode; - /** 是否可编辑 */ - editable?: boolean; - /** 是否可删除 */ - deletable?: boolean; - /** 是否可增加 */ - addable?: boolean; +export type ITreeNode = Omit & { title?: React.ReactNode; - children?: ITreeNode[]; -} + children?: ITreeNode[]; +} & U; -export enum InputMode { - Add = 'add', - Edit = 'edit', - Append = 'append', -} - -export const useTreeData = (): { - data: ITreeNode[]; +export const useTreeData = = {}>(): { + data: ITreeNode[]; loading: boolean; expandedKeys: CatalogueProps['expandedKeys']; - initData: (treeData: ITreeNode[]) => void; - onChange: (node?: ITreeNode, inputMode?: InputMode) => void; + onChange: (node: ITreeNode[]) => void; setLoading: React.Dispatch>; - setExpandedKeys: React.Dispatch>; + onExpand: React.Dispatch>; } => { - const [data, setData] = useState([]); + const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [expandedKeys, setExpandedKeys] = useState([]); - const initData = (treeData: ITreeNode[]) => { - setData(treeData); + const onChange = (data: ITreeNode[]) => { + setData(data); }; - const processData = (data: ITreeNode[]): ITreeNode[] => { - function traverse(item: ITreeNode): ITreeNode | null { - if (item.inputMode === InputMode.Edit) { - item.inputMode = undefined; - } - if (item.inputMode === InputMode.Add || item.inputMode === InputMode.Append) { - return null; - } - if (Array.isArray(item.children)) { - item.children = item.children.map(traverse).filter(Boolean) as ITreeNode[]; - } - return item; - } - return data.map(traverse).filter(Boolean) as ITreeNode[]; + return { + data, + loading, + expandedKeys, + onChange, + setLoading, + onExpand: setExpandedKeys, }; +}; - const onChange = (node?: ITreeNode, inputMode?: InputMode) => { - const newData = cloneDeep(data); - // 做 onBlur 清除数据 - if (!node && !inputMode) { - return setData(processData(newData)); +/** + * 查找 key 对应的节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 找到的对应节点 + */ +export const findNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode | null => { + for (const node of data) { + if (node.key === key) { + return node; } - if (!node && inputMode === InputMode.Add) - return setData([{ key: '', inputMode: InputMode.Add }, ...data]); - if (node && inputMode === InputMode.Append) { - const newExpandedKeys = expandedKeys ? [...expandedKeys] : []; - loopTree(newData, node.key, (item: ITreeNode) => { - const { children } = item; - item['children'] = [ - ...(children || []), - { key: node.key + 'new', inputMode: InputMode.Append }, - ]; - }); - setData(newData); - if (!expandedKeys?.includes(node.key)) { - newExpandedKeys.push(node.key); - } - return setExpandedKeys(newExpandedKeys); + if (node.children) { + const result = findNodeByKey(node.children, key); + if (result) return result; } - if (node && inputMode === InputMode.Edit) { - loopTree(newData, node.key, (item: ITreeNode) => { - item.inputMode = InputMode.Edit; - }); - setData(newData); + } + return null; +}; + +/** + * 更新 key 对应节点为编辑状态 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 更新之后 data + */ +export const updateTreeNodeEdit = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode[] => { + return data.map((node) => { + if (node.key === key) { + return { ...node, edit: true }; } - }; + if (node.children) { + return { ...node, children: updateTreeNodeEdit(node.children, key) }; + } + return node; + }); +}; - const loopTree = (data: ITreeNode[], key: ITreeNode['key'], callback: Function) => { - data.forEach((item, index, arr) => { - if (item.key === key) { - return callback(item, index, arr); - } - if (item.children) { - return loopTree(item.children, key, callback); +/** + * 查找 key 对应的父级节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 当前找到父级节点 + */ +export const findParentNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode | null => { + for (const node of data) { + if (node.children?.some((child) => child.key === key)) { + return node; + } + // 如果有子节点,递归查找 + if (node.children) { + const parent = findParentNodeByKey(node.children, key); + if (parent) return parent; + } + } + return null; +}; + +/** + * 在 key 节点中添加子节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 插入新数据之后的 data + */ +export const appendNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode[] => { + const newNode = { key: 'new_', title: '', edit: true }; + return data.map((node) => { + if (node.key === key) { + const updatedChildren = node.children ? [...node.children, newNode] : [newNode]; + return { ...node, children: updatedChildren }; + } + if (node.children) { + return { ...node, children: appendNodeByKey(node.children, key) }; + } + return node; + }); +}; + +/** + * 移除 key 节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 删除数据之后的 data + */ +export const removeNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode[] => { + return data + .filter((node) => node.key !== key) + .map((node) => { + if (node.children) { + return { + ...node, + children: removeNodeByKey(node.children, key), + }; } + return node; }); - }; +}; - return { - data, - loading, - expandedKeys, - onChange, - initData, - setLoading, - setExpandedKeys, - }; +/** + * 移除 edit 为 true 的节点 + * @param treeData + * @returns 移除之后的数据 + */ +export const removeEditNode = ( + treeData: ITreeNode[] +): ITreeNode[] => { + return treeData + .filter((node) => !node.edit) + .map((node) => ({ + ...node, + children: node.children ? removeEditNode(node.children) : undefined, + })); }; diff --git a/src/catalogue/utils.tsx b/src/catalogue/utils.tsx index 23c8f2133..963a1b6bd 100644 --- a/src/catalogue/utils.tsx +++ b/src/catalogue/utils.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ICatalogue } from './components/catalogue'; +import { CatalogueProps } from './components/catalogue'; import { FolderIcon, FolderOpenedIcon } from './components/icon'; import { ITreeNode } from './useTreeData'; @@ -17,7 +17,9 @@ export const getIcon: ITreeNode['icon'] = ({ selected, expanded }) => { /** * @description 轮询 Tree 数据,赋值搜索标识和leafIcon */ -export const loopTree = (data: ICatalogue['treeData']): ICatalogue['treeData'] => { +export const loopTree = >( + data: CatalogueProps['treeData'] +): CatalogueProps['treeData'] => { return data?.map((item) => { if (item.children) { return { From 1cb0c290575f8ab966e1d4aaa005e505609d30e8 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Thu, 26 Dec 2024 19:34:42 +0800 Subject: [PATCH 8/9] feat(catalogue): remove some unuse icon --- src/catalogue/components/catalogue.tsx | 10 +- src/catalogue/components/icon.tsx | 133 ------------------------- src/catalogue/demos/config.tsx | 10 +- src/catalogue/demos/drag.tsx | 10 +- src/catalogue/demos/operator.tsx | 10 +- src/catalogue/index.scss | 2 +- 6 files changed, 22 insertions(+), 153 deletions(-) diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index d113f7e9c..c1bf26135 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Dropdown, DropdownProps, Form, Input, Tabs } from 'antd'; -import { BlockHeader } from 'dt-react-component'; +import { BlockHeader, EllipsisText } from 'dt-react-component'; import { IBlockHeaderProps } from 'dt-react-component/blockHeader'; import { ITreeNode } from '../useTreeData'; @@ -74,7 +74,7 @@ const Catalogue = = {}, T extends readOnlyTab = a const defaultTitleRender = (item: ITreeNode) => { if (item.edit) { return ( -
+ = {}, T extends readOnlyTab = a } return (
-
{item.title}
+
+ +
{edit && renderNodeHover(item)}
); @@ -161,7 +163,7 @@ const Catalogue = = {}, T extends readOnlyTab = a return (
{ + onClick={(e) => { e.stopPropagation(); }} > diff --git a/src/catalogue/components/icon.tsx b/src/catalogue/components/icon.tsx index 4be8addfb..73203ed57 100644 --- a/src/catalogue/components/icon.tsx +++ b/src/catalogue/components/icon.tsx @@ -103,68 +103,6 @@ const CatalogIcon = function ({ className, ...rest }: IIcon) { ); }; -const PlusSquareIcon = function ({ className, ...rest }: IIcon) { - return ( - - - - - - ); -}; - -const MenuFoldIcon = function ({ className, ...rest }: IIcon) { - return ( - - - - - - ); -}; - -const MenuUnFoldIcon = function ({ className, ...rest }: IIcon) { - return ( - - - - - - - - ); -}; - const DragIcon = function ({ className, ...rest }: IIcon) { return ( @@ -209,71 +147,6 @@ const EllipsisIcon = function ({ className, ...rest }: IIcon) { ); }; -const EditIcon = function ({ className, ...rest }: IIcon) { - return ( - - - - - - - - ); -}; - -const DeleteIcon = function ({ className, ...rest }: IIcon) { - return ( - - - - - - - - ); -}; -const PlusCircleIcon = function ({ className, ...rest }: IIcon) { - return ( - - - - - - - - ); -}; - const SearchIcon = function ({ className, ...rest }: IIcon) { return ( @@ -319,16 +192,10 @@ const CloseIcon = function ({ className, ...rest }: IIcon) { export { CatalogIcon, CloseIcon, - DeleteIcon, DownTriangleIcon, DragIcon, - EditIcon, EllipsisIcon, FolderIcon, FolderOpenedIcon, - MenuFoldIcon, - MenuUnFoldIcon, - PlusCircleIcon, - PlusSquareIcon, SearchIcon, }; diff --git a/src/catalogue/demos/config.tsx b/src/catalogue/demos/config.tsx index 97ab083b7..9292af7ae 100644 --- a/src/catalogue/demos/config.tsx +++ b/src/catalogue/demos/config.tsx @@ -1,11 +1,11 @@ import React, { useEffect } from 'react'; +import { DeleteOutlined, EditOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { Menu } from 'antd'; import { Catalogue, EllipsisText } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; import { CatalogueProps } from '../components/catalogue'; -import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; import { appendNodeByKey, findNodeByKey, @@ -211,7 +211,7 @@ export default () => { treeData.onChange([ @@ -237,7 +237,7 @@ export default () => { disabled={!addable} onClick={() => addable && handleAdd(item.key)} > - + 新建目录 { disabled={!editable} onClick={() => editable && handleEdit(item.key)} > - + 编辑 { disabled={!deletable} onClick={() => deletable && handleDelete(item)} > - + 删除
diff --git a/src/catalogue/demos/drag.tsx b/src/catalogue/demos/drag.tsx index 2828237f2..cc6b91da5 100644 --- a/src/catalogue/demos/drag.tsx +++ b/src/catalogue/demos/drag.tsx @@ -1,11 +1,11 @@ import React, { useEffect } from 'react'; +import { DeleteOutlined, EditOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { Menu } from 'antd'; import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; import { CatalogueProps } from '../components/catalogue'; -import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; import { appendNodeByKey, findNodeByKey, @@ -200,7 +200,7 @@ export default () => { treeData.onChange([ @@ -226,7 +226,7 @@ export default () => { disabled={!addable} onClick={() => addable && handleAdd(item.key)} > - + 新建目录 { disabled={!editable} onClick={() => editable && handleEdit(item.key)} > - + 编辑 { disabled={!deletable} onClick={() => deletable && handleDelete(item)} > - + 删除 diff --git a/src/catalogue/demos/operator.tsx b/src/catalogue/demos/operator.tsx index 096d35cdf..55e2b350e 100644 --- a/src/catalogue/demos/operator.tsx +++ b/src/catalogue/demos/operator.tsx @@ -1,10 +1,10 @@ import React, { useEffect } from 'react'; +import { DeleteOutlined, EditOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { Menu } from 'antd'; import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; -import { DeleteIcon, EditIcon, PlusCircleIcon, PlusSquareIcon } from '../components/icon'; import { appendNodeByKey, findNodeByKey, @@ -200,7 +200,7 @@ export default () => { tooltip="嘿嘿,这是tooltip" addonAfter={ - treeData.onChange([ @@ -224,7 +224,7 @@ export default () => { disabled={!addable} onClick={() => addable && handleAdd(item.key)} > - + 新建目录 { disabled={!editable} onClick={() => editable && handleEdit(item.key)} > - + 编辑 { disabled={!deletable} onClick={() => deletable && handleDelete(item)} > - + 删除 diff --git a/src/catalogue/index.scss b/src/catalogue/index.scss index d7a923336..3daefe3f5 100644 --- a/src/catalogue/index.scss +++ b/src/catalogue/index.scss @@ -107,7 +107,7 @@ .ant-tree { height: 100%; .ant-tree-treenode-leaf-last .ant-tree-switcher-leaf-line::before { - height: 100% !important; + height: 50% !important; } .ant-tree-node-content-wrapper.ant-tree-node-selected { background: none; From d09bcee67b2043f9856c8075d1a141f21842fedb Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Thu, 26 Dec 2024 20:13:36 +0800 Subject: [PATCH 9/9] fix(catalogue): remove some function to utils --- src/catalogue/components/catalogue.tsx | 2 +- src/catalogue/demos/async.tsx | 46 ++++++++ src/catalogue/demos/config.tsx | 5 +- src/catalogue/demos/drag.tsx | 5 +- src/catalogue/demos/operator.tsx | 5 +- src/catalogue/index.md | 1 + src/catalogue/useTreeData.tsx | 133 --------------------- src/catalogue/utils.tsx | 154 +++++++++++++++++++++++++ 8 files changed, 208 insertions(+), 143 deletions(-) create mode 100644 src/catalogue/demos/async.tsx diff --git a/src/catalogue/components/catalogue.tsx b/src/catalogue/components/catalogue.tsx index c1bf26135..8a961ffa1 100644 --- a/src/catalogue/components/catalogue.tsx +++ b/src/catalogue/components/catalogue.tsx @@ -17,7 +17,7 @@ type readOnlyTab = readonly Tab[]; type TabKey = T[number]['key']; interface NormalCatalogueProps = {}> - extends Pick, + extends Partial>, ICatalogueTree { showSearch?: boolean; edit?: boolean; diff --git a/src/catalogue/demos/async.tsx b/src/catalogue/demos/async.tsx new file mode 100644 index 000000000..57bf688f9 --- /dev/null +++ b/src/catalogue/demos/async.tsx @@ -0,0 +1,46 @@ +import React, { useEffect } from 'react'; +import { Catalogue } from 'dt-react-component'; + +import { useTreeData } from '../useTreeData'; +import { insertChildIntoNode } from '../utils'; + +interface DataNode { + title: string; + key: string; + isLeaf?: boolean; + children?: DataNode[]; +} + +const initTreeData: DataNode[] = [ + { title: 'Expand to load', key: '0' }, + { title: 'Expand to load', key: '1' }, + { title: 'Tree Node', key: '2', isLeaf: true }, +]; + +const App: React.FC = () => { + const treeData = useTreeData(); + + useEffect(() => { + treeData.onChange(initTreeData); + }, []); + + const onLoadData = ({ key, children }: any) => + new Promise((resolve) => { + if (children) { + resolve(); + return; + } + setTimeout(() => { + const newData = insertChildIntoNode(treeData.data, key, [ + { title: 'Child Node', key: `${key}-0` }, + { title: 'Child Node', key: `${key}-1` }, + ]); + treeData.onChange(newData); + resolve(); + }, 1000); + }); + + return ; +}; + +export default App; diff --git a/src/catalogue/demos/config.tsx b/src/catalogue/demos/config.tsx index 9292af7ae..6cacf2f50 100644 --- a/src/catalogue/demos/config.tsx +++ b/src/catalogue/demos/config.tsx @@ -6,16 +6,15 @@ import { cloneDeep } from 'lodash'; import shortid from 'shortid'; import { CatalogueProps } from '../components/catalogue'; +import { ITreeNode, useTreeData } from '../useTreeData'; import { appendNodeByKey, findNodeByKey, findParentNodeByKey, - ITreeNode, removeEditNode, removeNodeByKey, updateTreeNodeEdit, - useTreeData, -} from '../useTreeData'; +} from '../utils'; const DEFAULT_DATA: ITreeNode[] = [ { diff --git a/src/catalogue/demos/drag.tsx b/src/catalogue/demos/drag.tsx index cc6b91da5..bb2a2c444 100644 --- a/src/catalogue/demos/drag.tsx +++ b/src/catalogue/demos/drag.tsx @@ -6,16 +6,15 @@ import { cloneDeep } from 'lodash'; import shortid from 'shortid'; import { CatalogueProps } from '../components/catalogue'; +import { ITreeNode, useTreeData } from '../useTreeData'; import { appendNodeByKey, findNodeByKey, findParentNodeByKey, - ITreeNode, removeEditNode, removeNodeByKey, updateTreeNodeEdit, - useTreeData, -} from '../useTreeData'; +} from '../utils'; interface IData { edit?: boolean; diff --git a/src/catalogue/demos/operator.tsx b/src/catalogue/demos/operator.tsx index 55e2b350e..dc6c0f1ba 100644 --- a/src/catalogue/demos/operator.tsx +++ b/src/catalogue/demos/operator.tsx @@ -5,16 +5,15 @@ import { Catalogue } from 'dt-react-component'; import { cloneDeep } from 'lodash'; import shortid from 'shortid'; +import { ITreeNode, useTreeData } from '../useTreeData'; import { appendNodeByKey, findNodeByKey, findParentNodeByKey, - ITreeNode, removeEditNode, removeNodeByKey, updateTreeNodeEdit, - useTreeData, -} from '../useTreeData'; +} from '../utils'; const DEFAULT_DATA = [ { diff --git a/src/catalogue/index.md b/src/catalogue/index.md index 3399eccb7..d99351311 100644 --- a/src/catalogue/index.md +++ b/src/catalogue/index.md @@ -22,6 +22,7 @@ demo: 可拖拽的目录树 配置操作项的目录树 配置操作项的目录树 +异步加载数据 ## API diff --git a/src/catalogue/useTreeData.tsx b/src/catalogue/useTreeData.tsx index dc4d72f97..b22bcca5a 100644 --- a/src/catalogue/useTreeData.tsx +++ b/src/catalogue/useTreeData.tsx @@ -10,14 +10,11 @@ export type ITreeNode = Omit & { export const useTreeData = = {}>(): { data: ITreeNode[]; - loading: boolean; expandedKeys: CatalogueProps['expandedKeys']; onChange: (node: ITreeNode[]) => void; - setLoading: React.Dispatch>; onExpand: React.Dispatch>; } => { const [data, setData] = useState[]>([]); - const [loading, setLoading] = useState(false); const [expandedKeys, setExpandedKeys] = useState([]); const onChange = (data: ITreeNode[]) => { @@ -26,138 +23,8 @@ export const useTreeData = = {}>(): { return { data, - loading, expandedKeys, onChange, - setLoading, onExpand: setExpandedKeys, }; }; - -/** - * 查找 key 对应的节点 - * @param data 遍历的数组 - * @param key 当前 key 值 - * @returns 找到的对应节点 - */ -export const findNodeByKey = ( - data: ITreeNode[], - key: ITreeNode['key'] -): ITreeNode | null => { - for (const node of data) { - if (node.key === key) { - return node; - } - if (node.children) { - const result = findNodeByKey(node.children, key); - if (result) return result; - } - } - return null; -}; - -/** - * 更新 key 对应节点为编辑状态 - * @param data 遍历的数组 - * @param key 当前 key 值 - * @returns 更新之后 data - */ -export const updateTreeNodeEdit = ( - data: ITreeNode[], - key: ITreeNode['key'] -): ITreeNode[] => { - return data.map((node) => { - if (node.key === key) { - return { ...node, edit: true }; - } - if (node.children) { - return { ...node, children: updateTreeNodeEdit(node.children, key) }; - } - return node; - }); -}; - -/** - * 查找 key 对应的父级节点 - * @param data 遍历的数组 - * @param key 当前 key 值 - * @returns 当前找到父级节点 - */ -export const findParentNodeByKey = ( - data: ITreeNode[], - key: ITreeNode['key'] -): ITreeNode | null => { - for (const node of data) { - if (node.children?.some((child) => child.key === key)) { - return node; - } - // 如果有子节点,递归查找 - if (node.children) { - const parent = findParentNodeByKey(node.children, key); - if (parent) return parent; - } - } - return null; -}; - -/** - * 在 key 节点中添加子节点 - * @param data 遍历的数组 - * @param key 当前 key 值 - * @returns 插入新数据之后的 data - */ -export const appendNodeByKey = ( - data: ITreeNode[], - key: ITreeNode['key'] -): ITreeNode[] => { - const newNode = { key: 'new_', title: '', edit: true }; - return data.map((node) => { - if (node.key === key) { - const updatedChildren = node.children ? [...node.children, newNode] : [newNode]; - return { ...node, children: updatedChildren }; - } - if (node.children) { - return { ...node, children: appendNodeByKey(node.children, key) }; - } - return node; - }); -}; - -/** - * 移除 key 节点 - * @param data 遍历的数组 - * @param key 当前 key 值 - * @returns 删除数据之后的 data - */ -export const removeNodeByKey = ( - data: ITreeNode[], - key: ITreeNode['key'] -): ITreeNode[] => { - return data - .filter((node) => node.key !== key) - .map((node) => { - if (node.children) { - return { - ...node, - children: removeNodeByKey(node.children, key), - }; - } - return node; - }); -}; - -/** - * 移除 edit 为 true 的节点 - * @param treeData - * @returns 移除之后的数据 - */ -export const removeEditNode = ( - treeData: ITreeNode[] -): ITreeNode[] => { - return treeData - .filter((node) => !node.edit) - .map((node) => ({ - ...node, - children: node.children ? removeEditNode(node.children) : undefined, - })); -}; diff --git a/src/catalogue/utils.tsx b/src/catalogue/utils.tsx index 963a1b6bd..69f88c132 100644 --- a/src/catalogue/utils.tsx +++ b/src/catalogue/utils.tsx @@ -35,3 +35,157 @@ export const loopTree = >( }; }); }; + +/** + * 查找 key 对应的节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 找到的对应节点 + */ +export const findNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode | null => { + for (const node of data) { + if (node.key === key) { + return node; + } + if (node.children) { + const result = findNodeByKey(node.children, key); + if (result) return result; + } + } + return null; +}; + +/** + * 更新 key 对应节点为编辑状态 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 更新之后 data + */ +export const updateTreeNodeEdit = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode[] => { + return data.map((node) => { + if (node.key === key) { + return { ...node, edit: true }; + } + if (node.children) { + return { ...node, children: updateTreeNodeEdit(node.children, key) }; + } + return node; + }); +}; + +/** + * 查找 key 对应的父级节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 当前找到父级节点 + */ +export const findParentNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode | null => { + for (const node of data) { + if (node.children?.some((child) => child.key === key)) { + return node; + } + // 如果有子节点,递归查找 + if (node.children) { + const parent = findParentNodeByKey(node.children, key); + if (parent) return parent; + } + } + return null; +}; + +/** + * 在 key 节点中添加子节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 插入新数据之后的 data + */ +export const appendNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode[] => { + const newNode = { key: 'new_', title: '', edit: true }; + return data.map((node) => { + if (node.key === key) { + const updatedChildren = node.children ? [...node.children, newNode] : [newNode]; + return { ...node, children: updatedChildren }; + } + if (node.children) { + return { ...node, children: appendNodeByKey(node.children, key) }; + } + return node; + }); +}; + +/** + * 移除 key 节点 + * @param data 遍历的数组 + * @param key 当前 key 值 + * @returns 删除数据之后的 data + */ +export const removeNodeByKey = ( + data: ITreeNode[], + key: ITreeNode['key'] +): ITreeNode[] => { + return data + .filter((node) => node.key !== key) + .map((node) => { + if (node.children) { + return { + ...node, + children: removeNodeByKey(node.children, key), + }; + } + return node; + }); +}; + +/** + * 移除 edit 为 true 的节点 + * @param treeData + * @returns 移除之后的数据 + */ +export const removeEditNode = ( + treeData: ITreeNode[] +): ITreeNode[] => { + return treeData + .filter((node) => !node.edit) + .map((node) => ({ + ...node, + children: node.children ? removeEditNode(node.children) : undefined, + })); +}; + +/** + * + */ + +export const insertChildIntoNode = ( + treeData: ITreeNode[], + key: ITreeNode['key'], + children: ITreeNode[] +): ITreeNode[] => { + return treeData.map((node) => { + if (node.key === key) { + return { + ...node, + children, + }; + } + if (node.children) { + return { + ...node, + children: insertChildIntoNode(node.children, key, children), + }; + } + return node; + }); +};