v1.0.0提交
This commit is contained in:
262
pages/todos.php
Normal file
262
pages/todos.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
/**
|
||||
* PerToolBox Front - 待办事项页面
|
||||
*
|
||||
* Copyright (C) 2024 Sea Network Technology Studio
|
||||
* Author: Canglan <admin@sea-studio.top>
|
||||
* License: AGPL v3
|
||||
*/
|
||||
|
||||
require_once '../config.php';
|
||||
include_once '../header.php';
|
||||
include_once '../sidebar.php';
|
||||
?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">✅ 待办事项</h1>
|
||||
<button id="addBtn" class="btn btn-primary">+ 添加</button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="flex gap-2 mb-4 flex-wrap">
|
||||
<button class="filter-btn active" data-filter="all">全部</button>
|
||||
<button class="filter-btn" data-filter="active">未完成</button>
|
||||
<button class="filter-btn" data-filter="completed">已完成</button>
|
||||
<select id="categoryFilter" class="border rounded px-2 py-1 text-sm">
|
||||
<option value="">全部分类</option>
|
||||
<option value="学习">学习</option>
|
||||
<option value="工作">工作</option>
|
||||
<option value="生活">生活</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 待办列表 -->
|
||||
<div id="todoList" class="space-y-2">
|
||||
<div class="text-center text-gray-400 py-8">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑模态框 -->
|
||||
<div id="modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg w-full max-w-md p-6">
|
||||
<h3 id="modalTitle" class="text-xl font-bold mb-4">添加待办</h3>
|
||||
<input type="text" id="todoTitle" placeholder="标题" class="form-input mb-3">
|
||||
<textarea id="todoDesc" rows="3" placeholder="描述(可选)" class="form-input mb-3"></textarea>
|
||||
<select id="todoPriority" class="form-input mb-3">
|
||||
<option value="1">低优先级</option>
|
||||
<option value="2" selected>中优先级</option>
|
||||
<option value="3">高优先级</option>
|
||||
</select>
|
||||
<select id="todoCategory" class="form-input mb-3">
|
||||
<option value="学习">学习</option>
|
||||
<option value="工作">工作</option>
|
||||
<option value="生活">生活</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
<input type="datetime-local" id="todoDueDate" class="form-input mb-4">
|
||||
<div class="flex gap-2">
|
||||
<button id="modalConfirm" class="btn btn-primary flex-1">确认</button>
|
||||
<button id="modalCancel" class="btn btn-secondary flex-1">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentEditId = null;
|
||||
let currentFilter = 'all';
|
||||
let currentCategory = '';
|
||||
|
||||
// 加载待办列表
|
||||
async function loadTodos() {
|
||||
let url = '/todos';
|
||||
const params = [];
|
||||
if (currentFilter === 'active') params.push('completed=false');
|
||||
if (currentFilter === 'completed') params.push('completed=true');
|
||||
if (currentCategory) params.push(`category=${encodeURIComponent(currentCategory)}`);
|
||||
if (params.length) url += '?' + params.join('&');
|
||||
|
||||
try {
|
||||
const todos = await apiRequest(url);
|
||||
renderTodos(todos);
|
||||
} catch (error) {
|
||||
document.getElementById('todoList').innerHTML = `<div class="text-center text-red-500 py-8">${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTodos(todos) {
|
||||
if (!todos.length) {
|
||||
document.getElementById('todoList').innerHTML = '<div class="text-center text-gray-400 py-8">暂无待办,添加一个吧~</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const priorityMap = {1: '低', 2: '中', 3: '高'};
|
||||
const priorityColor = {1: 'gray', 2: 'yellow', 3: 'red'};
|
||||
|
||||
const html = todos.map(todo => `
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<input type="checkbox"
|
||||
${todo.completed ? 'checked' : ''}
|
||||
onchange="toggleTodo(${todo.id})"
|
||||
class="w-5 h-5">
|
||||
<div class="flex-1">
|
||||
<span class="${todo.completed ? 'line-through text-gray-400' : 'text-gray-700'}">
|
||||
${escapeHtml(todo.title)}
|
||||
</span>
|
||||
${todo.description ? `<p class="text-xs text-gray-400 mt-1">${escapeHtml(todo.description)}</p>` : ''}
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-${priorityColor[todo.priority]}-100 text-${priorityColor[todo.priority]}-800">
|
||||
${priorityMap[todo.priority]}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">${escapeHtml(todo.category)}</span>
|
||||
${todo.due_date ? `<span class="text-xs text-gray-400">📅 ${new Date(todo.due_date).toLocaleDateString()}</span>` : ''}
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button onclick="editTodo(${todo.id})" class="text-blue-500 hover:text-blue-700">✏️</button>
|
||||
<button onclick="deleteTodo(${todo.id})" class="text-red-500 hover:text-red-700">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('todoList').innerHTML = html;
|
||||
}
|
||||
|
||||
// 切换完成状态
|
||||
async function toggleTodo(id) {
|
||||
try {
|
||||
const todo = await apiRequest(`/todos/${id}`);
|
||||
await apiRequest(`/todos/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ completed: !todo.completed })
|
||||
});
|
||||
loadTodos();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除待办
|
||||
async function deleteTodo(id) {
|
||||
if (!confirm('确定删除吗?')) return;
|
||||
try {
|
||||
await apiRequest(`/todos/${id}`, { method: 'DELETE' });
|
||||
loadTodos();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开添加模态框
|
||||
function openModal(editId = null) {
|
||||
currentEditId = editId;
|
||||
const modal = document.getElementById('modal');
|
||||
const title = document.getElementById('modalTitle');
|
||||
|
||||
if (editId) {
|
||||
title.textContent = '编辑待办';
|
||||
// 加载数据填充表单
|
||||
apiRequest(`/todos/${editId}`).then(todo => {
|
||||
document.getElementById('todoTitle').value = todo.title;
|
||||
document.getElementById('todoDesc').value = todo.description || '';
|
||||
document.getElementById('todoPriority').value = todo.priority;
|
||||
document.getElementById('todoCategory').value = todo.category;
|
||||
if (todo.due_date) {
|
||||
document.getElementById('todoDueDate').value = todo.due_date.slice(0, 16);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
title.textContent = '添加待办';
|
||||
document.getElementById('todoTitle').value = '';
|
||||
document.getElementById('todoDesc').value = '';
|
||||
document.getElementById('todoPriority').value = '2';
|
||||
document.getElementById('todoCategory').value = '学习';
|
||||
document.getElementById('todoDueDate').value = '';
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
currentEditId = null;
|
||||
}
|
||||
|
||||
// 保存待办
|
||||
async function saveTodo() {
|
||||
const data = {
|
||||
title: document.getElementById('todoTitle').value.trim(),
|
||||
description: document.getElementById('todoDesc').value,
|
||||
priority: parseInt(document.getElementById('todoPriority').value),
|
||||
category: document.getElementById('todoCategory').value,
|
||||
due_date: document.getElementById('todoDueDate').value || null
|
||||
};
|
||||
|
||||
if (!data.title) {
|
||||
showToast('请输入标题', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentEditId) {
|
||||
await apiRequest(`/todos/${currentEditId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
await apiRequest('/todos', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
closeModal();
|
||||
loadTodos();
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选器
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active', 'bg-blue-500', 'text-white'));
|
||||
btn.classList.add('active', 'bg-blue-500', 'text-white');
|
||||
currentFilter = btn.dataset.filter;
|
||||
loadTodos();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('categoryFilter').addEventListener('change', (e) => {
|
||||
currentCategory = e.target.value;
|
||||
loadTodos();
|
||||
});
|
||||
|
||||
document.getElementById('addBtn').addEventListener('click', () => openModal());
|
||||
document.getElementById('modalConfirm').addEventListener('click', saveTodo);
|
||||
document.getElementById('modalCancel').addEventListener('click', closeModal);
|
||||
|
||||
// 页面加载时上报热度
|
||||
recordUsage('todos');
|
||||
|
||||
loadTodos();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php include_once '../footer.php'; ?>
|
||||
Reference in New Issue
Block a user