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

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

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

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

    24927 引用 • 102683 回帖
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 1 评论 via Android

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

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

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

    嗯,我试试
    stevehfut
    确实,我刚刚思路跑偏了,确实可以实现多级嵌套
    stevehfut
  • FlyingY 1 评论

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

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

    @stevehfut

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

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

推荐标签 标签

  • 尊园地产

    昆明尊园房地产经纪有限公司,即:Kunming Zunyuan Property Agency Company Limited(简称“尊园地产”)于 2007 年 6 月开始筹备,2007 年 8 月 18 日正式成立,注册资本 200 万元,公司性质为股份经纪有限公司,主营业务为:代租、代售、代办产权过户、办理银行按揭、担保、抵押、评估等。

    1 引用 • 22 回帖 • 784 关注
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 55 关注
  • 链书

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

    链书社

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

    14 引用 • 257 回帖 • 3 关注
  • Webswing

    Webswing 是一个能将任何 Swing 应用通过纯 HTML5 运行在浏览器中的 Web 服务器,详细介绍请看 将 Java Swing 应用变成 Web 应用

    1 引用 • 15 回帖 • 643 关注
  • LaTeX

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

    12 引用 • 54 回帖 • 13 关注
  • Sillot

    Insights(注意当前设置 master 为默认分支)

    汐洛彖夲肜矩阵(Sillot T☳Converbenk Matrix),致力于服务智慧新彖乄,具有彖乄驱动、极致优雅、开发者友好的特点。其中汐洛绞架(Sillot-Gibbet)基于自思源笔记(siyuan-note),前身是思源笔记汐洛版(更早是思源笔记汐洛分支),是智慧新录乄终端(多端融合,移动端优先)。

    主仓库地址:Hi-Windom/Sillot

    文档地址:sillot.db.sc.cn

    注意事项:

    1. ⚠️ 汐洛仍在早期开发阶段,尚不稳定
    2. ⚠️ 汐洛并非面向普通用户设计,使用前请了解风险
    3. ⚠️ 汐洛绞架基于思源笔记,开发者尽最大努力与思源笔记保持兼容,但无法实现 100% 兼容
    29 引用 • 25 回帖 • 107 关注
  • ActiveMQ

    ActiveMQ 是 Apache 旗下的一款开源消息总线系统,它完整实现了 JMS 规范,是一个企业级的消息中间件。

    19 引用 • 13 回帖 • 676 关注
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 233 回帖 • 5 关注
  • Ruby

    Ruby 是一种开源的面向对象程序设计的服务器端脚本语言,在 20 世纪 90 年代中期由日本的松本行弘(まつもとゆきひろ/Yukihiro Matsumoto)设计并开发。在 Ruby 社区,松本也被称为马茨(Matz)。

    7 引用 • 31 回帖 • 255 关注
  • Quicker

    Quicker 您的指尖工具箱!操作更少,收获更多!

    36 引用 • 155 回帖
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    90 引用 • 59 回帖 • 8 关注
  • 微信

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

    132 引用 • 796 回帖
  • 心情

    心是产生任何想法的源泉,心本体会陷入到对自己本体不能理解的状态中,因为心能产生任何想法,不能分出对错,不能分出自己。

    59 引用 • 369 回帖 • 1 关注
  • Access
    1 引用 • 3 回帖 • 2 关注
  • Swift

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

    36 引用 • 37 回帖 • 542 关注
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖 • 1 关注
  • Anytype
    3 引用 • 31 回帖 • 14 关注
  • Spring

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

    946 引用 • 1460 回帖 • 1 关注
  • GraphQL

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

    4 引用 • 3 回帖 • 5 关注
  • Ngui

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

    7 引用 • 9 回帖 • 400 关注
  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 298 关注
  • Ant-Design

    Ant Design 是服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。

    17 引用 • 23 回帖
  • 印象笔记
    3 引用 • 16 回帖
  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    729 引用 • 1278 回帖 • 1 关注
  • Bootstrap

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

    18 引用 • 33 回帖 • 651 关注
  • 又拍云

    又拍云是国内领先的 CDN 服务提供商,国家工信部认证通过的“可信云”,乌云众测平台认证的“安全云”,为移动时代的创业者提供新一代的 CDN 加速服务。

    20 引用 • 37 回帖 • 573 关注
  • 人工智能

    人工智能(Artificial Intelligence)是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门技术科学。

    159 引用 • 305 回帖