回复: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>
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于