You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/web/src/components/modals/GroupDetail/Panel.tsx

228 lines
6.5 KiB
TypeScript

import React, { useCallback, useMemo, useRef, useState } from 'react';
import {
GroupPanelType,
useGroupInfo,
GroupPanel as GroupPanelInfo,
showToasts,
t,
} from 'tailchat-shared';
import { Button, Tree } from 'antd';
import type { NodeDragEventParams } from 'rc-tree/lib/contextTypes';
import type { DataNode, EventDataNode } from 'antd/lib/tree';
import type { Key } from 'rc-tree/lib/interface';
import type { AllowDrop } from 'rc-tree/lib/Tree';
import _cloneDeep from 'lodash/cloneDeep';
import _isNil from 'lodash/isNil';
import _isEqual from 'lodash/isEqual';
/**
* 将一维的面板列表构筑成立体的数组
*/
function buildTreeDataWithGroupPanel(
groupPanels: GroupPanelInfo[]
): DataNode[] {
return groupPanels
.filter((panel) => panel.type === GroupPanelType.GROUP)
.map<DataNode>((section) => ({
key: section.id,
title: section.name,
isLeaf: false,
children: groupPanels
.filter((panel) => panel.parentId === section.id)
.map<DataNode>((panel) => ({
key: panel.id,
title: panel.name,
isLeaf: true,
})),
}));
}
/**
* 重新构建面板顺序
*/
function rebuildGroupPanelOrder(
groupPanels: GroupPanelInfo[]
): GroupPanelInfo[] {
const originGroupPanels = _cloneDeep(groupPanels);
const newPanel = originGroupPanels.filter((panel) => _isNil(panel.parentId)); // 第一层
const len = newPanel.length;
for (let i = len - 1; i >= 0; i--) {
const currentId = newPanel[i].id;
newPanel.splice(
i + 1,
0,
...originGroupPanels.filter((p) => p.parentId === currentId)
);
}
return newPanel;
}
interface GroupPanelTree {
groupPanels: GroupPanelInfo[];
onChange: (newGroupPanels: GroupPanelInfo[]) => void;
}
/**
* 仅同层级相互拉的树形结构
*/
const GroupPanelTree: React.FC<GroupPanelTree> = React.memo((props) => {
const draggingNode = useRef<DataNode | null>(null);
const treeData: DataNode[] = useMemo(
() => buildTreeDataWithGroupPanel(props.groupPanels),
[props.groupPanels]
);
const handleDragStart = useCallback(
(info: NodeDragEventParams<HTMLDivElement>) => {
draggingNode.current = info.node;
},
[]
);
const handleDragEnd = useCallback(() => {
draggingNode.current = null;
}, []);
const handleAllowDrop = useCallback(
({ dropNode, dropPosition }: Parameters<AllowDrop>[0]) => {
if (draggingNode.current?.isLeaf === true) {
// 如果正在拖拽的节点是面板
if (dropPosition === 0) {
return !dropNode.isLeaf;
}
return true;
} else {
// 正在拖拽的节点是分组
if (dropPosition === 0) {
// 不允许容器之间产生父子节点
return false;
}
// 仅允许拖拽到分组的上下
return !dropNode.isLeaf;
}
},
[]
);
const handleDrop = useCallback(
(
info: NodeDragEventParams & {
dragNode: EventDataNode;
dragNodesKeys: Key[];
dropPosition: number;
dropToGap: boolean;
}
) => {
const newGroupPanels = _cloneDeep(props.groupPanels);
const dropNodePos = newGroupPanels.findIndex(
(panel) => panel.id === info.node.key
);
const dropGroupPanel = newGroupPanels[dropNodePos];
if (dropNodePos === -1) {
showToasts('异常, 目标节点未找到', 'error');
}
const dragPanelPos = newGroupPanels.findIndex(
(panel) => panel.id === info.dragNode.key
);
if (draggingNode.current?.isLeaf === true && info.node.isLeaf === true) {
// 如果拖拽节点是面板且目标也是面板
// 则更新它的父节点id为目标节点的父节点id
info.dragNodesKeys
// 获取所有的移动节点的位置
.map((key) => newGroupPanels.findIndex((panel) => panel.id === key))
// 过滤掉没找到的
.filter((index) => index !== -1)
.forEach((pos) => {
newGroupPanels[pos].parentId = dropGroupPanel.parentId;
});
} else if (
draggingNode.current?.isLeaf === true &&
info.dropToGap === false &&
info.node.isLeaf === false
) {
// 拖拽节点是面板拖动到组节点上
// 则更新它的父节点id为目标节点的id
info.dragNodesKeys
// 获取所有的移动节点的位置
.map((key) => newGroupPanels.findIndex((panel) => panel.id === key))
// 过滤掉没找到的
.filter((index) => index !== -1)
.forEach((pos) => {
newGroupPanels[pos].parentId = dropGroupPanel.id;
});
}
if (info.node.dragOverGapTop === true) {
// 移动到目标节点之前
newGroupPanels.splice(
dropNodePos,
0,
newGroupPanels.splice(dragPanelPos, 1)[0]
);
} else if (info.node.dragOverGapBottom === true) {
// 移动到目标节点之后
newGroupPanels.splice(
dragPanelPos,
0,
newGroupPanels.splice(dropNodePos, 1)[0]
);
}
if (typeof props.onChange === 'function') {
props.onChange(rebuildGroupPanelOrder(newGroupPanels));
}
},
[props.groupPanels, props.onChange]
);
return (
<Tree
treeData={treeData}
defaultExpandAll={true}
draggable={true}
onDrop={handleDrop}
// TODO: https://github.com/react-component/tree/pull/482
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
allowDrop={handleAllowDrop}
/>
);
});
GroupPanelTree.displayName = 'GroupPanelTree';
export const GroupPanel: React.FC<{
groupId: string;
}> = React.memo((props) => {
const groupId = props.groupId;
const groupInfo = useGroupInfo(groupId);
const groupPanels = groupInfo?.panels ?? [];
const [editingGroupPanels, setEditingGroupPanels] = useState(groupPanels);
const handleChange = useCallback((newGroupPanels: GroupPanelInfo[]) => {
setEditingGroupPanels(newGroupPanels);
}, []);
const handleSave = useCallback(() => {
console.log('editingGroupPanels', editingGroupPanels);
}, [editingGroupPanels]);
return (
<div>
<GroupPanelTree
groupPanels={editingGroupPanels}
onChange={handleChange}
/>
{!_isEqual(groupPanels, editingGroupPanels) && (
<Button className="mt-2" onClick={handleSave}>
{t('保存')}
</Button>
)}
</div>
);
});
GroupPanel.displayName = 'GroupPanel';