feat:右键,虚拟列表导致拖拽被阻挡,使用enum

This commit is contained in:
1708-huayu 2025-07-07 18:46:26 +08:00
parent 135bf4ff41
commit 55e5468d47
8 changed files with 210 additions and 83 deletions

View File

@ -2,27 +2,40 @@
* A layout is UI that is shared between multiple routes. On navigation, layouts preserve state, remain interactive, and do not re-render.
*/
'use client'
import React, {useEffect, useState} from 'react';
import React, {useContext, useEffect, useState} from 'react';
import {DragDropContext, DropResult} from 'react-beautiful-dnd';
import {DroppableTable} from "@/ui/task/drag/DroppableTable";
import {DataType, Request} from "@/lib/definitions";
import {TaskSelectVO} from "@/lib/task/drag/data";
import {selectTaskAPI} from "@/lib/task/drag/service";
import LocalContext from "@/ui/LocalContent";
import {useSearchParams} from "next/dist/client/components/navigation";
export default function Layout({children}: { children: React.ReactNode }) {
const [allTaskList, setAllTaskList] = useState<DataType[]>([]);
const data = useContext(LocalContext);
console.log('data',data);
let pid = useSearchParams().get('pid');
useEffect(() => {
addData()
}, [])
}, [data])
async function addData() {
function addData() {
const requestParam: Request<TaskSelectVO> = {
pageSize: 1000,
pageNumber: 1,
data: {state: '9'}
data: {state: data.taskState}
}
const res = await selectTaskAPI(requestParam)
setAllTaskList(res.data.content)
if (data.expectedStartTime.length>0){
const parse = JSON.parse(data.expectedStartTime);
requestParam.data.expectedStartTimeStart=parse[0].value;
requestParam.data.expectedStartTimeEnd=parse[1].value;
}
selectTaskAPI(requestParam).then(res=>{
setAllTaskList(res.data.content)
})
}
// 处理拖拽结束事件
@ -51,6 +64,5 @@ export default function Layout({children}: { children: React.ReactNode }) {
{/* 不紧急不重要 */}
<DroppableTable tableCode='0' taskList={allTaskList.filter(task => task.priority == '0')}/>
</DragDropContext></div>
)
;
);
}

View File

@ -3,4 +3,6 @@ export type TaskSelectVO = {
state?: string;
priority?: string;
allOverdueTasks?:boolean;
expectedStartTimeStart?:string;
expectedStartTimeEnd?:string;
}

View File

@ -9,7 +9,7 @@ export async function selectTaskAPI(requestParam: Request<TaskSelectVO>):
noStore();
// 使用 Axios 发送 PUT 请求获取数据
const response: AxiosResponse<ResponseVO<ResultPage<DataType>>> = await httpReq.post(
process.env.NEXT_PUBLIC_TODO_REQUEST_URL + '/task/select', requestParam);
process.env.NEXT_PUBLIC_TODO_REQUEST_URL + '/V2/task/select', requestParam);
// 从响应中提取数据并返回
return response.data;
}

View File

@ -116,7 +116,7 @@ export const taskPriorityList: DictType[] = [
code: '1',
name: '不重要紧急',
order: 2,
color: '#fafafa'
color: '#1677ff'
}, {
id: 0,
code: '0',
@ -216,5 +216,6 @@ export enum OPERATION_BUTTON_TYPE {
SHOW_FOUR,
SHOW_CALENDAR,
ADD,
UPDATE_PRIORITY
UPDATE_PRIORITY,
INIT
}

136
src/ui/task/RightOption.tsx Normal file
View File

@ -0,0 +1,136 @@
import React, {Fragment, useState} from 'react';
import {Form, MenuProps, message, Popconfirm} from 'antd';
import { Dropdown, theme } from 'antd';
import {commonUpdate, deleteTask, OPERATION_BUTTON_TYPE} from "@/lib/task/project/data";
import {QuestionCircleOutlined} from "@ant-design/icons";
import Link from "next/link";
import {DetailModelForm} from "@/ui/task/project/DetailModelForm";
interface OperationButtonProps {
itemId: string,
priority?:string,
pid: string,
pPid: string,
operationId?: OPERATION_BUTTON_TYPE,
refreshDate?: () => void,
children: React.ReactNode
}
const RightOption: React.FC<OperationButtonProps> = (props) => {
const [operateState, setOperateState] = useState({
operationId:OPERATION_BUTTON_TYPE.INIT,openModal:false});
const items: MenuProps['items'] = [
{
key: OPERATION_BUTTON_TYPE.DETAIL,
label: <a onClick={(e) => {
setOperateState({openModal: true, operationId: OPERATION_BUTTON_TYPE.DETAIL})
}}></a>,
},
{
key: OPERATION_BUTTON_TYPE.ADD_CHILD,
label: <a onClick={(e) => {
setOperateState({openModal: true, operationId: OPERATION_BUTTON_TYPE.ADD_CHILD})
}}>线</a>,
},
{
key: OPERATION_BUTTON_TYPE.UPDATE,
label: <a onClick={(e) => {
setOperateState({openModal: true, operationId: OPERATION_BUTTON_TYPE.UPDATE})
}}></a>,
},
{
key: OPERATION_BUTTON_TYPE.DELETE,
label: <Popconfirm
title="删除任务"
description="确认要删除任务?"
icon={<QuestionCircleOutlined style={{color: 'red'}}/>}
okText="确认"
cancelText="取消"
onConfirm={() => {
deleteTask(props.itemId).then((response => {
console.log('response', response)
if (response.status.success) {
message.success("删除任务成功:" + response.data)
props.refreshDate?.()
}
}));
}}
><a></a></Popconfirm>,
},
{
key: OPERATION_BUTTON_TYPE.COMPLETE,
label: <Popconfirm
title="完成任务"
description="确认要完成任务?"
okText="确认"
cancelText="取消"
onConfirm={() => {
commonUpdate({
updateColumnList:[{
name:'state',
code:'state',
value:'7'
}],
conditionColumnList:[{
name:'id',
code:'id',
operateType:'=',
value:props.itemId
}]
}).then((response => {
console.log('response', response)
if (response.status.success) {
message.success("完成任务成功")
props.refreshDate?.()
}
}));
}}
><a></a></Popconfirm>,
},
{
key: OPERATION_BUTTON_TYPE.SHOW_TREE,
label: <Link href={"/task/project?pid=" + props.itemId}></Link>,
},
{
key: OPERATION_BUTTON_TYPE.SHOW_FOUR,
label: <Link href={"/task/drag?pid=" + props.itemId}></Link>,
},
{
key: OPERATION_BUTTON_TYPE.SHOW_CALENDAR,
label: <Link href={"/task/calendar?pid=" + props.itemId}></Link>,
}
];
// 获取系统样式
const {
token: { colorBgLayout, colorTextTertiary },
} = theme.useToken();
const handleCancel = () => {
setOperateState({...operateState, openModal: false})
if (operateState.operationId !== OPERATION_BUTTON_TYPE.DETAIL) {
props.refreshDate?.()
}
}
const onClick: MenuProps['onClick'] = ({key}) => {
console.log(key)
};
return (
<Fragment>
<Dropdown menu={{ items }} trigger={['contextMenu']}>
{props.children}
</Dropdown>
{operateState.openModal&&<DetailModelForm
haveButton={false}
itemId={operateState.operationId === OPERATION_BUTTON_TYPE.UPDATE||operateState.operationId === OPERATION_BUTTON_TYPE.DETAIL?props.itemId:undefined}
pPid={props.pPid}
pid={operateState.operationId === OPERATION_BUTTON_TYPE.ADD_CHILD ?props.itemId:undefined}
operationId={operateState.operationId}
description={operateState.operationId === OPERATION_BUTTON_TYPE.DETAIL ? '任务详情' :
operateState.operationId === OPERATION_BUTTON_TYPE.ADD_CHILD ? '添加支线任务' :
operateState.operationId === OPERATION_BUTTON_TYPE.UPDATE ? '修改任务' : '未知操作'}
open={operateState.openModal}
reloadData={handleCancel}/>}
</Fragment>
);
};
export default RightOption;

View File

@ -7,9 +7,8 @@ import {DataType} from "@/lib/definitions";
import './index.modules.css'
import dayjs from "dayjs";
import {getTaskState, taskPriorityList} from "@/lib/task/project/data";
import {List as VirtualizedList, AutoSizer} from "react-virtualized";
import 'react-virtualized/styles.css';
import OperationButton from "@/ui/task/OperationButton";
import RightOption from "@/ui/task/RightOption";
interface DroppableTableProps {
tableCode: string,
@ -35,13 +34,14 @@ export const DroppableTable = React.memo((props: DroppableTableProps) => {
const getItemStyle = React.useCallback((isDragging: boolean, draggableStyle: any): CSSProperties => ({
userSelect: "none",
background: isDragging ? "lightgreen" : "white",
position: 'relative', // 确保 zIndex 生效
zIndex: isDragging ? 2147483647 : 'auto', // 使用 'auto' 代替 0 可能更好
position: 'relative',
zIndex: isDragging ? 2147483647 : 'auto',
...draggableStyle,
borderBottom: '1px solid #f0f0f0',
boxSizing: 'border-box',
// verticalAlign: 'middle',
// textAlign: 'center',
verticalAlign: 'middle',
textAlign: 'center',
borderColor:taskPriorityList.find((item) => item.code === props.tableCode)?.color
}), []);
const getListStyle = React.useCallback((isDraggingOver: boolean) => ({
@ -50,55 +50,10 @@ export const DroppableTable = React.memo((props: DroppableTableProps) => {
width: '50vw'
}), []);
const rowRenderer = React.useCallback(({index, key, style}: {
index: number;
key: string;
style: React.CSSProperties
}) => {
const record = props.taskList[index];
if (!record) return null;
return (
<Draggable key={record.id} draggableId={record.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, {...provided.draggableProps.style, ...style})}
className="virtualized-row displayFlexRow"
>
<div style={{width: '15%'}} className='displayFlexRow'>
<Tooltip placement="topLeft" title={record.name} className='displayFlexRow'>
<div className='displayFlexRow'>{record.name}</div>
</Tooltip>
</div>
<div style={{width: '40%' , boxSizing: 'border-box', minWidth: 0}} className='displayFlexRow'>
<Tooltip placement="topLeft" title={record.description}>
<div className='displayFlexRow'>{record.description}</div>
</Tooltip>
</div>
<div style={{
width: '10%',
}} className='displayFlexRow'>{getTaskState(record.state) ? getTaskState(record.state).name : ""}</div>
<div style={{width: '25%'}} className='displayFlexColumn'>
<div>: {record.expectedStartTime ? dayjs(record.expectedStartTime).format("YYYY-MM-DD HH:mm") : ""}</div>
<div>: {record.expectedStartTime ? dayjs(record.expectedStartTime).format("YYYY-MM-DD HH:mm") : ""}</div>
</div>
<div style={{width: '10%'}} className='displayFlexRow'>
<OperationButton itemId={record.id} priority={record.priority} pid={record.pid}
pPid={record.pPid}/>
</div>
</div>
)}
</Draggable>
);
}, [props.taskList, getItemStyle]);
const headerStyle = React.useMemo(() => ({
backgroundColor: taskPriorityList.find((item) => item.code === props.tableCode)?.color,
height: '55px',
borderBottom: '1px solid #f0f0f0',
// borderBottom: '1px solid #f0f0f0',
fontWeight: 'bold'
}), [props.tableCode]);
@ -109,32 +64,54 @@ export const DroppableTable = React.memo((props: DroppableTableProps) => {
ref={provided.innerRef}
{...provided.droppableProps}
style={getListStyle(snapshot.isDraggingOver)}
className="droppable-table"
className="droppable-table scrollHidden"
>
<ConfigProvider>
{/* 表头 */}
<div style={headerStyle} className='displayFlexRow'>
<div style={{width: '15%'}} className='displayFlexRow'>{stateName}</div>
<div style={{width: '40%'}} className='displayFlexRow'></div>
<div style={{width: '20%'}} className='displayFlexRow'>{stateName}</div>
<div style={{width: '45%'}} className='displayFlexRow'></div>
<div style={{width: '15%'}} className='displayFlexRow'></div>
<div style={{width: '20%'}} className='displayFlexRow'></div>
<div style={{width: '10%'}} className='displayFlexRow'></div>
</div>
{/* 虚拟列表主体 */}
<div style={{height: 'calc(50vh - 76px)'}}>
<AutoSizer>
{({height, width}) => (
<VirtualizedList
className={"scrollHidden"}
height={height}
width={width}
rowCount={props.taskList.length}
rowHeight={80} // 与你的行高匹配
rowRenderer={rowRenderer}
overscanRowCount={8} // 预渲染的行数
/>
)}
</AutoSizer>
<div style={{height: 'calc(50vh - 76px)',overflow: 'auto'}}>
{props.taskList.map((record, index) => {
return <Draggable key={record.id} draggableId={record.id} index={index}>
{(provided, snapshot) => (
<RightOption itemId={record.id} pid={record.pid} pPid={record.pPid} children={<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, {...provided.draggableProps.style})}
className="virtualized-row displayFlexRow"
>
<div style={{width: '20%'}} className='displayFlexRow'>
<Tooltip placement="topLeft" title={record.name}
className='displayFlexRow'>
<div className='displayFlexRow'>{record.name}</div>
</Tooltip>
</div>
<div style={{width: '45%', boxSizing: 'border-box', minWidth: 0}}
className='displayFlexRow'>
<Tooltip placement="topLeft" title={record.description}>
<div className='displayFlexRow'>{record.description}</div>
</Tooltip>
</div>
<div style={{
width: '10%',
}}
className='displayFlexRow'>{getTaskState(record.state) ? getTaskState(record.state).name : ""}</div>
<div style={{width: '25%'}} className='displayFlexColumn'>
<div>: {record.expectedStartTime ? dayjs(record.expectedStartTime).format("YYYY-MM-DD HH:mm") : ""}</div>
<div>: {record.expectedEndTime ? dayjs(record.expectedEndTime).format("YYYY-MM-DD HH:mm") : ""}</div>
</div>
</div>}>
</RightOption>
)}
</Draggable>
})}
</div>
</ConfigProvider>
{provided.placeholder}

View File

@ -2,7 +2,6 @@
box-sizing: border-box;
border: 1px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.virtualized-row {

View File

@ -25,7 +25,7 @@ export type DetailModelFormProps={
// 祖宗任务id
pPid?:string,
// 操作id
operationId: number,
operationId: OPERATION_BUTTON_TYPE,
// 标题描述
description:string,
// 是否打开界面,用于非按钮操作