feat: Реализован drag-and-drop на странице канбан доски

- часть c задачами реализована с помощью websocket, а категории временно нет
This commit is contained in:
Vladiysss
2026-03-29 17:42:17 +03:00
parent b343d33184
commit e625795ce3
4 changed files with 190 additions and 12 deletions

View File

@@ -119,7 +119,9 @@ export const useBoardLogic = (id, setError, setInfo, setCategories, setLoading,
} }
await updateTaskAPI(editedTaskId, taskTitle, taskDescription, taskPosition, taskDeadline, taskCategory); await updateTaskAPI(editedTaskId, taskTitle, taskDescription, taskPosition, taskDeadline, taskCategory);
await loadBoardData(); await loadBoardData();
if(modalEditTask !== null) {
modalEditTask({}, null)(); modalEditTask({}, null)();
}
} catch (err) { } catch (err) {
setError(err.response?.data?.message || 'Ошибка редактирования задачи'); setError(err.response?.data?.message || 'Ошибка редактирования задачи');
} finally { } finally {

View File

@@ -13,6 +13,8 @@ const KBBoard = () => {
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [isOwner, setIsOwner] = useState(null); const [isOwner, setIsOwner] = useState(null);
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [draggedItem, setDraggedItem] = useState(null);
const [draggedType, setDraggedType] = useState(null);
const [memList, setMemList] = useState(false); const [memList, setMemList] = useState(false);
const [crTask, setCrTask] = useState(false); const [crTask, setCrTask] = useState(false);
@@ -36,12 +38,11 @@ const KBBoard = () => {
const [taskDescription, setTaskDescription] = useState(''); const [taskDescription, setTaskDescription] = useState('');
const [taskCategory, setTaskCategory] = useState(null); const [taskCategory, setTaskCategory] = useState(null);
const [taskCategori, setTaskCategori] = useState(null); const [taskCategori, setTaskCategori] = useState(null);
const [taskDeadline, setTaskDeadline] = useState(''); const [taskDeadline, setTaskDeadline] = useState(null);
const [assignedMembers, setAssignedMembers] = useState(null); const [assignedMembers, setAssignedMembers] = useState(null);
const [editedTask, setEditedTask] = useState({}); const [editedTask, setEditedTask] = useState({});
const [editedCateg, setEditedCateg] = useState({}); const [editedCateg, setEditedCateg] = useState({});
const [taskPosition, setTaskPosition] = useState(null); const [taskPosition, setTaskPosition] = useState(null);
const [categoryPosition, setCategoryPosition] = useState(null);
const [assignedMember, setAssignedMember] = useState(0); const [assignedMember, setAssignedMember] = useState(0);
const [addedUsername, setAddedUsername] = useState(''); const [addedUsername, setAddedUsername] = useState('');
const [deletedMember, setDeletedMember] = useState(''); const [deletedMember, setDeletedMember] = useState('');
@@ -78,6 +79,24 @@ const KBBoard = () => {
); );
}; };
const [socket, setSocket] = useState(null);
useEffect(() => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//https://back.fool-stack.ru/api/boards/ws/`+id);
ws.onopen = () => {
console.log('WebSocket соединение установлено');
setSocket(ws);
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
loadBoardData();
};
ws.onclose = () => console.log('WebSocket соединение закрыто');
ws.onerror = (error) => console.error('Ошибка WebSocket:', error);
return () => ws.close();
}, [id, loadBoardData]);
useEffect(() => { useEffect(() => {
if (id) loadBoardData(); if (id) loadBoardData();
}, [id, loadBoardData]); }, [id, loadBoardData]);
@@ -94,7 +113,7 @@ const KBBoard = () => {
setTaskCategori(categori); setTaskCategori(categori);
setTaskTitle(''); setTaskTitle('');
setTaskDescription(''); setTaskDescription('');
setTaskDeadline('') setTaskDeadline(null)
} }
const modalCrCateg = () => { const modalCrCateg = () => {
setCrCateg(!crCateg); setCrCateg(!crCateg);
@@ -119,7 +138,6 @@ const KBBoard = () => {
setEdCateg(!edCateg); setEdCateg(!edCateg);
setEditedCateg(categ); setEditedCateg(categ);
setCategoryTitle(categ.title); setCategoryTitle(categ.title);
setCategoryPosition(categ.position);
} }
const modalDelTask = () => { const modalDelTask = () => {
setDelTask(!delTask); setDelTask(!delTask);
@@ -194,6 +212,137 @@ const KBBoard = () => {
await deleteMember(id, deletedMember, modalDeleteMember); await deleteMember(id, deletedMember, modalDeleteMember);
}; };
const handleCategoryDragStart = (e, category) => {
e.stopPropagation();
setDraggedType('c');
setDraggedItem(category);
e.dataTransfer.setData('text/plain', JSON.stringify({
id: category.id,
type: 'c',
position: category.position
}));
e.currentTarget.style.opacity = '0.5';
};
const handleTaskDragStart = (e, task, categoryId) => {
e.stopPropagation();
setDraggedType('t');
setDraggedItem({ ...task, categoryId });
e.dataTransfer.setData('text/plain', JSON.stringify({
id: task.id,
type: 't',
position: task.position,
categoryId: categoryId
}));
e.currentTarget.style.opacity = '0.5';
e.currentTarget.style.transform = 'scale(0.95)';
};
const handleDragEnd = (e) => {
setDraggedType(null);
setDraggedItem(null);
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'scale(1)';
};
const handleDragOver = (e) => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
if (!data) return;
try {
const draggedData = JSON.parse(data);
if (draggedData.type === 't') {
e.currentTarget.style.boxShadow = '0 0 10px rgba(0, 0, 255, 0.3)';
e.currentTarget.style.border = '2px dashed #007bff';
} else if (draggedData.type === 'c') {
e.currentTarget.style.backgroundColor = '#f0f0f0';
}
} catch (err) {
console.error('Ошибка парсинга данных drag:', err);
}
};
const handleDragLeave = (e) => {
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.boxShadow = '';
e.currentTarget.style.border = '';
};
const handleCategoryReorder = async (e, targetPosition) => {
e.preventDefault();
e.currentTarget.style.boxShadow = '';
e.currentTarget.style.border = '';
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.transform = '';
const data = e.dataTransfer.getData('text/plain');
if (!data) return;
try {
const draggedData = JSON.parse(data);
if (draggedData.type !== 'c') return;
if (draggedType === 'c' && draggedItem.position !== targetPosition) {
setCategories((prevItems) => {
const draggedIndex = prevItems.findIndex(item => item.position === draggedItem.position);
const targetIndex = prevItems.findIndex(item => item.position === targetPosition);
const newItems = [...prevItems];
newItems.splice(draggedIndex, 1);
newItems.splice(targetIndex, 0, draggedItem);
const updatedItems = newItems.map((item, index) => ({
...item,
position: index
}));
return updatedItems;
});
await editCategoryPosition(draggedItem.id, targetPosition);
}
} catch (err) {
console.error('Ошибка при переупорядочивании категорий:', err);
setError('Не удалось переупорядочить категории');
}
};
const handleTaskReorder = async (e, targetPosition, targetCategoryPos) => {
e.stopPropagation();
e.preventDefault();
e.currentTarget.style.boxShadow = '';
e.currentTarget.style.border = '';
e.currentTarget.style.backgroundColor = '';
e.currentTarget.style.transform = '';
const data = e.dataTransfer.getData('text/plain');
if (!data) return;
if (!draggedItem || draggedType !== 't') return;
try {
const draggedData = JSON.parse(data);
// Обрабатываем только задачи
if (draggedData.type !== 't' || !draggedItem) return;
// Находим целевую категорию по позиции
const targetCat = categories.find(cat => cat.position === targetCategoryPos);
if (!targetCat) return;
// Проверяем, что задача не перемещается в ту же позицию той же категории
if ((draggedItem.position === targetPosition ||
(draggedItem.position === targetCat.length-1 && targetPosition === targetCat.length)) &&
draggedItem.category_id === targetCat.id) return;
await editTask( draggedItem.id, draggedItem.title,
draggedItem.description, targetPosition,
draggedItem.deadline, targetCat.id, null );
} catch (err) {
console.error('Ошибка при переупорядочивании задач:', err);
setError('Не удалось переупорядочить задачи');
}
};
return ( return (
<div className="app-container"> <div className="app-container">
@@ -260,12 +409,16 @@ const KBBoard = () => {
</div> </div>
<div className="board-panel" > <div className="board-panel" >
{categories.map((category) => ( {categories.map((category) => (
<div className="categori" key={category.position}> <div className="categori" key={category.position}
draggable="true"
onDragStart={(e) => handleCategoryDragStart(e, category)}
onDragLeave={handleDragLeave}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDrop={(e) => handleCategoryReorder(e, category.position)}
>
<button onClick={modalEditCateg(category)}><h3>{category.title}</h3></button> <button onClick={modalEditCateg(category)}><h3>{category.title}</h3></button>
<div className="categ-h">
<p>Позиция: {category.position}</p>
<p>Задач: {category.tasks.length}</p>
</div>
<div className='task create'> <div className='task create'>
<button onClick={modalCrTask(category.id)}> <button onClick={modalCrTask(category.id)}>
Новая задача Новая задача
@@ -274,9 +427,17 @@ const KBBoard = () => {
<div className='task-list'> <div className='task-list'>
{category.tasks.length > 0 ? ( {category.tasks.length > 0 ? (
category.tasks.map((task) => ( category.tasks.map((task) => (
<button className='task' onClick={modalEditTask(task, category.id)} key={task.id}> <button className='task' onClick={modalEditTask(task, category.id)} key={task.position}
draggable="true"
onDragStart={(e) => handleTaskDragStart(e, task, category.position)}
onDragLeave={handleDragLeave}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDrop={(e) => handleTaskReorder(e, task.position, category.position)}
>
<div>{task.title}</div> <div>{task.title}</div>
<div>{task.description}</div> <div>{task.description} </div>
{task.deadline && ( {task.deadline && (
<div>Дедлайн: {new Date(task.deadline).toLocaleString('ru-RU', { <div>Дедлайн: {new Date(task.deadline).toLocaleString('ru-RU', {
day: '2-digit', day: '2-digit',
@@ -299,6 +460,12 @@ const KBBoard = () => {
) : ( ) : (
<p>Нет задач</p> <p>Нет задач</p>
)} )}
<div className='task move'
onDrop={(e) => handleTaskReorder(e, category.tasks.length, category.position)}
onDragLeave={handleDragLeave}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
></div>
</div> </div>
</div> </div>
))} ))}

View File

@@ -260,6 +260,14 @@
margin-right: 0px; margin-right: 0px;
} }
.task.move {
min-height: 24px;
margin-top: 0;
opacity: 0;
max-height: 100%;
height: 100%;
}
label { label {
text-align: left; text-align: left;
} }

View File

@@ -5,6 +5,7 @@ module.exports = function(app) {
'/api', '/api',
createProxyMiddleware({ createProxyMiddleware({
target: 'https://back.fool-stack.ru', target: 'https://back.fool-stack.ru',
ws: true,
changeOrigin: true, changeOrigin: true,
pathRewrite: { '^/api': '/api' }, pathRewrite: { '^/api': '/api' },
}) })