使用思源笔记数据库搭建父子任务面板的想法

本贴最后更新于 333 天前,其中的信息可能已经时移世异

回复:STtools 插件(0.4.0):日程管理 2.0(和思源紧紧相拥的日历视图) - Achuan-2 的回 - 链滴,由于评论有字数限制,所以只能另发帖

数据库添加列

  • 存储父任务信息
  • 存储排序顺序

AI 写的 demo 代码

  • 支持添加时间,支持设置时间段任务,支持显示过期时间
  • 支持设置优先级
  • 支持设置任务当前状态
  • 支持拖拽排序,任务可以被拖拽成为某个任务的子任务
  • 支持任务添加自定义分类
  • 支持列表视图:平铺展示全部 or 分类下的所有任务,支持按状态、优先级、标题排序
  • 支持看板视图:显示全部 or 分类下的任务进展状态(按待处理、正在进行、已完成展示三个看板)

PixPin20250122201428.png

PixPin20250122201438.png

<!DOCTYPE html>
<html>

<head>
    <title>高级任务管理系统</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            margin: 20px;
            background: #f5f5f5;
        }

        .task-tree {
            list-style: none;
            padding: 0;
            margin: 0;
            max-width: 800px;
            margin: 0 auto;
        }

        .task-item {
            margin: 2px 0;
            background: white;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            cursor: move;
            transition: all 0.2s ease;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
        }

        .task-item-content {
            padding: 8px 12px;
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .task-item:hover {
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .task-item.dragging {
            opacity: 0.6;
            transform: scale(0.98);
        }

        /* 树形结构样式 */
        .subtasks {
            list-style: none;
            padding-left: 24px;
            margin: 0;
            position: relative;
        }

        .subtasks::before {
            content: '';
            position: absolute;
            left: 12px;
            top: 0;
            bottom: 0;
            width: 1px;
            background: #e0e0e0;
        }


        .status-tag {
            font-size: 0.8em;
            padding: 3px 8px;
            border-radius: 12px;
            flex-shrink: 0;
        }

        .priority-tag {
            font-size: 0.8em;
            padding: 3px 8px;
            border-radius: 12px;
            margin-left: auto;
        }

        .status-todo {
            background: #94a3b8;
            color: white;
        }

        .status-doing {
            background: #3b82f6;
            color: white;
        }

        .status-done {
            background: #22c55e;
            color: white;
        }

        .priority-high {
            background: #fecaca;
            color: #dc2626;
        }

        .priority-medium {
            background: #fde68a;
            color: #d97706;
        }

        .priority-low {
            background: #bbf7d0;
            color: #16a34a;
        }
                .priority-none {
            background: #dadada;
            color: #373737;
        }


        /* 优化后的右键菜单样式 */
        .context-menu {
            position: fixed;
            background: white;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            z-index: 1000;
            min-width: 180px;
            display: none;
        }

        .menu-item {
            padding: 8px 16px;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 8px;
            transition: background 0.2s;
        }

        .menu-item:hover {
            background: #f8fafc;
        }

        .menu-separator {
            border-top: 1px solid #f1f5f9;
            margin: 4px 0;
        }

        .menu-header {
            padding: 12px 16px;
            font-weight: 500;
            color: #64748b;
            background: #f8fafc;
            border-radius: 8px 8px 0 0;
        }

        /* 菜单优先级标签调整 */
        .context-menu .priority-tag {
            margin-left: 0px !important;
        }

        .menu-subtitle {
            padding: 8px 16px;
            font-size: 0.9em;
            color: #64748b;
        }

        .task-input-container {
            max-width: 800px;
            margin: 0 auto 20px auto;
            display: flex;
            gap: 10px;
        }

        .task-input {
            flex: 1;
            padding: 8px 12px;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            font-size: 14px;
        }

        .task-add-btn {
            padding: 8px 16px;
            background: #3b82f6;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            transition: background 0.2s;
        }

        .task-add-btn:hover {
            background: #2563eb;
        }

        .sort-container {
            max-width: 800px;
            margin: 0 auto 10px auto;
            display: flex;
            gap: 10px;
            justify-content: flex-end;
        }

        .sort-btn {
            padding: 6px 12px;
            background: white;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            color: #64748b;
            transition: all 0.2s;
        }

        .sort-btn:hover {
            background: #f8fafc;
            border-color: #94a3b8;
        }

        .sort-btn.active {
            background: #3b82f6;
            color: white;
            border-color: #3b82f6;
        }

        .menu-item.delete {
            color: #ef4444;
        }

        .menu-item.delete:hover {
            background: #fef2f2;
        }

        .view-switch {
            max-width: 800px;
            margin: 0 auto 10px auto;
            display: flex;
            gap: 10px;
            justify-content: flex-end;
        }

        .tab-container {
            max-width: 800px;
            margin: 0 auto;
            border-bottom: 1px solid #e0e0e0;
            display: flex;
            align-items: center;
            margin-bottom: 20px;
            overflow-x: auto;
            display: flex;
            align-items: center;
            justify-content: flex-start;
            gap: 8px;
            flex-wrap: wrap;
        }

        .tab-container .hide-done-toggle {
            margin-left: auto; /* 将开关推到最右侧 */
        }

        .tab-item {
            padding: 8px 16px;
            cursor: pointer;
            border-bottom: 2px solid transparent;
            white-space: nowrap;
            position: relative;
        }

        .tab-item.active {
            border-bottom-color: #3b82f6;
            color: #3b82f6;
        }

        .add-category-btn {
            padding: 4px 8px;
            background: #f1f5f9;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-left: auto;
            color: #64748b;
            margin-left: 0; /* 重置margin */
        }

        .add-category-btn:hover {
            background: #e2e8f0;
        }

        .kanban-view {
            max-width: 100%;
            margin: 0 auto;
            display: flex;
            gap: 20px;
            overflow-x: auto;
            padding: 20px 0;
        }

        .kanban-column {
            flex: 1;
            min-width: 320px; /* 增加最小宽度 */
            background: #f8fafc;
            border-radius: 8px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); /* 添加阴影 */
            border: 1px solid rgba(0, 0, 0, 0.05); /* 添加边框 */
        }

        .kanban-column .task-tree {
            flex: 1;
            min-height: 100px;
            background: #ffffff;
            border-radius: 6px;
            padding: 12px; /* 增加内边距 */
            margin-top: 12px;
            width: 100%; /* 确保宽度填充满列 */
            box-sizing: border-box; /* 防止padding导致溢出 */
            box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02); /* 添加内阴影 */
        }

        .kanban-column .task-item {
            margin: 8px 0;
        }

        .kanban-status-header {
            font-weight: 500;
            color: #475569;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .kanban-status-header .status-tag {
            margin-right: 8px;
        }

        .kanban-status-count {
            font-size: 0.9em;
            color: #64748b;
            font-weight: normal;
        }

        .view-switch-btn {
            padding: 6px 12px;
            background: white;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            color: #64748b;
        }

        .view-switch-btn.active {
            background: #3b82f6;
            color: white;
            border-color: #3b82f6;
        }

        /* 添加隐藏已完成任务开关样式 */
        .hide-done-toggle {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-left: 16px;
            color: #64748b;
            font-size: 14px;
        }

        .hide-done-toggle input[type="checkbox"] {
            width: 16px;
            height: 16px;
            cursor: pointer;
        }

        /* 调整看板布局样式 */
        .kanban-container {
            max-width: 1200px;  /* 修改最大宽度 */
            margin: 0 auto;
            padding: 20px 0;
        }

        /* 添加分类标签右键菜单样式 */
        .category-context-menu {
            position: fixed;
            background: white;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            z-index: 1000;
            min-width: 160px;
            display: none;
        }

        .category-menu-item {
            padding: 8px 16px;
            cursor: pointer;
            transition: background 0.2s;
        }

        .category-menu-item:hover {
            background: #f8fafc;
        }

        .category-menu-item.delete {
            color: #ef4444;
        }

        .category-menu-item.delete:hover {
            background: #fef2f2;
        }

        /* 给标签添加右键菜单提示 */
        .tab-item:not([data-category="all"]):not([data-category="none"])::after {
            content: '⋮';
            margin-left: 4px;
            color: #64748b;
            opacity: 0.5;
        }

        /* 看板列添加任务样式 */
        .kanban-quick-add {
            margin-top: 12px;
            display: flex;
            gap: 8px;
        }

        .kanban-quick-add input {
            flex: 1;
            padding: 6px 10px;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            font-size: 13px;
        }

        .kanban-quick-add button {
            padding: 6px 12px;
            background: #3b82f6;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        /* 任务时间样式 */
        .task-time {
            font-size: 0.85em;
            color: #64748b;
            display: flex;
            align-items: center;
            gap: 4px;
        }

        .task-time.overdue {
            color: #ef4444;
        }

        .task-time-icon {
            width: 14px;
            height: 14px;
        }

        /* 时间选择弹窗样式 */
        .time-picker-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            z-index: 1001;
            width: 300px;
        }

        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }

        .time-picker-form {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .time-picker-form label {
            display: block;
            margin-bottom: 4px;
            color: #475569;
        }

        .time-picker-form input {
            width: 100%;
            padding: 6px;
            border: 1px solid #e2e8f0;
            border-radius: 4px;
        }

        .time-picker-actions {
            display: flex;
            justify-content: flex-end;
            gap: 8px;
            margin-top: 16px;
        }

        .time-picker-actions button {
            padding: 6px 12px;
            border-radius: 4px;
            cursor: pointer;
        }

        .time-picker-actions button.cancel {
            background: #e2e8f0;
            border: none;
            color: #475569;
        }

        .time-picker-actions button.confirm {
            background: #3b82f6;
            border: none;
            color: white;
        }

        /* 修改列表视图样式 */
        .list-view-container {
            max-width: 1200px;
            margin: 0 auto;
            display: flex;
            gap: 20px;
            padding: 20px 0;
        }

        .list-category {
            flex: 1;
            min-width: 320px;
            max-width: 400px;
            background: #f8fafc;
            border-radius: 8px;
            padding: 16px;
            display: flex;
            flex-direction: column;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
            border: 1px solid rgba(0, 0, 0, 0.05);
        }

        .list-category-header {
            font-weight: 500;
            color: #475569;
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 12px;
        }

        .list-category-header .task-count {
            font-size: 0.9em;
            color: #64748b;
            font-weight: normal;
        }

        .list-category .task-tree {
            flex: 1;
            min-height: 100px;
            background: #ffffff;
            border-radius: 6px;
            padding: 12px;
            margin-top: 12px;
            width: 100%;
            box-sizing: border-box;
            box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
        }

        /* 添加任务分类标签样式 */
        .task-category {
            font-size: 0.8em;
            color: #64748b;
            margin-top: 4px;
        }
    </style>
</head>

<body>
    <div class="container">
        <!-- 添加视图切换按钮 -->
        <div class="view-switch">
            <button class="view-switch-btn active" data-view="tab">列表视图</button>
            <button class="view-switch-btn" data-view="kanban">看板视图</button>
        </div>
      
        <!-- 列表视图 -->
        <div id="tabView">
            <div class="tab-container">
                <div class="tab-item active" data-category="all">全部</div>
                <div class="tab-item" data-category="none">未分类</div>
                <!-- 动态添加分类标签 -->
                <button class="add-category-btn">
                    <span>+</span>
                </button>
                <div class="hide-done-toggle">
                    <input type="checkbox" id="hideDoneToggleTab">
                    <label for="hideDoneToggleTab">隐藏已完成任务</label>
                </div>
            </div>
            <div class="task-input-container">
                <input type="text" id="newTaskInput" class="task-input" placeholder="添加新任务...">
                <button id="addTaskBtn" class="task-add-btn">添加</button>
            </div>
            <div class="sort-container">
                <button class="sort-btn" data-sort="status">按状态</button>
                <button class="sort-btn" data-sort="priority">按优先级</button>
                <button class="sort-btn" data-sort="title">按标题</button>
            </div>
            <div class="task-tree" id="taskTree">
                <!-- 动态添加任务列表 -->
            </div>
        </div>

        <!-- 看板视图 -->
        <div id="kanbanView" style="display: none;">
            <div class="tab-container">
                <div class="tab-item active" data-category="all">全部</div>
                <div class="tab-item" data-category="none">未分类</div>
                <!-- 动态添加分类标签 -->
                <button class="add-category-btn">
                    <span>+</span>
                </button>
                <div class="hide-done-toggle">
                    <input type="checkbox" id="hideDoneToggle">
                    <label for="hideDoneToggle">隐藏已完成任务</label>
                </div>
            </div>
            <div class="kanban-container">
                <div class="kanban-view">
                    <!-- 动态添加状态列 -->
                </div>
            </div>
        </div>
    </div>

    <!-- 修改后的右键菜单 -->
    <div id="contextMenu" class="context-menu">
        <div class="menu-header">任务操作</div>
        <div class="menu-item" data-action="addSubtask">
            <span>添加子任务</span>
        </div>
        <div class="menu-item delete" data-action="delete">
            <span>删除任务</span>
        </div>
        <div class="menu-separator"></div>
        <div class="menu-item" data-action="setTime">
            <span>设置时间</span>
        </div>
        <div class="menu-separator"></div>
        <div class="menu-subtitle">任务状态</div>
        <div class="menu-item" data-action="status" data-value="todo">
            <span class="status-tag status-todo">待处理</span>
        </div>
        <div class="menu-item" data-action="status" data-value="doing">
            <span class="status-tag status-doing">进行中</span>
        </div>
        <div class="menu-item" data-action="status" data-value="done">
            <span class="status-tag status-done">已完成</span>
        </div>
        <div class="menu-separator"></div>
        <div class="menu-subtitle">优先级</div>
        <div class="menu-item" data-action="priority" data-value="high">
            <span class="priority-tag priority-high">优先级:高</span>
        </div>
        <div class="menu-item" data-action="priority" data-value="medium">
            <span class="priority-tag priority-medium">优先级:中</span>
        </div>
        <div class="menu-item" data-action="priority" data-value="low">
            <span class="priority-tag priority-low">优先级:低</span>
        </div>
        <div class="menu-item" data-action="priority" data-value="none">
            <span class="priority-tag priority-none">优先级:无</span>
        </div>
        <div class="menu-separator"></div>
        <div class="menu-subtitle">分类</div>
        <div class="menu-item" data-action="setCategory" data-value="">
            <span>设置分类...</span>
        </div>
    </div>

    <!-- 添加分类右键菜单 -->
    <div id="categoryContextMenu" class="category-context-menu">
        <div class="category-menu-item" data-action="rename">重命名分类</div>
        <div class="category-menu-item delete" data-action="delete">删除分类</div>
    </div>

    <!-- 添加时间选择弹窗 -->
    <div id="timePickerModal" class="time-picker-modal" style="display: none;">
        <div class="time-picker-form">
            <div>
                <label>任务类型</label>
                <select id="timeType">
                    <option value="deadline">截止日期</option>
                    <option value="period">时间段</option>
                </select>
            </div>
            <div id="deadlineInputs">
                <div>
                    <label>截止日期时间</label>
                    <input type="datetime-local" id="deadlineTime">
                </div>
            </div>
            <div id="periodInputs" style="display: none;">
                <div>
                    <label>开始时间</label>
                    <input type="datetime-local" id="startTime">
                </div>
                <div>
                    <label>结束时间</label>
                    <input type="datetime-local" id="endTime">
                </div>
            </div>
            <div class="time-picker-actions">
                <button class="cancel" onclick="closeTimePicker()">取消</button>
                <button class="confirm" onclick="confirmTimePicker()">确定</button>
            </div>
        </div>
    </div>
    <div id="modalOverlay" class="modal-overlay" style="display: none;"></div>

    <script>
        // 添加分类相关数据
        let categories = [];
        let currentView = 'tab'; // 'tab' 或 'kanban'
        let currentCategory = 'all';

        const taskData = [
            {
                id: "task1",
                title: "项目规划",
                status: "doing",
                parentId: null,
                order: 1,
                priority: "high",
                category: '', // 新增category字段
                timeType: null, // 'deadline' or 'period'
                deadlineTime: null,
                startTime: null,
                endTime: null
            },
            {
                id: "task2",
                title: "需求分析",
                status: "done",
                parentId: "task1",
                order: 1,
                priority: "medium",
                category: '', // 新增category字段
                timeType: null, // 'deadline' or 'period'
                deadlineTime: null,
                startTime: null,
                endTime: null
            }
        ];

        let tasks = [];
        let draggedItem = null;
        let currentTaskId = null;
        let currentSort = ''; // 当前排序方式
        let hideCompletedTasks = false; // 添加隐藏已完成任务状态
        let currentEditingTaskId = null;

        document.addEventListener('DOMContentLoaded', () => {
            loadTasks();
            loadCategories();
          
            // 视图切换按钮事件
            const viewSwitchBtns = document.querySelectorAll('.view-switch-btn');
            viewSwitchBtns.forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const view = e.target.dataset.view;
                    switchView(view);
                });
            });

            // 分类标签点击事件
            const tabContainer = document.querySelector('.tab-container');
            if (tabContainer) {
                tabContainer.addEventListener('click', (e) => {
                    const tab = e.target.closest('.tab-item');
                    if (tab) {
                        document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
                        tab.classList.add('active');
                        currentCategory = tab.dataset.category;
                        renderCategories();
                    }
                });
            }

            // 添加分类按钮事件
            document.querySelectorAll('.add-category-btn').forEach(btn => {
                btn.addEventListener('click', () => {
                    const name = prompt('请输入分类名称:');
                    if (name) addCategory(name.trim());
                });
            });

            // 时间类型选择事件
            const timeTypeSelect = document.getElementById('timeType');
            if (timeTypeSelect) {
                timeTypeSelect.addEventListener('change', toggleTimeInputs);
            }

            // 隐藏已完成任务切换事件
            const hideDoneToggle = document.getElementById('hideDoneToggle');
            const hideDoneToggleTab = document.getElementById('hideDoneToggleTab');
          
            if (hideDoneToggle) {
                hideDoneToggle.addEventListener('change', (e) => {
                    hideCompletedTasks = e.target.checked;
                    if (hideDoneToggleTab) hideDoneToggleTab.checked = e.target.checked;
                    if (currentView === 'kanban') {
                        renderKanbanView();
                    } else {
                        renderCategories();
                    }
                });
            }

            if (hideDoneToggleTab) {
                hideDoneToggleTab.addEventListener('change', (e) => {
                    hideCompletedTasks = e.target.checked;
                    if (hideDoneToggle) hideDoneToggle.checked = e.target.checked;
                    renderCategories();
                });
            }

            // 初始化视图
            renderCategories();
        });

        function loadTasks() {
            const saved = localStorage.getItem('tasks');
            tasks = saved ? JSON.parse(saved) : [...taskData];
        }

        function saveTasks() {
            localStorage.setItem('tasks', JSON.stringify(tasks));
        }

        function renderTasks() {
            const container = document.getElementById('taskTree');
            if (!container) return; // 添加空检查
          
            container.innerHTML = '';
            buildTaskTree(container, null);
            addDragListeners();
        }

        function buildTaskTree(container, parentId) {
            let filtered = tasks.filter(t => t.parentId === parentId);
          
            // 根据当前分类过滤
            if (currentView === 'tab' && currentCategory !== 'all') {
                filtered = filtered.filter(t => 
                    currentCategory === 'none' ? !t.category : t.category === currentCategory
                );
            }

            // 隐藏已完成任务
            if (hideCompletedTasks) {
                filtered = filtered.filter(t => t.status !== 'done');
            }
          
            // 根据当前排序方式对任务进行排序
            if (currentSort) {
                filtered.sort((a, b) => {
                    switch (currentSort) {
                        case 'priority':
                            const priorityOrder = { high: 1, medium: 2, low: 3, none: 4 };
                            return (priorityOrder[a.priority] || 4) - (priorityOrder[b.priority] || 4);
                        case 'status':
                            const statusOrder = { doing: 1, todo: 2, done: 3 };
                            return statusOrder[a.status] - statusOrder[b.status];
                        case 'title':
                            return a.title.localeCompare(b.title);
                        default:
                            return a.order - b.order;
                    }
                });
            } else {
                filtered.sort((a, b) => a.order - b.order);
            }

            filtered.forEach(task => {
                const li = document.createElement('li');
                li.className = 'task-item';
                li.draggable = true;
                li.dataset.id = task.id;

                const content = document.createElement('div');
                content.className = 'task-item-content';
                content.innerHTML = renderTaskContent(task);
                li.appendChild(content);

                const hasChildren = tasks.some(t => t.parentId === task.id);
                if (hasChildren) {
                    const subList = document.createElement('ul');
                    subList.className = 'subtasks';
                    li.appendChild(subList);
                    buildTaskTree(subList, task.id);
                }

                container.appendChild(li);
            });
        }

        function addDragListeners() {
            document.querySelectorAll('.task-item').forEach(item => {
                item.addEventListener('dragstart', handleDragStart);
                item.addEventListener('dragover', handleDragOver);
                item.addEventListener('dragend', handleDragEnd);
                item.addEventListener('drop', handleDrop);
            });
        }

        function handleDragStart(e) {
            draggedItem = e.target.closest('.task-item');
            draggedItem.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        }

        function handleDragOver(e) {
            e.preventDefault();
            if (!draggedItem) return;

            const targetItem = e.target.closest('.task-item');
            if (!targetItem || targetItem === draggedItem) return;

            const rect = targetItem.getBoundingClientRect();
            const mouseY = e.clientY;
            const mouseX = e.clientX;
          
            // 定义拖拽区域
            const topThreshold = rect.top + rect.height * 0.25;
            const bottomThreshold = rect.bottom - rect.height * 0.25;
            const middleZone = mouseY > topThreshold && mouseY < bottomThreshold;
          
            // 水平方向判断
            const leftEdge = rect.left;
            const horizontalThreshold = 50; // 向左拖动50px时取消父子关系
            const shouldBeSibling = mouseX < leftEdge - horizontalThreshold;

            // 清除所有临时样式
            document.querySelectorAll('.task-item').forEach(item => {
                item.style.borderTop = '';
                item.style.borderBottom = '';
                item.style.backgroundColor = '';
            });

            const currentList = targetItem.parentElement;
            const isDifferentList = draggedItem.parentElement !== currentList;

            if (middleZone && !shouldBeSibling && !isDifferentList) {
                // 在中间区域且不满足向左拖动条件时,表示将成为子任务
                targetItem.style.backgroundColor = '#f0f9ff';
              
                let subtasksList = targetItem.querySelector('.subtasks');
                if (!subtasksList) {
                    subtasksList = document.createElement('ul');
                    subtasksList.className = 'subtasks';
                    targetItem.appendChild(subtasksList);
                }
              
                const afterElement = getInsertPosition(subtasksList, mouseY);
                if (afterElement) {
                    subtasksList.insertBefore(draggedItem, afterElement);
                } else {
                    subtasksList.appendChild(draggedItem);
                }
            } else {
                // 如果是在同一列表中移动,先从原位置移除
                if (!isDifferentList) {
                    draggedItem.parentNode.removeChild(draggedItem);
                }

                // 在上下区域或满足向左拖动条件时,作为同级任务插入
                const parentList = shouldBeSibling ? targetItem.parentElement.parentElement : targetItem.parentElement;
                if (mouseY < rect.top + rect.height / 2) {
                    targetItem.style.borderTop = '2px solid #3b82f6';
                    parentList.insertBefore(draggedItem, targetItem);
                } else {
                    targetItem.style.borderBottom = '2px solid #3b82f6';
                    parentList.insertBefore(draggedItem, targetItem.nextSibling);
                }

                if (shouldBeSibling) {
                    targetItem.style.backgroundColor = '#f1f5f9';
                }
            }

            e.stopPropagation();
        }

        function getInsertPosition(container, y) {
            const draggableElements = [...container.querySelectorAll('.task-item:not(.dragging)')];
          
            return draggableElements.reduce((closest, child) => {
                const box = child.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;
              
                if (offset < 0 && offset > closest.offset) {
                    return { offset: offset, element: child };
                } else {
                    return closest;
                }
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }

        function handleDragEnd(e) {
            // 清除所有临时样式
            document.querySelectorAll('.task-item').forEach(item => {
                item.style.borderTop = '';
                item.style.borderBottom = '';
                item.style.backgroundColor = '';
            });
          
            draggedItem.classList.remove('dragging');
          
            // 在看板视图中更新任务状态
            if (currentView === 'kanban') {
                const taskId = draggedItem.dataset.id;
                const task = tasks.find(t => t.id === taskId);
                const newStatusColumn = draggedItem.closest('.task-tree');
              
                if (task && newStatusColumn) {
                    const newStatus = newStatusColumn.dataset.status;
                    if (newStatus && task.status !== newStatus) {
                        task.status = newStatus;
                        saveTasks();
                    }
                }
            }

            updateTaskOrders();
            saveTasks();
            if (currentView === 'kanban') {
                renderKanbanView();
            } else {
                renderTasks();
            }
        }

        function handleDrop(e) {
            e.preventDefault();
        }

        function getDragAfterElement(container, y) {
            const items = [...container.querySelectorAll('.task-item:not(.dragging)')];
            return items.reduce((closest, child) => {
                const box = child.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;
                return offset < 0 && offset > closest.offset
                    ? { offset: offset, element: child }
                    : closest;
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }

        function updateTaskOrders() {
            if (!currentSort) {
                // 只在没有自定义排序时更新order
                document.querySelectorAll('.task-item').forEach((item, index) => {
                    const parentList = item.parentElement;
                    const parentItem = parentList.closest('.task-item');
                    const task = tasks.find(t => t.id === item.dataset.id);

                    if (task) {
                        task.order = index + 1;
                        task.parentId = parentItem ? parentItem.dataset.id : null;
                    }
                });
            } else {
                // 在排序模式下只更新父子关系
                document.querySelectorAll('.task-item').forEach((item) => {
                    const parentList = item.parentElement;
                    const parentItem = parentList.closest('.task-item');
                    const task = tasks.find(t => t.id === item.dataset.id);

                    if (task) {
                        task.parentId = parentItem ? parentItem.dataset.id : null;
                    }
                });
            }
        }

        document.addEventListener('contextmenu', (e) => {
            const taskItem = e.target.closest('.task-item');
            if (taskItem) {
                e.preventDefault();
                currentTaskId = taskItem.dataset.id;
                showContextMenu(e.clientX, e.clientY);
            }
        });

        document.addEventListener('click', hideContextMenu);

        function showContextMenu(x, y) {
            const menu = document.getElementById('contextMenu');
            menu.style.display = 'block';

            const winWidth = window.innerWidth;
            const winHeight = window.innerHeight;
            const menuRect = menu.getBoundingClientRect();

            x = x + menuRect.width > winWidth ? winWidth - menuRect.width : x;
            y = y + menuRect.height > winHeight ? winHeight - menuRect.height : y;

            menu.style.left = `${x}px`;
            menu.style.top = `${y}px`;

            menu.querySelectorAll('.menu-item').forEach(item => {
                item.removeEventListener('click', handleMenuClick);
                item.addEventListener('click', handleMenuClick);
            });
        }

        function hideContextMenu() {
            const menu = document.getElementById('contextMenu');
            menu.style.display = 'none';
            currentTaskId = null;
        }

        // 添加删除任务的函数
        function deleteTask(taskId) {
            // 递归删除子任务
            function recursiveDelete(id) {
                const children = tasks.filter(t => t.parentId === id);
                children.forEach(child => recursiveDelete(child.id));
                tasks = tasks.filter(t => t.id !== id);
            }

            if (confirm('确定要删除这个任务吗?这将同时删除其所有子任务。')) {
                recursiveDelete(taskId);
                saveTasks();
                renderTasks();
            }
        }

        // 修改菜单点击处理函数
        function handleMenuClick(e) {
            const action = e.currentTarget.dataset.action;
            const value = e.currentTarget.dataset.value;
            const task = tasks.find(t => t.id === currentTaskId);

            if (!task) return;

            switch (action) {
                case 'status':
                    task.status = value;
                    break;
                case 'priority':
                    task.priority = value === 'none' ? null : value;
                    break;
                case 'addSubtask':
                    const title = prompt('请输入子任务名称:');
                    if (title && title.trim()) {
                        addTask(title.trim(), task.id);
                    }
                    break;
                case 'delete':
                    deleteTask(task.id);
                    break;
                case 'setCategory':
                    // 检查是否为顶级任务
                    if (task.parentId !== null) {
                        alert('只有顶级任务支持设置分类');
                        return;
                    }
                    const categoryList = ['无分类', '删除分类', ...categories];
                    const category = prompt('请输入或选择分类:\n当前分类:' + (task.category || '无') + '\n' + categoryList.join('\n'));
                    if (category === '无分类' || category === '删除分类') {
                        task.category = '';
                    } else if (category && category.trim()) {
                        const trimmedCategory = category.trim();
                        if (!categories.includes(trimmedCategory)) {
                            categories.push(trimmedCategory);
                            saveCategories();
                            updateCategoryTabs();
                        }
                        task.category = trimmedCategory;
                    }
                    break;
                case 'setTime':
                    showTimePicker(currentTaskId);
                    break;
            }

            saveTasks();
            if (currentView === 'kanban') {
                renderKanbanView();
            } else {
                renderTasks();
            }
            hideContextMenu();
        }

        function getStatusText(status) {
            return {
                todo: '待处理',
                doing: '进行中',
                done: '已完成'
            }[status] || '未知状态';
        }

        function getPriorityText(priority) {
            return `优先级:${priority}`;
        }

        // 生成唯一ID的函数
        function generateId() {
            return 'task' + Date.now() + Math.random().toString(36).substr(2, 5);
        }

        // 添加新任务的函数
        function addTask(title, parentId = null) {
            // 获取同级任务
            const siblingTasks = tasks.filter(t => t.parentId === parentId);
          
            // 将所有同级任务的 order 加 1
            siblingTasks.forEach(task => {
                task.order += 1;
            });

            const newTask = {
                id: generateId(),
                title: title,
                status: 'todo',
                parentId: parentId,
                order: 1, // 新任务总是放在最前面,order 为 1
                priority: 'none',
                category: '', // 新增category字段
                timeType: null, // 'deadline' or 'period'
                deadlineTime: null,
                startTime: null,
                endTime: null
            };
          
            tasks.push(newTask);
            saveTasks();
            renderTasks();
            return newTask;
        }

        // 绑定添加任务按钮事件
        document.addEventListener('DOMContentLoaded', () => {
            const input = document.getElementById('newTaskInput');
            const addBtn = document.getElementById('addTaskBtn');

            function handleAddTask() {
                const title = input.value.trim();
                if (title) {
                    addTask(title);
                    input.value = '';
                }
            }

            if (addBtn && input) {
                addBtn.addEventListener('click', handleAddTask);
                input.addEventListener('keypress', (e) => {
                    if (e.key === 'Enter') {
                        handleAddTask();
                    }
                });
            }

            loadTasks();
            renderTasks();

            // 添加排序按钮事件监听
            document.querySelectorAll('.sort-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const sortType = e.target.dataset.sort;
                  
                    // 更新按钮状态
                    document.querySelectorAll('.sort-btn').forEach(b => {
                        b.classList.remove('active');
                    });
                  
                    if (currentSort === sortType) {
                        // 再次点击同一个排序按钮时取消排序
                        currentSort = '';
                    } else {
                        currentSort = sortType;
                        e.target.classList.add('active');
                    }
                  
                    renderTasks();
                });
            });

            // 视图切换按钮事件
            document.querySelectorAll('.view-switch-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const view = e.target.dataset.view;
                    switchView(view);
                });
            });
          
            // 分类标签点击事件
            const tabContainer = document.querySelector('.tab-container');
            if (tabContainer) {
                tabContainer.addEventListener('click', (e) => {
                    const tab = e.target.closest('.tab-item');
                    if (tab) {
                        document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
                        tab.classList.add('active');
                        currentCategory = tab.dataset.category;
                        renderCategories();
                    }
                });
            }
          
            // 添加分类按钮事件
            document.querySelectorAll('.add-category-btn').forEach(btn => {
                btn.addEventListener('click', () => {
                    const name = prompt('请输入分类名称:');
                    if (name) addCategory(name.trim());
                });
            });
          
            loadCategories();
            renderCategories();
          
            // 初始化隐藏已完成任务开关状态
            const hideDoneToggle = document.getElementById('hideDoneToggle');
            const hideDoneToggleTab = document.getElementById('hideDoneToggleTab');
          
            if (hideDoneToggle) {
                hideDoneToggle.checked = hideCompletedTasks;
                hideDoneToggle.addEventListener('change', (e) => {
                    hideCompletedTasks = e.target.checked;
                    if (hideDoneToggleTab) hideDoneToggleTab.checked = e.target.checked;
                    if (currentView === 'kanban') {
                        renderKanbanView();
                    } else {
                        renderCategories();
                    }
                });
            }

            if (hideDoneToggleTab) {
                hideDoneToggleTab.checked = hideCompletedTasks;
                hideDoneToggleTab.addEventListener('change', (e) => {
                    hideCompletedTasks = e.target.checked;
                    if (hideDoneToggle) hideDoneToggle.checked = e.target.checked;
                    renderCategories();
                });
            }
        });

        // 加载分类数据
        function loadCategories() {
            const saved = localStorage.getItem('categories');
            categories = saved ? JSON.parse(saved) : [];
        }

        // 保存分类数据
        function saveCategories() {
            localStorage.setItem('categories', JSON.stringify(categories));
        }

        // 添加分类
        function addCategory(name) {
            if (name && !categories.includes(name)) {
                categories.push(name);
                saveCategories();
                updateCategoryTabs();
                // 如果在看板视图,重新渲染
                if (currentView === 'kanban') {
                    renderKanbanView();
                }
            }
        }

        // 渲染分类标签或看板
        function renderCategories() {
            if (currentView === 'tab') {
                // 更新分类标签
                const tabContainer = document.querySelector('.tab-container');
                const addCategoryBtn = tabContainer.querySelector('.add-category-btn');
                const hideDoneToggle = tabContainer.querySelector('.hide-done-toggle');
              
                // 清空现有标签,保留添加按钮和隐藏开关
                Array.from(tabContainer.children).forEach(child => {
                    if (!child.classList.contains('add-category-btn') && 
                        !child.classList.contains('hide-done-toggle')) {
                        child.remove();
                    }
                });

                // 添加"全部"和"未分类"标签
                const allTab = document.createElement('div');
                allTab.className = `tab-item${currentCategory === 'all' ? ' active' : ''}`;
                allTab.dataset.category = 'all';
                allTab.textContent = '全部';
              
                const noneTab = document.createElement('div');
                noneTab.className = `tab-item${currentCategory === 'none' ? ' active' : ''}`;
                noneTab.dataset.category = 'none';
                noneTab.textContent = '未分类';
              
                // 插入标签到添加按钮前
                tabContainer.insertBefore(allTab, addCategoryBtn);
                tabContainer.insertBefore(noneTab, addCategoryBtn);
              
                // 添加其他分类标签
                categories.forEach(category => {
                    const tab = document.createElement('div');
                    tab.className = `tab-item${currentCategory === category ? ' active' : ''}`;
                    tab.dataset.category = category;
                    tab.textContent = category;
                  
                    // 添加右键菜单监听
                    tab.addEventListener('contextmenu', (e) => {
                        e.preventDefault();
                        currentCategoryElement = tab;
                        showCategoryContextMenu(e.clientX, e.clientY);
                    });
                  
                    tabContainer.insertBefore(tab, addCategoryBtn);
                });

                // 重新渲染任务列表
                renderTasks();
            } else {
                // 原有的看板视图渲染代码
                const kanbanView = document.querySelector('.kanban-view');
                kanbanView.innerHTML = '';
              
                // 定义状态列
                const statuses = [
                    { key: 'todo', text: '待处理', class: 'status-todo' },
                    { key: 'doing', text: '进行中', class: 'status-doing' },
                    { key: 'done', text: '已完成', class: 'status-done' }
                ];
              
                // 过滤任务
                let filteredTasks = tasks;
                if (currentCategory !== 'all') {
                    filteredTasks = filteredTasks.filter(t => 
                        currentCategory === 'none' ? !t.category : t.category === currentCategory
                    );
                }
                if (hideCompletedTasks) {
                    filteredTasks = filteredTasks.filter(t => t.status !== 'done');
                }

                // 获取实际要显示的列数
                let visibleColumns = hideCompletedTasks ? 2 : 3;
              
                // 设置列宽
                const columnWidth = `${100 / visibleColumns}%`;
              
                // 创建状态列
                statuses.forEach(status => {
                    // 如果隐藏已完成任务且是已完成状态,则跳过
                    if (hideCompletedTasks && status.key === 'done') return;

                    const column = document.createElement('div');
                    column.className = 'kanban-column';
                    column.style.width = columnWidth; // 设置列宽
                    column.style.flexGrow = '1'; // 允许列伸展
                  
                    const tasksInStatus = filteredTasks
                      .filter(t => t.status === status.key && !t.parentId)
                      .sort((a, b) => a.order - b.order);
                  
                    column.innerHTML = `
                        <div class="kanban-status-header">
                            <span class="status-tag ${status.class}">${status.text}</span>
                            <span class="kanban-status-count">${tasksInStatus.length}</span>
                        </div>
                        <div class="kanban-quick-add">
                            <input type="text" placeholder="添加新任务..." data-status="${status.key}">
                            <button onclick="quickAddTask(this)">添加</button>
                        </div>
                        <ul class="task-tree" data-status="${status.key}"></ul>
                    `;
                  
                    kanbanView.appendChild(column);
                  
                    const taskList = column.querySelector('.task-tree');
                    tasksInStatus.forEach(task => {
                        renderKanbanTask(task, taskList);
                    });
                });

                addDragListeners();
            }
        }

        // 添加列表分类函数
        function addListCategory(name, tasks, category) {
            const listContainer = document.querySelector('.list-view-container');
            const div = document.createElement('div');
            div.className = 'list-category';
            div.innerHTML = `
                <div class="list-category-header">
                    <span>${name}</span>
                    <span class="task-count">${tasks.length}</span>
                </div>
                <div class="kanban-quick-add">
                    <input type="text" placeholder="添加新任务..." data-category="${category}">
                    <button onclick="quickAddTask(this)">添加</button>
                </div>
                <ul class="task-tree" data-category="${category}"></ul>
            `;
            listContainer.appendChild(div);

            const taskList = div.querySelector('.task-tree');
            const rootTasks = tasks.filter(t => !t.parentId);
            rootTasks.forEach(task => {
                renderKanbanTask(task, taskList);
            });
        }

        // 添加快速添加任务功能
        function quickAddTask(btn) {
            const input = btn.previousElementSibling;
            const title = input.value.trim();
            if (!title) return;

            const newTask = addTask(title);
          
            // 根据当前视图设置任务属性
            if (currentView === 'kanban') {
                newTask.status = input.dataset.status;
            }
          
            // 设置分类
            const category = input.dataset.category;
            if (category && category !== 'all') {
                newTask.category = category === 'none' ? '' : category;
            }
          
            input.value = '';
            if (currentView === 'kanban') {
                renderKanbanView();
            } else {
                renderCategories();
            }
        }

        // 修改视图切换函数
        function switchView(view) {
            currentView = view;
            document.querySelectorAll('.view-switch-btn').forEach(btn => {
                btn.classList.toggle('active', btn.dataset.view === view);
            });
          
            const tabView = document.getElementById('tabView');
            const kanbanView = document.getElementById('kanbanView');
          
            if (view === 'kanban') {
                tabView.style.display = 'none';
                kanbanView.style.display = 'block';
                // 在切换到看板视图时更新分类标签
                updateCategoryTabs();
                renderKanbanView();
            } else {
                tabView.style.display = 'block';
                kanbanView.style.display = 'none';
                renderCategories();
            }

            // 绑定分类标签点击事件
            document.querySelectorAll('.tab-container .tab-item').forEach(tab => {
                tab.addEventListener('click', (e) => {
                    const category = e.target.dataset.category;
                    if (category) {
                        currentCategory = category;
                        document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
                        e.target.classList.add('active');
                        if (currentView === 'kanban') {
                            renderKanbanView();
                        } else {
                            renderCategories();
                        }
                    }
                });
            });

            // 同步隐藏已完成任务状态
            document.getElementById('hideDoneToggle').checked = hideCompletedTasks;
            document.getElementById('hideDoneToggleTab').checked = hideCompletedTasks;
        }

        // 新增看板视图渲染函数
        function renderKanbanView() {
            const kanbanView = document.querySelector('.kanban-view');
            kanbanView.innerHTML = '';
          
            // 定义状态列
            const statuses = [
                { key: 'todo', text: '待处理', class: 'status-todo' },
                { key: 'doing', text: '进行中', class: 'status-doing' },
                { key: 'done', text: '已完成', class: 'status-done' }
            ];
          
            // 过滤任务
            let filteredTasks = tasks;
            if (currentCategory !== 'all') {
                filteredTasks = filteredTasks.filter(t => 
                    currentCategory === 'none' ? !t.category : t.category === currentCategory
                );
            }
            if (hideCompletedTasks) {
                filteredTasks = filteredTasks.filter(t => t.status !== 'done');
            }

            // 获取实际要显示的列数
            let visibleColumns = hideCompletedTasks ? 2 : 3;
          
            // 设置列宽
            const columnWidth = `${100 / visibleColumns}%`;
          
            // 创建状态列
            statuses.forEach(status => {
                // 如果隐藏已完成任务且是已完成状态,则跳过
                if (hideCompletedTasks && status.key === 'done') return;

                const column = document.createElement('div');
                column.className = 'kanban-column';
                column.style.width = columnWidth; // 设置列宽
                column.style.flexGrow = '1'; // 允许列伸展
              
                const tasksInStatus = filteredTasks
                  .filter(t => t.status === status.key && !t.parentId)
                  .sort((a, b) => a.order - b.order);
              
                column.innerHTML = `
                    <div class="kanban-status-header">
                        <span class="status-tag ${status.class}">${status.text}</span>
                        <span class="kanban-status-count">${tasksInStatus.length}</span>
                    </div>
                    <div class="kanban-quick-add">
                        <input type="text" placeholder="添加新任务..." data-status="${status.key}">
                        <button onclick="quickAddTask(this)">添加</button>
                    </div>
                    <ul class="task-tree" data-status="${status.key}"></ul>
                `;
              
                kanbanView.appendChild(column);
              
                const taskList = column.querySelector('.task-tree');
                tasksInStatus.forEach(task => {
                    renderKanbanTask(task, taskList);
                });
            });

            addDragListeners();
        }

        // 新增看板任务渲染函数
        function renderKanbanTask(task, container) {
            const li = document.createElement('li');
            li.className = 'task-item';
            li.draggable = true;
            li.dataset.id = task.id;

            const content = document.createElement('div');
            content.className = 'task-item-content';
            content.innerHTML = renderTaskContent(task);
            li.appendChild(content);

            // 渲染子任务
            const children = tasks
              .filter(t => t.parentId === task.id)
              .sort((a, b) => a.order - b.order);
            if (children.length > 0) {
                const subList = document.createElement('ul');
                subList.className = 'subtasks';
                children.forEach(child => renderKanbanTask(child, subList));
                li.appendChild(subList);
            }

            container.appendChild(li);
        }

        // 显示分类右键菜单
        function showCategoryContextMenu(x, y) {
            const menu = document.getElementById('categoryContextMenu');
            menu.style.display = 'block';

            // 调整菜单位置
            const winWidth = window.innerWidth;
            const winHeight = window.innerHeight;
            const menuRect = menu.getBoundingClientRect();

            x = Math.min(x, winWidth - menuRect.width);
            y = Math.min(y, winHeight - menuRect.height);

            menu.style.left = `${x}px`;
            menu.style.top = `${y}px`;

            // 添加菜单项点击事件
            menu.querySelectorAll('.category-menu-item').forEach(item => {
                item.removeEventListener('click', handleCategoryMenuClick);
                item.addEventListener('click', handleCategoryMenuClick);
            });
        }

        // 隐藏分类右键菜单
        function hideCategoryContextMenu() {
            const menu = document.getElementById('categoryContextMenu');
            menu.style.display = 'none';
            currentCategoryElement = null;
        }

        // 处理分类菜单点击事件
        function handleCategoryMenuClick(e) {
            const action = e.currentTarget.dataset.action;
            const category = currentCategoryElement.dataset.category;

            switch (action) {
                case 'rename':
                    const newName = prompt('请输入新的分类名称:', category);
                    if (newName && newName.trim() && newName !== category) {
                        // 更新分类名称
                        const index = categories.indexOf(category);
                        if (index !== -1) {
                            categories[index] = newName;
                            // 更新所有使用该分类的任务
                            tasks.forEach(task => {
                                if (task.category === category) {
                                    task.category = newName;
                                }
                            });
                            saveCategories();
                            saveTasks();
                            renderCategories();
                            if (currentView === 'kanban') {
                                renderKanbanView();
                            } else {
                                renderTasks();
                            }
                        }
                    }
                    break;
                case 'delete':
                    if (confirm(`确定要删除分类"${category}"吗?\n该分类下的任务将变为未分类。`)) {
                        // 删除分类
                        const index = categories.indexOf(category);
                        if (index !== -1) {
                            categories.splice(index, 1);
                            // 将该分类下的任务设为未分类
                            tasks.forEach(task => {
                                if (task.category === category) {
                                    task.category = '';
                                }
                            });
                            saveCategories();
                            saveTasks();
                            // 如果当前显示的是被删除的分类,切换到"全部"
                            if (currentCategory === category) {
                                currentCategory = 'all';
                            }
                            renderCategories();
                            if (currentView === 'kanban') {
                                renderKanbanView();
                            } else {
                                renderTasks();
                            }
                        }
                    }
                    break;
            }

            hideCategoryContextMenu();
        }

        // 修改任务渲染函数,添加时间显示
        function renderTaskContent(task) {
            const timeHtml = getTaskTimeHtml(task);
            const categoryHtml = task.category && currentCategory === 'all' ? 
                `<div class="task-category">分类:${task.category}</div>` : '';
          
            return `
                <div>
                    <div style="display: flex; align-items: center; gap: 12px;">
                        <span class="status-tag status-${task.status}">${getStatusText(task.status)}</span>
                        <span class="title">${task.title}</span>
                        ${timeHtml}
                        ${task.priority && task.priority !== 'none' ? 
                            `<span class="priority-tag priority-${task.priority}">${getPriorityText(task.priority)}</span>` : ''}
                    </div>
                    ${categoryHtml}
                </div>
            `;
        }

        function getTaskTimeHtml(task) {
            if (!task.timeType) return '';
          
            const now = new Date();
            let isOverdue = false;
            let timeText = '';
          
            if (task.timeType === 'deadline') {
                const deadline = new Date(task.deadlineTime);
                isOverdue = now > deadline && task.status !== 'done';
                timeText = formatRelativeDateTime(deadline, isOverdue);
            } else if (task.timeType === 'period') {
                const start = new Date(task.startTime);
                const end = new Date(task.endTime);
                isOverdue = now > end && task.status !== 'done';
                timeText = `${formatRelativeDateTime(start)} - ${formatRelativeDateTime(end, isOverdue)}`;
            }
          
            return `<span class="task-time ${isOverdue ? 'overdue' : ''}">${timeText}</span>`;
        }

        function formatRelativeDateTime(date, isOverdue = false) {
            const now = new Date();
            const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
            const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
            const diffDays = Math.floor((targetDate - today) / (1000 * 60 * 60 * 24));
          
            let timeStr = date.toLocaleString('zh-CN', {
                hour: '2-digit',
                minute: '2-digit'
            });

            if (isOverdue) {
                // 检查是否是同一天但是时间已过
                if (diffDays === 0) {
                    const nowTime = now.getTime();
                    const targetTime = date.getTime();
                    if (targetTime < nowTime) {
                        return `已过期 ${formatTime(date)}`;
                    }
                }
                const overdueDays = Math.abs(diffDays);
                return `已过期${overdueDays}天`;
            }

            // 相对日期显示
            if (diffDays === 0) {
                return `今天 ${formatTime(date)}`;
            } else if (diffDays === 1) {
                return `明天 ${formatTime(date)}`;
            } else if (diffDays === -1) {
                return `昨天 ${formatTime(date)}`;
            }
          
            return date.toLocaleString('zh-CN', {
                month: 'numeric',
                day: 'numeric',
                hour: '2-digit',
                minute: '2-digit'
            });
        }

        function formatTime(date) {
            return date.toLocaleString('zh-CN', {
                hour: '2-digit',
                minute: '2-digit'
            });
        }

        // 添加时间选择相关功能
        function showTimePicker(taskId) {
            currentEditingTaskId = taskId;
            const task = tasks.find(t => t.id === taskId);
          
            // 设置当前值
            document.getElementById('timeType').value = task.timeType || 'deadline';
            document.getElementById('deadlineTime').value = task.deadlineTime || '';
            document.getElementById('startTime').value = task.startTime || '';
            document.getElementById('endTime').value = task.endTime || '';
          
            toggleTimeInputs();
          
            document.getElementById('timePickerModal').style.display = 'block';
            document.getElementById('modalOverlay').style.display = 'block';
        }

        function closeTimePicker() {
            document.getElementById('timePickerModal').style.display = 'none';
            document.getElementById('modalOverlay').style.display = 'none';
            currentEditingTaskId = null;
        }

        function confirmTimePicker() {
            const task = tasks.find(t => t.id === currentEditingTaskId);
            if (!task) return;
          
            const timeType = document.getElementById('timeType').value;
            task.timeType = timeType;
          
            if (timeType === 'deadline') {
                task.deadlineTime = document.getElementById('deadlineTime').value;
                task.startTime = null;
                task.endTime = null;
            } else {
                task.deadlineTime = null;
                task.startTime = document.getElementById('startTime').value;
                task.endTime = document.getElementById('endTime').value;
            }
          
            saveTasks();
            closeTimePicker();
            if (currentView === 'kanban') {
                renderKanbanView();
            } else {
                renderTasks();
            }
        }

        function toggleTimeInputs() {
            const timeType = document.getElementById('timeType').value;
            document.getElementById('deadlineInputs').style.display = 
                timeType === 'deadline' ? 'block' : 'none';
            document.getElementById('periodInputs').style.display = 
                timeType === 'period' ? 'block' : 'none';
        }

        // 更新分类标签
        function updateCategoryTabs() {
            const tabContainers = document.querySelectorAll('.tab-container');
            tabContainers.forEach(tabContainer => {
                const addCategoryBtn = tabContainer.querySelector('.add-category-btn');
                const hideDoneToggle = tabContainer.querySelector('.hide-done-toggle');
              
                // 清空现有标签
                Array.from(tabContainer.children).forEach(child => {
                    if (!child.classList.contains('add-category-btn') && 
                        !child.classList.contains('hide-done-toggle')) {
                        child.remove();
                    }
                });

                // 添加基本标签
                const allTab = createTabElement('全部', 'all');
                const noneTab = createTabElement('未分类', 'none');
              
                tabContainer.insertBefore(allTab, addCategoryBtn);
                tabContainer.insertBefore(noneTab, addCategoryBtn);
              
                // 添加分类标签
                categories.forEach(category => {
                    const tab = createTabElement(category, category);
                    // 为自定义分类添加右键菜单
                    if (category !== 'all' && category !== 'none') {
                        tab.addEventListener('contextmenu', (e) => {
                            e.preventDefault();
                            currentCategoryElement = tab;
                            showCategoryContextMenu(e.clientX, e.clientY);
                        });
                    }
                    tabContainer.insertBefore(tab, addCategoryBtn);
                });
            });
        }

        function createTabElement(text, category) {
            const tab = document.createElement('div');
            tab.className = `tab-item${currentCategory === category ? ' active' : ''}`;
            tab.dataset.category = category;
            tab.textContent = text;
            return tab;
        }

        // 添加点击事件监听器来关闭分类右键菜单
        document.addEventListener('click', (e) => {
            if (!e.target.closest('#categoryContextMenu') && !e.target.closest('.tab-item')) {
                hideCategoryContextMenu();
            }
        });
    </script>
</body>

</html>
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119783 回帖
3 操作
Achuan-2 在 2025-01-22 20:51:50 更新了该帖
Achuan-2 在 2025-01-22 20:18:04 更新了该帖
Achuan-2 在 2025-01-22 10:12:54 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Achuan-2 2 评论

    用关联列创建父任务列而不是子任务列,关联自身,应该就可以实现父子任务嵌套了,见我示例代码的 json 数据

    嗯,我试试
    stevehfut
    确实,我刚刚思路跑偏了,确实可以实现多级嵌套
    stevehfut
  • 其他回帖
  • Achuan-2 2 评论

    用关联列好处是用户方便查看,但有明显弊端,太不灵活了

    关联列关联数据库本身,可以,这个想法好的,超赞,我自己关联列用得少,因为觉得关联其他数据库好麻烦,维护太累了,但是关联自身,就方便多了

    1 操作
    Achuan-2 在 2025-01-22 10:48:14 更新了该回帖
    但这样只能嵌套一层,如果要实现父子孙两层嵌套任务,就感觉太冗杂了,直接用 ID 实现的话,可读性比较差,我希望的是即使再不使用插件的前提下也能保证数据的完整可读(防止我哪天跑路不维护插件,而导致用户的数据变得复杂,毕竟目前数据库的 api 不怎么稳定,感觉 D 大后面出画廊视图时会大改一波数据库 api)
    stevehfut
    我先试试就一层嵌套的模式吧(主观感觉一般一层就够了)
    stevehfut
  • 期待大佬们整出来中,目前父子任务我还是用任务块在笔记里看,数据库管理父任务用悬浮窗也能看到子任务。如果数据库里又多一套父子嵌套感觉要维护两次父子任务关系了,或者就完全抛弃任务块的父子关系了,完全在数据库管理,这个就看大佬的完成度了哈哈

  • Achuan-2 1 评论

    @stevehfut 我帖子更新了下写的 demo 功能,你可以看看

    • 支持列表视图:平铺展示全部 or 分类下的所有任务,支持按状态、优先级、标题排序
    • 支持看板视图:显示全部 or 分类下的任务进展状态(按待处理、正在进行、已完成展示三个看板)
    • 支持添加时间,支持设置时间段任务,支持显示过期时间
    • 支持设置优先级
    • 支持设置任务当前状态
    • 支持拖拽排序,任务可以被拖拽成为某个任务的子任务
    • 支持任务添加分类

    PixPin20250122201925.png

    PixPin20250122201930.png

    感觉不错,我后面写界面就参考这个了
    stevehfut
  • 查看全部回帖
Achuan-2
公众号:https://mp.weixin.qq.com/s/_NrGwjJnEta0oT5a6EKdiA 知乎:https://www.zhihu.com/column/c_1922583788405359964 上海

推荐标签 标签

  • 以太坊

    以太坊(Ethereum)并不是一个机构,而是一款能够在区块链上实现智能合约、开源的底层系统。以太坊是一个平台和一种编程语言 Solidity,使开发人员能够建立和发布下一代去中心化应用。 以太坊可以用来编程、分散、担保和交易任何事物:投票、域名、金融交易所、众筹、公司管理、合同和知识产权等等。

    34 引用 • 367 回帖 • 1 关注
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 403 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    135 引用 • 798 回帖 • 2 关注
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    14 引用 • 84 回帖
  • 工具

    子曰:“工欲善其事,必先利其器。”

    308 引用 • 773 回帖
  • Visio
    1 引用 • 2 回帖 • 1 关注
  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    174 引用 • 414 回帖 • 344 关注
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 350 关注
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 458 关注
  • BookxNote

    BookxNote 是一款全新的电子书学习工具,助力您的学习与思考,让您的大脑更高效的记忆。

    笔记整理交给我,一心只读圣贤书。

    1 引用 • 1 回帖 • 1 关注
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖 • 1 关注
  • Dubbo

    Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,是 [阿里巴巴] SOA 服务化治理方案的核心框架,每天为 2,000+ 个服务提供 3,000,000,000+ 次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。

    60 引用 • 82 回帖 • 636 关注
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    71 引用 • 535 回帖 • 847 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 724 关注
  • SQLServer

    SQL Server 是由 [微软] 开发和推广的关系数据库管理系统(DBMS),它最初是由 微软、Sybase 和 Ashton-Tate 三家公司共同开发的,并于 1988 年推出了第一个 OS/2 版本。

    21 引用 • 31 回帖 • 1 关注
  • sts
    2 引用 • 2 回帖 • 260 关注
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 702 关注
  • 分享

    有什么新发现就分享给大家吧!

    251 引用 • 1801 回帖 • 1 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • 黑曜石

    黑曜石是一款强大的知识库工具,支持本地 Markdown 文件编辑,支持双向链接和关系图。

    A second brain, for you, forever.

    34 引用 • 333 回帖 • 1 关注
  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    201 引用 • 120 回帖
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖 • 1 关注
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 565 关注
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    110 引用 • 153 回帖
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 429 关注
  • Office

    Office 现已更名为 Microsoft 365. Microsoft 365 将高级 Office 应用(如 Word、Excel 和 PowerPoint)与 1 TB 的 OneDrive 云存储空间、高级安全性等结合在一起,可帮助你在任何设备上完成操作。

    6 引用 • 35 回帖
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 56 关注