feat:去除四象限滚动条

This commit is contained in:
1708-huayu 2025-06-26 18:59:31 +08:00
parent 47aeb7c228
commit 135bf4ff41
13 changed files with 293 additions and 180 deletions

2
.gitignore vendored
View File

@ -12,7 +12,7 @@
# next.js
/.next/
/out/
/docker/out/
# production
/build

52
package-lock.json generated
View File

@ -22,6 +22,7 @@
"react-big-calendar": "^1.12.2",
"react-device-detect": "^2.2.3",
"react-dom": "^18",
"react-virtualized": "^9.22.6",
"sass": "^1.77.3",
"tailwindcss": "3.3.3"
},
@ -31,6 +32,7 @@
"@types/react": "^18",
"@types/react-big-calendar": "^1.8.9",
"@types/react-dom": "^18",
"@types/react-virtualized": "^9.22.2",
"@vercel/style-guide": "^5.0.1",
"eslint": "^8",
"eslint-config-next": "14.2.29",
@ -1647,6 +1649,16 @@
"redux": "^4.0.0"
}
},
"node_modules/@types/react-virtualized": {
"version": "9.22.2",
"resolved": "https://registry.npmmirror.com/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/react": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -6318,6 +6330,23 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/react-virtualized": {
"version": "9.22.6",
"resolved": "https://registry.npmmirror.com/react-virtualized/-/react-virtualized-9.22.6.tgz",
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
"dependencies": {
"@babel/runtime": "^7.7.2",
"clsx": "^1.0.4",
"dom-helpers": "^5.1.3",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz",
@ -8919,6 +8948,16 @@
"redux": "^4.0.0"
}
},
"@types/react-virtualized": {
"version": "9.22.2",
"resolved": "https://registry.npmmirror.com/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/react": "*"
}
},
"@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -12437,6 +12476,19 @@
}
}
},
"react-virtualized": {
"version": "9.22.6",
"resolved": "https://registry.npmmirror.com/react-virtualized/-/react-virtualized-9.22.6.tgz",
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
"requires": {
"@babel/runtime": "^7.7.2",
"clsx": "^1.0.4",
"dom-helpers": "^5.1.3",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-lifecycles-compat": "^3.0.4"
}
},
"reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz",

View File

@ -23,6 +23,7 @@
"react-big-calendar": "^1.12.2",
"react-device-detect": "^2.2.3",
"react-dom": "^18",
"react-virtualized": "^9.22.6",
"sass": "^1.77.3",
"tailwindcss": "3.3.3"
},
@ -32,6 +33,7 @@
"@types/react": "^18",
"@types/react-big-calendar": "^1.8.9",
"@types/react-dom": "^18",
"@types/react-virtualized": "^9.22.2",
"@vercel/style-guide": "^5.0.1",
"eslint": "^8",
"eslint-config-next": "14.2.29",

View File

@ -21,8 +21,11 @@ export default function RootLayout({
<title></title>
<link rel="icon" href="/favicon.ico"/>
<script src="/static/iconfont.js"></script>
{/*FOUCFlash of Unstyled Content*/}
{/*<link rel="preload" href="https://cdn.jsdelivr.net/npm/antd/dist/antd.min.css" as="style"/>*/}
{/*<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/antd/dist/antd.min.css"/>*/}
</head>
<body style={{margin: 0,padding:0,maxHeight:"100vh",overflow:"hidden"}}>{children}</body>
<body style={{margin: 0, padding: 0, maxHeight: "100vh", overflow: "hidden"}}>{children}</body>
</html>
);
}

View File

@ -2,11 +2,10 @@
* 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, { useState} from 'react';
import React, {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 {useEffect} from "react";
import {TaskSelectVO} from "@/lib/task/drag/data";
import {selectTaskAPI} from "@/lib/task/drag/service";
@ -14,13 +13,13 @@ export default function Layout({children}: { children: React.ReactNode }) {
const [allTaskList, setAllTaskList] = useState<DataType[]>([]);
useEffect(() => {
addData()
},[])
}, [])
async function addData() {
const requestParam: Request<TaskSelectVO> = {
pageSize: 1000,
pageNumber: 1,
data: {state:'9'}
data: {state: '9'}
}
const res = await selectTaskAPI(requestParam)
setAllTaskList(res.data.content)
@ -41,9 +40,8 @@ export default function Layout({children}: { children: React.ReactNode }) {
}
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<div style={{display: 'flex',flexWrap: 'wrap',boxSizing: 'border-box',width:'100vw'}}>
return (<div style={{display: 'flex', flexWrap: 'wrap', boxSizing: 'border-box', width: '100vw'}}>
<DragDropContext onDragEnd={onDragEnd}>
{/* 紧急重要 */}
<DroppableTable tableCode='3' taskList={allTaskList.filter(task => task.priority == '3')}/>
{/* 不紧急重要 */}
@ -52,7 +50,7 @@ export default function Layout({children}: { children: React.ReactNode }) {
<DroppableTable tableCode="1" taskList={allTaskList.filter(task => task.priority == '1')}/>
{/* 不紧急不重要 */}
<DroppableTable tableCode='0' taskList={allTaskList.filter(task => task.priority == '0')}/>
</div>
</DragDropContext>
);
</DragDropContext></div>
)
;
}

View File

@ -1,3 +0,0 @@
export default function Page() {
return <p>Dashboard test</p>;
}

View File

@ -88,6 +88,15 @@ export async function deleteTaskAPI(id: string): Promise<ResponseVO<string>> {
return response.data;
}
export function getTaskState(code:string){
return taskStateList.filter(taskState => code==taskState.code)[0];
}
export const TASK_TYPE:{label:string,value:string}[]=[
{label:"常规",value:"0"},
{label:"团队",value:"1"},
{label:"顺序",value:"2"},
{label:"周期",value:"3"}
]
//0重要紧急红色1,重要不紧急黄色2不重要紧急灰色3不重要不紧急绿色
export const taskPriorityList: DictType[] = [
{

View File

@ -31,3 +31,23 @@ html {
overflow: hidden;
padding-right: 0.5em;
}
.displayFlexRow{
display: flex;
justify-content: flex-start; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
.displayFlexColumn{
display: flex;
flex-direction: column;
justify-content: flex-start; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
.scrollHidden{
overflow: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE和Edge */
}
.scrollHidden::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}

View File

@ -163,7 +163,7 @@ class OperationButton extends React.Component<OperationButtonProps, OperationMod
},
{
key: OPERATION_BUTTON_TYPE.SHOW_FOUR,
label: <Link href={"/task/four?pid=" + this.props.itemId}></Link>,
label: <Link href={"/task/drag?pid=" + this.props.itemId}></Link>,
},
{
key: OPERATION_BUTTON_TYPE.SHOW_CALENDAR,

View File

@ -43,9 +43,9 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
}}></Button>
}
{
!usePathname().startsWith("/task/four") &&
!usePathname().startsWith("/task/drag") &&
<Button type="primary" onClick={() => {
replace("/task/four");
replace("/task/drag");
// setCurrentPath("/task/four");
}}></Button>
}
@ -78,7 +78,7 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
}
{
/*四相线需要状态时间*/
usePathname().startsWith("/task/four") && <Fragment>
usePathname().startsWith("/task/drag") && <Fragment>
<span style={{whiteSpace: 'nowrap'}}>:</span>
<RangePicker
placeholder={['开始时间', '结束时间']}
@ -114,6 +114,12 @@ export const TitleOperation: React.FC<TitleOperationProps> = ({
/>
</Fragment>
}
{
/*日历需要状态*/
!usePathname().startsWith("/task/project") && <Fragment>
<Button></Button>
</Fragment>
}
</Space>
<svg style={{height: "32px", width: "32px", alignItems: "center"}} className="icon" aria-hidden="true">
<use xlinkHref="#icon-user__easyico"></use>

View File

@ -1,176 +1,145 @@
'use client'
import React, {CSSProperties, useEffect} from "react";
import React, {CSSProperties} from "react";
import {Draggable, Droppable} from "react-beautiful-dnd";
import {ConfigProvider, Table, TableProps, Tooltip} from "antd";
import {ConfigProvider, Tooltip} from "antd";
import {DataType} from "@/lib/definitions";
import './index.modules.css'
import dayjs, {Dayjs} from "dayjs";
import {taskPriorityList} from "@/lib/task/project/data";
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";
interface DroppableTableProps // extends DragDropContextProps
{
tableCode:string,
taskList:DataType[]
interface DroppableTableProps {
tableCode: string,
taskList: DataType[]
}
export const DroppableTable: React.FC<DroppableTableProps> = (props) => {
const [stateName, setStateName] = React.useState("");
useEffect(() => {
if(props.tableCode=='0'){
setStateName('不紧急不重要');
}else if (props.tableCode=='1'){
setStateName('紧急不重要');
}else if (props.tableCode=='2'){
setStateName('不紧急重要');
}else if (props.tableCode=='3'){
setStateName('紧急重要');
export const DroppableTable = React.memo((props: DroppableTableProps) => {
const stateName = React.useMemo(() => {
switch (props.tableCode) {
case '0':
return '不紧急不重要';
case '1':
return '紧急不重要';
case '2':
return '不紧急重要';
case '3':
return '紧急重要';
default:
return '';
}
}, [props]);
const getItemStyle = (isDragging:any, draggableStyle:any):CSSProperties => {
console.log({draggableStyle})
return {
// some basic styles to make the items look a bit nicer
}, [props.tableCode]);
const getItemStyle = React.useCallback((isDragging: boolean, draggableStyle: any): CSSProperties => ({
userSelect: "none",
// change background colour if dragging
background: isDragging ? "lightgreen" : "grey",
// styles we need to apply on draggables
background: isDragging ? "lightgreen" : "white",
position: 'relative', // 确保 zIndex 生效
zIndex: isDragging ? 2147483647 : 'auto', // 使用 'auto' 代替 0 可能更好
...draggableStyle,
height:80
}};
borderBottom: '1px solid #f0f0f0',
boxSizing: 'border-box',
// verticalAlign: 'middle',
// textAlign: 'center',
}), []);
const getListStyle = (isDraggingOver:boolean) => ({
background: isDraggingOver ? "lightblue" : "lightgrey",
overflow:'hidden'
});
const getListStyle = React.useCallback((isDraggingOver: boolean) => ({
background: isDraggingOver ? "lightblue" : "white",
height: 'calc((100vh - 42px)/2)',
width: '50vw'
}), []);
function isDayjs(obj: unknown): obj is Dayjs {
return obj != null && typeof (obj as Dayjs).format === 'function';
}
const rowRenderer = React.useCallback(({index, key, style}: {
index: number;
key: string;
style: React.CSSProperties
}) => {
const record = props.taskList[index];
if (!record) return null;
// 渲染表格行
// @ts-ignore
const renderTableRow = (data: DataType[], droppableId: string): TableProps<Item>['components']['body']['row'] =>
(props:any) => {
// console.log({props, droppableId});
const { children, ...restProps } = props;
const record = data.find((item) => item.id === restProps['data-row-key']);
const index = data.findIndex((item) => item.id === restProps['data-row-key']);
if (!record) {
// console.error('Invalid record at index:', index);
return null; // 或者返回一个占位符
}
return (
<Draggable key={record.id} draggableId={record.id} index={index}>
{(provided,snapshot) => (
<tr
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
{/*<td>{record.code}</td>*/}
<td>{record.name}</td>
<td>{record?.description}</td>
<td>{record.state}</td>
<td>:{record.expectedStartTime? dayjs(record.expectedStartTime).format("YYYY-MM-DD HH:mm") :""}<br/>
:{record.expectedStartTime? dayjs(record.expectedStartTime).format("YYYY-MM-DD HH:mm") : ""}</td>
</tr>
)}
</Draggable>
);
};
const calculateScrollHeight = () => {
// 计算50%视窗高度减去21px
const height = `calc(50vh - 80px)`;
return { y: height };
};
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',
fontWeight: 'bold'
}), [props.tableCode]);
return (
<Droppable droppableId={props.tableCode} key={props.tableCode} >
{(provided,snapshot) => (
<div ref={provided.innerRef}
{...provided.droppableProps}
style={getListStyle(snapshot.isDraggingOver)}
className="droppable-table"
<Droppable droppableId={props.tableCode} key={props.tableCode}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={getListStyle(snapshot.isDraggingOver)}
className="droppable-table"
>
<ConfigProvider
theme={{
components: {
Table: {
headerBg: taskPriorityList.find((item) => item.code === props.tableCode)?.color
/* 这里是你的组件 token */
},
},
}}
>
<Table<DataType>
dataSource={props.taskList}
scroll={calculateScrollHeight()}
columns={[
// {
// title: '任务编码',
// dataIndex: 'code',
// key: 'code',
// width: '10%',
// },
{
title: stateName,
dataIndex: 'name',
key: 'name',
width: '10%',
ellipsis: {
showTitle: false,
},
render: (address) => (
<Tooltip placement="topLeft" title={address}>
{address}
</Tooltip>
),
},
{
title: '任务描述',
dataIndex: 'description',
key: 'description',
width: '20%',
ellipsis: {
showTitle: false,
},
render: (description) => (
<Tooltip placement="topLeft" title={description}>
{description}
</Tooltip>
),
},
{
title: '任务状态',
dataIndex: 'state',
width: '5%',
key: 'state',
},
{
title: '期望时间',
dataIndex: 'expectedStartTime',
width: '10%',
key: 'expectedStartTime',
},
]}
rowKey="id"
components={{
body: {
row: renderTableRow(props.taskList, props.tableCode),
},
}}
pagination={false}
/>
<ConfigProvider>
{/* 表头 */}
<div style={headerStyle} className='displayFlexRow'>
<div style={{width: '15%'}} className='displayFlexRow'>{stateName}</div>
<div style={{width: '40%'}} 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>
</ConfigProvider>
{provided.placeholder}
</div>
)}
</Droppable>
)
}
});

View File

@ -1,5 +1,36 @@
.droppable-table{
width: 50vw;
height: calc(50vh - 21px);
overflow-y: auto;
.droppable-table {
box-sizing: border-box;
border: 1px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.virtualized-row {
display: flex;
align-items: center;
}
.text-ellipsis {
/* 单行省略 */
/*width: 100%;*/
/*box-sizing: border-box;*/
/*white-space: nowrap;*/
/*overflow: hidden;*/
/*text-overflow: ellipsis;*/
width: 100%;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5em; /* 根据实际字体大小调整 */
max-height: 3em; /* 2行 x 1.5em */
word-break: break-word; /* 允许长单词换行 */
}
/* 拖拽时的样式增强 */
.ReactVirtualized__List {
outline: none;
}

View File

@ -11,7 +11,7 @@ import React, {useEffect, useState} from "react";
import {
addTask, deleteTask, getTask,
getTaskTreeResult,
OPERATION_BUTTON_TYPE,
OPERATION_BUTTON_TYPE, TASK_TYPE,
taskPriorityList,
taskStateList, updateTask
} from "@/lib/task/project/data";
@ -44,6 +44,8 @@ export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => {
const [form] = Form.useForm<DataType>();
const [pid, setPid] = useState<string>(props.pid?props.pid:'0');
const [editFormDisable, setEditFormDisable] = useState(props.operationId === OPERATION_BUTTON_TYPE.DETAIL)
// 团队第一层 pid必须为0
const [taskType, setTaskType] = useState('0')
useEffect(() => {
if (props.itemId!=undefined&&(
props.operationId === OPERATION_BUTTON_TYPE.DETAIL || props.operationId === OPERATION_BUTTON_TYPE.UPDATE)) {
@ -209,8 +211,31 @@ export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => {
<ProFormText width="sm" name="code" initialValue={props.itemId} hidden={true} label="任务编码" />
<ProFormText width="sm" name="pPid" initialValue={props.pPid} hidden={true} label="祖宗id" />
<ProForm.Group>
<ProFormSelect
required={true}
options={TASK_TYPE}
width="sm"
name="taskType"
label="任务类型"
initialValue='0'
disabled ={editFormDisable}
onChange={(value:string, option)=>{
setTaskType(value)
}}
/>
<ProFormText
required={true}
hidden={taskType!='1'}
width="sm"
name="fName"
label="团队名称"
tooltip="最长为 10 位"
placeholder="请输入团队名称"
disabled ={editFormDisable}
/>
<ProFormTreeSelect
width="md"
hidden={taskType=='1'}
width="sm"
request={() =>{
return getTaskTreeResult(JSON.stringify(
{pageSize:1000,pageNumber:1,data:[{code:'pid',value:'0',operateType:'='},{code:'state',value:'8,9',operateType:'IN'},{code:'',value:true,operateType: "TREE"}]}
@ -222,10 +247,11 @@ export const DetailModelForm: React.FC<DetailModelFormProps> = (props) => {
disabled ={editFormDisable}
/>
<ProFormText
width="md"
required={true}
width="sm"
name="name"
label="任务名称"
tooltip="最长为 24 位"
tooltip="最长为 10 位"
placeholder="请输入任务名称"
disabled ={editFormDisable}
/>