Ching L cae33176af
Some checks failed
continuous-integration/drone/push Build is failing
feat: add web interface for task management
- Added full-featured web UI with drag-and-drop task sorting
- Implemented CRUD API endpoints for task management
- Added task reordering endpoint with priority support
- Created responsive HTML interface with inline styles
- Updated Docker configuration for web service deployment
- Added requirements.txt for dependency management
- Enhanced README with web interface documentation
- Made Google Calendar integration optional
2025-09-11 16:04:17 +08:00

599 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务管理</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 10px;
}
.add-task-form {
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.form-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.form-group {
flex: 1;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-danger {
background: #ff4757;
color: white;
}
.btn-danger:hover {
background: #ff3838;
}
.btn-success {
background: #00b894;
color: white;
}
.btn-success:hover {
background: #00a885;
}
.task-list {
padding: 20px 30px;
min-height: 300px;
}
.task-item {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s;
cursor: move;
}
.task-item:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.task-item.dragging {
opacity: 0.5;
transform: rotate(5deg);
}
.task-item.drag-over {
border: 2px dashed #667eea;
background: #f5f5ff;
}
.drag-handle {
color: #999;
cursor: grab;
font-size: 20px;
}
.drag-handle:active {
cursor: grabbing;
}
.task-color {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.task-color.green {
background: #00b894;
}
.task-color.yellow {
background: #fdcb6e;
}
.task-color.red {
background: #ff4757;
}
.task-info {
flex: 1;
}
.task-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 5px;
}
.task-details {
font-size: 12px;
color: #666;
}
.task-actions {
display: flex;
gap: 5px;
}
.btn-small {
padding: 5px 10px;
font-size: 12px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state svg {
width: 100px;
height: 100px;
margin-bottom: 20px;
opacity: 0.3;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
animation: fadeIn 0.3s;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: white;
padding: 30px;
border-radius: 12px;
max-width: 500px;
width: 90%;
animation: slideIn 0.3s;
}
.modal-header {
margin-bottom: 20px;
}
.modal-header h2 {
font-size: 20px;
color: #333;
}
.modal-footer {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📋 任务管理系统</h1>
<p>拖拽排序 · 实时同步 · 智能提醒</p>
</div>
<div class="add-task-form">
<h3 style="margin-bottom: 15px; color: #333;">新增任务</h3>
<form id="addTaskForm">
<div class="form-row">
<div class="form-group">
<label for="taskName">任务名称</label>
<input type="text" id="taskName" placeholder="输入任务名称" required>
</div>
<div class="form-group" style="max-width: 150px;">
<label for="minInterval">最少间隔天数</label>
<input type="number" id="minInterval" value="7" min="1" required>
</div>
</div>
<button type="submit" class="btn btn-primary">
<span> 添加任务</span>
</button>
</form>
</div>
<div class="task-list" id="taskList">
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p>暂无任务,点击上方添加第一个任务</p>
</div>
</div>
</div>
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>编辑任务</h2>
</div>
<form id="editTaskForm">
<input type="hidden" id="editTaskId">
<div class="form-group">
<label for="editTaskName">任务名称</label>
<input type="text" id="editTaskName" required>
</div>
<div class="form-group">
<label for="editMinInterval">最少间隔天数</label>
<input type="number" id="editMinInterval" min="1" required>
</div>
<div class="modal-footer">
<button type="button" class="btn" onclick="closeEditModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<script>
const API_KEY = '{{ api_key }}';
const API_URL = '';
let tasks = [];
let draggedElement = null;
async function fetchTasks() {
try {
const response = await fetch(`${API_URL}/tasks`, {
headers: {
'X-API-Key': API_KEY
}
});
tasks = await response.json();
renderTasks();
} catch (error) {
console.error('获取任务失败:', error);
}
}
function renderTasks() {
const taskList = document.getElementById('taskList');
if (tasks.length === 0) {
taskList.innerHTML = `
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p>暂无任务,点击上方添加第一个任务</p>
</div>
`;
return;
}
taskList.innerHTML = tasks.map(task => `
<div class="task-item" draggable="true" data-id="${task.id}">
<span class="drag-handle">☰</span>
<div class="task-color ${task.color}"></div>
<div class="task-info">
<div class="task-name">${task.name}</div>
<div class="task-details">
间隔: ${task.min_interval_days}
${task.last_execution_time ? ` | 上次执行: ${new Date(task.last_execution_time).toLocaleDateString('zh-CN')}` : ''}
</div>
</div>
<div class="task-actions">
<button class="btn btn-success btn-small" onclick="scheduleTask(${task.id})">📅 执行</button>
<button class="btn btn-primary btn-small" onclick="editTask(${task.id})">✏️ 编辑</button>
<button class="btn btn-danger btn-small" onclick="deleteTask(${task.id})">🗑️ 删除</button>
</div>
</div>
`).join('');
initDragAndDrop();
}
function initDragAndDrop() {
const taskItems = document.querySelectorAll('.task-item');
taskItems.forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragenter', handleDragEnter);
item.addEventListener('dragleave', handleDragLeave);
item.addEventListener('dragend', handleDragEnd);
});
}
function handleDragStart(e) {
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
if (this !== draggedElement) {
this.classList.add('drag-over');
}
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedElement !== this) {
const taskList = document.getElementById('taskList');
const allItems = [...taskList.querySelectorAll('.task-item')];
const draggedIndex = allItems.indexOf(draggedElement);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedElement, this);
}
updateTaskOrder();
}
return false;
}
function handleDragEnd(e) {
const taskItems = document.querySelectorAll('.task-item');
taskItems.forEach(item => {
item.classList.remove('dragging', 'drag-over');
});
}
async function updateTaskOrder() {
const taskItems = document.querySelectorAll('.task-item');
const taskIds = Array.from(taskItems).map(item => parseInt(item.dataset.id));
try {
await fetch(`${API_URL}/tasks/reorder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify({ task_ids: taskIds })
});
fetchTasks();
} catch (error) {
console.error('更新排序失败:', error);
}
}
async function addTask(event) {
event.preventDefault();
const name = document.getElementById('taskName').value;
const minInterval = document.getElementById('minInterval').value;
try {
await fetch(`${API_URL}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify({
name: name,
min_interval_days: parseInt(minInterval)
})
});
document.getElementById('addTaskForm').reset();
fetchTasks();
} catch (error) {
console.error('添加任务失败:', error);
}
}
function editTask(taskId) {
const task = tasks.find(t => t.id === taskId);
if (task) {
document.getElementById('editTaskId').value = task.id;
document.getElementById('editTaskName').value = task.name;
document.getElementById('editMinInterval').value = task.min_interval_days;
document.getElementById('editModal').classList.add('show');
}
}
function closeEditModal() {
document.getElementById('editModal').classList.remove('show');
}
async function updateTask(event) {
event.preventDefault();
const taskId = document.getElementById('editTaskId').value;
const name = document.getElementById('editTaskName').value;
const minInterval = document.getElementById('editMinInterval').value;
try {
await fetch(`${API_URL}/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify({
name: name,
min_interval_days: parseInt(minInterval)
})
});
closeEditModal();
fetchTasks();
} catch (error) {
console.error('更新任务失败:', error);
}
}
async function deleteTask(taskId) {
if (confirm('确定要删除这个任务吗?')) {
try {
await fetch(`${API_URL}/tasks/${taskId}`, {
method: 'DELETE',
headers: {
'X-API-Key': API_KEY
}
});
fetchTasks();
} catch (error) {
console.error('删除任务失败:', error);
}
}
}
async function scheduleTask(taskId) {
try {
await fetch(`${API_URL}/tasks/${taskId}/schedule`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY
}
});
alert('任务已添加到日历!');
fetchTasks();
} catch (error) {
console.error('安排任务失败:', error);
alert('安排任务失败,请检查日历配置');
}
}
document.getElementById('addTaskForm').addEventListener('submit', addTask);
document.getElementById('editTaskForm').addEventListener('submit', updateTask);
window.onclick = function(event) {
if (event.target === document.getElementById('editModal')) {
closeEditModal();
}
}
fetchTasks();
</script>
</body>
</html>