Files
worklist/frontend/js/app.js
2025-12-30 09:39:40 +00:00

901 lines
31 KiB
JavaScript
Raw 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.

// 主应用类
class WorkListApp {
constructor() {
this.tasks = [];
this.currentEditingTask = null;
this.currentFilter = 'all';
this.excludedStatuses = new Set();
this.currentUser = null;
this.init();
}
// 初始化应用
async init() {
// 先检查登录状态
await this.checkAuthStatus();
// 如果已登录,绑定事件并加载任务
if (this.currentUser) {
this.bindEvents();
this.loadTasks();
this.setupDatePicker();
}
}
// 检查认证状态
async checkAuthStatus() {
try {
const response = await api.checkAuth();
if (response.authenticated) {
this.currentUser = response.user;
this.showMainApp();
} else {
this.showLoginPage();
}
} catch (error) {
console.log('未登录:', error);
this.showLoginPage();
}
}
// 显示登录页面
showLoginPage() {
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
// 绑定登录表单事件
const loginForm = document.getElementById('loginForm');
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
await this.handleLogin();
});
}
// 显示主应用页面
showMainApp() {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('mainApp').style.display = 'block';
// 显示用户名
if (this.currentUser) {
document.getElementById('currentUsername').textContent = this.currentUser.username;
}
}
// 处理登录
async handleLogin() {
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
const errorDiv = document.getElementById('loginError');
try {
errorDiv.style.display = 'none';
const response = await api.login(username, password);
if (response.user) {
this.currentUser = response.user;
this.showMainApp();
// 绑定事件并加载任务
this.bindEvents();
this.loadTasks();
this.setupDatePicker();
}
} catch (error) {
errorDiv.textContent = error.message || '登录失败,请检查用户名和密码';
errorDiv.style.display = 'block';
}
}
// 处理登出
async handleLogout() {
try {
await api.logout();
this.currentUser = null;
this.tasks = [];
this.showLoginPage();
} catch (error) {
console.error('登出失败:', error);
alert('登出失败: ' + error.message);
}
}
// 绑定事件
bindEvents() {
// 登出按钮
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn && !logoutBtn.hasAttribute('data-bound')) {
logoutBtn.addEventListener('click', () => {
this.handleLogout();
});
logoutBtn.setAttribute('data-bound', 'true');
}
// 添加任务按钮
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, '&quot;')})">
<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();
});