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

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

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

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

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

相关帖子

欢迎来到这里!

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

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

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

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

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

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

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

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

    我目前想到的思路

    1737511973179.png

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

    3 回复
  • Achuan-2

    @stevehfut

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

推荐标签 标签

  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖 • 1 关注
  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    345 引用 • 724 回帖
  • 负能量

    上帝为你关上了一扇门,然后就去睡觉了....努力不一定能成功,但不努力一定很轻松 (° ー °〃)

    88 引用 • 1235 回帖 • 406 关注
  • Access
    1 引用 • 3 回帖 • 6 关注
  • Kotlin

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

    19 引用 • 33 回帖 • 73 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    945 引用 • 1460 回帖 • 2 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    289 引用 • 4492 回帖 • 657 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3194 引用 • 8214 回帖
  • Spark

    Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。

    74 引用 • 46 回帖 • 567 关注
  • SQLServer

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

    21 引用 • 31 回帖 • 1 关注
  • C++

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

    107 引用 • 153 回帖 • 1 关注
  • QQ

    1999 年 2 月腾讯正式推出“腾讯 QQ”,在线用户由 1999 年的 2 人(马化腾和张志东)到现在已经发展到上亿用户了,在线人数超过一亿,是目前使用最广泛的聊天软件之一。

    45 引用 • 557 回帖
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 210 关注
  • 电影

    这是一个不能说的秘密。

    122 引用 • 608 回帖
  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 636 关注
  • FreeMarker

    FreeMarker 是一款好用且功能强大的 Java 模版引擎。

    23 引用 • 20 回帖 • 463 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    7 引用 • 27 回帖
  • SEO

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

    35 引用 • 200 回帖 • 22 关注
  • Bootstrap

    Bootstrap 是 Twitter 推出的一个用于前端开发的开源工具包。它由 Twitter 的设计师 Mark Otto 和 Jacob Thornton 合作开发,是一个 CSS / HTML 框架。

    18 引用 • 33 回帖 • 661 关注
  • BND

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

    107 引用 • 1281 回帖 • 29 关注
  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖
  • 强迫症

    强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。

    15 引用 • 161 回帖
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 721 关注
  • RYMCU

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

    4 引用 • 6 回帖 • 52 关注
  • 爬虫

    网络爬虫(Spider、Crawler),是一种按照一定的规则,自动地抓取万维网信息的程序。

    106 引用 • 275 回帖
  • RESTful

    一种软件架构设计风格而不是标准,提供了一组设计原则和约束条件,主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

    30 引用 • 114 回帖 • 4 关注