You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

看板应用LocalStorage数据恢复及拖拽样式、代码优化求助

看板应用问题解决方案

问题概述

开发的看板应用包含「未开始」「进行中」「已完成」三列,已实现任务增删改及列间拖拽功能,数据存储在LocalStorage,但页面加载时无法正确恢复数据,同时需要拖拽样式优化和代码优化方案。


一、LocalStorage数据恢复问题修复

你当前直接存储HTML结构的方式不仅冗余,还会丢失事件绑定,导致恢复后功能失效。正确的做法是存储任务数据而非DOM结构,页面加载时根据数据重新渲染任务并绑定事件。

修复步骤

  1. 定义结构化的任务数据数组,维护各列的任务信息
  2. 页面加载时从LocalStorage读取数据,同步到本地数据结构
  3. 根据数据渲染列和任务,替换直接存储HTML的逻辑
  4. 修改存储函数,只保存任务数据而非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;
}

优化说明:

  • 拖拽中的任务半透明+轻微放大+加深阴影,突出当前操作元素
  • 拖拽经过的任务列表添加浅色背景,明确可放置区域
  • 切换光标状态,给用户清晰的操作提示

三、代码优化方案

  1. 分离数据层与视图层:单独维护任务数据,视图根据数据渲染,避免直接操作DOM存储数据
  2. 事件委托:通过父容器绑定事件,避免给每个任务重复绑定,提升性能
  3. 模块化拆分:将列渲染、任务创建、事件绑定拆分为独立函数,代码更易维护
  4. 修复拼写错误:原代码中dragable应为draggable,已在优化代码中修正
  5. 唯一ID生成:用时间戳作为任务ID,避免重复
  6. 避免重复渲染:只操作新增/修改的任务元素,而非全量重绘

内容的提问来源于stack exchange,提问作者Mohammed-Abol-Fotouh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 07:27:02