- 这段代码是一个基于 React 18+ 和 Tailwind CSS 的练手 Demo,灵感来自渡一的拖拽教程。直接上代码,注释已经在代码里面了。由于我的 React 水平有限,而且 Demo 也不符合 React 的最佳实践和规范,如果发现任何问题,请大佬们多多指教。

import { useEffect, useRef, useState } from "react";
import { Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import "./DropTable.scss";
function DropTable() {
const WEEK_DAYS = [
{ label: "星期一", value: "monday" },
{ label: "星期二", value: "tuesday" },
{ label: "星期三", value: "wednesday" },
{ label: "星期四", value: "thursday" },
{ label: "星期五", value: "friday" },
{ label: "星期六", value: "saturday" },
{ label: "星期日", value: "sunday" },
];
const COURSES = 4;
const COURSE_NAMES = [
{ label: "语文", value: "chinese", bgColor: "#463f34" },
{ label: "数学", value: "math", bgColor: "#4387fb" },
{ label: "英语", value: "english", bgColor: "#ccc" },
{ label: "政治", value: "politics", bgColor: "tomato" },
{ label: "历史", value: "history", bgColor: "green" },
{ label: "地理", value: "geography", bgColor: "#658682" },
{ label: "化学", value: "chemical", bgColor: "#23fb67" },
{ label: "物理", value: "physics", bgColor: "#986fb2" },
{ label: "体育", value: "sports", bgColor: "#ff1900" },
];
let currentSource;
let currentTarget;
const divRef = useRef(null);
const [tableData, setTableData] = useState(() => {
const initData = JSON.parse(localStorage.getItem("submitData") || "[]");
return initData.map(item => item.data).flat(1);
});
const getDropNode = node => {
while (node) {
if (node.dataset?.drop) return node;
node = node.parentNode;
}
return null;
};
const clearStyles = () => {
document.querySelectorAll(".active_drag").forEach(node => {
node.classList.remove("active_drag");
});
};
const handleDrag = () => {
const DOM = divRef.current;
DOM.ondragstart = event => {
const target = event.target;
currentSource = target;
event.dataTransfer.effectAllowed = target.dataset["effect"];
event.dataTransfer.setData("value", target.dataset["value"]);
if (target.dataset["effect"] === "move") {
currentTarget = target.parentNode;
}
};
DOM.ondragover = event => {
event.preventDefault();
};
DOM.ondragenter = event => {
clearStyles();
const dropNode = getDropNode(event.target);
if (!dropNode) return;
if (currentSource.dataset["effect"] === dropNode.dataset["drop"]) {
dropNode.classList.add("active_drag");
}
};
DOM.ondrop = event => {
const dropNode = getDropNode(event.target);
if (!dropNode) return;
if (currentSource.dataset["effect"] !== dropNode.dataset["drop"]) return;
clearStyles();
if (dropNode.dataset["drop"] === "copy") {
const cloneNode = currentSource.cloneNode(true);
cloneNode.dataset["effect"] = "move";
dropNode.innerHTML = "";
dropNode.appendChild(cloneNode);
const sourceData = event.dataTransfer.getData("value");
const targetData = dropNode.dataset["value"];
const tableValue = { courses: JSON.parse(sourceData), weeks: JSON.parse(targetData) };
setTableData(prevData => {
const existingItem = prevData.find(({ weeks }) => {
if (weeks.week === tableValue.weeks.week && weeks.course === tableValue.weeks.course) {
return true;
}
return false;
});
if (existingItem) {
if (existingItem.courses.value === tableValue.courses.value) {
return prevData;
} else {
return [...prevData.filter(item => JSON.stringify(item) !== JSON.stringify(existingItem)), tableValue];
}
}
return [...prevData, tableValue];
});
} else {
currentSource.remove();
const sourceData = event.dataTransfer.getData("value");
const targetData = currentTarget.dataset["value"];
const tableValue = { courses: JSON.parse(sourceData), weeks: JSON.parse(targetData) };
setTableData(prevData => {
return prevData.filter(item => JSON.stringify(item) !== JSON.stringify(tableValue));
});
}
};
};
const handleTableData = array => {
const list = Array.from(WEEK_DAYS.map(item => ({ ...item, data: [] })));
for (let i = 0, len = list.length; i < len; i++) {
const element = list[i];
array.forEach(item => {
if (element.value === item.weeks.week) element.data.push(item);
});
element.data.sort((item1, item2) => item1.weeks.course - item2.weeks.course);
}
return list;
};
const onSubmit = () => {
const submitData = handleTableData(tableData);
localStorage.setItem("submitData", JSON.stringify(submitData));
};
const handleEchoData = () => {
const echoData = JSON.parse(localStorage.getItem("submitData") || "[]");
if (!echoData.length) return;
const tdNodes = document.querySelectorAll("tbody td");
const nodeValueList = Array.from(tdNodes)
.filter(node => node.dataset.value)
.map(node => ({ weeks: JSON.parse(node.dataset.value), node }));
const nodeData = handleTableData(nodeValueList);
for (let index = 0, len = echoData.length; index < len; index++) {
const nodeItem = nodeData[index];
const weekItem = echoData[index];
const nodeLen = nodeItem.data.length;
const weekLen = weekItem.data.length;
outerLoop: for (let i = 0; i < nodeLen; i++) {
const item = nodeItem.data[i];
if (item.node.children.length) item.node.innerHTML = "";
for (let j = 0; j < weekLen; j++) {
const { weeks, courses } = weekItem.data[j];
if (item.weeks.course === weeks.course) {
const div = document.createElement("div");
div.className = "h-40 leading-[40px] text-center text-white mt-4 first:mt-0";
div.style.backgroundColor = courses.bgColor;
div.setAttribute("draggable", "true");
div.setAttribute("data-effect", "move");
div.setAttribute("data-value", JSON.stringify({ courses }));
div.innerHTML = courses.label;
item.node.appendChild(div);
continue outerLoop;
}
}
}
}
};
useEffect(() => {
handleEchoData();
}, []);
useEffect(() => {
handleDrag();
}, [tableData]);
return (
<div>
<div className="text-center font-bold text-24">课程安排</div>
{/* */}
<div className="flex items-center justify-center mt-24" ref={divRef}>
{/* 课程名称列表 */}
<div className="mr-24 w-100 h-full border border-[#ccc] border-solid p-4" data-drop="move">
{COURSE_NAMES.map(item => {
return (
<div
className="h-40 leading-[40px] text-center text-white mt-4 first:mt-0"
style={{ backgroundColor: item.bgColor }}
key={item.label}
draggable
data-effect="copy"
data-value={JSON.stringify(item)}
>
{item.label}
</div>
);
})}
</div>
{/* 课程表 */}
<table className="border-collapse w-800 text-center select-none">
<thead className="h-40">
<tr>
{/* 占位 */}
<th className="border border-[#ccc] border-solid"></th>
{WEEK_DAYS.map(item => {
return (
<td className="border border-[#ccc] border-solid" key={item.value}>
{item.label}
</td>
);
})}
</tr>
</thead>
<tbody>
<tr className="h-43 box-border">
<td className="border border-[#ccc] border-solid" rowSpan={4}>
上午
</td>
{WEEK_DAYS.map(item => {
return (
<td
className="border border-[#ccc] border-solid"
key={item.value}
data-drop="copy"
data-value={JSON.stringify({ week: item.value, course: 1 })}
></td>
);
})}
</tr>
{Array.from({ length: COURSES - 1 }).map((_, index) => {
return (
<tr className="h-43 box-border" key={index}>
{WEEK_DAYS.map(item => {
return (
<td
className="border border-[#ccc] border-solid"
key={item.value}
data-drop="copy"
data-value={JSON.stringify({ week: item.value, course: index + 2 })}
></td>
);
})}
</tr>
);
})}
{/* */}
<tr className="h-43 box-border">
<td className="border border-[#ccc] border-solid" rowSpan={4}>
下午
</td>
{WEEK_DAYS.map(item => {
return (
<td
className="border border-[#ccc] border-solid"
key={item.value}
data-drop="copy"
data-value={JSON.stringify({ week: item.value, course: 5 })}
></td>
);
})}
</tr>
{Array.from({ length: COURSES - 1 }).map((_, index) => {
return (
<tr className="h-43 box-border" key={index}>
{WEEK_DAYS.map(item => {
return (
<td
className="border border-[#ccc] border-solid"
key={item.value}
data-drop="copy"
data-value={JSON.stringify({ week: item.value, course: index + 6 })}
></td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
{/* */}
<div className="flex justify-center mt-24">
<Button type="primary" icon={<PlusOutlined />} onClick={onSubmit}>
保存课程表
</Button>
</div>
</div>
);
}
export default DropTable;