v1.0.0提交

This commit is contained in:
2026-03-31 15:54:32 +08:00
parent 79bfeb0e18
commit 314e53bf9c
16 changed files with 2110 additions and 1 deletions

172
pages/crypto.php Normal file
View File

@@ -0,0 +1,172 @@
<?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">
<h1 class="text-2xl font-bold mb-6">🔒 加密工具箱</h1>
<!-- 哈希计算 -->
<div class="mb-8 border-b pb-6">
<h2 class="text-xl font-semibold mb-4">哈希计算</h2>
<div class="flex gap-2 mb-3">
<select id="hashAlgo" class="form-input w-32">
<option value="md5">MD5</option>
<option value="sha1">SHA1</option>
<option value="sha256">SHA256</option>
<option value="sha512">SHA512</option>
</select>
<input type="text" id="hashInput" placeholder="输入文本" class="form-input flex-1">
<button id="hashBtn" class="btn btn-primary">计算</button>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-500">结果:</div>
<div id="hashResult" class="font-mono text-sm break-all"></div>
</div>
</div>
<!-- Base64 编解码 -->
<div class="mb-8 border-b pb-6">
<h2 class="text-xl font-semibold mb-4">Base64 编解码</h2>
<textarea id="base64Input" rows="3" placeholder="输入文本" class="form-input mb-3"></textarea>
<div class="flex gap-2 mb-3">
<button id="base64Encode" class="btn btn-primary">编码</button>
<button id="base64Decode" class="btn btn-secondary">解码</button>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-500">结果:</div>
<div id="base64Result" class="font-mono text-sm break-all"></div>
</div>
</div>
<!-- URL 编解码 -->
<div class="mb-8 border-b pb-6">
<h2 class="text-xl font-semibold mb-4">URL 编解码</h2>
<textarea id="urlInput" rows="3" placeholder="输入文本" class="form-input mb-3"></textarea>
<div class="flex gap-2 mb-3">
<button id="urlEncode" class="btn btn-primary">编码</button>
<button id="urlDecode" class="btn btn-secondary">解码</button>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-500">结果:</div>
<div id="urlResult" class="font-mono text-sm break-all"></div>
</div>
</div>
<!-- AES 加解密 -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">AES 加解密</h2>
<div class="grid md:grid-cols-2 gap-4 mb-3">
<select id="aesMode" class="form-input">
<option value="ECB">ECB</option>
<option value="CBC">CBC</option>
<option value="GCM">GCM</option>
</select>
<input type="text" id="aesKey" placeholder="密钥 (16/24/32字节)" class="form-input">
</div>
<input type="text" id="aesIv" placeholder="IV (CBC/GCM模式需要16字节)" class="form-input mb-3">
<textarea id="aesInput" rows="3" placeholder="输入文本" class="form-input mb-3"></textarea>
<div class="flex gap-2 mb-3">
<button id="aesEncrypt" class="btn btn-primary">加密</button>
<button id="aesDecrypt" class="btn btn-secondary">解密</button>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-500">结果:</div>
<div id="aesResult" class="font-mono text-sm break-all"></div>
</div>
</div>
</div>
</div>
<script>
// 哈希计算
document.getElementById('hashBtn').addEventListener('click', async () => {
const algo = document.getElementById('hashAlgo').value;
const text = document.getElementById('hashInput').value;
if (!text) { showToast('请输入文本', 'error'); return; }
try {
const data = await apiRequest('/crypto/hash', {
method: 'POST',
body: JSON.stringify({ algorithm: algo, text })
});
document.getElementById('hashResult').textContent = data.result;
} catch (error) {
showToast(error.message, 'error');
}
});
// Base64
async function base64Process(action) {
const text = document.getElementById('base64Input').value;
if (!text) { showToast('请输入文本', 'error'); return; }
try {
const data = await apiRequest('/crypto/base64', {
method: 'POST',
body: JSON.stringify({ action, text })
});
document.getElementById('base64Result').textContent = data.result;
} catch (error) {
showToast(error.message, 'error');
}
}
document.getElementById('base64Encode').addEventListener('click', () => base64Process('encode'));
document.getElementById('base64Decode').addEventListener('click', () => base64Process('decode'));
// URL
async function urlProcess(action) {
const text = document.getElementById('urlInput').value;
if (!text) { showToast('请输入文本', 'error'); return; }
try {
const data = await apiRequest('/crypto/url', {
method: 'POST',
body: JSON.stringify({ action, text })
});
document.getElementById('urlResult').textContent = data.result;
} catch (error) {
showToast(error.message, 'error');
}
}
document.getElementById('urlEncode').addEventListener('click', () => urlProcess('encode'));
document.getElementById('urlDecode').addEventListener('click', () => urlProcess('decode'));
// AES
async function aesProcess(action) {
const mode = document.getElementById('aesMode').value;
const key = document.getElementById('aesKey').value;
const iv = document.getElementById('aesIv').value;
const text = document.getElementById('aesInput').value;
if (!key) { showToast('请输入密钥', 'error'); return; }
if (!text) { showToast('请输入文本', 'error'); return; }
if ((mode === 'CBC' || mode === 'GCM') && !iv) {
showToast('CBC/GCM模式需要IV', 'error');
return;
}
try {
const data = await apiRequest('/crypto/aes', {
method: 'POST',
body: JSON.stringify({ mode, action, key, iv: iv || null, text })
});
document.getElementById('aesResult').textContent = data.result;
} catch (error) {
showToast(error.message, 'error');
}
}
document.getElementById('aesEncrypt').addEventListener('click', () => aesProcess('encrypt'));
document.getElementById('aesDecrypt').addEventListener('click', () => aesProcess('decrypt'));
recordUsage('crypto_hash');
</script>
<?php include_once '../footer.php'; ?>

82
pages/json.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
/**
* PerToolBox Front - JSON 校验器页面
*
* 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">
<h1 class="text-2xl font-bold mb-6">📋 JSON 校验器</h1>
<div class="mb-4">
<label class="form-label">输入 JSON</label>
<textarea id="jsonInput" rows="10" placeholder='{"key": "value"}' class="form-input font-mono text-sm"></textarea>
</div>
<button id="validateBtn" class="btn btn-primary mb-6">校验并格式化</button>
<div id="resultArea" class="hidden">
<div class="flex justify-between items-center mb-2">
<h3 class="font-semibold">结果:</h3>
<button id="copyBtn" class="text-sm text-blue-500 hover:text-blue-700">复制</button>
</div>
<pre id="jsonResult" class="bg-gray-50 p-4 rounded-lg overflow-x-auto font-mono text-sm"></pre>
</div>
<div id="errorArea" class="hidden">
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<div class="text-red-600 font-semibold mb-2">❌ JSON 无效</div>
<div id="errorMsg" class="text-red-500 text-sm"></div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('validateBtn').addEventListener('click', async () => {
const jsonString = document.getElementById('jsonInput').value;
if (!jsonString) {
showToast('请输入 JSON', 'error');
return;
}
try {
const data = await apiRequest('/json/validate', {
method: 'POST',
body: JSON.stringify({ json_string: jsonString })
});
if (data.valid) {
document.getElementById('resultArea').classList.remove('hidden');
document.getElementById('errorArea').classList.add('hidden');
document.getElementById('jsonResult').textContent = data.formatted;
} else {
document.getElementById('resultArea').classList.add('hidden');
document.getElementById('errorArea').classList.remove('hidden');
document.getElementById('errorMsg').textContent = `第 ${data.error.line} 行,第 ${data.error.column} 列:${data.error.message}`;
}
} catch (error) {
showToast(error.message, 'error');
}
});
document.getElementById('copyBtn').addEventListener('click', () => {
const result = document.getElementById('jsonResult').textContent;
if (result) {
navigator.clipboard.writeText(result);
showToast('已复制到剪贴板');
}
});
recordUsage('json');
</script>
<?php include_once '../footer.php'; ?>

157
pages/notes.php Normal file
View File

@@ -0,0 +1,157 @@
<?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 id="noteGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="text-center text-gray-400 py-8 col-span-full">加载中...</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-lg p-6">
<h3 id="modalTitle" class="text-xl font-bold mb-4">新建便签</h3>
<input type="text" id="noteTitle" placeholder="标题" class="form-input mb-3">
<textarea id="noteContent" rows="6" placeholder="内容" class="form-input mb-3"></textarea>
<input type="text" id="noteTags" placeholder="标签(用逗号分隔)" 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;
async function loadNotes() {
try {
const notes = await apiRequest('/notes');
renderNotes(notes);
} catch (error) {
document.getElementById('noteGrid').innerHTML = `<div class="text-center text-red-500 py-8 col-span-full">${error.message}</div>`;
}
}
function renderNotes(notes) {
if (!notes.length) {
document.getElementById('noteGrid').innerHTML = '<div class="text-center text-gray-400 py-8 col-span-full">暂无便签,新建一个吧~</div>';
return;
}
const html = notes.map(note => `
<div class="border rounded-lg p-4 hover:shadow-md transition bg-white">
<div class="flex justify-between items-start mb-2">
<h3 class="font-bold text-lg">${escapeHtml(note.title)}</h3>
<div class="flex gap-1">
<button onclick="editNote(${note.id})" class="text-blue-500 hover:text-blue-700">✏️</button>
<button onclick="deleteNote(${note.id})" class="text-red-500 hover:text-red-700">🗑️</button>
</div>
</div>
<p class="text-gray-600 whitespace-pre-wrap text-sm">${escapeHtml(note.content || '').substring(0, 200)}${(note.content || '').length > 200 ? '...' : ''}</p>
${note.tags && note.tags.length ? `<div class="mt-2 flex gap-1 flex-wrap">${note.tags.map(t => `<span class="text-xs bg-gray-100 px-2 py-0.5 rounded">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
<div class="mt-2 text-xs text-gray-400">${new Date(note.created_at).toLocaleString()}</div>
</div>
`).join('');
document.getElementById('noteGrid').innerHTML = html;
}
function openModal(editId = null) {
currentEditId = editId;
const modal = document.getElementById('modal');
const title = document.getElementById('modalTitle');
if (editId) {
title.textContent = '编辑便签';
apiRequest(`/notes/${editId}`).then(note => {
document.getElementById('noteTitle').value = note.title;
document.getElementById('noteContent').value = note.content || '';
document.getElementById('noteTags').value = (note.tags || []).join(',');
});
} else {
title.textContent = '新建便签';
document.getElementById('noteTitle').value = '';
document.getElementById('noteContent').value = '';
document.getElementById('noteTags').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 saveNote() {
const data = {
title: document.getElementById('noteTitle').value.trim(),
content: document.getElementById('noteContent').value,
tags: document.getElementById('noteTags').value.split(',').map(t => t.trim()).filter(t => t)
};
if (!data.title) {
showToast('请输入标题', 'error');
return;
}
try {
if (currentEditId) {
await apiRequest(`/notes/${currentEditId}`, {
method: 'PUT',
body: JSON.stringify(data)
});
} else {
await apiRequest('/notes', {
method: 'POST',
body: JSON.stringify(data)
});
}
closeModal();
loadNotes();
} catch (error) {
showToast(error.message, 'error');
}
}
async function deleteNote(id) {
if (!confirm('确定删除吗?')) return;
try {
await apiRequest(`/notes/${id}`, { method: 'DELETE' });
loadNotes();
} catch (error) {
showToast(error.message, 'error');
}
}
document.getElementById('addBtn').addEventListener('click', () => openModal());
document.getElementById('modalConfirm').addEventListener('click', saveNote);
document.getElementById('modalCancel').addEventListener('click', closeModal);
recordUsage('notes');
loadNotes();
</script>
<?php include_once '../footer.php'; ?>

108
pages/password.php Normal file
View File

@@ -0,0 +1,108 @@
<?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">
<h1 class="text-2xl font-bold mb-6">🔑 密码生成器</h1>
<div class="mb-6">
<div class="flex gap-2">
<input type="text" id="password" readonly class="flex-1 font-mono text-xl border rounded-lg px-4 py-3 bg-gray-50">
<button id="copyBtn" class="btn btn-secondary">复制</button>
<button id="generateBtn" class="btn btn-primary">生成</button>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<label class="flex items-center gap-2"><input type="checkbox" id="useUpper" checked> 大写字母</label>
<label class="flex items-center gap-2"><input type="checkbox" id="useLower" checked> 小写字母</label>
<label class="flex items-center gap-2"><input type="checkbox" id="useDigits" checked> 数字</label>
<label class="flex items-center gap-2"><input type="checkbox" id="useSymbols" checked> 符号</label>
</div>
<div class="mb-6">
<label>密码长度: <span id="lengthValue">12</span></label>
<input type="range" id="length" min="4" max="64" value="12" class="w-full">
</div>
<div class="mb-6">
<label>批量生成数量:</label>
<div class="flex gap-2 mt-1">
<input type="number" id="count" min="1" max="10" value="1" class="form-input w-24">
<button id="batchBtn" class="btn btn-secondary">批量生成</button>
</div>
</div>
<div id="batchResults" class="hidden mt-4 p-4 bg-gray-50 rounded-lg">
<h3 class="font-bold mb-2">生成的密码:</h3>
<div id="passwordList" class="space-y-1 font-mono text-sm"></div>
</div>
</div>
</div>
<script>
async function generatePassword() {
const length = document.getElementById('length').value;
const upper = document.getElementById('useUpper').checked;
const lower = document.getElementById('useLower').checked;
const digits = document.getElementById('useDigits').checked;
const symbols = document.getElementById('useSymbols').checked;
try {
const data = await apiRequest(`/password/generate?length=${length}&upper=${upper}&lower=${lower}&digits=${digits}&symbols=${symbols}`);
document.getElementById('password').value = data.passwords;
} catch (error) {
showToast(error.message, 'error');
}
}
async function batchGenerate() {
const length = document.getElementById('length').value;
const count = document.getElementById('count').value;
const upper = document.getElementById('useUpper').checked;
const lower = document.getElementById('useLower').checked;
const digits = document.getElementById('useDigits').checked;
const symbols = document.getElementById('useSymbols').checked;
try {
const data = await apiRequest(`/password/generate?length=${length}&upper=${upper}&lower=${lower}&digits=${digits}&symbols=${symbols}&count=${count}`);
const passwords = Array.isArray(data.passwords) ? data.passwords : [data.passwords];
const listHtml = passwords.map(p => `<div>• ${escapeHtml(p)}</div>`).join('');
document.getElementById('passwordList').innerHTML = listHtml;
document.getElementById('batchResults').classList.remove('hidden');
} catch (error) {
showToast(error.message, 'error');
}
}
function copyPassword() {
const pwd = document.getElementById('password').value;
if (pwd) {
navigator.clipboard.writeText(pwd);
showToast('已复制到剪贴板');
}
}
document.getElementById('length').addEventListener('input', (e) => {
document.getElementById('lengthValue').textContent = e.target.value;
});
document.getElementById('generateBtn').addEventListener('click', generatePassword);
document.getElementById('copyBtn').addEventListener('click', copyPassword);
document.getElementById('batchBtn').addEventListener('click', batchGenerate);
recordUsage('password');
generatePassword();
</script>
<?php include_once '../footer.php'; ?>

84
pages/qrcode.php Normal file
View File

@@ -0,0 +1,84 @@
<?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">
<h1 class="text-2xl font-bold mb-6">📱 二维码生成器</h1>
<div class="grid md:grid-cols-2 gap-8">
<div>
<label class="form-label">内容/链接:</label>
<textarea id="content" rows="4" placeholder="输入文本或URL..." class="form-input mb-4"></textarea>
<label class="form-label">尺寸:<span id="sizeValue">10</span></label>
<input type="range" id="size" min="5" max="20" value="10" class="w-full mb-4">
<button id="generateBtn" class="btn btn-primary w-full">生成二维码</button>
</div>
<div class="text-center">
<div id="qrContainer" class="bg-gray-50 rounded-lg p-4 min-h-[300px] flex items-center justify-center">
<div class="text-gray-400">点击生成二维码</div>
</div>
<button id="downloadBtn" class="btn btn-secondary mt-4 hidden">下载二维码</button>
</div>
</div>
</div>
</div>
<script>
let currentQRCode = null;
async function generateQR() {
const content = document.getElementById('content').value.trim();
if (!content) {
showToast('请输入内容', 'error');
return;
}
const size = document.getElementById('size').value;
try {
const data = await apiRequest('/qrcode/generate', {
method: 'POST',
body: JSON.stringify({ content, size: parseInt(size) })
});
currentQRCode = data.qr_code;
document.getElementById('qrContainer').innerHTML = `<img src="${data.qr_code}" alt="二维码" class="max-w-full mx-auto">`;
document.getElementById('downloadBtn').classList.remove('hidden');
} catch (error) {
showToast(error.message, 'error');
}
}
function downloadQR() {
if (currentQRCode) {
const link = document.createElement('a');
link.download = 'qrcode.png';
link.href = currentQRCode;
link.click();
}
}
document.getElementById('size').addEventListener('input', (e) => {
document.getElementById('sizeValue').textContent = e.target.value;
});
document.getElementById('generateBtn').addEventListener('click', generateQR);
document.getElementById('downloadBtn').addEventListener('click', downloadQR);
recordUsage('qrcode');
</script>
<?php include_once '../footer.php'; ?>

262
pages/todos.php Normal file
View 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'; ?>