init
This commit is contained in:
115
frontend/js/api.js
Normal file
115
frontend/js/api.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// API调用模块
|
||||
class WorkListAPI {
|
||||
constructor() {
|
||||
this.baseURL = '/api';
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP错误: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API请求失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 任务管理API
|
||||
async getTasks() {
|
||||
return this.request('/tasks');
|
||||
}
|
||||
|
||||
async createTask(taskData) {
|
||||
return this.request('/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(taskData)
|
||||
});
|
||||
}
|
||||
|
||||
async updateTask(taskId, taskData) {
|
||||
return this.request(`/tasks/${taskId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(taskData)
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTask(taskId) {
|
||||
return this.request(`/tasks/${taskId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
async polishTaskDescription(taskId) {
|
||||
return this.request(`/tasks/${taskId}/polish`, {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
// 计时器API
|
||||
async startTimer(taskId) {
|
||||
return this.request('/timer/start', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task_id: taskId })
|
||||
});
|
||||
}
|
||||
|
||||
async stopTimer(taskId) {
|
||||
return this.request('/timer/stop', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task_id: taskId })
|
||||
});
|
||||
}
|
||||
|
||||
async getTimerStatus(taskId) {
|
||||
return this.request(`/timer/status/${taskId}`);
|
||||
}
|
||||
|
||||
async getTimerStatuses(taskIds) {
|
||||
return this.request('/timer/status/batch', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task_ids: taskIds })
|
||||
});
|
||||
}
|
||||
|
||||
// 报表API
|
||||
async getDailyReport(date) {
|
||||
const params = date ? `?date=${date}` : '';
|
||||
return this.request(`/reports/daily${params}`);
|
||||
}
|
||||
|
||||
async getSummaryReport(days = 7) {
|
||||
return this.request(`/reports/summary?days=${days}`);
|
||||
}
|
||||
|
||||
// 时间段历史API
|
||||
async getTaskTimeHistory(taskId, days = 30, page = 1, perPage = 20) {
|
||||
return this.request(`/tasks/${taskId}/time-history?days=${days}&page=${page}&per_page=${perPage}`);
|
||||
}
|
||||
|
||||
async getAllTimeHistory(days = 7, taskId = null) {
|
||||
const params = new URLSearchParams({ days: days });
|
||||
if (taskId) {
|
||||
params.append('task_id', taskId);
|
||||
}
|
||||
return this.request(`/time-history?${params.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局API实例
|
||||
const api = new WorkListAPI();
|
||||
807
frontend/js/app.js
Normal file
807
frontend/js/app.js
Normal file
@@ -0,0 +1,807 @@
|
||||
// 主应用类
|
||||
class WorkListApp {
|
||||
constructor() {
|
||||
this.tasks = [];
|
||||
this.currentEditingTask = null;
|
||||
this.currentFilter = 'all';
|
||||
this.excludedStatuses = new Set();
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadTasks();
|
||||
this.setupDatePicker();
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
// 添加任务按钮
|
||||
document.getElementById('addTaskBtn').addEventListener('click', () => {
|
||||
this.showTaskModal();
|
||||
});
|
||||
|
||||
// 报表按钮
|
||||
document.getElementById('reportBtn').addEventListener('click', () => {
|
||||
this.showReportModal();
|
||||
});
|
||||
|
||||
// 时间历史按钮
|
||||
document.getElementById('timeHistoryBtn').addEventListener('click', () => {
|
||||
this.showTimeHistoryModal();
|
||||
});
|
||||
|
||||
// 模态框关闭
|
||||
document.querySelectorAll('.close').forEach(closeBtn => {
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
const modal = e.target.closest('.modal');
|
||||
this.hideModal(modal);
|
||||
});
|
||||
});
|
||||
|
||||
// 点击模态框外部关闭
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.hideModal(modal);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 任务表单
|
||||
document.getElementById('saveBtn').addEventListener('click', () => {
|
||||
this.saveTask();
|
||||
});
|
||||
|
||||
document.getElementById('cancelBtn').addEventListener('click', () => {
|
||||
this.hideModal(document.getElementById('taskModal'));
|
||||
});
|
||||
|
||||
// AI润色按钮
|
||||
document.getElementById('polishBtn').addEventListener('click', () => {
|
||||
this.polishDescription();
|
||||
});
|
||||
|
||||
// 润色相关按钮
|
||||
document.getElementById('usePolishedBtn').addEventListener('click', () => {
|
||||
this.usePolishedDescription();
|
||||
});
|
||||
|
||||
document.getElementById('discardPolishedBtn').addEventListener('click', () => {
|
||||
this.discardPolishedDescription();
|
||||
});
|
||||
|
||||
// 报表加载按钮
|
||||
document.getElementById('loadReportBtn').addEventListener('click', () => {
|
||||
this.loadReport();
|
||||
});
|
||||
|
||||
// 历史记录加载按钮
|
||||
document.getElementById('loadHistoryBtn').addEventListener('click', () => {
|
||||
this.loadTimeHistory();
|
||||
});
|
||||
|
||||
// 任务历史加载按钮
|
||||
document.getElementById('loadTaskHistoryBtn').addEventListener('click', () => {
|
||||
this.loadTaskTimeHistory();
|
||||
});
|
||||
|
||||
// 任务筛选按钮
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
const filter = event.currentTarget.dataset.filter;
|
||||
this.setTaskFilter(filter);
|
||||
});
|
||||
});
|
||||
|
||||
// 排除状态按钮
|
||||
document.querySelectorAll('.exclusion-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
const status = event.currentTarget.dataset.status;
|
||||
this.toggleExcludeStatus(status);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 设置日期选择器
|
||||
setupDatePicker() {
|
||||
const dateInput = document.getElementById('reportDate');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
dateInput.value = today;
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
showLoading() {
|
||||
document.getElementById('loadingOverlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 隐藏加载提示
|
||||
hideLoading() {
|
||||
document.getElementById('loadingOverlay').style.display = 'none';
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
showModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// 隐藏模态框
|
||||
hideModal(modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
// 显示任务模态框
|
||||
showTaskModal(task = null) {
|
||||
this.currentEditingTask = task;
|
||||
const modal = document.getElementById('taskModal');
|
||||
const form = document.getElementById('taskForm');
|
||||
|
||||
if (task) {
|
||||
// 编辑模式
|
||||
document.getElementById('modalTitle').textContent = '编辑任务';
|
||||
document.getElementById('taskTitle').value = task.title;
|
||||
document.getElementById('taskDescription').value = task.description || '';
|
||||
document.getElementById('taskStatus').value = task.status;
|
||||
} else {
|
||||
// 新建模式
|
||||
document.getElementById('modalTitle').textContent = '添加任务';
|
||||
form.reset();
|
||||
}
|
||||
|
||||
// 隐藏润色内容
|
||||
document.getElementById('polishedDescription').style.display = 'none';
|
||||
|
||||
this.showModal('taskModal');
|
||||
}
|
||||
|
||||
// 保存任务
|
||||
async saveTask() {
|
||||
try {
|
||||
this.showLoading();
|
||||
|
||||
const formData = {
|
||||
title: document.getElementById('taskTitle').value.trim(),
|
||||
description: document.getElementById('taskDescription').value.trim(),
|
||||
status: document.getElementById('taskStatus').value
|
||||
};
|
||||
|
||||
if (!formData.title) {
|
||||
throw new Error('任务标题不能为空');
|
||||
}
|
||||
|
||||
let savedTask;
|
||||
if (this.currentEditingTask) {
|
||||
// 更新任务
|
||||
savedTask = await api.updateTask(this.currentEditingTask.id, formData);
|
||||
} else {
|
||||
// 创建任务
|
||||
savedTask = await api.createTask(formData);
|
||||
}
|
||||
|
||||
this.hideModal(document.getElementById('taskModal'));
|
||||
await this.loadTasks();
|
||||
this.showNotification('任务保存成功', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存任务失败:', error);
|
||||
this.showNotification(error.message || '保存任务失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
async deleteTask(taskId) {
|
||||
if (!confirm('确定要删除这个任务吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
await api.deleteTask(taskId);
|
||||
await this.loadTasks();
|
||||
this.showNotification('任务删除成功', 'success');
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error);
|
||||
this.showNotification(error.message || '删除任务失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
async startTimer(taskId) {
|
||||
try {
|
||||
this.showLoading();
|
||||
await taskTimer.startTimer(taskId);
|
||||
this.updateTaskStatusLocal(taskId, 'in_progress');
|
||||
this.renderTasks();
|
||||
this.updateStats();
|
||||
this.showNotification('计时开始', 'success');
|
||||
} catch (error) {
|
||||
console.error('开始计时失败:', error);
|
||||
this.showNotification(error.message || '开始计时失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 停止计时
|
||||
async stopTimer(taskId) {
|
||||
try {
|
||||
this.showLoading();
|
||||
await taskTimer.stopTimer(taskId);
|
||||
this.updateTaskStatusLocal(taskId, 'pending');
|
||||
this.renderTasks();
|
||||
this.updateStats();
|
||||
this.showNotification('计时结束', 'success');
|
||||
} catch (error) {
|
||||
console.error('停止计时失败:', error);
|
||||
this.showNotification(error.message || '停止计时失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// AI润色描述
|
||||
async polishDescription() {
|
||||
const description = document.getElementById('taskDescription').value.trim();
|
||||
if (!description) {
|
||||
this.showNotification('请先输入任务描述', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
|
||||
// 如果有正在编辑的任务,使用其ID;否则提示用户先保存任务
|
||||
if (!this.currentEditingTask) {
|
||||
this.showNotification('请先保存任务,然后再进行润色', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.polishTaskDescription(this.currentEditingTask.id);
|
||||
|
||||
// 显示润色结果
|
||||
const polishedDiv = document.getElementById('polishedDescription');
|
||||
const polishedText = polishedDiv.querySelector('.polished-text');
|
||||
polishedText.textContent = result.polished;
|
||||
polishedDiv.style.display = 'block';
|
||||
|
||||
this.showNotification('AI润色完成', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI润色失败:', error);
|
||||
this.showNotification(error.message || 'AI润色失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用润色版本
|
||||
usePolishedDescription() {
|
||||
const polishedText = document.querySelector('.polished-text').textContent;
|
||||
document.getElementById('taskDescription').value = polishedText;
|
||||
document.getElementById('polishedDescription').style.display = 'none';
|
||||
this.showNotification('已使用润色版本', 'success');
|
||||
}
|
||||
|
||||
// 丢弃润色版本
|
||||
discardPolishedDescription() {
|
||||
document.getElementById('polishedDescription').style.display = 'none';
|
||||
}
|
||||
|
||||
// 加载任务列表
|
||||
async loadTasks() {
|
||||
try {
|
||||
this.tasks = await api.getTasks();
|
||||
// 恢复未结束的计时器(需要在渲染之前)
|
||||
await taskTimer.restoreTimers(this.tasks);
|
||||
this.renderTasks();
|
||||
this.updateStats();
|
||||
} catch (error) {
|
||||
console.error('加载任务失败:', error);
|
||||
this.showNotification('加载任务失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置任务状态筛选
|
||||
setTaskFilter(filter = 'all') {
|
||||
const normalized = filter || 'all';
|
||||
if (this.currentFilter !== normalized) {
|
||||
this.currentFilter = normalized;
|
||||
}
|
||||
this.renderTasks();
|
||||
}
|
||||
|
||||
// 更新筛选按钮状态
|
||||
updateFilterButtons() {
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
const isActive = btn.dataset.filter === this.currentFilter;
|
||||
btn.classList.toggle('active', isActive);
|
||||
});
|
||||
}
|
||||
|
||||
// 切换排除状态
|
||||
toggleExcludeStatus(status) {
|
||||
if (this.excludedStatuses.has(status)) {
|
||||
this.excludedStatuses.delete(status);
|
||||
} else {
|
||||
this.excludedStatuses.add(status);
|
||||
}
|
||||
this.renderTasks();
|
||||
}
|
||||
|
||||
// 更新排除按钮状态
|
||||
updateExclusionButtons() {
|
||||
document.querySelectorAll('.exclusion-btn').forEach(btn => {
|
||||
const status = btn.dataset.status;
|
||||
btn.classList.toggle('active', this.excludedStatuses.has(status));
|
||||
});
|
||||
}
|
||||
|
||||
// 获取筛选后的任务列表
|
||||
getFilteredTasks() {
|
||||
if (this.currentFilter === 'all') {
|
||||
return this.tasks.filter(task => !this.excludedStatuses.has(task.status));
|
||||
}
|
||||
return this.tasks
|
||||
.filter(task => task.status === this.currentFilter)
|
||||
.filter(task => !this.excludedStatuses.has(task.status));
|
||||
}
|
||||
|
||||
// 获取筛选名称
|
||||
getFilterDisplayName(filter) {
|
||||
const map = {
|
||||
'all': '全部任务',
|
||||
'pending': '未开始任务',
|
||||
'in_progress': '进行中任务',
|
||||
'completed': '已完成任务'
|
||||
};
|
||||
return map[filter] || '任务';
|
||||
}
|
||||
|
||||
// 渲染任务列表
|
||||
renderTasks() {
|
||||
const taskList = document.getElementById('taskList');
|
||||
this.updateFilterButtons();
|
||||
this.updateExclusionButtons();
|
||||
const filteredTasks = this.getFilteredTasks();
|
||||
|
||||
if (this.tasks.length === 0) {
|
||||
taskList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-tasks"></i>
|
||||
<h3>暂无任务</h3>
|
||||
<p>点击"添加任务"开始记录你的工作</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
const filterLabel = this.getFilterDisplayName(this.currentFilter);
|
||||
taskList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-filter"></i>
|
||||
<h3>暂无${filterLabel}</h3>
|
||||
<p>试试切换其他状态,或者新建一条任务</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
taskList.innerHTML = filteredTasks.map(task => this.renderTaskItem(task)).join('');
|
||||
}
|
||||
|
||||
// 渲染单个任务项
|
||||
renderTaskItem(task) {
|
||||
const statusClass = task.status;
|
||||
const statusText = this.getStatusText(task.status);
|
||||
const isRunning = taskTimer.hasActiveTimer(task.id);
|
||||
|
||||
// 如果正在计时,显示当前会话时长;否则显示总时长
|
||||
let displayTime;
|
||||
if (isRunning) {
|
||||
const timer = taskTimer.activeTimers.get(task.id);
|
||||
const currentDuration = timer ? timer.currentSessionDuration || 0 : 0;
|
||||
displayTime = this.formatTime(currentDuration);
|
||||
} else {
|
||||
displayTime = this.formatTime(task.total_time || 0);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="task-item ${statusClass} fade-in" data-task-id="${task.id}">
|
||||
<div class="task-header">
|
||||
<div>
|
||||
<div class="task-title">${this.escapeHtml(task.title)}</div>
|
||||
<div class="task-status ${statusClass}">${statusText}</div>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
<button class="btn btn-outline btn-sm" onclick="app.showTaskModal(${JSON.stringify(task).replace(/"/g, '"')})">
|
||||
<i class="fas fa-edit"></i> 编辑
|
||||
</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="app.showTaskTimeHistory(${task.id})">
|
||||
<i class="fas fa-clock"></i> 时间历史
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="app.deleteTask(${task.id})">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${task.description ? `
|
||||
<div class="task-description ${task.polished_description ? 'polished' : ''}">
|
||||
${this.escapeHtml(task.description)}
|
||||
${task.polished_description ? `
|
||||
<div class="polished-version">
|
||||
<strong>润色版本:</strong>
|
||||
${this.escapeHtml(task.polished_description)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="task-timer">
|
||||
<div class="timer-display">${displayTime}</div>
|
||||
<div class="timer-controls">
|
||||
<button class="btn btn-success btn-sm start-timer-btn"
|
||||
onclick="app.startTimer(${task.id})"
|
||||
style="display: ${isRunning ? 'none' : 'inline-flex'}">
|
||||
<i class="fas fa-play"></i> 开始
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm stop-timer-btn"
|
||||
onclick="app.stopTimer(${task.id})"
|
||||
style="display: ${isRunning ? 'inline-flex' : 'none'}">
|
||||
<i class="fas fa-stop"></i> 停止
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-stats">
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span>总时长: ${this.formatTime(task.total_time || 0)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<span>创建: ${this.formatDate(task.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 更新本地任务状态
|
||||
updateTaskStatusLocal(taskId, status) {
|
||||
const task = this.tasks.find(item => item.id === taskId);
|
||||
if (task) {
|
||||
task.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
updateStats() {
|
||||
const totalTime = this.tasks.reduce((sum, task) => sum + (task.total_time || 0), 0);
|
||||
const completedTasks = this.tasks.filter(task => task.status === 'completed').length;
|
||||
const activeTasks = this.tasks.filter(task => task.status === 'in_progress').length;
|
||||
|
||||
document.getElementById('totalTime').textContent = this.formatTime(totalTime);
|
||||
document.getElementById('completedTasks').textContent = completedTasks;
|
||||
document.getElementById('activeTasks').textContent = activeTasks;
|
||||
}
|
||||
|
||||
// 显示报表模态框
|
||||
showReportModal() {
|
||||
this.showModal('reportModal');
|
||||
this.loadReport();
|
||||
}
|
||||
|
||||
// 显示时间历史模态框
|
||||
showTimeHistoryModal() {
|
||||
this.showModal('timeHistoryModal');
|
||||
this.loadTaskFilter();
|
||||
this.loadTimeHistory();
|
||||
}
|
||||
|
||||
// 显示任务时间历史模态框
|
||||
showTaskTimeHistory(taskId) {
|
||||
this.currentViewingTaskId = taskId;
|
||||
const task = this.tasks.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
document.getElementById('taskHistoryTitle').textContent = `"${task.title}" 的时间历史`;
|
||||
}
|
||||
this.showModal('taskTimeHistoryModal');
|
||||
this.loadTaskTimeHistory();
|
||||
}
|
||||
|
||||
// 加载报表
|
||||
async loadReport() {
|
||||
try {
|
||||
this.showLoading();
|
||||
const date = document.getElementById('reportDate').value;
|
||||
const report = await api.getDailyReport(date);
|
||||
this.renderReport(report);
|
||||
} catch (error) {
|
||||
console.error('加载报表失败:', error);
|
||||
this.showNotification('加载报表失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染报表
|
||||
renderReport(report) {
|
||||
const reportContent = document.getElementById('reportContent');
|
||||
|
||||
if (report.task_stats.length === 0) {
|
||||
reportContent.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
<h3>暂无数据</h3>
|
||||
<p>${report.date} 没有记录任何工作时间</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const totalTime = this.formatTime(report.total_time);
|
||||
|
||||
reportContent.innerHTML = `
|
||||
<div class="report-day">
|
||||
<h4>${report.date} - 总工作时长: ${totalTime}</h4>
|
||||
${report.task_stats.map(taskStat => `
|
||||
<div class="report-task">
|
||||
<div class="report-task-title">${this.escapeHtml(taskStat.task.title)}</div>
|
||||
<div class="report-task-time">${this.formatTime(taskStat.total_duration)}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 加载任务筛选器
|
||||
loadTaskFilter() {
|
||||
const taskFilter = document.getElementById('historyTaskFilter');
|
||||
taskFilter.innerHTML = '<option value="">所有任务</option>';
|
||||
|
||||
this.tasks.forEach(task => {
|
||||
const option = document.createElement('option');
|
||||
option.value = task.id;
|
||||
option.textContent = task.title;
|
||||
taskFilter.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载时间历史
|
||||
async loadTimeHistory() {
|
||||
try {
|
||||
this.showLoading();
|
||||
const days = parseInt(document.getElementById('historyDays').value);
|
||||
const taskId = document.getElementById('historyTaskFilter').value || null;
|
||||
|
||||
const history = await api.getAllTimeHistory(days, taskId);
|
||||
this.renderTimeHistory(history);
|
||||
} catch (error) {
|
||||
console.error('加载时间历史失败:', error);
|
||||
this.showNotification('加载时间历史失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载任务时间历史
|
||||
async loadTaskTimeHistory() {
|
||||
if (!this.currentViewingTaskId) return;
|
||||
|
||||
try {
|
||||
this.showLoading();
|
||||
const days = parseInt(document.getElementById('taskHistoryDays').value);
|
||||
|
||||
const history = await api.getTaskTimeHistory(this.currentViewingTaskId, days);
|
||||
this.renderTaskTimeHistory(history);
|
||||
} catch (error) {
|
||||
console.error('加载任务时间历史失败:', error);
|
||||
this.showNotification('加载任务时间历史失败', 'error');
|
||||
} finally {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染时间历史
|
||||
renderTimeHistory(history) {
|
||||
const content = document.getElementById('timeHistoryContent');
|
||||
|
||||
if (history.daily_tasks.length === 0) {
|
||||
content.innerHTML = `
|
||||
<div class="history-empty">
|
||||
<i class="fas fa-history"></i>
|
||||
<h3>暂无时间记录</h3>
|
||||
<p>在指定时间段内没有找到任何工作时间记录</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
content.innerHTML = history.daily_tasks.map(day => `
|
||||
<div class="history-day">
|
||||
<h4>
|
||||
${this.formatDate(day.date)}
|
||||
<span class="day-total-time">${this.formatTime(day.total_time)}</span>
|
||||
</h4>
|
||||
${day.tasks.map(task => `
|
||||
<div class="history-task">
|
||||
<div class="history-task-header">
|
||||
<div class="history-task-title">${this.escapeHtml(task.task.title)}</div>
|
||||
<div class="history-task-total">${this.formatTime(task.total_duration)}</div>
|
||||
</div>
|
||||
<div class="time-segments">
|
||||
${task.segments.map(segment => `
|
||||
<div class="time-segment">
|
||||
<div class="segment-timespan">
|
||||
${this.formatDateTime(segment.start_time)} - ${this.formatDateTime(segment.end_time)}
|
||||
</div>
|
||||
<div class="segment-time">
|
||||
<div class="segment-duration">${this.formatTime(segment.duration)}</div>
|
||||
<div class="segment-period">${this.formatTimePeriod(segment.start_time, segment.end_time)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 渲染任务时间历史
|
||||
renderTaskTimeHistory(history) {
|
||||
const content = document.getElementById('taskTimeHistoryContent');
|
||||
|
||||
if (history.daily_segments.length === 0) {
|
||||
content.innerHTML = `
|
||||
<div class="history-empty">
|
||||
<i class="fas fa-clock"></i>
|
||||
<h3>暂无时间记录</h3>
|
||||
<p>该任务在指定时间段内没有工作时间记录</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
content.innerHTML = history.daily_segments.map(day => `
|
||||
<div class="history-day">
|
||||
<h4>
|
||||
${this.formatDate(day.date)}
|
||||
<span class="day-total-time">${this.formatTime(day.total_duration)}</span>
|
||||
</h4>
|
||||
<div class="time-segments">
|
||||
${day.segments.map(segment => `
|
||||
<div class="time-segment">
|
||||
<div class="segment-timespan">
|
||||
${this.formatDateTime(segment.start_time)} - ${this.formatDateTime(segment.end_time)}
|
||||
</div>
|
||||
<div class="segment-time">
|
||||
<div class="segment-duration">${this.formatTime(segment.duration)}</div>
|
||||
<div class="segment-period">${this.formatTimePeriod(segment.start_time, segment.end_time)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '待开始',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 后端时间为UTC(无Z),需要按UTC解析并按上海时区显示
|
||||
parseUtcISO(dateTimeString) {
|
||||
if (!dateTimeString) return null;
|
||||
// 如果字符串不包含时区信息,则按UTC处理(补上Z)
|
||||
const hasTimezone = /[zZ]|[\+\-]\d{2}:?\d{2}$/.test(dateTimeString);
|
||||
const normalized = hasTimezone ? dateTimeString : `${dateTimeString}Z`;
|
||||
return new Date(normalized);
|
||||
}
|
||||
|
||||
formatDate(dateString) {
|
||||
const date = this.parseUtcISO(dateString);
|
||||
if (!date) return '';
|
||||
return date.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||
}
|
||||
|
||||
formatDateTime(dateTimeString) {
|
||||
const date = this.parseUtcISO(dateTimeString);
|
||||
if (!date) return '';
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZone: 'Asia/Shanghai'
|
||||
});
|
||||
}
|
||||
|
||||
formatTimePeriod(startTime, endTime) {
|
||||
const start = this.parseUtcISO(startTime);
|
||||
const end = this.parseUtcISO(endTime);
|
||||
const duration = Math.floor((end - start) / 1000);
|
||||
|
||||
if (duration < 60) {
|
||||
return `${duration}秒`;
|
||||
} else if (duration < 3600) {
|
||||
const minutes = Math.floor(duration / 60);
|
||||
const seconds = duration % 60;
|
||||
return `${minutes}分${seconds}秒`;
|
||||
} else {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// 简单的通知实现
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
z-index: 3000;
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
// 根据类型设置背景色
|
||||
const colors = {
|
||||
'success': '#48bb78',
|
||||
'error': '#f56565',
|
||||
'warning': '#ed8936',
|
||||
'info': '#4299e1'
|
||||
};
|
||||
notification.style.backgroundColor = colors[type] || colors.info;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.app = new WorkListApp();
|
||||
});
|
||||
268
frontend/js/timer.js
Normal file
268
frontend/js/timer.js
Normal file
@@ -0,0 +1,268 @@
|
||||
// 计时器模块
|
||||
class TaskTimer {
|
||||
constructor() {
|
||||
this.activeTimers = new Map(); // 存储活跃的计时器
|
||||
this.timerIntervals = new Map(); // 存储定时器ID
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
async startTimer(taskId) {
|
||||
try {
|
||||
// 检查是否已有活跃计时器
|
||||
if (this.activeTimers.has(taskId)) {
|
||||
throw new Error('该任务已在计时中');
|
||||
}
|
||||
|
||||
// 调用API开始计时
|
||||
const timeRecord = await api.startTimer(taskId);
|
||||
|
||||
// 创建本地计时器
|
||||
const timer = {
|
||||
taskId: taskId,
|
||||
startTime: new Date(),
|
||||
currentSessionDuration: 0, // 当前会话时长(从0开始)
|
||||
isRunning: true
|
||||
};
|
||||
|
||||
this.activeTimers.set(taskId, timer);
|
||||
|
||||
// 启动定时器更新
|
||||
const intervalId = setInterval(() => {
|
||||
this.updateTimer(taskId);
|
||||
}, 1000);
|
||||
|
||||
this.timerIntervals.set(taskId, intervalId);
|
||||
|
||||
// 更新UI
|
||||
this.updateTimerDisplay(taskId);
|
||||
this.updateTaskStatus(taskId, 'in_progress');
|
||||
|
||||
return timer;
|
||||
|
||||
} catch (error) {
|
||||
console.error('开始计时失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止计时
|
||||
async stopTimer(taskId) {
|
||||
try {
|
||||
const timer = this.activeTimers.get(taskId);
|
||||
if (!timer) {
|
||||
throw new Error('没有找到活跃的计时器');
|
||||
}
|
||||
|
||||
// 调用API停止计时
|
||||
await api.stopTimer(taskId);
|
||||
|
||||
// 清除本地计时器
|
||||
this.clearTimer(taskId);
|
||||
|
||||
// 更新UI
|
||||
this.updateTaskStatus(taskId, 'pending');
|
||||
this.updateTimerDisplay(taskId);
|
||||
|
||||
// 刷新任务列表以显示总时长
|
||||
if (window.app) {
|
||||
window.app.loadTasks();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('停止计时失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 清除计时器
|
||||
clearTimer(taskId) {
|
||||
const intervalId = this.timerIntervals.get(taskId);
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
this.timerIntervals.delete(taskId);
|
||||
}
|
||||
this.activeTimers.delete(taskId);
|
||||
}
|
||||
|
||||
// 更新计时器
|
||||
updateTimer(taskId) {
|
||||
const timer = this.activeTimers.get(taskId);
|
||||
if (!timer) return;
|
||||
|
||||
const now = new Date();
|
||||
// 计算当前会话的时长(从开始计时到现在)
|
||||
timer.currentSessionDuration = Math.floor((now - timer.startTime) / 1000);
|
||||
|
||||
this.updateTimerDisplay(taskId);
|
||||
}
|
||||
|
||||
// 更新计时器显示
|
||||
updateTimerDisplay(taskId) {
|
||||
const timer = this.activeTimers.get(taskId);
|
||||
const displayElement = document.querySelector(`[data-task-id="${taskId}"] .timer-display`);
|
||||
|
||||
if (!displayElement) return;
|
||||
|
||||
if (timer && timer.isRunning) {
|
||||
// 显示当前会话的时长(从0开始)
|
||||
const duration = timer.currentSessionDuration || 0;
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
const seconds = duration % 60;
|
||||
|
||||
displayElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
displayElement.style.color = '#48bb78';
|
||||
} else {
|
||||
// 显示总时长
|
||||
this.updateTotalTimeDisplay(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新总时长显示
|
||||
async updateTotalTimeDisplay(taskId) {
|
||||
try {
|
||||
const tasks = await api.getTasks();
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
|
||||
if (task) {
|
||||
const displayElement = document.querySelector(`[data-task-id="${taskId}"] .timer-display`);
|
||||
if (displayElement) {
|
||||
const totalSeconds = task.total_time || 0;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
displayElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
displayElement.style.color = '#4a5568';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新总时长显示失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
updateTaskStatus(taskId, status) {
|
||||
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||
if (!taskElement) return;
|
||||
|
||||
// 更新状态类
|
||||
taskElement.className = taskElement.className.replace(/in-progress|completed|pending/g, '');
|
||||
taskElement.classList.add(status);
|
||||
|
||||
// 更新状态显示
|
||||
const statusElement = taskElement.querySelector('.task-status');
|
||||
if (statusElement) {
|
||||
statusElement.textContent = this.getStatusText(status);
|
||||
statusElement.className = `task-status ${status}`;
|
||||
}
|
||||
|
||||
// 更新按钮状态
|
||||
this.updateTimerButtons(taskId, status);
|
||||
}
|
||||
|
||||
// 更新计时器按钮
|
||||
updateTimerButtons(taskId, status) {
|
||||
const startBtn = document.querySelector(`[data-task-id="${taskId}"] .start-timer-btn`);
|
||||
const stopBtn = document.querySelector(`[data-task-id="${taskId}"] .stop-timer-btn`);
|
||||
|
||||
if (startBtn && stopBtn) {
|
||||
if (status === 'in_progress') {
|
||||
startBtn.style.display = 'none';
|
||||
stopBtn.style.display = 'inline-flex';
|
||||
} else {
|
||||
startBtn.style.display = 'inline-flex';
|
||||
stopBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'pending': '待开始',
|
||||
'in_progress': '进行中',
|
||||
'completed': '已完成'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
formatTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 检查是否有活跃计时器
|
||||
hasActiveTimer(taskId) {
|
||||
return this.activeTimers.has(taskId);
|
||||
}
|
||||
|
||||
// 获取所有活跃计时器
|
||||
getActiveTimers() {
|
||||
return Array.from(this.activeTimers.values());
|
||||
}
|
||||
|
||||
// 停止所有计时器
|
||||
async stopAllTimers() {
|
||||
const promises = Array.from(this.activeTimers.keys()).map(taskId =>
|
||||
this.stopTimer(taskId).catch(error => {
|
||||
console.error(`停止任务${taskId}计时失败:`, error);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
// 恢复未结束的计时器(用于页面重新加载后)
|
||||
async restoreTimers(tasks) {
|
||||
try {
|
||||
const taskIds = tasks.map(task => task.id);
|
||||
if (!taskIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statuses = await api.getTimerStatuses(taskIds);
|
||||
|
||||
for (const task of tasks) {
|
||||
const timerStatus = statuses?.[task.id];
|
||||
|
||||
if (timerStatus && timerStatus.is_running) {
|
||||
// 使用服务器返回的开始时间(从ISO格式字符串创建Date对象)
|
||||
const startTime = new Date(timerStatus.start_time);
|
||||
const now = new Date();
|
||||
|
||||
// 恢复计时器
|
||||
const timer = {
|
||||
taskId: task.id,
|
||||
startTime: startTime,
|
||||
currentSessionDuration: Math.floor((now - startTime) / 1000),
|
||||
isRunning: true
|
||||
};
|
||||
|
||||
this.activeTimers.set(task.id, timer);
|
||||
|
||||
// 启动定时器更新
|
||||
const intervalId = setInterval(() => {
|
||||
this.updateTimer(task.id);
|
||||
}, 1000);
|
||||
|
||||
this.timerIntervals.set(task.id, intervalId);
|
||||
|
||||
// 更新UI
|
||||
this.updateTimerDisplay(task.id);
|
||||
this.updateTaskStatus(task.id, 'in_progress');
|
||||
|
||||
console.log(`恢复了任务 ${task.id} 的计时器`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('恢复计时器失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 创建全局计时器实例
|
||||
const taskTimer = new TaskTimer();
|
||||
Reference in New Issue
Block a user