This commit is contained in:
2025-12-30 09:03:29 +00:00
commit 32294ebec1
19 changed files with 3146 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

200
README.md Normal file
View File

@@ -0,0 +1,200 @@
# 工作任务管理系统
一个简单而强大的工作任务管理工具,帮助你记录每日工作任务和对应的时间投入。
## 功能特性
-**任务管理**: 添加、编辑、删除工作任务
- ⏱️ **时间追踪**: 点击开始/停止计时,自动记录任务时长
- 📊 **时间统计**: 查看每日、每周的工作时间统计
- 🤖 **AI润色**: 使用AI优化任务描述让工作记录更专业
- 📱 **响应式设计**: 支持桌面和移动设备
- 💾 **本地存储**: 数据存储在本地SQLite数据库中
## 技术栈
- **前端**: HTML5 + CSS3 + JavaScript (Vanilla JS)
- **后端**: Python Flask
- **数据库**: SQLite
- **AI服务**: OpenAI API (可选)
## 快速开始
### 1. 环境要求
- Python 3.7 或更高版本
- 现代浏览器 (Chrome, Firefox, Safari, Edge)
### 2. 安装和运行
#### 方法一:使用启动脚本(推荐)
```bash
# 克隆或下载项目到本地
cd worklist
# 运行启动脚本
python start.py
```
启动脚本会自动:
- 检查Python版本
- 安装所需依赖
- 创建配置文件
- 启动服务器
- 自动打开浏览器
#### 方法二:手动启动
```bash
# 1. 安装Python依赖
pip install -r backend/requirements.txt
# 2. 启动服务器
cd backend
python app.py
# 3. 在浏览器中访问
# http://localhost:5000
```
### 3. 配置AI润色功能可选
如需使用AI润色功能
1. 获取OpenAI API密钥
2. 编辑 `backend/.env` 文件
3. 设置 `OPENAI_API_KEY=your_api_key_here`
## 使用指南
### 基本操作
1. **添加任务**
- 点击"添加任务"按钮
- 填写任务标题和描述
- 选择任务状态
- 点击"保存"
2. **时间追踪**
- 在任务列表中点击"开始"按钮开始计时
- 点击"停止"按钮结束计时
- 系统自动记录和累计任务时长
3. **查看统计**
- 点击"查看报表"按钮
- 选择日期查看当日工作统计
- 查看各任务的时间投入
4. **AI润色**
- 在编辑任务时,点击"AI润色"按钮
- 系统会优化任务描述
- 可选择使用润色版本或保持原版
### 界面说明
- **任务列表**: 显示所有任务,包括状态、时长等信息
- **统计面板**: 显示今日总工作时长、完成任务数等
- **计时器**: 实时显示当前任务计时或总时长
- **报表**: 详细的每日工作时间统计
## 项目结构
```
worklist/
├── backend/ # 后端代码
│ ├── app.py # Flask主应用
│ ├── models.py # 数据库模型
│ ├── routes.py # API路由
│ ├── ai_service.py # AI服务
│ ├── requirements.txt # Python依赖
│ └── .env # 环境变量配置
├── frontend/ # 前端代码
│ ├── index.html # 主页面
│ ├── css/
│ │ └── style.css # 样式文件
│ └── js/
│ ├── app.js # 主应用逻辑
│ ├── api.js # API调用
│ └── timer.js # 计时器功能
├── start.py # 启动脚本
└── README.md # 说明文档
```
## API接口
### 任务管理
- `GET /api/tasks` - 获取任务列表
- `POST /api/tasks` - 创建任务
- `PUT /api/tasks/:id` - 更新任务
- `DELETE /api/tasks/:id` - 删除任务
- `POST /api/tasks/:id/polish` - AI润色任务描述
### 时间追踪
- `POST /api/timer/start` - 开始计时
- `POST /api/timer/stop` - 停止计时
- `GET /api/timer/status/:id` - 获取计时状态
### 统计报表
- `GET /api/reports/daily` - 获取日报表
- `GET /api/reports/summary` - 获取汇总报表
## 数据模型
### 任务 (Task)
- `id`: 任务ID
- `title`: 任务标题
- `description`: 任务描述
- `polished_description`: AI润色后的描述
- `status`: 任务状态 (pending/in_progress/completed)
- `created_at`: 创建时间
- `updated_at`: 更新时间
### 时间记录 (TimeRecord)
- `id`: 记录ID
- `task_id`: 关联的任务ID
- `start_time`: 开始时间
- `end_time`: 结束时间
- `duration`: 时长(秒)
- `created_at`: 创建时间
## 常见问题
### Q: 如何备份数据?
A: 数据库文件位于 `backend/worklist.db`,直接复制此文件即可备份。
### Q: 可以多人使用吗?
A: 当前版本为单用户设计,如需多用户支持需要进一步开发。
### Q: AI润色功能需要付费吗
A: 需要OpenAI API密钥按使用量计费具体费用请查看OpenAI官网。
### Q: 支持数据导出吗?
A: 当前版本暂不支持但可以通过API获取数据后续版本会添加导出功能。
## 开发说明
### 本地开发
1. 克隆项目
2. 安装依赖: `pip install -r backend/requirements.txt`
3. 启动开发服务器: `cd backend && python app.py`
4. 访问: `http://localhost:5000`
### 添加新功能
1. 后端: 在 `routes.py` 中添加新的API端点
2. 前端: 在相应的JS文件中添加功能逻辑
3. 数据库: 如需新表,在 `models.py` 中定义
## 许可证
MIT License
## 贡献
欢迎提交Issue和Pull Request
---
**开始使用**: 运行 `python start.py` 即可开始使用!

8
backend/.env Normal file
View File

@@ -0,0 +1,8 @@
# OpenAI API配置可选用于AI润色功能
OPENAI_API_KEY=your_openai_api_key_here
# 数据库配置
DATABASE_URL=sqlite:///worklist.db
# Flask配置
SECRET_KEY=your-secret-key-here

59
backend/ai_service.py Normal file
View File

@@ -0,0 +1,59 @@
import openai
import os
from dotenv import load_dotenv
load_dotenv()
class AIService:
def __init__(self):
# 设置OpenAI API密钥
self.api_key = os.getenv('OPENAI_API_KEY')
# 延迟初始化客户端避免在没有API密钥时出错
self.client = None
def polish_description(self, description):
"""
使用AI润色任务描述
"""
if not description or not description.strip():
return description
# 检查API密钥
if not self.api_key or self.api_key == 'your_openai_api_key_here':
print("AI润色功能需要配置OpenAI API密钥")
return description
try:
# 使用旧版本OpenAI API
openai.api_key = self.api_key
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "你是一个专业的工作任务描述润色助手。请将用户提供的工作任务描述润色得更加专业、清晰、具体。保持原意不变,但让描述更加规范和易于理解。"
},
{
"role": "user",
"content": f"请润色以下工作任务描述:\n\n{description}"
}
],
max_tokens=500,
temperature=0.7
)
polished = response.choices[0].message.content.strip()
return polished
except Exception as e:
print(f"AI润色失败: {e}")
# 如果AI服务失败返回原始描述
return description
def is_available(self):
"""检查AI服务是否可用"""
return bool(self.api_key and self.api_key != 'your_openai_api_key_here')
# 创建全局AI服务实例
ai_service = AIService()

39
backend/app.py Normal file
View File

@@ -0,0 +1,39 @@
from flask import Flask, send_from_directory
from flask_cors import CORS
from models import db, Task, TimeRecord
from routes import api
import os
def create_app():
app = Flask(__name__)
# 配置
app.config['SECRET_KEY'] = 'your-secret-key-here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///worklist.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 初始化扩展
db.init_app(app)
CORS(app) # 允许跨域请求
# 注册蓝图
app.register_blueprint(api, url_prefix='/api')
# 创建数据库表
with app.app_context():
db.create_all()
# 静态文件服务(用于前端)
@app.route('/')
def index():
return send_from_directory('../frontend', 'index.html')
@app.route('/<path:filename>')
def static_files(filename):
return send_from_directory('../frontend', filename)
return app
if __name__ == '__main__':
app = create_app()
app.run(debug=True, host='0.0.0.0', port=5000)

8
backend/env_example.txt Normal file
View File

@@ -0,0 +1,8 @@
# OpenAI API配置
OPENAI_API_KEY=your_openai_api_key_here
# 数据库配置
DATABASE_URL=sqlite:///worklist.db
# Flask配置
SECRET_KEY=your-secret-key-here

Binary file not shown.

66
backend/models.py Normal file
View File

@@ -0,0 +1,66 @@
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from sqlalchemy import func
db = SQLAlchemy()
class Task(db.Model):
"""任务模型"""
__tablename__ = 'tasks'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
polished_description = db.Column(db.Text) # AI润色后的描述
status = db.Column(db.String(20), default='pending') # pending, in_progress, completed
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联关系
time_records = db.relationship('TimeRecord', backref='task', lazy=True, cascade='all, delete-orphan')
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'description': self.description,
'polished_description': self.polished_description,
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'total_time': self.get_total_time()
}
def get_total_time(self):
"""获取任务总时长(秒)"""
total_seconds = db.session.query(func.sum(TimeRecord.duration)).filter(
TimeRecord.task_id == self.id
).scalar() or 0
return int(total_seconds)
class TimeRecord(db.Model):
"""时间记录模型"""
__tablename__ = 'time_records'
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.Integer, db.ForeignKey('tasks.id'), nullable=False)
start_time = db.Column(db.DateTime, nullable=False)
end_time = db.Column(db.DateTime)
duration = db.Column(db.Integer) # 时长(秒)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'task_id': self.task_id,
'start_time': self.start_time.isoformat() if self.start_time else None,
'end_time': self.end_time.isoformat() if self.end_time else None,
'duration': self.duration,
'created_at': self.created_at.isoformat() if self.created_at else None
}
def calculate_duration(self):
"""计算时长"""
if self.start_time and self.end_time:
self.duration = int((self.end_time - self.start_time).total_seconds())
return self.duration

6
backend/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
Flask==2.3.3
Flask-CORS==4.0.0
SQLAlchemy==2.0.21
Flask-SQLAlchemy==3.0.5
python-dotenv==1.0.0
openai==0.28.1

408
backend/routes.py Normal file
View File

@@ -0,0 +1,408 @@
from flask import Blueprint, request, jsonify
from datetime import datetime, timedelta
from models import db, Task, TimeRecord
from ai_service import ai_service
import json
api = Blueprint('api', __name__)
# 任务管理API
@api.route('/tasks', methods=['GET'])
def get_tasks():
"""获取所有任务"""
tasks = Task.query.order_by(Task.created_at.desc()).all()
return jsonify([task.to_dict() for task in tasks])
@api.route('/tasks', methods=['POST'])
def create_task():
"""创建新任务"""
data = request.get_json()
if not data or 'title' not in data:
return jsonify({'error': '任务标题不能为空'}), 400
task = Task(
title=data['title'],
description=data.get('description', ''),
status=data.get('status', 'pending')
)
db.session.add(task)
db.session.commit()
return jsonify(task.to_dict()), 201
@api.route('/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
"""更新任务"""
task = Task.query.get_or_404(task_id)
data = request.get_json()
if 'title' in data:
task.title = data['title']
if 'description' in data:
task.description = data['description']
if 'status' in data:
task.status = data['status']
task.updated_at = datetime.utcnow()
db.session.commit()
return jsonify(task.to_dict())
@api.route('/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
"""删除任务"""
task = Task.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
return jsonify({'message': '任务删除成功'})
@api.route('/tasks/<int:task_id>/polish', methods=['POST'])
def polish_task_description(task_id):
"""AI润色任务描述"""
task = Task.query.get_or_404(task_id)
if not ai_service.is_available():
return jsonify({'error': 'AI服务不可用请检查API密钥配置'}), 500
if not task.description:
return jsonify({'error': '任务描述为空,无法润色'}), 400
polished_description = ai_service.polish_description(task.description)
task.polished_description = polished_description
task.updated_at = datetime.utcnow()
db.session.commit()
return jsonify({
'original': task.description,
'polished': polished_description
})
# 计时器API
@api.route('/timer/start', methods=['POST'])
def start_timer():
"""开始计时"""
data = request.get_json()
task_id = data.get('task_id')
if not task_id:
return jsonify({'error': '任务ID不能为空'}), 400
task = Task.query.get_or_404(task_id)
# 检查是否已有进行中的计时
active_record = TimeRecord.query.filter_by(
task_id=task_id,
end_time=None
).first()
if active_record:
return jsonify({'error': '该任务已在计时中'}), 400
# 创建新的时间记录
time_record = TimeRecord(
task_id=task_id,
start_time=datetime.utcnow()
)
db.session.add(time_record)
task.status = 'in_progress'
db.session.commit()
return jsonify(time_record.to_dict())
@api.route('/timer/stop', methods=['POST'])
def stop_timer():
"""停止计时"""
data = request.get_json()
task_id = data.get('task_id')
if not task_id:
return jsonify({'error': '任务ID不能为空'}), 400
# 查找进行中的时间记录
time_record = TimeRecord.query.filter_by(
task_id=task_id,
end_time=None
).first()
if not time_record:
return jsonify({'error': '没有找到进行中的计时'}), 400
# 结束计时
time_record.end_time = datetime.utcnow()
time_record.calculate_duration()
# 更新任务状态
task = Task.query.get(task_id)
if task:
task.status = 'pending' # 或者根据业务逻辑设置其他状态
db.session.commit()
return jsonify(time_record.to_dict())
@api.route('/timer/status/<int:task_id>', methods=['GET'])
def get_timer_status(task_id):
"""获取任务计时状态"""
active_record = TimeRecord.query.filter_by(
task_id=task_id,
end_time=None
).first()
if active_record:
return jsonify({
'is_running': True,
'start_time': active_record.start_time.isoformat(),
'duration': int((datetime.utcnow() - active_record.start_time).total_seconds())
})
else:
return jsonify({'is_running': False})
@api.route('/timer/status/batch', methods=['POST'])
def get_timer_status_batch():
"""批量获取任务计时状态"""
data = request.get_json() or {}
task_ids = data.get('task_ids', [])
if not isinstance(task_ids, list) or not task_ids:
return jsonify({'error': '任务ID列表不能为空'}), 400
# 初始化默认状态
statuses = {int(task_id): {'is_running': False} for task_id in task_ids}
active_records = TimeRecord.query.filter(
TimeRecord.task_id.in_(task_ids),
TimeRecord.end_time.is_(None)
).all()
now = datetime.utcnow()
for record in active_records:
statuses[record.task_id] = {
'is_running': True,
'start_time': record.start_time.isoformat(),
'duration': int((now - record.start_time).total_seconds())
}
return jsonify(statuses)
# 统计报表API
@api.route('/reports/daily', methods=['GET'])
def get_daily_report():
"""获取日报表"""
date_str = request.args.get('date')
if date_str:
try:
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
return jsonify({'error': '日期格式错误请使用YYYY-MM-DD格式'}), 400
else:
target_date = datetime.now().date()
# 获取指定日期的所有时间记录
start_datetime = datetime.combine(target_date, datetime.min.time())
end_datetime = datetime.combine(target_date, datetime.max.time())
records = TimeRecord.query.filter(
TimeRecord.start_time >= start_datetime,
TimeRecord.start_time <= end_datetime,
TimeRecord.end_time.isnot(None)
).all()
# 按任务分组统计
task_stats = {}
total_time = 0
for record in records:
task_id = record.task_id
if task_id not in task_stats:
task = Task.query.get(task_id)
task_stats[task_id] = {
'task': task.to_dict() if task else None,
'total_duration': 0,
'records': []
}
task_stats[task_id]['total_duration'] += record.duration or 0
task_stats[task_id]['records'].append(record.to_dict())
total_time += record.duration or 0
return jsonify({
'date': target_date.isoformat(),
'total_time': total_time,
'task_stats': list(task_stats.values())
})
@api.route('/reports/summary', methods=['GET'])
def get_summary_report():
"""获取汇总报表"""
days = int(request.args.get('days', 7)) # 默认最近7天
end_date = datetime.now().date()
start_date = end_date - timedelta(days=days-1)
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# 获取时间范围内的记录
records = TimeRecord.query.filter(
TimeRecord.start_time >= start_datetime,
TimeRecord.start_time <= end_datetime,
TimeRecord.end_time.isnot(None)
).all()
# 按日期分组
daily_stats = {}
for record in records:
date_key = record.start_time.date().isoformat()
if date_key not in daily_stats:
daily_stats[date_key] = {
'date': date_key,
'total_time': 0,
'tasks': {}
}
task_id = record.task_id
if task_id not in daily_stats[date_key]['tasks']:
task = Task.query.get(task_id)
daily_stats[date_key]['tasks'][task_id] = {
'task_title': task.title if task else f'任务{task_id}',
'total_time': 0
}
duration = record.duration or 0
daily_stats[date_key]['total_time'] += duration
daily_stats[date_key]['tasks'][task_id]['total_time'] += duration
return jsonify({
'period': f'{start_date.isoformat()}{end_date.isoformat()}',
'daily_stats': list(daily_stats.values())
})
# 时间段历史API
@api.route('/tasks/<int:task_id>/time-history', methods=['GET'])
def get_task_time_history(task_id):
"""获取任务的时间段历史"""
task = Task.query.get_or_404(task_id)
# 获取参数
days = int(request.args.get('days', 30)) # 默认最近30天
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 20))
# 计算日期范围
end_date = datetime.now().date()
start_date = end_date - timedelta(days=days-1)
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# 查询时间记录
query = TimeRecord.query.filter(
TimeRecord.task_id == task_id,
TimeRecord.start_time >= start_datetime,
TimeRecord.start_time <= end_datetime,
TimeRecord.end_time.isnot(None)
).order_by(TimeRecord.start_time.desc())
# 分页
pagination = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
# 按日期分组
daily_segments = {}
for record in pagination.items:
date_key = record.start_time.date().isoformat()
if date_key not in daily_segments:
daily_segments[date_key] = {
'date': date_key,
'total_duration': 0,
'segments': []
}
daily_segments[date_key]['total_duration'] += record.duration or 0
daily_segments[date_key]['segments'].append(record.to_dict())
return jsonify({
'task': task.to_dict(),
'period': f'{start_date.isoformat()}{end_date.isoformat()}',
'daily_segments': list(daily_segments.values()),
'pagination': {
'page': pagination.page,
'pages': pagination.pages,
'per_page': pagination.per_page,
'total': pagination.total,
'has_next': pagination.has_next,
'has_prev': pagination.has_prev
}
})
@api.route('/time-history', methods=['GET'])
def get_all_time_history():
"""获取所有任务的时间段历史"""
# 获取参数
days = int(request.args.get('days', 7)) # 默认最近7天
task_id = request.args.get('task_id') # 可选的任务ID过滤
# 计算日期范围
end_date = datetime.now().date()
start_date = end_date - timedelta(days=days-1)
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# 构建查询
query = TimeRecord.query.filter(
TimeRecord.start_time >= start_datetime,
TimeRecord.start_time <= end_datetime,
TimeRecord.end_time.isnot(None)
)
if task_id:
query = query.filter(TimeRecord.task_id == task_id)
# 按开始时间排序
records = query.order_by(TimeRecord.start_time.desc()).all()
# 按日期和任务分组
daily_tasks = {}
for record in records:
date_key = record.start_time.date().isoformat()
task_id = record.task_id
if date_key not in daily_tasks:
daily_tasks[date_key] = {}
if task_id not in daily_tasks[date_key]:
task = Task.query.get(task_id)
daily_tasks[date_key][task_id] = {
'task': task.to_dict() if task else None,
'total_duration': 0,
'segments': []
}
daily_tasks[date_key][task_id]['total_duration'] += record.duration or 0
daily_tasks[date_key][task_id]['segments'].append(record.to_dict())
# 转换为列表格式
result = []
for date, tasks in daily_tasks.items():
day_data = {
'date': date,
'total_time': sum(task['total_duration'] for task in tasks.values()),
'tasks': list(tasks.values())
}
result.append(day_data)
# 按日期排序(最新的在前)
result.sort(key=lambda x: x['date'], reverse=True)
return jsonify({
'period': f'{start_date.isoformat()}{end_date.isoformat()}',
'daily_tasks': result
})

814
frontend/css/style.css Normal file
View File

@@ -0,0 +1,814 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.header h1 {
color: #4a5568;
font-size: 2rem;
font-weight: 700;
}
.header h1 i {
color: #667eea;
margin-right: 10px;
}
.header-actions {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
/* 按钮样式 */
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f7fafc;
color: #4a5568;
border: 2px solid #e2e8f0;
}
.btn-secondary:hover {
background: #edf2f7;
border-color: #cbd5e0;
}
.btn-outline {
background: transparent;
color: #667eea;
border: 2px solid #667eea;
}
.btn-outline:hover {
background: #667eea;
color: white;
}
.btn-success {
background: #48bb78;
color: white;
}
.btn-success:hover {
background: #38a169;
}
.btn-danger {
background: #f56565;
color: white;
}
.btn-danger:hover {
background: #e53e3e;
}
.btn-sm {
padding: 8px 16px;
font-size: 12px;
}
/* 主要内容区域 */
.main-content {
display: grid;
grid-template-columns: 1fr 300px;
gap: 30px;
align-items: start;
}
/* 任务区域 */
.task-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.task-section h2 {
color: #4a5568;
margin-bottom: 25px;
font-size: 1.5rem;
}
.task-filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.task-exclusions {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 25px;
flex-wrap: wrap;
padding: 12px 16px;
background: rgba(103, 126, 234, 0.08);
border-radius: 10px;
}
.exclusion-label {
color: #4a5568;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
}
.filter-btn {
min-width: 90px;
}
.filter-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #ffffff;
border-color: transparent;
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
}
.exclusion-btn {
min-width: 80px;
border-color: #cbd5e0;
color: #4a5568;
}
.exclusion-btn.active {
background: #f56565;
border-color: #f56565;
color: #ffffff;
box-shadow: 0 6px 20px rgba(245, 101, 101, 0.3);
}
.task-list {
display: flex;
flex-direction: column;
gap: 15px;
}
/* 任务项样式 */
.task-item {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
position: relative;
}
.task-item:hover {
border-color: #667eea;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.1);
}
.task-item.in-progress {
border-color: #48bb78;
background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%);
}
.task-item.completed {
border-color: #a0aec0;
background: #f7fafc;
opacity: 0.8;
}
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.task-title {
font-size: 1.2rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 5px;
}
.task-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.task-status.pending {
background: #fed7d7;
color: #c53030;
}
.task-status.in-progress {
background: #c6f6d5;
color: #2f855a;
}
.task-status.completed {
background: #e2e8f0;
color: #4a5568;
}
.task-description {
color: #718096;
line-height: 1.6;
margin-bottom: 15px;
}
.task-description.polished {
background: #f0f9ff;
padding: 10px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.task-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.task-timer {
display: flex;
align-items: center;
gap: 15px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e2e8f0;
}
.timer-display {
font-family: 'Courier New', monospace;
font-size: 1.5rem;
font-weight: bold;
color: #2d3748;
background: #f7fafc;
padding: 8px 16px;
border-radius: 8px;
border: 2px solid #e2e8f0;
}
.timer-controls {
display: flex;
gap: 8px;
}
.task-stats {
display: flex;
gap: 20px;
margin-top: 10px;
font-size: 14px;
color: #718096;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
/* 统计面板 */
.stats-panel {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
position: sticky;
top: 20px;
}
.stats-panel h3 {
color: #4a5568;
margin-bottom: 20px;
font-size: 1.3rem;
}
.stats-card {
display: flex;
flex-direction: column;
gap: 15px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #e2e8f0;
}
.stat-item:last-child {
border-bottom: none;
}
.stat-label {
color: #718096;
font-size: 14px;
}
.stat-value {
font-size: 1.2rem;
font-weight: 700;
color: #2d3748;
}
/* 模态框样式 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background: white;
margin: 5% auto;
padding: 0;
border-radius: 15px;
width: 90%;
max-width: 600px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.3s ease;
}
.modal-content.large {
max-width: 800px;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 25px 30px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
color: #2d3748;
font-size: 1.5rem;
}
.close {
color: #a0aec0;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
}
.close:hover {
color: #4a5568;
}
.modal-body {
padding: 30px;
}
.modal-footer {
padding: 20px 30px;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: flex-end;
gap: 15px;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #4a5568;
font-weight: 600;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-actions {
margin-top: 10px;
}
.polished-content {
margin-top: 15px;
padding: 15px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #bee3f8;
}
.polished-content h4 {
color: #2b6cb0;
margin-bottom: 10px;
font-size: 14px;
}
.polished-text {
color: #2d3748;
line-height: 1.6;
margin-bottom: 15px;
padding: 10px;
background: white;
border-radius: 6px;
border: 1px solid #e2e8f0;
}
.polished-actions {
display: flex;
gap: 10px;
}
/* 报表样式 */
.report-controls {
display: flex;
gap: 15px;
align-items: end;
margin-bottom: 25px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
}
/* 历史记录样式 */
.history-controls {
display: flex;
gap: 15px;
align-items: end;
margin-bottom: 25px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
flex-wrap: wrap;
}
.history-content {
max-height: 600px;
overflow-y: auto;
}
.history-day {
margin-bottom: 25px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.history-day h4 {
color: #2d3748;
margin-bottom: 15px;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.day-total-time {
color: #667eea;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.history-task {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.history-task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
}
.history-task-title {
color: #2d3748;
font-weight: 600;
font-size: 1.1rem;
}
.history-task-total {
color: #667eea;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.time-segments {
display: flex;
flex-direction: column;
gap: 10px;
}
.time-segment {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #f7fafc;
border-radius: 6px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.time-segment:hover {
background: #edf2f7;
border-color: #cbd5e0;
}
.segment-time {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.segment-duration {
color: #667eea;
font-weight: 600;
font-family: 'Courier New', monospace;
font-size: 1.1rem;
}
.segment-period {
color: #718096;
font-size: 0.9rem;
}
.segment-timespan {
color: #4a5568;
font-size: 0.9rem;
}
/* 空状态样式 */
.history-empty {
text-align: center;
padding: 60px 20px;
color: #a0aec0;
}
.history-empty i {
font-size: 3rem;
margin-bottom: 15px;
display: block;
}
.history-empty h3 {
font-size: 1.3rem;
margin-bottom: 10px;
}
.history-empty p {
font-size: 1rem;
}
.report-content {
max-height: 500px;
overflow-y: auto;
}
.report-day {
margin-bottom: 25px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.report-day h4 {
color: #2d3748;
margin-bottom: 15px;
font-size: 1.1rem;
}
.report-task {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #e2e8f0;
}
.report-task:last-child {
border-bottom: none;
}
.report-task-title {
color: #4a5568;
font-weight: 500;
}
.report-task-time {
color: #667eea;
font-weight: 600;
font-family: 'Courier New', monospace;
}
/* 加载提示 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(5px);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
}
.loading-spinner {
text-align: center;
color: #667eea;
}
.loading-spinner i {
font-size: 2rem;
margin-bottom: 15px;
display: block;
}
.loading-spinner span {
font-size: 1.1rem;
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
text-align: center;
}
.header-actions {
justify-content: center;
}
.task-header {
flex-direction: column;
gap: 10px;
}
.task-actions {
justify-content: center;
}
.timer-controls {
flex-wrap: wrap;
justify-content: center;
}
.modal-content {
margin: 10% auto;
width: 95%;
}
.report-controls {
flex-direction: column;
align-items: stretch;
}
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #a0aec0;
}
.empty-state i {
font-size: 4rem;
margin-bottom: 20px;
display: block;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 10px;
}
.empty-state p {
font-size: 1rem;
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-in {
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}

229
frontend/index.html Normal file
View File

@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工作任务管理系统</title>
<link rel="stylesheet" href="css/style.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- 头部 -->
<header class="header">
<h1><i class="fas fa-tasks"></i> 工作任务管理系统</h1>
<div class="header-actions">
<button id="addTaskBtn" class="btn btn-primary">
<i class="fas fa-plus"></i> 添加任务
</button>
<button id="reportBtn" class="btn btn-secondary">
<i class="fas fa-chart-bar"></i> 查看报表
</button>
<button id="timeHistoryBtn" class="btn btn-secondary">
<i class="fas fa-history"></i> 时间历史
</button>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 任务列表 -->
<section class="task-section">
<h2>任务列表</h2>
<div class="task-filters" role="group" aria-label="任务状态筛选">
<button type="button" class="btn btn-secondary filter-btn active" data-filter="all">
<i class="fas fa-globe"></i> 全部
</button>
<button type="button" class="btn btn-secondary filter-btn" data-filter="pending">
<i class="fas fa-circle-notch"></i> 未开始
</button>
<button type="button" class="btn btn-secondary filter-btn" data-filter="in_progress">
<i class="fas fa-play-circle"></i> 进行中
</button>
<button type="button" class="btn btn-secondary filter-btn" data-filter="completed">
<i class="fas fa-check-circle"></i> 已完成
</button>
</div>
<div class="task-exclusions" role="group" aria-label="排除任务状态">
<span class="exclusion-label">
<i class="fas fa-ban"></i> 排除状态:
</span>
<button type="button" class="btn btn-outline exclusion-btn" data-status="pending">
未开始
</button>
<button type="button" class="btn btn-outline exclusion-btn" data-status="in_progress">
进行中
</button>
<button type="button" class="btn btn-outline exclusion-btn" data-status="completed">
已完成
</button>
</div>
<div id="taskList" class="task-list">
<!-- 任务项将通过JavaScript动态生成 -->
</div>
</section>
<!-- 时间统计面板 -->
<aside class="stats-panel">
<h3>今日统计</h3>
<div class="stats-card">
<div class="stat-item">
<span class="stat-label">总时长</span>
<span id="totalTime" class="stat-value">00:00:00</span>
</div>
<div class="stat-item">
<span class="stat-label">完成任务</span>
<span id="completedTasks" class="stat-value">0</span>
</div>
<div class="stat-item">
<span class="stat-label">进行中</span>
<span id="activeTasks" class="stat-value">0</span>
</div>
</div>
</aside>
</main>
</div>
<!-- 添加/编辑任务模态框 -->
<div id="taskModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">添加任务</h3>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<form id="taskForm">
<div class="form-group">
<label for="taskTitle">任务标题 *</label>
<input type="text" id="taskTitle" name="title" required>
</div>
<div class="form-group">
<label for="taskDescription">任务描述</label>
<textarea id="taskDescription" name="description" rows="4" placeholder="描述任务的具体内容和要求..."></textarea>
<div class="form-actions">
<button type="button" id="polishBtn" class="btn btn-outline">
<i class="fas fa-magic"></i> AI润色
</button>
</div>
<div id="polishedDescription" class="polished-content" style="display: none;">
<h4>润色后的描述:</h4>
<div class="polished-text"></div>
<div class="polished-actions">
<button type="button" id="usePolishedBtn" class="btn btn-primary btn-sm">使用润色版本</button>
<button type="button" id="discardPolishedBtn" class="btn btn-outline btn-sm">保持原版</button>
</div>
</div>
</div>
<div class="form-group">
<label for="taskStatus">状态</label>
<select id="taskStatus" name="status">
<option value="pending">待开始</option>
<option value="in_progress">进行中</option>
<option value="completed">已完成</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" id="cancelBtn" class="btn btn-outline">取消</button>
<button type="button" id="saveBtn" class="btn btn-primary">保存</button>
</div>
</div>
</div>
<!-- 报表模态框 -->
<div id="reportModal" class="modal">
<div class="modal-content large">
<div class="modal-header">
<h3>时间统计报表</h3>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="report-controls">
<div class="form-group">
<label for="reportDate">选择日期</label>
<input type="date" id="reportDate">
</div>
<button id="loadReportBtn" class="btn btn-primary">加载报表</button>
</div>
<div id="reportContent" class="report-content">
<!-- 报表内容将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
<!-- 时间历史模态框 -->
<div id="timeHistoryModal" class="modal">
<div class="modal-content large">
<div class="modal-header">
<h3>时间历史记录</h3>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="history-controls">
<div class="form-group">
<label for="historyDays">查看天数</label>
<select id="historyDays">
<option value="7">最近7天</option>
<option value="14">最近14天</option>
<option value="30">最近30天</option>
<option value="90">最近90天</option>
</select>
</div>
<div class="form-group">
<label for="historyTaskFilter">任务筛选</label>
<select id="historyTaskFilter">
<option value="">所有任务</option>
</select>
</div>
<button id="loadHistoryBtn" class="btn btn-primary">加载历史</button>
</div>
<div id="timeHistoryContent" class="history-content">
<!-- 时间历史内容将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
<!-- 任务时间历史模态框 -->
<div id="taskTimeHistoryModal" class="modal">
<div class="modal-content large">
<div class="modal-header">
<h3 id="taskHistoryTitle">任务时间历史</h3>
<span class="close">&times;</span>
</div>
<div class="modal-body">
<div class="history-controls">
<div class="form-group">
<label for="taskHistoryDays">查看天数</label>
<select id="taskHistoryDays">
<option value="7">最近7天</option>
<option value="14">最近14天</option>
<option value="30">最近30天</option>
<option value="90">最近90天</option>
</select>
</div>
<button id="loadTaskHistoryBtn" class="btn btn-primary">加载历史</button>
</div>
<div id="taskTimeHistoryContent" class="history-content">
<!-- 任务时间历史内容将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
<!-- 加载提示 -->
<div id="loadingOverlay" class="loading-overlay" style="display: none;">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
<span>处理中...</span>
</div>
</div>
<!-- JavaScript文件 -->
<script src="js/api.js"></script>
<script src="js/timer.js"></script>
<script src="js/app.js"></script>
</body>
</html>

115
frontend/js/api.js Normal file
View 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
View 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, '&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();
});

268
frontend/js/timer.js Normal file
View 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();

6
main.py Normal file
View File

@@ -0,0 +1,6 @@
def main():
print("Hello from worklist!")
if __name__ == "__main__":
main()

7
pyproject.toml Normal file
View File

@@ -0,0 +1,7 @@
[project]
name = "worklist"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = []

95
start.py Normal file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
工作任务管理系统启动脚本
"""
import os
import sys
import subprocess
import webbrowser
import time
from pathlib import Path
def check_python_version():
"""检查Python版本"""
if sys.version_info < (3, 7):
print("错误: 需要Python 3.7或更高版本")
sys.exit(1)
def install_dependencies():
"""安装依赖"""
print("正在安装Python依赖...")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "backend/requirements.txt"])
print("✓ Python依赖安装完成")
except subprocess.CalledProcessError as e:
print(f"错误: 安装依赖失败 - {e}")
sys.exit(1)
def create_env_file():
"""创建环境变量文件"""
env_file = Path("backend/.env")
if not env_file.exists():
print("创建环境变量文件...")
with open(env_file, "w", encoding="utf-8") as f:
f.write("""# OpenAI API配置可选用于AI润色功能
OPENAI_API_KEY=your_openai_api_key_here
# 数据库配置
DATABASE_URL=sqlite:///worklist.db
# Flask配置
SECRET_KEY=your-secret-key-here
""")
print("✓ 环境变量文件已创建: backend/.env")
print("提示: 如需使用AI润色功能请在.env文件中设置OPENAI_API_KEY")
def start_server():
"""启动服务器"""
print("正在启动服务器...")
os.chdir("backend")
try:
# 启动Flask应用
subprocess.run([sys.executable, "app.py"])
except KeyboardInterrupt:
print("\n服务器已停止")
except Exception as e:
print(f"错误: 启动服务器失败 - {e}")
sys.exit(1)
def main():
"""主函数"""
print("=" * 50)
print("工作任务管理系统")
print("=" * 50)
# 检查Python版本
check_python_version()
# 安装依赖
install_dependencies()
# 创建环境变量文件
create_env_file()
print("\n启动说明:")
print("1. 服务器启动后,请在浏览器中访问: http://localhost:5000")
print("2. 按 Ctrl+C 停止服务器")
print("3. 如需使用AI润色功能请配置OpenAI API密钥")
print("\n正在启动服务器...")
# 延迟2秒后自动打开浏览器
def open_browser():
time.sleep(2)
webbrowser.open("http://localhost:5000")
import threading
browser_thread = threading.Thread(target=open_browser)
browser_thread.daemon = True
browser_thread.start()
# 启动服务器
start_server()
if __name__ == "__main__":
main()