回复:STtools 插件(0.4.0):日程管理 2.0(和思源紧紧相拥的日历视图) - Achuan-2 的回 - 链滴,由于评论有字数限制,所以只能另发帖
数据库添加列
- 存储父任务信息
- 存储排序顺序
AI 写的 demo 代码
- 支持添加时间,支持设置时间段任务,支持显示过期时间
- 支持设置优先级
- 支持设置任务当前状态
- 支持拖拽排序,任务可以被拖拽成为某个任务的子任务
- 支持任务添加自定义分类
- 支持列表视图:平铺展示全部 or 分类下的所有任务,支持按状态、优先级、标题排序
- 支持看板视图:显示全部 or 分类下的任务进展状态(按待处理、正在进行、已完成展示三个看板)
<!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>
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于