用户登录功能

This commit is contained in:
2025-12-30 09:39:40 +00:00
parent 9edc0ae2ca
commit 8c3200829a
13 changed files with 539 additions and 23 deletions

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# Flask配置
SECRET_KEY=your-secret-key-here-please-change-this
FLASK_ENV=production
# OpenAI API配置(可选,用于AI润色功能)
OPENAI_API_KEY=
# 默认用户配置
# Docker首次启动时会自动创建此用户
DEFAULT_USERNAME=admin
DEFAULT_PASSWORD=admin123
# 注意: 生产环境请务必修改默认密码!

View File

@@ -24,10 +24,14 @@ RUN pip install --no-cache-dir --upgrade pip && \
# 复制项目文件 # 复制项目文件
COPY backend/ /app/backend/ COPY backend/ /app/backend/
COPY frontend/ /app/frontend/ COPY frontend/ /app/frontend/
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
# 创建数据目录 # 创建数据目录
RUN mkdir -p /app/data RUN mkdir -p /app/data
# 设置启动脚本权限
RUN chmod +x /app/docker-entrypoint.sh
# 暴露端口 # 暴露端口
EXPOSE 5000 EXPOSE 5000
@@ -35,4 +39,4 @@ EXPOSE 5000
WORKDIR /app/backend WORKDIR /app/backend
# 启动应用 # 启动应用
CMD ["python", "app.py"] CMD ["/app/docker-entrypoint.sh"]

View File

@@ -4,12 +4,14 @@
## 功能特性 ## 功能特性
- 🔐 **用户认证**: 密码登录保护,确保数据安全
-**任务管理**: 添加、编辑、删除工作任务 -**任务管理**: 添加、编辑、删除工作任务
- ⏱️ **时间追踪**: 点击开始/停止计时,自动记录任务时长 - ⏱️ **时间追踪**: 点击开始/停止计时,自动记录任务时长
- 📊 **时间统计**: 查看每日、每周的工作时间统计 - 📊 **时间统计**: 查看每日、每周的工作时间统计
- 🤖 **AI润色**: 使用AI优化任务描述让工作记录更专业 - 🤖 **AI润色**: 使用AI优化任务描述让工作记录更专业
- 📱 **响应式设计**: 支持桌面和移动设备 - 📱 **响应式设计**: 支持桌面和移动设备
- 💾 **本地存储**: 数据存储在本地SQLite数据库中 - 💾 **本地存储**: 数据存储在本地SQLite数据库中
- 🐳 **Docker支持**: 一键部署,开箱即用
## 技术栈 ## 技术栈
@@ -20,12 +22,40 @@
## 快速开始 ## 快速开始
### 1. 环境要求 ### 方法一: 使用Docker (推荐)
```bash
# 1. 克隆项目
git clone <repository-url>
cd worklist
# 2. (可选) 配置环境变量
cp .env.example .env
# 编辑 .env 文件,设置自定义用户名和密码
# 3. 启动Docker容器
docker-compose up -d
# 4. 访问应用
# 打开浏览器访问 http://localhost:5000
# 默认用户名: admin
# 默认密码: admin123
```
**首次启动说明:**
- Docker会自动创建数据库和默认用户
- 默认用户名: `admin`, 默认密码: `admin123`
- 可通过环境变量 `DEFAULT_USERNAME``DEFAULT_PASSWORD` 自定义
- 登录后请立即修改密码
### 方法二: 本地运行
#### 1. 环境要求
- Python 3.7 或更高版本 - Python 3.7 或更高版本
- 现代浏览器 (Chrome, Firefox, Safari, Edge) - 现代浏览器 (Chrome, Firefox, Safari, Edge)
### 2. 安装和运行 #### 2. 安装和运行
#### 方法一:使用启动脚本(推荐) #### 方法一:使用启动脚本(推荐)
@@ -44,30 +74,64 @@ python start.py
- 启动服务器 - 启动服务器
- 自动打开浏览器 - 自动打开浏览器
**首次使用需创建用户:**
```bash
# 运行用户创建脚本
python create_user.py
```
#### 方法二:手动启动 #### 方法二:手动启动
```bash ```bash
# 1. 安装Python依赖 # 1. 安装Python依赖
pip install -r backend/requirements.txt pip install -r backend/requirements.txt
# 2. 启动服务器 # 2. 创建初始用户
python create_user.py
# 3. 启动服务器
cd backend cd backend
python app.py python app.py
# 3. 在浏览器中访问 # 4. 在浏览器中访问
# http://localhost:5000 # http://localhost:5000
``` ```
### 3. 配置AI润色功能可选 ### 3. 配置说明
如需使用AI润色功能 #### 环境变量配置
1. 获取OpenAI API密钥 #### 环境变量配置
2. 编辑 `backend/.env` 文件
3. 设置 `OPENAI_API_KEY=your_api_key_here` 创建 `.env` 文件 (可复制 `.env.example`):
```bash
# Flask配置
SECRET_KEY=your-secret-key-here-please-change-this
# OpenAI API配置(可选,用于AI润色功能)
OPENAI_API_KEY=your_openai_api_key
# Docker首次启动时的默认用户(仅Docker部署时有效)
DEFAULT_USERNAME=admin
DEFAULT_PASSWORD=admin123
```
**安全建议:**
- 生产环境务必修改 `SECRET_KEY`
- 修改默认用户名和密码
- 登录后立即在系统中修改密码
#### AI润色功能可选
## 使用指南 ## 使用指南
### 登录系统
1. 首次访问会显示登录页面
2. 输入用户名和密码
3. 登录成功后进入主界面
### 基本操作 ### 基本操作
1. **添加任务** 1. **添加任务**

View File

@@ -6,16 +6,21 @@ import os
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
# 配置 # 配置
app.config['SECRET_KEY'] = 'your-secret-key-here' app.config['SECRET_KEY'] = 'your-secret-key-here-change-in-production'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///worklist.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///worklist.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Session配置
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24小时
# 初始化扩展 # 初始化扩展
db.init_app(app) db.init_app(app)
CORS(app) # 允许跨域请求 CORS(app, supports_credentials=True) # 允许跨域请求并支持凭证
# 注册蓝图 # 注册蓝图
app.register_blueprint(api, url_prefix='/api') app.register_blueprint(api, url_prefix='/api')

View File

@@ -1,9 +1,34 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from datetime import datetime from datetime import datetime
from sqlalchemy import func from sqlalchemy import func
from werkzeug.security import generate_password_hash, check_password_hash
db = SQLAlchemy() db = SQLAlchemy()
class User(db.Model):
"""用户模型"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(200), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
"""设置密码(哈希加密)"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""验证密码"""
return check_password_hash(self.password_hash, password)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class Task(db.Model): class Task(db.Model):
"""任务模型""" """任务模型"""
__tablename__ = 'tasks' __tablename__ = 'tasks'

View File

@@ -1,11 +1,81 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify, session
from datetime import datetime, timedelta from datetime import datetime, timedelta
from models import db, Task, TimeRecord from models import db, Task, TimeRecord, User
from ai_service import ai_service from ai_service import ai_service
import json import json
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
# 认证API
@api.route('/auth/login', methods=['POST'])
def login():
"""用户登录"""
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({'error': '用户名和密码不能为空'}), 400
username = data['username']
password = data['password']
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
# 登录成功设置session
session['user_id'] = user.id
session['username'] = user.username
return jsonify({
'message': '登录成功',
'user': user.to_dict()
})
else:
return jsonify({'error': '用户名或密码错误'}), 401
@api.route('/auth/logout', methods=['POST'])
def logout():
"""用户登出"""
session.clear()
return jsonify({'message': '登出成功'})
@api.route('/auth/check', methods=['GET'])
def check_auth():
"""检查登录状态"""
if 'user_id' in session:
user = User.query.get(session['user_id'])
if user:
return jsonify({
'authenticated': True,
'user': user.to_dict()
})
return jsonify({'authenticated': False}), 401
@api.route('/auth/register', methods=['POST'])
def register():
"""用户注册(可选,用于创建初始用户)"""
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({'error': '用户名和密码不能为空'}), 400
username = data['username']
password = data['password']
# 检查用户是否已存在
if User.query.filter_by(username=username).first():
return jsonify({'error': '用户名已存在'}), 400
# 创建新用户
user = User(username=username)
user.set_password(password)
db.session.add(user)
db.session.commit()
return jsonify({
'message': '注册成功',
'user': user.to_dict()
}), 201
# 任务管理API # 任务管理API
@api.route('/tasks', methods=['GET']) @api.route('/tasks', methods=['GET'])
def get_tasks(): def get_tasks():

38
create_user.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
创建初始用户脚本
用于创建管理员账户
"""
from backend.app import create_app
from backend.models import db, User
def create_initial_user():
"""创建初始用户"""
app = create_app()
with app.app_context():
# 检查是否已有用户
existing_user = User.query.first()
if existing_user:
print(f"用户已存在: {existing_user.username}")
return
# 创建默认管理员用户
username = input("请输入用户名 (默认: admin): ").strip() or "admin"
password = input("请输入密码 (默认: admin123): ").strip() or "admin123"
user = User(username=username)
user.set_password(password)
db.session.add(user)
db.session.commit()
print(f"用户创建成功!")
print(f"用户名: {username}")
print(f"请妥善保管密码!")
if __name__ == '__main__':
create_initial_user()

View File

@@ -14,6 +14,9 @@ services:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
- SECRET_KEY=${SECRET_KEY:-your-secret-key-here} - SECRET_KEY=${SECRET_KEY:-your-secret-key-here}
- OPENAI_API_KEY=${OPENAI_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-}
# 默认用户配置
- DEFAULT_USERNAME=${DEFAULT_USERNAME:-admin}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-admin123}
volumes: volumes:
# 持久化数据库 # 持久化数据库
- ./data:/app/data - ./data:/app/data

44
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
set -e
echo "正在启动工作任务管理系统..."
# 切换到backend目录
cd /app/backend
# 等待数据库初始化
echo "初始化数据库..."
python -c "
from app import create_app
from models import db, User
import os
app = create_app()
with app.app_context():
# 创建所有表
db.create_all()
# 检查是否已有用户
existing_user = User.query.first()
if not existing_user:
# 从环境变量获取默认用户信息
default_username = os.getenv('DEFAULT_USERNAME', 'admin')
default_password = os.getenv('DEFAULT_PASSWORD', 'admin123')
# 创建默认用户
user = User(username=default_username)
user.set_password(default_password)
db.session.add(user)
db.session.commit()
print(f'已创建默认用户: {default_username}')
print(f'默认密码: {default_password}')
print('请登录后立即修改密码!')
else:
print('用户已存在,跳过初始化')
"
echo "启动Flask应用..."
# 启动应用
exec python app.py

View File

@@ -812,3 +812,98 @@ body {
from { transform: translateX(-100%); } from { transform: translateX(-100%); }
to { transform: translateX(0); } to { transform: translateX(0); }
} }
/* 登录页面样式 */
.login-page {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
width: 100%;
max-width: 450px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.5s ease;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header i {
font-size: 3rem;
color: #667eea;
margin-bottom: 15px;
display: block;
}
.login-header h1 {
color: #2d3748;
font-size: 1.8rem;
font-weight: 700;
margin: 0;
}
.login-form {
display: flex;
flex-direction: column;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: #4a5568;
font-weight: 600;
}
.login-form label i {
color: #667eea;
}
.btn-block {
width: 100%;
justify-content: center;
margin-top: 10px;
}
.error-message {
background: #fed7d7;
color: #c53030;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 15px;
font-size: 14px;
border: 1px solid #fc8181;
}
/* 用户信息显示 */
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: #4a5568;
font-weight: 600;
padding: 8px 16px;
background: #f7fafc;
border-radius: 8px;
border: 2px solid #e2e8f0;
}
.user-info i {
color: #667eea;
font-size: 1.2rem;
}

View File

@@ -8,11 +8,44 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head> </head>
<body> <body>
<div class="container"> <!-- 登录页面 -->
<div id="loginPage" class="login-page">
<div class="login-container">
<div class="login-header">
<i class="fas fa-tasks"></i>
<h1>工作任务管理系统</h1>
</div>
<form id="loginForm" class="login-form">
<div class="form-group">
<label for="loginUsername">
<i class="fas fa-user"></i> 用户名
</label>
<input type="text" id="loginUsername" name="username" required autofocus>
</div>
<div class="form-group">
<label for="loginPassword">
<i class="fas fa-lock"></i> 密码
</label>
<input type="password" id="loginPassword" name="password" required>
</div>
<div id="loginError" class="error-message" style="display: none;"></div>
<button type="submit" class="btn btn-primary btn-block">
<i class="fas fa-sign-in-alt"></i> 登录
</button>
</form>
</div>
</div>
<!-- 主应用页面 -->
<div id="mainApp" class="container" style="display: none;">
<!-- 头部 --> <!-- 头部 -->
<header class="header"> <header class="header">
<h1><i class="fas fa-tasks"></i> 工作任务管理系统</h1> <h1><i class="fas fa-tasks"></i> 工作任务管理系统</h1>
<div class="header-actions"> <div class="header-actions">
<span class="user-info">
<i class="fas fa-user-circle"></i>
<span id="currentUsername"></span>
</span>
<button id="addTaskBtn" class="btn btn-primary"> <button id="addTaskBtn" class="btn btn-primary">
<i class="fas fa-plus"></i> 添加任务 <i class="fas fa-plus"></i> 添加任务
</button> </button>
@@ -22,6 +55,9 @@
<button id="timeHistoryBtn" class="btn btn-secondary"> <button id="timeHistoryBtn" class="btn btn-secondary">
<i class="fas fa-history"></i> 时间历史 <i class="fas fa-history"></i> 时间历史
</button> </button>
<button id="logoutBtn" class="btn btn-outline">
<i class="fas fa-sign-out-alt"></i> 退出
</button>
</div> </div>
</header> </header>

View File

@@ -8,6 +8,7 @@ class WorkListAPI {
async request(endpoint, options = {}) { async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`; const url = `${this.baseURL}${endpoint}`;
const config = { const config = {
credentials: 'same-origin', // 包含cookies以支持session
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options.headers ...options.headers
@@ -17,7 +18,7 @@ class WorkListAPI {
try { try {
const response = await fetch(url, config); const response = await fetch(url, config);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP错误: ${response.status}`); throw new Error(errorData.error || `HTTP错误: ${response.status}`);
@@ -30,6 +31,31 @@ class WorkListAPI {
} }
} }
// 认证API
async login(username, password) {
return this.request('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
}
async logout() {
return this.request('/auth/logout', {
method: 'POST'
});
}
async checkAuth() {
return this.request('/auth/check');
}
async register(username, password) {
return this.request('/auth/register', {
method: 'POST',
body: JSON.stringify({ username, password })
});
}
// 任务管理API // 任务管理API
async getTasks() { async getTasks() {
return this.request('/tasks'); return this.request('/tasks');

View File

@@ -5,18 +5,111 @@ class WorkListApp {
this.currentEditingTask = null; this.currentEditingTask = null;
this.currentFilter = 'all'; this.currentFilter = 'all';
this.excludedStatuses = new Set(); this.excludedStatuses = new Set();
this.currentUser = null;
this.init(); this.init();
} }
// 初始化应用 // 初始化应用
init() { async init() {
this.bindEvents(); // 先检查登录状态
this.loadTasks(); await this.checkAuthStatus();
this.setupDatePicker();
// 如果已登录,绑定事件并加载任务
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() { 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', () => { document.getElementById('addTaskBtn').addEventListener('click', () => {
this.showTaskModal(); this.showTaskModal();