You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
13 KiB
JavaScript
401 lines
13 KiB
JavaScript
// URL 参数处理
|
|
function getQueryParams() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return {
|
|
query: params.get('q') || '',
|
|
is_done: params.get('done') === null ? null : params.get('done') === 'true',
|
|
limit: parseInt(params.get('limit')) || 10,
|
|
sort: params.get('sort') || 'urgency'
|
|
};
|
|
}
|
|
|
|
function updateQueryParams(params) {
|
|
const url = new URL(window.location);
|
|
if (params.query) url.searchParams.set('q', params.query);
|
|
else url.searchParams.delete('q');
|
|
|
|
if (params.is_done !== null) url.searchParams.set('done', params.is_done);
|
|
else url.searchParams.delete('done');
|
|
|
|
if (params.limit !== 10) url.searchParams.set('limit', params.limit);
|
|
else url.searchParams.delete('limit');
|
|
|
|
if (params.sort !== 'urgency') url.searchParams.set('sort', params.sort);
|
|
else url.searchParams.delete('sort');
|
|
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
|
|
// 表单初始化
|
|
function initializeFormValues() {
|
|
const params = getQueryParams();
|
|
document.getElementById('searchInput').value = params.query;
|
|
document.getElementById('statusFilter').value = params.is_done === null ? '' : params.is_done;
|
|
document.getElementById('limitFilter').value = params.limit;
|
|
document.getElementById('sortFilter').value = params.sort;
|
|
}
|
|
|
|
// 时间处理函数
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
function calculateUrgency(task) {
|
|
if (!task.deadline || task.completed) return Infinity;
|
|
const now = new Date();
|
|
const deadline = new Date(task.deadline);
|
|
const hoursLeft = (deadline - now) / (1000 * 60 * 60);
|
|
return hoursLeft;
|
|
}
|
|
|
|
function formatTimeDiff(diffMs) {
|
|
const hours = Math.floor(Math.abs(diffMs) / (1000 * 60 * 60));
|
|
const minutes = Math.floor((Math.abs(diffMs) % (1000 * 60 * 60)) / (1000 * 60));
|
|
const seconds = Math.floor((Math.abs(diffMs) % (1000 * 60)) / 1000);
|
|
|
|
if (hours > 2) {
|
|
return `${hours} 小时`;
|
|
} else if (hours > 0) {
|
|
return `${hours} 小时 ${minutes} 分钟`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes} 分钟 ${seconds} 秒`;
|
|
} else {
|
|
return `${seconds} 秒`;
|
|
}
|
|
}
|
|
|
|
function getDeadlineStatus(deadline, completed) {
|
|
if (!deadline) return null;
|
|
if (completed) return { status: 'completed', class: 'text-gray-500' };
|
|
|
|
const now = new Date();
|
|
const deadlineDate = new Date(deadline);
|
|
const hoursLeft = (deadlineDate - now) / (1000 * 60 * 60);
|
|
|
|
if (hoursLeft < 0) return { status: 'error', class: 'text-red-600 font-bold' };
|
|
if (hoursLeft <= 12) return { status: 'warning', class: 'text-yellow-600 font-bold' };
|
|
return { status: 'normal', class: 'text-gray-500' };
|
|
}
|
|
|
|
// 倒计时处理
|
|
function updateCountdown(element, deadline) {
|
|
const now = new Date();
|
|
const diffMs = deadline - now;
|
|
|
|
if (diffMs <= 0) {
|
|
loadTasks();
|
|
return;
|
|
}
|
|
|
|
let text = `截止: ${formatDate(deadline)}`;
|
|
if (diffMs > 0) {
|
|
text += ` (剩余 ${formatTimeDiff(diffMs)})`;
|
|
} else {
|
|
text += ` (已超出 ${formatTimeDiff(Math.abs(diffMs))})`;
|
|
}
|
|
|
|
element.textContent = text;
|
|
}
|
|
|
|
// Task 列表处理
|
|
async function loadTasks() {
|
|
const params = getQueryParams();
|
|
try {
|
|
const response = await fetch('/task/api', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'list',
|
|
list: params
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
renderTasks(data.task_list);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load tasks:', error);
|
|
}
|
|
}
|
|
|
|
function renderTasks(tasks) {
|
|
const list = document.getElementById('taskList');
|
|
const template = document.getElementById('taskTemplate');
|
|
list.innerHTML = '';
|
|
|
|
// 清除所有现有的倒计时定时器
|
|
if (window.countdownTimers) {
|
|
window.countdownTimers.forEach(timer => clearInterval(timer));
|
|
}
|
|
window.countdownTimers = [];
|
|
|
|
// 排序
|
|
const sortType = document.getElementById('sortFilter').value;
|
|
tasks.sort((a, b) => {
|
|
if (sortType === 'urgency') {
|
|
return calculateUrgency(a) - calculateUrgency(b);
|
|
} else if (sortType === 'deadline') {
|
|
if (!a.deadline) return 1;
|
|
if (!b.deadline) return -1;
|
|
return new Date(a.deadline) - new Date(b.deadline);
|
|
} else { // created
|
|
return new Date(b.created_at) - new Date(a.created_at);
|
|
}
|
|
});
|
|
|
|
tasks.forEach(task => {
|
|
const clone = template.content.cloneNode(true);
|
|
const item = clone.querySelector('.task-item');
|
|
|
|
item.dataset.id = task.id;
|
|
item.dataset.task = JSON.stringify(task);
|
|
item.querySelector('.task-checkbox').checked = task.completed;
|
|
item.querySelector('.task-title').textContent = task.title;
|
|
item.querySelector('.task-content').textContent = task.content;
|
|
item.querySelector('.task-created').textContent = `创建: ${formatDate(task.created_at)}`;
|
|
|
|
if (task.deadline) {
|
|
const deadlineEl = item.querySelector('.task-deadline');
|
|
const status = getDeadlineStatus(task.deadline, task.completed);
|
|
deadlineEl.className = `task-deadline ${status.class}`;
|
|
|
|
const now = new Date();
|
|
const deadline = new Date(task.deadline);
|
|
const hoursLeft = (deadline - now) / (1000 * 60 * 60);
|
|
const needsCountdown = !task.completed && hoursLeft > 0 && hoursLeft <= 2;
|
|
|
|
if (needsCountdown) {
|
|
updateCountdown(deadlineEl, deadline);
|
|
const timer = setInterval(() => {
|
|
updateCountdown(deadlineEl, deadline);
|
|
}, 1000);
|
|
window.countdownTimers.push(timer);
|
|
} else {
|
|
const diffMs = deadline - now;
|
|
let text = `截止: ${formatDate(task.deadline)}`;
|
|
if (!task.completed) {
|
|
text += ` (${diffMs > 0 ? '剩余 ' : '已超出 '}${formatTimeDiff(diffMs)})`;
|
|
}
|
|
deadlineEl.textContent = text;
|
|
}
|
|
}
|
|
|
|
if (task.completed) {
|
|
item.querySelector('.task-title').classList.add('line-through', 'text-gray-500');
|
|
item.querySelector('.task-content').classList.add('line-through', 'text-gray-500');
|
|
}
|
|
|
|
list.appendChild(clone);
|
|
});
|
|
}
|
|
|
|
// 对话框处理
|
|
function openAddDialog() {
|
|
document.getElementById('addDialog').classList.remove('hidden');
|
|
document.getElementById('addForm').reset();
|
|
}
|
|
|
|
function closeAddDialog() {
|
|
document.getElementById('addDialog').classList.add('hidden');
|
|
}
|
|
|
|
function openEditDialog(task) {
|
|
const dialog = document.getElementById('editDialog');
|
|
const form = document.getElementById('editForm');
|
|
|
|
form.id.value = task.id;
|
|
form.title.value = task.title;
|
|
form.content.value = task.content;
|
|
form.deadline.value = task.deadline ? task.deadline.slice(0, 16) : '';
|
|
|
|
dialog.classList.remove('hidden');
|
|
}
|
|
|
|
function closeEditDialog() {
|
|
document.getElementById('editDialog').classList.add('hidden');
|
|
}
|
|
|
|
// 事件处理
|
|
function debounce(func, delay) {
|
|
let timer;
|
|
return function() {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => func.apply(this, arguments), delay);
|
|
}
|
|
}
|
|
|
|
const handleFilterChange = debounce(() => {
|
|
const params = {
|
|
query: document.getElementById('searchInput').value,
|
|
is_done: document.getElementById('statusFilter').value === '' ? null : document.getElementById('statusFilter').value === 'true',
|
|
limit: parseInt(document.getElementById('limitFilter').value),
|
|
sort: document.getElementById('sortFilter').value
|
|
};
|
|
updateQueryParams(params);
|
|
loadTasks();
|
|
}, 300);
|
|
|
|
// 在文件顶部添加定时器变量
|
|
let autoRefreshTimer;
|
|
|
|
// 初始化事件监听
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// 添加任务
|
|
document.getElementById('addTaskBtn').addEventListener('click', openAddDialog);
|
|
document.getElementById('addForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const task = {
|
|
title: form.title.value,
|
|
content: form.content.value,
|
|
deadline: form.deadline.value
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/task/api', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'add',
|
|
task: task
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
closeAddDialog();
|
|
form.reset();
|
|
loadTasks();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to add task:', error);
|
|
}
|
|
});
|
|
|
|
// 更新任务状态
|
|
document.getElementById('taskList').addEventListener('change', async (e) => {
|
|
if (e.target.matches('.task-checkbox')) {
|
|
const item = e.target.closest('.task-item');
|
|
const id = item.dataset.id;
|
|
const completed = e.target.checked;
|
|
|
|
try {
|
|
const response = await fetch('/task/api', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'update',
|
|
task: { id, completed }
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
loadTasks();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update task:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 删除任务
|
|
document.getElementById('taskList').addEventListener('click', async (e) => {
|
|
if (e.target.closest('.delete-btn')) {
|
|
const item = e.target.closest('.task-item');
|
|
const id = item.dataset.id;
|
|
|
|
if (!confirm('确定要删除这个任务吗?')) return;
|
|
|
|
try {
|
|
const response = await fetch('/task/api', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'delete',
|
|
task: { id }
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
loadTasks();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete task:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 编辑任务
|
|
document.getElementById('taskList').addEventListener('click', async (e) => {
|
|
if (e.target.closest('.edit-btn')) {
|
|
const item = e.target.closest('.task-item');
|
|
const task = JSON.parse(item.dataset.task);
|
|
openEditDialog(task);
|
|
}
|
|
});
|
|
|
|
document.getElementById('editForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const task = {
|
|
id: form.id.value,
|
|
title: form.title.value,
|
|
content: form.content.value,
|
|
deadline: form.deadline.value
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/task/api', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'update',
|
|
task: task
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
if (data.status === 'success') {
|
|
closeEditDialog();
|
|
loadTasks();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update task:', error);
|
|
}
|
|
});
|
|
|
|
// 筛选事件
|
|
document.getElementById('searchInput').addEventListener('input', handleFilterChange);
|
|
document.getElementById('statusFilter').addEventListener('change', handleFilterChange);
|
|
document.getElementById('limitFilter').addEventListener('change', handleFilterChange);
|
|
document.getElementById('sortFilter').addEventListener('change', handleFilterChange);
|
|
|
|
// 浏览器前进/后退
|
|
window.addEventListener('popstate', () => {
|
|
initializeFormValues();
|
|
loadTasks();
|
|
});
|
|
|
|
// 设置自动刷新定时器
|
|
autoRefreshTimer = setInterval(loadTasks, 10000); // 每10秒刷新一次
|
|
|
|
// 修改页面卸载事件处理,清理所有定时器
|
|
window.addEventListener('beforeunload', () => {
|
|
if (window.countdownTimers) {
|
|
window.countdownTimers.forEach(timer => clearInterval(timer));
|
|
}
|
|
if (autoRefreshTimer) {
|
|
clearInterval(autoRefreshTimer);
|
|
}
|
|
});
|
|
|
|
// 初始化
|
|
initializeFormValues();
|
|
loadTasks();
|
|
});
|