-
-
-
+
+
-
-
+
+
From 13741d130663b5fe5a7a973b65afc87411f67e72 Mon Sep 17 00:00:00 2001
From: HaibinLai <12211612@mail.sustech.edu.cn>
Date: Fri, 17 Apr 2026 18:50:17 +0000
Subject: [PATCH 033/100] =?UTF-8?q?=E6=96=B0=E5=A2=9E2D=E8=89=BE=E6=A3=AE?=
=?UTF-8?q?=E8=B1=AA=E5=A8=81=E5=B0=94=E7=9F=A9=E9=98=B5=E8=A7=86=E5=9B=BE?=
=?UTF-8?q?=EF=BC=9AX=E8=BD=B4=E7=B4=A7=E6=80=A5=E5=BA=A6(=E6=88=AA?=
=?UTF-8?q?=E6=AD=A2=E6=97=A5=E6=9C=9F)=E3=80=81Y=E8=BD=B4=E9=87=8D?=
=?UTF-8?q?=E8=A6=81=E5=BA=A6(1-5=E6=98=9F)=EF=BC=8C=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E5=88=97=E8=A1=A8/=E7=9F=A9=E9=98=B5=E5=8F=8C=E8=A7=86?=
=?UTF-8?q?=E5=9B=BE=E5=88=87=E6=8D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Opus 4.6
---
common/todo/todo.css | 82 +++++
common/todo/todo.js | 703 ++++++++++++++++++++++++---------------
include/todo/api.php | 16 +-
include/todo/install.php | 14 +
template/page/todo.php | 28 +-
5 files changed, 556 insertions(+), 287 deletions(-)
diff --git a/common/todo/todo.css b/common/todo/todo.css
index 3cdbf9f..6e4ef43 100644
--- a/common/todo/todo.css
+++ b/common/todo/todo.css
@@ -453,6 +453,74 @@
font-size: 13px;
}
+/* 重要度星星 */
+.todo-importance-tag {
+ color: #f59e0b;
+ font-size: var(--theme-text-more-mini, 0.69rem);
+ letter-spacing: -1px;
+}
+
+/* 重要度滑块 */
+.todo-add-importance {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: var(--theme-secondary, 0.78rem);
+}
+
+.todo-add-importance input[type="range"] {
+ width: 80px;
+ accent-color: #f59e0b;
+}
+
+.todo-add-importance .importance-label {
+ color: #f59e0b;
+ font-size: var(--theme-text-more-mini, 0.69rem);
+ min-width: 60px;
+ letter-spacing: -1px;
+}
+
+/* 视图切换按钮 */
+.todo-views {
+ display: flex;
+ gap: 4px;
+ background: var(--theme-border-color, #eee);
+ border-radius: 8px;
+ padding: 3px;
+}
+
+.todo-view-btn {
+ padding: 5px 14px;
+ border: none;
+ border-radius: 6px;
+ background: transparent;
+ color: var(--theme-text-secondary, rgba(0, 0, 0, 0.65));
+ font-size: var(--theme-secondary, 0.78rem);
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.todo-view-btn.active {
+ background: var(--theme-bg-color, #fff);
+ color: var(--theme-text-color, #333);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+/* 2D图容器 */
+.todo-chart-wrap {
+ width: 100%;
+ display: none;
+ border: 1px solid var(--theme-border-color, #e8e8e8);
+ border-radius: 12px;
+ overflow: hidden;
+ background: var(--theme-bg-color, #fff);
+}
+
+.todo-chart-wrap canvas {
+ display: block;
+ width: 100%;
+}
+
/* 深色模式适配 */
.dark .todo-add {
border-color: #3a3a3a;
@@ -534,6 +602,20 @@
background: rgba(5, 150, 105, 0.15);
}
+.dark .todo-views {
+ background: #2a2a2b;
+}
+
+.dark .todo-view-btn.active {
+ background: #3a3a3a;
+ color: var(--theme-text-color);
+}
+
+.dark .todo-chart-wrap {
+ border-color: #3a3a3a;
+ background: var(--theme-front-color, #2a2a2b);
+}
+
/* 响应式 */
@media (max-width: 768px) {
.todo-container {
diff --git a/common/todo/todo.js b/common/todo/todo.js
index 77c3921..dfea84a 100644
--- a/common/todo/todo.js
+++ b/common/todo/todo.js
@@ -1,6 +1,6 @@
/**
* 待办事项前端逻辑
- * LocalStorage 缓存 + AJAX 同步
+ * LocalStorage 缓存 + AJAX 同步 + 2D 艾森豪威尔矩阵视图
* @author Haibin
* @date 2026-04-17
*/
@@ -11,272 +11,171 @@
const AJAX_URL = window.HOME + '/wp-admin/admin-ajax.php';
let todos = [];
- let currentFilter = 'all'; // all | active | completed
+ let currentFilter = 'all';
+ let currentView = 'list'; // list | chart
let editingId = null;
let dragItem = null;
- /* ========== LocalStorage 层 ========== */
+ // Chart state
+ let chartCanvas = null;
+ let chartCtx = null;
+ let chartPoints = []; // {x, y, todo, screenX, screenY}
+ let hoveredPoint = null;
+ let chartPadding = { top: 40, right: 40, bottom: 50, left: 60 };
+ /* ========== LocalStorage ========== */
function saveToLocal() {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
- } catch (e) { }
+ try { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); } catch (e) { }
}
-
function loadFromLocal() {
- try {
- const data = localStorage.getItem(STORAGE_KEY);
- return data ? JSON.parse(data) : [];
- } catch (e) {
- return [];
- }
+ try { var d = localStorage.getItem(STORAGE_KEY); return d ? JSON.parse(d) : []; } catch (e) { return []; }
}
- /* ========== AJAX 层 ========== */
-
+ /* ========== AJAX ========== */
function ajax(action, data) {
return new Promise(function (resolve, reject) {
- var formData = new FormData();
- formData.append('action', action);
- if (data) {
- Object.keys(data).forEach(function (key) {
- formData.append(key, data[key]);
- });
- }
- fetch(AJAX_URL, { method: 'POST', body: formData, credentials: 'same-origin' })
+ var fd = new FormData();
+ fd.append('action', action);
+ if (data) Object.keys(data).forEach(function (k) { fd.append(k, data[k]); });
+ fetch(AJAX_URL, { method: 'POST', body: fd, credentials: 'same-origin' })
.then(function (r) { return r.json(); })
- .then(function (res) {
- if (res.success) resolve(res.data);
- else reject(res.data);
- })
+ .then(function (res) { res.success ? resolve(res.data) : reject(res.data); })
.catch(reject);
});
}
-
function syncFromServer() {
- ajax('todo_list').then(function (data) {
- todos = data;
- saveToLocal();
- render();
- }).catch(function () { /* 离线模式,使用本地数据 */ });
+ ajax('todo_list').then(function (data) { todos = data; saveToLocal(); render(); }).catch(function () { });
}
- /* ========== 操作方法 ========== */
+ /* ========== 日期计算 ========== */
+ function getDaysRemaining(dateStr) {
+ if (!dateStr) return null;
+ var today = new Date(); today.setHours(0, 0, 0, 0);
+ var due = new Date(dateStr); due.setHours(0, 0, 0, 0);
+ return Math.ceil((due - today) / 86400000);
+ }
+ function formatDate(dateStr) {
+ if (!dateStr) return '';
+ var d = new Date(dateStr);
+ return (d.getMonth() + 1) + '月' + d.getDate() + '日';
+ }
+ function getCountdownHtml(todo) {
+ if (!todo.due_date || todo.completed == 1) return '';
+ var days = getDaysRemaining(todo.due_date);
+ if (days === null) return '';
+ var cls, text;
+ if (days < 0) { cls = 'overdue'; text = '已过期' + Math.abs(days) + '天'; }
+ else if (days === 0) { cls = 'urgent'; text = '今天截止'; }
+ else if (days === 1) { cls = 'urgent'; text = '明天截止'; }
+ else if (days <= 3) { cls = 'soon'; text = '剩余' + days + '天'; }
+ else { cls = 'normal'; text = '剩余' + days + '天'; }
+ return '' + text + '';
+ }
+ function isOverdue(todo) {
+ if (!todo.due_date || todo.completed == 1) return false;
+ var d = getDaysRemaining(todo.due_date); return d !== null && d < 0;
+ }
+ function priorityLabel(p) { return { high: '紧急', medium: '普通', low: '低优' }[p] || '普通'; }
+ function importanceLabel(v) { return ['', '不重要', '较低', '一般', '重要', '非常重要'][v] || '一般'; }
+ /* ========== CRUD ========== */
function addTodo() {
var input = document.getElementById('todo-input');
- var prioritySelect = document.getElementById('todo-priority');
- var dateInput = document.getElementById('todo-date');
-
var title = input.value.trim();
if (!title) return;
+ var priority = document.getElementById('todo-priority').value;
+ var dueDate = document.getElementById('todo-date').value;
+ var importance = parseInt(document.getElementById('todo-importance').value) || 3;
- var priority = prioritySelect.value;
- var dueDate = dateInput.value;
-
- // 乐观更新:临时 ID
var tempId = 'temp_' + Date.now();
var tempTodo = {
- id: tempId,
- title: title,
- completed: 0,
- priority: priority,
- due_date: dueDate || null,
- sort_order: 0,
+ id: tempId, title: title, completed: 0, priority: priority,
+ importance: importance, due_date: dueDate || null, sort_order: 0,
created_at: new Date().toISOString().slice(0, 19).replace('T', ' '),
updated_at: new Date().toISOString().slice(0, 19).replace('T', ' ')
};
+ todos.unshift(tempTodo); saveToLocal(); render();
+ input.value = ''; document.getElementById('todo-date').value = ''; input.focus();
- todos.unshift(tempTodo);
- saveToLocal();
- render();
- input.value = '';
- dateInput.value = '';
- input.focus();
-
- // 提交到服务器
- ajax('todo_create', { title: title, priority: priority, due_date: dueDate })
+ ajax('todo_create', { title: title, priority: priority, due_date: dueDate, importance: importance })
.then(function (row) {
var idx = todos.findIndex(function (t) { return t.id === tempId; });
if (idx !== -1) todos[idx] = row;
- saveToLocal();
- render();
+ saveToLocal(); render();
})
.catch(function () {
todos = todos.filter(function (t) { return t.id !== tempId; });
- saveToLocal();
- render();
+ saveToLocal(); render();
});
}
function toggleTodo(id) {
var todo = todos.find(function (t) { return t.id == id; });
if (!todo) return;
-
todo.completed = todo.completed == 1 ? 0 : 1;
- saveToLocal();
- render();
-
+ saveToLocal(); render();
ajax('todo_update', { id: id, completed: todo.completed });
}
function deleteTodo(id) {
if (!confirm('确定删除这条待办?')) return;
-
todos = todos.filter(function (t) { return t.id != id; });
- saveToLocal();
- render();
-
+ saveToLocal(); render();
ajax('todo_delete', { id: id });
}
- function startEdit(id) {
- editingId = id;
- render();
- var el = document.getElementById('edit-title-' + id);
- if (el) { el.focus(); el.select(); }
- }
+ function startEdit(id) { editingId = id; render(); var el = document.getElementById('edit-title-' + id); if (el) { el.focus(); el.select(); } }
function saveEdit(id) {
var el = document.getElementById('edit-title-' + id);
var priorityEl = document.getElementById('edit-priority-' + id);
var dateEl = document.getElementById('edit-date-' + id);
-
+ var impEl = document.getElementById('edit-importance-' + id);
var todo = todos.find(function (t) { return t.id == id; });
if (!todo) return;
-
- var newTitle = el ? el.value.trim() : todo.title;
- if (!newTitle) newTitle = todo.title;
-
var data = { id: id };
- todo.title = newTitle;
- data.title = newTitle;
-
- if (priorityEl) {
- todo.priority = priorityEl.value;
- data.priority = priorityEl.value;
- }
- if (dateEl) {
- todo.due_date = dateEl.value || null;
- data.due_date = dateEl.value || '';
- }
-
- editingId = null;
- saveToLocal();
- render();
+ var newTitle = el ? el.value.trim() : todo.title;
+ todo.title = newTitle || todo.title; data.title = todo.title;
+ if (priorityEl) { todo.priority = priorityEl.value; data.priority = priorityEl.value; }
+ if (dateEl) { todo.due_date = dateEl.value || null; data.due_date = dateEl.value || ''; }
+ if (impEl) { todo.importance = parseInt(impEl.value) || 3; data.importance = todo.importance; }
+ editingId = null; saveToLocal(); render();
ajax('todo_update', data);
}
- function cancelEdit() {
- editingId = null;
+ function cancelEdit() { editingId = null; render(); }
+ function setFilter(f) { currentFilter = f; render(); }
+ function setView(v) {
+ currentView = v;
+ document.querySelectorAll('.todo-view-btn').forEach(function (b) { b.classList.toggle('active', b.dataset.view === v); });
+ document.getElementById('todo-list').style.display = v === 'list' ? '' : 'none';
+ document.getElementById('todo-chart-wrap').style.display = v === 'chart' ? '' : 'none';
render();
}
- function setFilter(filter) {
- currentFilter = filter;
- render();
- }
-
- /* ========== 拖拽排序 ========== */
-
- function handleDragStart(e) {
- dragItem = e.currentTarget;
- dragItem.classList.add('dragging');
- e.dataTransfer.effectAllowed = 'move';
- }
-
+ /* ========== 拖拽 ========== */
+ function handleDragStart(e) { dragItem = e.currentTarget; dragItem.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; }
function handleDragOver(e) {
- e.preventDefault();
- var target = e.target.closest('.todo-item');
+ e.preventDefault(); var target = e.target.closest('.todo-item');
if (target && target !== dragItem) {
var list = document.getElementById('todo-list');
var items = Array.from(list.children);
- var dragIdx = items.indexOf(dragItem);
- var targetIdx = items.indexOf(target);
- if (dragIdx < targetIdx) {
- list.insertBefore(dragItem, target.nextSibling);
- } else {
- list.insertBefore(dragItem, target);
- }
+ if (items.indexOf(dragItem) < items.indexOf(target)) list.insertBefore(dragItem, target.nextSibling);
+ else list.insertBefore(dragItem, target);
}
}
-
function handleDragEnd() {
- if (dragItem) dragItem.classList.remove('dragging');
- dragItem = null;
-
+ if (dragItem) dragItem.classList.remove('dragging'); dragItem = null;
var list = document.getElementById('todo-list');
- var items = Array.from(list.children);
var orders = [];
- items.forEach(function (item, idx) {
- var id = item.dataset.id;
- var todo = todos.find(function (t) { return t.id == id; });
- if (todo) {
- todo.sort_order = idx;
- orders.push({ id: id, sort_order: idx });
- }
+ Array.from(list.children).forEach(function (item, idx) {
+ var id = item.dataset.id; var todo = todos.find(function (t) { return t.id == id; });
+ if (todo) { todo.sort_order = idx; orders.push({ id: id, sort_order: idx }); }
});
- saveToLocal();
- ajax('todo_reorder', { orders: JSON.stringify(orders) });
- }
-
- /* ========== 日期与倒计时 ========== */
-
- function getDaysRemaining(dateStr) {
- if (!dateStr) return null;
- var today = new Date();
- today.setHours(0, 0, 0, 0);
- var due = new Date(dateStr);
- due.setHours(0, 0, 0, 0);
- return Math.ceil((due - today) / (1000 * 60 * 60 * 24));
- }
-
- function formatDate(dateStr) {
- if (!dateStr) return '';
- var d = new Date(dateStr);
- var m = d.getMonth() + 1;
- var day = d.getDate();
- return m + '月' + day + '日';
- }
-
- function getCountdownHtml(todo) {
- if (!todo.due_date || todo.completed == 1) return '';
- var days = getDaysRemaining(todo.due_date);
- if (days === null) return '';
-
- var cls, text;
- if (days < 0) {
- cls = 'overdue';
- text = '已过期' + Math.abs(days) + '天';
- } else if (days === 0) {
- cls = 'urgent';
- text = '今天截止';
- } else if (days === 1) {
- cls = 'urgent';
- text = '明天截止';
- } else if (days <= 3) {
- cls = 'soon';
- text = '剩余' + days + '天';
- } else {
- cls = 'normal';
- text = '剩余' + days + '天';
- }
- return '' + text + '';
- }
-
- function isOverdue(todo) {
- if (!todo.due_date || todo.completed == 1) return false;
- var days = getDaysRemaining(todo.due_date);
- return days !== null && days < 0;
- }
-
- function priorityLabel(p) {
- var map = { high: '紧急', medium: '普通', low: '低优' };
- return map[p] || '普通';
+ saveToLocal(); ajax('todo_reorder', { orders: JSON.stringify(orders) });
}
/* ========== 渲染 ========== */
-
function getFilteredTodos() {
return todos.filter(function (t) {
if (currentFilter === 'active') return t.completed == 0;
@@ -289,69 +188,39 @@
var filtered = getFilteredTodos();
var total = todos.length;
var done = todos.filter(function (t) { return t.completed == 1; }).length;
- var active = total - done;
var percent = total > 0 ? Math.round(done / total * 100) : 0;
- // 统计
- var statsEl = document.getElementById('todo-stats');
- statsEl.innerHTML = '共 ' + total + ' 项'
- + '待完成 ' + active + ''
- + '已完成 ' + done + ''
- + '' + percent + '%';
-
- // 进度条
- var progressBar = document.getElementById('todo-progress-bar');
- if (progressBar) {
- progressBar.style.width = percent + '%';
- }
+ document.getElementById('todo-stats').innerHTML = '共 ' + total + ' 项待完成 ' + (total - done) + '已完成 ' + done + '' + percent + '%';
+ var pb = document.getElementById('todo-progress-bar'); if (pb) pb.style.width = percent + '%';
+ document.querySelectorAll('.todo-filter-btn').forEach(function (b) { b.classList.toggle('active', b.dataset.filter === currentFilter); });
- // 筛选按钮高亮
- document.querySelectorAll('.todo-filter-btn').forEach(function (btn) {
- btn.classList.toggle('active', btn.dataset.filter === currentFilter);
- });
+ if (currentView === 'list') renderList(filtered);
+ else renderChart(filtered);
+ }
- // 列表
+ function renderList(filtered) {
var listEl = document.getElementById('todo-list');
-
if (filtered.length === 0) {
- listEl.innerHTML = ''
- + '
📋
'
- + (currentFilter === 'all' ? '暂无待办事项,添加一个吧' :
- currentFilter === 'active' ? '所有任务都完成了,太棒了!' : '还没有已完成的待办')
- + '
';
+ listEl.innerHTML = '📋
' +
+ (currentFilter === 'all' ? '暂无待办事项,添加一个吧' : currentFilter === 'active' ? '所有任务都完成了!' : '还没有已完成的待办') + '
';
return;
}
-
var html = '';
filtered.forEach(function (todo) {
var isEditing = editingId == todo.id;
var overdue = isOverdue(todo);
-
- // 外层容器
- html += '';
-
- // 左侧优先级彩条
+ html += '
';
html += '
';
-
- // 主体
html += '
';
-
- // 复选框
html += '
';
-
- // 内容区
html += '
';
if (isEditing) {
- html += '
';
+ html += '
';
html += '
';
- html += '
';
+ html += '
';
+ html += '
';
html += '
';
html += '
';
html += '
';
@@ -360,32 +229,23 @@
html += '
' + escapeHtml(todo.title) + '
';
html += '
';
html += '' + priorityLabel(todo.priority) + '';
+ html += '' + importanceStars(todo.importance) + '';
if (todo.due_date) {
- html += ''
- + '📅'
- + formatDate(todo.due_date)
- + '';
+ html += '📅' + formatDate(todo.due_date) + '';
html += getCountdownHtml(todo);
}
html += '
';
}
- html += '
'; // .todo-content
-
- // 操作按钮
+ html += '
';
if (!isEditing) {
html += '
';
html += '';
html += '';
html += '
';
}
-
- html += '
'; // .todo-body
- html += '
'; // .todo-item
+ html += '
';
});
-
listEl.innerHTML = html;
-
- // 绑定拖拽
listEl.querySelectorAll('.todo-item').forEach(function (item) {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragover', handleDragOver);
@@ -393,50 +253,339 @@
});
}
- function escapeHtml(str) {
- var div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
+ function importanceStars(v) {
+ v = parseInt(v) || 3;
+ var s = '';
+ for (var i = 0; i < 5; i++) s += i < v ? '★' : '☆';
+ return s;
}
- /* ========== 初始化 ========== */
+ /* ========== 2D Chart ========== */
+
+ function renderChart(filtered) {
+ var wrap = document.getElementById('todo-chart-wrap');
+ chartCanvas = document.getElementById('todo-chart');
+ if (!chartCanvas) return;
+ chartCtx = chartCanvas.getContext('2d');
+
+ // Responsive sizing
+ var rect = wrap.getBoundingClientRect();
+ var dpr = window.devicePixelRatio || 1;
+ var w = rect.width;
+ var h = Math.max(400, Math.min(w * 0.65, 520));
+ chartCanvas.width = w * dpr;
+ chartCanvas.height = h * dpr;
+ chartCanvas.style.width = w + 'px';
+ chartCanvas.style.height = h + 'px';
+ chartCtx.scale(dpr, dpr);
+
+ var isDark = document.documentElement.classList.contains('dark') || document.body.classList.contains('dark');
+ var p = chartPadding;
+ var cw = w - p.left - p.right;
+ var ch = h - p.top - p.bottom;
+
+ // Clear
+ chartCtx.clearRect(0, 0, w, h);
+
+ // Draw quadrant backgrounds
+ var halfX = p.left + cw / 2;
+ var halfY = p.top + ch / 2;
+
+ // Q1: top-left (urgent + important) — red
+ chartCtx.fillStyle = isDark ? 'rgba(239,68,68,0.08)' : 'rgba(239,68,68,0.06)';
+ chartCtx.fillRect(p.left, p.top, cw / 2, ch / 2);
+ // Q2: top-right (not urgent + important) — orange
+ chartCtx.fillStyle = isDark ? 'rgba(245,158,11,0.08)' : 'rgba(245,158,11,0.06)';
+ chartCtx.fillRect(halfX, p.top, cw / 2, ch / 2);
+ // Q3: bottom-left (urgent + not important) — yellow
+ chartCtx.fillStyle = isDark ? 'rgba(234,179,8,0.08)' : 'rgba(234,179,8,0.06)';
+ chartCtx.fillRect(p.left, halfY, cw / 2, ch / 2);
+ // Q4: bottom-right (not urgent + not important) — green
+ chartCtx.fillStyle = isDark ? 'rgba(16,185,129,0.08)' : 'rgba(16,185,129,0.06)';
+ chartCtx.fillRect(halfX, halfY, cw / 2, ch / 2);
+
+ // Quadrant labels
+ var labelColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.15)';
+ chartCtx.font = '13px sans-serif';
+ chartCtx.fillStyle = labelColor;
+ chartCtx.textAlign = 'center';
+ chartCtx.fillText('紧急且重要 · 立即做', p.left + cw / 4, p.top + 24);
+ chartCtx.fillText('重要不紧急 · 计划做', halfX + cw / 4, p.top + 24);
+ chartCtx.fillText('紧急不重要 · 委托做', p.left + cw / 4, halfY + 24);
+ chartCtx.fillText('不急不重要 · 可删除', halfX + cw / 4, halfY + 24);
+
+ // Divider lines
+ chartCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)';
+ chartCtx.setLineDash([6, 4]);
+ chartCtx.beginPath();
+ chartCtx.moveTo(halfX, p.top); chartCtx.lineTo(halfX, p.top + ch);
+ chartCtx.moveTo(p.left, halfY); chartCtx.lineTo(p.left + cw, halfY);
+ chartCtx.stroke();
+ chartCtx.setLineDash([]);
+
+ // Axes
+ var axisColor = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.3)';
+ chartCtx.strokeStyle = axisColor;
+ chartCtx.lineWidth = 1.5;
+ chartCtx.beginPath();
+ chartCtx.moveTo(p.left, p.top); chartCtx.lineTo(p.left, p.top + ch); chartCtx.lineTo(p.left + cw, p.top + ch);
+ chartCtx.stroke();
+ chartCtx.lineWidth = 1;
+
+ // Axis labels
+ var textColor = isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.5)';
+ chartCtx.fillStyle = textColor;
+ chartCtx.font = '12px sans-serif';
+ chartCtx.textAlign = 'center';
+
+ // X axis labels
+ var xLabels = ['已过期', '今天', '3天', '7天', '14天', '30天+'];
+ var xPositions = [0, 0.1, 0.25, 0.45, 0.65, 0.9];
+ for (var i = 0; i < xLabels.length; i++) {
+ chartCtx.fillText(xLabels[i], p.left + cw * xPositions[i], p.top + ch + 20);
+ }
+ chartCtx.fillText('← 紧急 不紧急 →', p.left + cw / 2, p.top + ch + 40);
+
+ // Y axis labels
+ chartCtx.textAlign = 'right';
+ for (var j = 1; j <= 5; j++) {
+ var yy = p.top + ch - (j - 1) / 4 * ch;
+ chartCtx.fillText(j + '★', p.left - 8, yy + 4);
+ }
+ chartCtx.save();
+ chartCtx.translate(14, p.top + ch / 2);
+ chartCtx.rotate(-Math.PI / 2);
+ chartCtx.textAlign = 'center';
+ chartCtx.fillText('重要程度', 0, 0);
+ chartCtx.restore();
+
+ // Y axis grid lines
+ chartCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)';
+ for (var g = 1; g <= 5; g++) {
+ var gy = p.top + ch - (g - 1) / 4 * ch;
+ chartCtx.beginPath(); chartCtx.moveTo(p.left, gy); chartCtx.lineTo(p.left + cw, gy); chartCtx.stroke();
+ }
+ // Compute points
+ chartPoints = [];
+ var MAX_DAYS = 35; // X轴最大天数
+ filtered.forEach(function (todo) {
+ var days = getDaysRemaining(todo.due_date);
+ var xVal;
+ if (days === null) xVal = MAX_DAYS; // 无截止日期放最右
+ else if (days < -5) xVal = -5; // 限制最左
+ else xVal = days;
+
+ var imp = parseInt(todo.importance) || 3;
+ // Map to canvas coords
+ // X: days -> left(urgent) to right(not urgent): -5..MAX_DAYS -> left..right
+ var xRatio = (xVal + 5) / (MAX_DAYS + 5);
+ var yRatio = (imp - 1) / 4; // 1..5 -> 0..1, bottom..top
+ var sx = p.left + xRatio * cw;
+ var sy = p.top + ch - yRatio * ch;
+
+ chartPoints.push({ x: xVal, y: imp, screenX: sx, screenY: sy, todo: todo });
+ });
+
+ // Draw points
+ chartPoints.forEach(function (pt) {
+ var completed = pt.todo.completed == 1;
+ var r = completed ? 6 : 9;
+ var alpha = completed ? 0.3 : 0.85;
+
+ // Color based on quadrant
+ var color;
+ var isUrgent = pt.x <= 7;
+ var isImportant = pt.y >= 3;
+ if (isUrgent && isImportant) color = 'rgba(239,68,68,' + alpha + ')'; // red
+ else if (!isUrgent && isImportant) color = 'rgba(245,158,11,' + alpha + ')'; // orange
+ else if (isUrgent && !isImportant) color = 'rgba(234,179,8,' + alpha + ')'; // yellow
+ else color = 'rgba(16,185,129,' + alpha + ')'; // green
+
+ chartCtx.beginPath();
+ chartCtx.arc(pt.screenX, pt.screenY, r, 0, Math.PI * 2);
+ chartCtx.fillStyle = color;
+ chartCtx.fill();
+
+ // Border
+ chartCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.15)';
+ chartCtx.lineWidth = 1;
+ chartCtx.stroke();
+
+ // Completed strikethrough
+ if (completed) {
+ chartCtx.beginPath();
+ chartCtx.moveTo(pt.screenX - r, pt.screenY);
+ chartCtx.lineTo(pt.screenX + r, pt.screenY);
+ chartCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)';
+ chartCtx.lineWidth = 2;
+ chartCtx.stroke();
+ }
+
+ // Hovered point highlight
+ if (hoveredPoint && hoveredPoint.todo.id === pt.todo.id) {
+ chartCtx.beginPath();
+ chartCtx.arc(pt.screenX, pt.screenY, r + 4, 0, Math.PI * 2);
+ chartCtx.strokeStyle = color;
+ chartCtx.lineWidth = 2;
+ chartCtx.stroke();
+ }
+ });
+
+ // Draw tooltip
+ if (hoveredPoint) {
+ drawTooltip(hoveredPoint, w, h, isDark);
+ }
+ }
+
+ function drawTooltip(pt, canvasW, canvasH, isDark) {
+ var todo = pt.todo;
+ var lines = [escapeHtmlPlain(todo.title)];
+ lines.push('重要度: ' + importanceStars(todo.importance));
+ if (todo.due_date) {
+ var days = getDaysRemaining(todo.due_date);
+ var dueText = formatDate(todo.due_date);
+ if (days < 0) dueText += ' (已过期' + Math.abs(days) + '天)';
+ else if (days === 0) dueText += ' (今天)';
+ else dueText += ' (剩余' + days + '天)';
+ lines.push('截止: ' + dueText);
+ } else {
+ lines.push('截止: 未设定');
+ }
+ if (todo.completed == 1) lines.push('✓ 已完成');
+
+ chartCtx.font = '12px sans-serif';
+ var maxW = 0;
+ lines.forEach(function (l) { var m = chartCtx.measureText(l).width; if (m > maxW) maxW = m; });
+ var tw = maxW + 20;
+ var th = lines.length * 20 + 14;
+ var tx = pt.screenX + 14;
+ var ty = pt.screenY - th / 2;
+
+ // Keep in bounds
+ if (tx + tw > canvasW - 10) tx = pt.screenX - tw - 14;
+ if (ty < 5) ty = 5;
+ if (ty + th > canvasH - 5) ty = canvasH - th - 5;
+
+ // Background
+ chartCtx.fillStyle = isDark ? 'rgba(40,40,40,0.95)' : 'rgba(255,255,255,0.97)';
+ chartCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.1)';
+ chartCtx.lineWidth = 1;
+ roundRect(chartCtx, tx, ty, tw, th, 8);
+ chartCtx.fill();
+ chartCtx.stroke();
+
+ // Shadow
+ chartCtx.shadowColor = 'rgba(0,0,0,0.1)';
+ chartCtx.shadowBlur = 8;
+ chartCtx.shadowOffsetX = 0;
+ chartCtx.shadowOffsetY = 2;
+ roundRect(chartCtx, tx, ty, tw, th, 8);
+ chartCtx.fill();
+ chartCtx.shadowColor = 'transparent';
+
+ // Text
+ chartCtx.fillStyle = isDark ? 'rgba(255,255,255,0.9)' : '#333';
+ chartCtx.textAlign = 'left';
+ chartCtx.font = 'bold 12px sans-serif';
+ chartCtx.fillText(lines[0], tx + 10, ty + 20);
+ chartCtx.font = '11px sans-serif';
+ chartCtx.fillStyle = isDark ? 'rgba(255,255,255,0.65)' : '#666';
+ for (var i = 1; i < lines.length; i++) {
+ chartCtx.fillText(lines[i], tx + 10, ty + 20 + i * 20);
+ }
+ }
+
+ function roundRect(ctx, x, y, w, h, r) {
+ ctx.beginPath();
+ ctx.moveTo(x + r, y);
+ ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r);
+ ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
+ ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r);
+ ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y);
+ ctx.closePath();
+ }
+
+ function findPointAt(mx, my) {
+ var closest = null, minDist = 20; // 20px tolerance
+ chartPoints.forEach(function (pt) {
+ var dx = pt.screenX - mx, dy = pt.screenY - my;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ if (dist < minDist) { minDist = dist; closest = pt; }
+ });
+ return closest;
+ }
+
+ function initChartEvents() {
+ var canvas = document.getElementById('todo-chart');
+ if (!canvas) return;
+
+ canvas.addEventListener('mousemove', function (e) {
+ var rect = canvas.getBoundingClientRect();
+ var mx = e.clientX - rect.left, my = e.clientY - rect.top;
+ var pt = findPointAt(mx, my);
+ if (pt !== hoveredPoint) {
+ hoveredPoint = pt;
+ canvas.style.cursor = pt ? 'pointer' : 'default';
+ if (currentView === 'chart') renderChart(getFilteredTodos());
+ }
+ });
+
+ canvas.addEventListener('mouseleave', function () {
+ if (hoveredPoint) { hoveredPoint = null; if (currentView === 'chart') renderChart(getFilteredTodos()); }
+ });
+
+ canvas.addEventListener('click', function (e) {
+ var rect = canvas.getBoundingClientRect();
+ var mx = e.clientX - rect.left, my = e.clientY - rect.top;
+ var pt = findPointAt(mx, my);
+ if (pt) {
+ // Switch to list view and edit
+ setView('list');
+ startEdit(pt.todo.id);
+ }
+ });
+ }
+
+ /* ========== Helpers ========== */
+ function escapeHtml(str) { var d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
+ function escapeHtmlPlain(str) {
+ return str.replace(/&/g, '&').replace(//g, '>');
+ }
+
+ /* ========== Init ========== */
function init() {
- // 先从 LocalStorage 渲染
todos = loadFromLocal();
render();
-
- // 再从服务器同步
syncFromServer();
- // 添加按钮
document.getElementById('todo-add-btn').addEventListener('click', addTodo);
- document.getElementById('todo-input').addEventListener('keydown', function (e) {
- if (e.key === 'Enter') addTodo();
+ document.getElementById('todo-input').addEventListener('keydown', function (e) { if (e.key === 'Enter') addTodo(); });
+ document.querySelectorAll('.todo-filter-btn').forEach(function (b) { b.addEventListener('click', function () { setFilter(this.dataset.filter); }); });
+ document.querySelectorAll('.todo-view-btn').forEach(function (b) { b.addEventListener('click', function () { setView(this.dataset.view); }); });
+
+ initChartEvents();
+
+ // Resize handler for chart
+ var resizeTimer;
+ window.addEventListener('resize', function () {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(function () { if (currentView === 'chart') render(); }, 200);
});
- // 筛选按钮
- document.querySelectorAll('.todo-filter-btn').forEach(function (btn) {
- btn.addEventListener('click', function () {
- setFilter(this.dataset.filter);
+ // importance slider label
+ var impSlider = document.getElementById('todo-importance');
+ var impLabel = document.getElementById('todo-importance-label');
+ if (impSlider && impLabel) {
+ impSlider.addEventListener('input', function () {
+ impLabel.textContent = importanceStars(this.value);
});
- });
+ }
}
- // 暴露方法
- window._todo = {
- toggle: toggleTodo,
- deleteTodo: deleteTodo,
- startEdit: startEdit,
- saveEdit: saveEdit,
- cancelEdit: cancelEdit
- };
-
- // DOM Ready
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
+ window._todo = { toggle: toggleTodo, deleteTodo: deleteTodo, startEdit: startEdit, saveEdit: saveEdit, cancelEdit: cancelEdit };
+
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
+ else init();
})();
diff --git a/include/todo/api.php b/include/todo/api.php
index 04d592b..66e5d09 100644
--- a/include/todo/api.php
+++ b/include/todo/api.php
@@ -42,6 +42,7 @@ function document_todo_create() {
$title = sanitize_text_field( $_POST['title'] ?? '' );
$priority = sanitize_text_field( $_POST['priority'] ?? 'medium' );
$due_date = sanitize_text_field( $_POST['due_date'] ?? '' );
+ $importance = intval( $_POST['importance'] ?? 3 );
if ( empty( $title ) ) {
wp_send_json_error( '标题不能为空' );
@@ -51,12 +52,15 @@ function document_todo_create() {
$priority = 'medium';
}
+ $importance = max( 1, min( 5, $importance ) );
+
$data = [
- 'title' => $title,
- 'priority' => $priority,
- 'due_date' => $due_date ?: null,
+ 'title' => $title,
+ 'priority' => $priority,
+ 'importance' => $importance,
+ 'due_date' => $due_date ?: null,
];
- $format = [ '%s', '%s', '%s' ];
+ $format = [ '%s', '%s', '%d', '%s' ];
$wpdb->insert( $table, $data, $format );
$id = $wpdb->insert_id;
@@ -105,6 +109,10 @@ function document_todo_update() {
$update['due_date'] = sanitize_text_field( $_POST['due_date'] ) ?: null;
$format[] = '%s';
}
+ if ( isset( $_POST['importance'] ) ) {
+ $update['importance'] = max( 1, min( 5, intval( $_POST['importance'] ) ) );
+ $format[] = '%d';
+ }
if ( empty( $update ) ) {
wp_send_json_error( '没有要更新的字段' );
diff --git a/include/todo/install.php b/include/todo/install.php
index 1f34123..549fe2e 100644
--- a/include/todo/install.php
+++ b/include/todo/install.php
@@ -15,6 +15,7 @@ function document_todo_create_table() {
title varchar(500) NOT NULL DEFAULT '',
completed tinyint(1) NOT NULL DEFAULT 0,
priority varchar(10) NOT NULL DEFAULT 'medium',
+ importance tinyint(1) NOT NULL DEFAULT 3,
due_date date DEFAULT NULL,
sort_order int(11) NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -22,6 +23,7 @@ function document_todo_create_table() {
PRIMARY KEY (id),
KEY idx_completed (completed),
KEY idx_priority (priority),
+ KEY idx_importance (importance),
KEY idx_sort_order (sort_order)
) $charset_collate;";
@@ -41,3 +43,15 @@ function document_todo_maybe_create_table() {
}
}
add_action( 'init', 'document_todo_maybe_create_table' );
+
+/* 升级:为已有表添加 importance 字段 */
+function document_todo_maybe_add_importance() {
+ global $wpdb;
+ $table_name = $wpdb->prefix . 'document_todos';
+ $column = $wpdb->get_results( "SHOW COLUMNS FROM $table_name LIKE 'importance'" );
+ if ( empty( $column ) ) {
+ $wpdb->query( "ALTER TABLE $table_name ADD COLUMN importance tinyint(1) NOT NULL DEFAULT 3 AFTER priority" );
+ $wpdb->query( "ALTER TABLE $table_name ADD KEY idx_importance (importance)" );
+ }
+}
+add_action( 'init', 'document_todo_maybe_add_importance' );
diff --git a/template/page/todo.php b/template/page/todo.php
index dd02859..77fcf8f 100644
--- a/template/page/todo.php
+++ b/template/page/todo.php
@@ -37,6 +37,11 @@
+
+ 重要:
+
+ ★★★☆☆
+