|
@@ -0,0 +1,205 @@ |
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react'; |
|
|
|
|
|
import { Tree, Space, Input } from 'antd'; |
|
|
|
|
|
import type { InputRef } from 'antd'; |
|
|
|
|
|
import type { DataNode, TreeProps } from 'antd/es/tree'; |
|
|
|
|
|
import { t } from '@/utils/i18n'; |
|
|
|
|
|
import { DownOutlined, EditOutlined, DeleteOutlined, PlusOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; |
|
|
|
|
|
import { useRequest } from '@/hooks/use-request'; |
|
|
|
|
|
import { antdUtils } from '@/utils/antd'; |
|
|
|
|
|
import { MaterialClassifyVO } from '@/models'; |
|
|
|
|
|
import classifyService from '@/request/service/material-classify'; |
|
|
|
|
|
import { Key } from 'antd/lib/table/interface'; |
|
|
|
|
|
|
|
|
|
|
|
interface TreeDataNode extends DataNode { |
|
|
|
|
|
parentId? : Key; |
|
|
|
|
|
isNewNode: boolean |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const convert2DataNode = (source: MaterialClassifyVO) => { |
|
|
|
|
|
const node: TreeDataNode = { |
|
|
|
|
|
title: source.classifyName, |
|
|
|
|
|
key: source.id, |
|
|
|
|
|
parentId: source.parentId??-1, |
|
|
|
|
|
isNewNode: false |
|
|
|
|
|
}; |
|
|
|
|
|
if (source.childrens && source.childrens.length > 0) { |
|
|
|
|
|
node.children = source.childrens.map(it => convert2DataNode(it)) |
|
|
|
|
|
} |
|
|
|
|
|
return node; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export default () => { |
|
|
|
|
|
const { runAsync: getTreeApi } = useRequest(classifyService.getMaterialClassifyTreeApi, { manual: true }); |
|
|
|
|
|
const { runAsync: deleteApi } = useRequest(classifyService.deleteMaterialClassifyApi, { manual: true }); |
|
|
|
|
|
const { runAsync: updateApi } = useRequest(classifyService.updateMaterialClassifyApi, { manual: true }); |
|
|
|
|
|
const { runAsync: createApi } = useRequest(classifyService.createMaterialClassifyApi, { manual: true }); |
|
|
|
|
|
|
|
|
|
|
|
const [dataSource, setDataSource] = useState<TreeDataNode[]>([]); |
|
|
|
|
|
const [selectedItem, setSelectedItem] = useState<Key | null>(null); |
|
|
|
|
|
const [hoveredItem, setHoveredItem] = useState<Key | null>(null); |
|
|
|
|
|
const [editItem, setEditItem] = useState<TreeDataNode | null>(null); |
|
|
|
|
|
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]); |
|
|
|
|
|
const inputRef = useRef<InputRef>(null); |
|
|
|
|
|
const [inputStatus, setInputStatus] = useState<"" | "error" | "warning">(""); |
|
|
|
|
|
|
|
|
|
|
|
const load = async () => { |
|
|
|
|
|
const [error, { data }] = await getTreeApi(); |
|
|
|
|
|
if (!error) { |
|
|
|
|
|
setDataSource([{ |
|
|
|
|
|
title: '全部素材', |
|
|
|
|
|
key: -1, |
|
|
|
|
|
isNewNode: false, |
|
|
|
|
|
children: data.map(it => convert2DataNode(it)) |
|
|
|
|
|
}]); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onSelect: TreeProps['onSelect'] = (selectedKeys) => { |
|
|
|
|
|
if (selectedKeys.length > 0) { |
|
|
|
|
|
setSelectedItem(selectedKeys[0]); |
|
|
|
|
|
} else { |
|
|
|
|
|
setSelectedItem(null); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onHover = (node: TreeDataNode) => { |
|
|
|
|
|
setHoveredItem(node.key); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
load(); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const onEdit = (node: TreeDataNode) => { |
|
|
|
|
|
// 编辑节点title |
|
|
|
|
|
onQuitEdit(); |
|
|
|
|
|
setEditItem(node); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onAdd = (node: TreeDataNode) => { |
|
|
|
|
|
// 新增子节点 |
|
|
|
|
|
if (!node.children) { |
|
|
|
|
|
node.children = []; |
|
|
|
|
|
} |
|
|
|
|
|
const subNode: TreeDataNode = { |
|
|
|
|
|
title: '新增分类', |
|
|
|
|
|
key: new Date().getTime(), |
|
|
|
|
|
parentId: node.key, |
|
|
|
|
|
isNewNode: true |
|
|
|
|
|
}; |
|
|
|
|
|
node.children?.push(subNode); |
|
|
|
|
|
setDataSource([...dataSource]); |
|
|
|
|
|
if (expandedKeys.indexOf(node.key) < 0) { |
|
|
|
|
|
setExpandedKeys([...expandedKeys, node.key]) |
|
|
|
|
|
} |
|
|
|
|
|
onQuitEdit(); |
|
|
|
|
|
setEditItem(subNode); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const filterNode: (nodes: TreeDataNode[], item: TreeDataNode) => TreeDataNode[] = (nodes: TreeDataNode[], item: TreeDataNode) => { |
|
|
|
|
|
return nodes.filter(child => child.key !== item.key) |
|
|
|
|
|
.map(child => { |
|
|
|
|
|
if (child.children && child.children.length > 0) { |
|
|
|
|
|
return { ...child, children: filterNode(child.children as TreeDataNode[] || [], item) } |
|
|
|
|
|
} else { |
|
|
|
|
|
return { ...child } |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
const onDelete = async (node: TreeDataNode) => { |
|
|
|
|
|
// 删除子节点 |
|
|
|
|
|
if(!node.isNewNode) { |
|
|
|
|
|
await deleteApi(node.key) |
|
|
|
|
|
} |
|
|
|
|
|
const newTreeData = filterNode(dataSource, node) |
|
|
|
|
|
setDataSource(newTreeData); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onSaveEditValue = async (node: TreeDataNode) => { |
|
|
|
|
|
const newValue = inputRef.current?.input?.value; |
|
|
|
|
|
const api = node.isNewNode ? createApi : updateApi; |
|
|
|
|
|
const param = node.isNewNode? { |
|
|
|
|
|
parentId: node.parentId??-1 < 0 ? 0 : node.parentId, |
|
|
|
|
|
classifyName: inputRef.current?.input?.value |
|
|
|
|
|
}: { |
|
|
|
|
|
id: node.key as number, |
|
|
|
|
|
classifyName: inputRef.current?.input?.value |
|
|
|
|
|
} |
|
|
|
|
|
const [error, { code, msg }] = await api(param); |
|
|
|
|
|
if(!error && code === 0) { |
|
|
|
|
|
node.title = newValue; |
|
|
|
|
|
setEditItem(null); |
|
|
|
|
|
} else { |
|
|
|
|
|
antdUtils.message?.error(msg); |
|
|
|
|
|
setInputStatus('error'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const onQuitEdit = () => { |
|
|
|
|
|
if(editItem != null && editItem.isNewNode) { |
|
|
|
|
|
onDelete(editItem); |
|
|
|
|
|
} |
|
|
|
|
|
setEditItem(null); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const shouldShowActions = (key: Key) => { |
|
|
|
|
|
return key === selectedItem || key === hoveredItem; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const renderEditItem = (node: TreeDataNode) => { |
|
|
|
|
|
return ( |
|
|
|
|
|
<Space> |
|
|
|
|
|
<Input size='small' status={inputStatus} ref={inputRef} defaultValue={node.title as string} /> |
|
|
|
|
|
<CheckOutlined onClick={() => onSaveEditValue(node)} /> |
|
|
|
|
|
<CloseOutlined onClick={onQuitEdit} /> |
|
|
|
|
|
</Space> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const renderItem = (node: TreeDataNode) => { |
|
|
|
|
|
return (<div className='flex' |
|
|
|
|
|
onMouseEnter={() => { onHover(node) }} |
|
|
|
|
|
onMouseLeave={() => { setHoveredItem(null) }}> |
|
|
|
|
|
{ |
|
|
|
|
|
editItem?.key === node.key ? (renderEditItem(node)) : node.title as string |
|
|
|
|
|
} |
|
|
|
|
|
{ |
|
|
|
|
|
shouldShowActions(node.key) && editItem?.key !== node.key ? (<div className='ml-4'> |
|
|
|
|
|
<Space> |
|
|
|
|
|
<PlusOutlined onClick={() => onAdd(node)} /> |
|
|
|
|
|
{ |
|
|
|
|
|
node.key !== -1 ? (<> |
|
|
|
|
|
<EditOutlined onClick={() => onEdit(node)} /> |
|
|
|
|
|
<DeleteOutlined onClick={() => onDelete(node)} /> |
|
|
|
|
|
</>) : null |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
</Space> |
|
|
|
|
|
</div>) : null |
|
|
|
|
|
} |
|
|
|
|
|
</div>) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const onExpand: TreeProps['onExpand'] = (expandedKeys) => { |
|
|
|
|
|
console.log(expandedKeys) |
|
|
|
|
|
setExpandedKeys(expandedKeys) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
<> |
|
|
|
|
|
<div> |
|
|
|
|
|
<Tree |
|
|
|
|
|
showLine |
|
|
|
|
|
switcherIcon={<DownOutlined />} |
|
|
|
|
|
onSelect={onSelect} |
|
|
|
|
|
treeData={dataSource} |
|
|
|
|
|
onExpand={onExpand} |
|
|
|
|
|
expandedKeys={expandedKeys} |
|
|
|
|
|
autoExpandParent={true} |
|
|
|
|
|
titleRender={(node) => renderItem(node)} |
|
|
|
|
|
> |
|
|
|
|
|
</Tree> |
|
|
|
|
|
</div> |
|
|
|
|
|
</> |
|
|
|
|
|
); |
|
|
|
|
|
}; |