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

回复: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>
  • 思源笔记

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

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

    25120 引用 • 103566 回帖 • 1 关注
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

    @stevehfut

    1. 实现了父子任务显示
    2. 排序改动任务顺序、父任务归属
    3. 右键修改优先级、完成状态
  • stevehfut

    其是我最想问的是在思源数据库如何体现

    直接加一个父任务 ID 列,然后直接显示父任务的 ID 吗?但这样做数据库中的可读性比较差(只显示个 ID 不知道父任务具体是什么

    我目前想到的思路

    1737511973179.png

    1737512399936.png
    但还是只能嵌套一层

    3 回复
  • Achuan-2 2 评论

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

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

    1 操作
    Achuan-2 在 2025-01-22 10:48:14 更新了该回帖
    但这样只能嵌套一层,如果要实现父子孙两层嵌套任务,就感觉太冗杂了,直接用 ID 实现的话,可读性比较差,我希望的是即使再不使用插件的前提下也能保证数据的完整可读(防止我哪天跑路不维护插件,而导致用户的数据变得复杂,毕竟目前数据库的 api 不怎么稳定,感觉 D 大后面出画廊视图时会大改一波数据库 api)
    stevehfut
    我先试试就一层嵌套的模式吧(主观感觉一般一层就够了)
    stevehfut
  • FlyingY 1 评论

    如果是在数据库中添加父任务和子任务,那是不是在所有数据库,只要我自己添加了这两列就能实现这个功能,那样这个插件的功能就强大了。

    父子任务管理功能还没做,现在还在想思路
    stevehfut
  • Achuan-2 2 评论

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

    嗯,我试试
    stevehfut
    确实,我刚刚思路跑偏了,确实可以实现多级嵌套
    stevehfut
  • 期待大佬们整出来中,目前父子任务我还是用任务块在笔记里看,数据库管理父任务用悬浮窗也能看到子任务。如果数据库里又多一套父子嵌套感觉要维护两次父子任务关系了,或者就完全抛弃任务块的父子关系了,完全在数据库管理,这个就看大佬的完成度了哈哈

  • Achuan-2 1 评论

    如果要对任务进行分类,应该可以用视图来分类,因为视图可以单独获取视图数据,减少解析压力

    嗯,确实,我目前是读取数据库的事件也是靠视图获取的(直接读数据库文件,解析 json 挺难受的)
    stevehfut
  • Achuan-2 1 评论

    @stevehfut

    让 AI 写了一个任务管理的面板,是我比较理想的任务面板了PixPin20250122155902.png

    川佬行动力好强 👍 我后面实现的时候参考一下这个界面,下午看了 fullcalendar 的看板视图,想直接用,但似乎得买许可才行,看来得让 ai 手搓一个了
    stevehfut
  • Achuan-2 1 评论

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

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

    PixPin20250122201925.png

    PixPin20250122201930.png

    感觉不错,我后面写界面就参考这个了
    stevehfut
  • njtutnx

    请教一下报错“目标数据库未设置关联自身的列”

请输入回帖内容 ...

推荐标签 标签

  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 649 关注
  • Facebook

    Facebook 是一个联系朋友的社交工具。大家可以通过它和朋友、同事、同学以及周围的人保持互动交流,分享无限上传的图片,发布链接和视频,更可以增进对朋友的了解。

    4 引用 • 15 回帖 • 442 关注
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 5 关注
  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    7 引用 • 69 回帖
  • 服务

    提供一个服务绝不仅仅是简单的把硬件和软件累加在一起,它包括了服务的可靠性、服务的标准化、以及对服务的监控、维护、技术支持等。

    41 引用 • 24 回帖 • 2 关注
  • BND

    BND(Baidu Netdisk Downloader)是一款图形界面的百度网盘不限速下载器,支持 Windows、Linux 和 Mac,详细介绍请看这里

    107 引用 • 1281 回帖 • 38 关注
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    588 引用 • 3538 回帖
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    410 引用 • 3588 回帖
  • 浅吟主题

    Jeffrey Chen 制作的思源笔记主题,项目仓库:https://github.com/TCOTC/Whisper

    1 引用 • 28 回帖 • 1 关注
  • 黑曜石

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

    A second brain, for you, forever.

    22 引用 • 213 回帖
  • Angular

    AngularAngularJS 的新版本。

    26 引用 • 66 回帖 • 545 关注
  • Scala

    Scala 是一门多范式的编程语言,集成面向对象编程和函数式编程的各种特性。

    13 引用 • 11 回帖 • 158 关注
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 80 关注
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖 • 2 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    108 引用 • 295 回帖
  • jsDelivr

    jsDelivr 是一个开源的 CDN 服务,可为 npm 包、GitHub 仓库提供免费、快速并且可靠的全球 CDN 加速服务。

    5 引用 • 31 回帖 • 106 关注
  • CAP

    CAP 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。

    12 引用 • 5 回帖 • 636 关注
  • Dubbo

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

    60 引用 • 82 回帖 • 609 关注
  • SendCloud

    SendCloud 由搜狐武汉研发中心孵化的项目,是致力于为开发者提供高质量的触发邮件服务的云端邮件发送平台,为开发者提供便利的 API 接口来调用服务,让邮件准确迅速到达用户收件箱并获得强大的追踪数据。

    2 引用 • 8 回帖 • 494 关注
  • SEO

    发布对别人有帮助的原创内容是最好的 SEO 方式。

    35 引用 • 200 回帖 • 27 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 634 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    16 引用 • 236 回帖 • 266 关注
  • Sandbox

    如果帖子标签含有 Sandbox ,则该帖子会被视为“测试帖”,主要用于测试社区功能,排查 bug 等,该标签下内容不定期进行清理。

    432 引用 • 1250 回帖 • 597 关注
  • Access
    1 引用 • 3 回帖 • 2 关注
  • Node.js

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

    139 引用 • 269 回帖 • 3 关注