看板应用LocalStorage数据恢复及拖拽样式、代码优化求助
看板应用问题解决方案
问题概述
开发的看板应用包含「未开始」「进行中」「已完成」三列,已实现任务增删改及列间拖拽功能,数据存储在LocalStorage,但页面加载时无法正确恢复数据,同时需要拖拽样式优化和代码优化方案。
一、LocalStorage数据恢复问题修复
你当前直接存储HTML结构的方式不仅冗余,还会丢失事件绑定,导致恢复后功能失效。正确的做法是存储任务数据而非DOM结构,页面加载时根据数据重新渲染任务并绑定事件。
修复步骤
- 定义结构化的任务数据数组,维护各列的任务信息
- 页面加载时从LocalStorage读取数据,同步到本地数据结构
- 根据数据渲染列和任务,替换直接存储HTML的逻辑
- 修改存储函数,只保存任务数据而非DOM结构
修改后的核心代码
const board = document.getElementById("container"); // 初始化列数据,包含任务列表 const columns = [ { id: 1, title: "未开始", tasks: [] }, { id: 2, title: "进行中", tasks: [] }, { id: 3, title: "已完成", tasks: [] }, ]; let drag = null; // 从LocalStorage加载数据 function loadFromLocalStorage() { const savedData = localStorage.getItem("kanbanTasks"); if (savedData) { const parsedData = JSON.parse(savedData); columns.forEach(col => { const savedCol = parsedData.find(c => c.id === col.id); if (savedCol) col.tasks = savedCol.tasks; }); } } // 保存数据到LocalStorage const updateLocalStorage = () => { localStorage.setItem("kanbanTasks", JSON.stringify(columns)); }; // 创建列并渲染已有任务 function renderColumns() { columns.forEach((column) => { const columnDiv = document.createElement("div"); columnDiv.className = "column"; columnDiv.id = column.id; // 列标题 const columnTitle = document.createElement("h3"); columnTitle.textContent = column.title; columnDiv.appendChild(columnTitle); // 任务列表 const tasksList = document.createElement("ul"); tasksList.className = "tasks"; // 渲染已保存的任务 column.tasks.forEach(task => { const taskItem = createTaskElement(task.id, task.content); tasksList.appendChild(taskItem); }); columnDiv.appendChild(tasksList); // 添加任务按钮 const addTaskBtn = document.createElement("button"); addTaskBtn.className = "btn"; addTaskBtn.textContent = "+ 添加任务"; addTaskBtn.addEventListener("click", () => { const taskItem = createTaskElement(Date.now(), ""); // 用时间戳做临时ID tasksList.appendChild(taskItem); taskItem.querySelector(".field").focus(); }); columnDiv.appendChild(addTaskBtn); board.appendChild(columnDiv); }); // 统一绑定事件 bindTaskEvents(); bindDragDropEvents(); } // 创建单个任务DOM元素 function createTaskElement(id, content) { const inputContainer = document.createElement("li"); inputContainer.className = "input-container"; inputContainer.dataset.taskId = id; inputContainer.draggable = !!content; // 有内容才允许拖拽 inputContainer.innerHTML = ` <input type='text' class='field' value='${content}' placeholder='输入任务${content ? "" : "内容"}' ${content ? "disabled" : ""}/> <ion-icon name="pencil-outline" class='icon edit'></ion-icon> <ion-icon name="trash-outline" class="icon remove"></ion-icon> `; return inputContainer; } // 绑定任务的编辑、删除事件(事件委托) function bindTaskEvents() { board.addEventListener("click", (e) => { const taskItem = e.target.closest(".input-container"); if (!taskItem) return; const input = taskItem.querySelector(".field"); const taskId = parseInt(taskItem.dataset.taskId); const columnId = parseInt(taskItem.closest(".column").id); const column = columns.find(c => c.id === columnId); if (e.target.classList.contains("edit")) { input.disabled = false; input.focus(); taskItem.draggable = false; } else if (e.target.classList.contains("remove")) { taskItem.remove(); column.tasks = column.tasks.filter(t => t.id !== taskId); updateLocalStorage(); } }); // 保存任务(失去焦点或按回车) board.addEventListener("focusout", (e) => { if (e.target.classList.contains("field") && e.target.value.trim()) { const taskItem = e.target.closest(".input-container"); const taskId = parseInt(taskItem.dataset.taskId); const columnId = parseInt(taskItem.closest(".column").id); const column = columns.find(c => c.id === columnId); e.target.disabled = true; e.target.style.cursor = "move"; taskItem.draggable = true; // 更新数据 const taskIndex = column.tasks.findIndex(t => t.id === taskId); if (taskIndex !== -1) { column.tasks[taskIndex].content = e.target.value; } else { column.tasks.push({ id: taskId, content: e.target.value }); } updateLocalStorage(); } }); board.addEventListener("keypress", (e) => { if (e.target.classList.contains("field") && e.key === "Enter") { e.target.blur(); } }); } // 绑定拖拽事件(事件委托) function bindDragDropEvents() { board.addEventListener("dragstart", (e) => { if (e.target.classList.contains("input-container")) { drag = e.target; e.target.classList.add("dragging"); setTimeout(() => e.target.style.opacity = "0.5", 0); } }); board.addEventListener("dragend", (e) => { if (e.target.classList.contains("input-container")) { drag = null; e.target.classList.remove("dragging"); e.target.style.opacity = "1"; // 更新数据:同步任务所在列 const taskId = parseInt(e.target.dataset.taskId); const oldColumnId = parseInt(e.target.closest(".column").id); const newColumnId = parseInt(e.target.parentElement.closest(".column").id); if (oldColumnId !== newColumnId) { const oldColumn = columns.find(c => c.id === oldColumnId); const newColumn = columns.find(c => c.id === newColumnId); const task = oldColumn.tasks.find(t => t.id === taskId); if (task) { oldColumn.tasks = oldColumn.tasks.filter(t => t.id !== taskId); newColumn.tasks.push(task); updateLocalStorage(); } } } }); // 拖拽经过处理 board.addEventListener("dragover", (e) => { e.preventDefault(); const tasksList = e.target.closest(".tasks"); if (tasksList && drag) { tasksList.classList.add("drag-over"); // 计算插入位置 const afterElement = getDragAfterElement(tasksList, e.clientY); afterElement ? tasksList.insertBefore(drag, afterElement) : tasksList.appendChild(drag); } }); board.addEventListener("dragleave", (e) => { const tasksList = e.target.closest(".tasks"); if (tasksList) tasksList.classList.remove("drag-over"); }); board.addEventListener("drop", (e) => { e.preventDefault(); const tasksList = e.target.closest(".tasks"); if (tasksList) tasksList.classList.remove("drag-over"); }); } // 获取拖拽位置的下一个元素 function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll(".input-container:not(.dragging)")]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; return offset < 0 && offset > closest.offset ? { offset, element: child } : closest; }, { offset: Number.NEGATIVE_INFINITY }).element; } // 初始化应用 loadFromLocalStorage(); renderColumns();
二、拖拽样式优化建议
添加以下CSS样式,增强拖拽过程中的视觉反馈:
/* 拖拽中的任务样式 */ .input-container.dragging { opacity: 0.5; transform: scale(1.05); box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: grabbing; } /* 拖拽经过的任务列表样式 */ .tasks.drag-over { background-color: rgba(255,255,255,0.1); border-radius: 4px; padding: 1rem; } /* 任务光标状态 */ .input-container { cursor: grab; transition: all 0.2s ease; } .input-container:active { cursor: grabbing; }
优化说明:
- 拖拽中的任务半透明+轻微放大+加深阴影,突出当前操作元素
- 拖拽经过的任务列表添加浅色背景,明确可放置区域
- 切换光标状态,给用户清晰的操作提示
三、代码优化方案
- 分离数据层与视图层:单独维护任务数据,视图根据数据渲染,避免直接操作DOM存储数据
- 事件委托:通过父容器绑定事件,避免给每个任务重复绑定,提升性能
- 模块化拆分:将列渲染、任务创建、事件绑定拆分为独立函数,代码更易维护
- 修复拼写错误:原代码中
dragable应为draggable,已在优化代码中修正 - 唯一ID生成:用时间戳作为任务ID,避免重复
- 避免重复渲染:只操作新增/修改的任务元素,而非全量重绘
内容的提问来源于stack exchange,提问作者Mohammed-Abol-Fotouh
相关产品推荐
相关产品推荐

