44 KiB
用户管理与班级管理实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 扩展钢琴练习方案系统,增加用户管理、角色权限管理、班级管理功能
Architecture: 在现有 User 模型上增加 role 字段,新建 Class 表,Student 表增加 class_id 外键。前端动态渲染导航栏,后端通过装饰器实现权限控制。
Tech Stack: Flask + SQLAlchemy + SQLite + Bootstrap5
文件修改概览
| 操作 | 文件 | 说明 |
|---|---|---|
| 修改 | app/models.py |
User 增加 role 字段,新建 Class 模型,Student 增加 class_id |
| 修改 | app/routes/auth.py |
增加权限装饰器,修改密码API |
| 新建 | app/routes/classes.py |
班级管理 API |
| 新建 | app/routes/users.py |
用户管理 API |
| 修改 | app/templates/index.html |
导航栏动态渲染,学员列表增加班级列 |
| 新建 | app/templates/users.html |
用户管理页面 |
| 新建 | app/templates/classes.html |
班级管理页面 |
| 修改 | app/templates/login.html |
登录后返回 role 信息 |
Task 1: 数据库模型扩展
Files:
-
Modify:
app/models.py:1-180 -
Step 1: 修改 User 模型,添加 role 字段
在 app/models.py 中找到 class User 定义,在 created_at 字段后添加 role 字段:
class User(db.Model):
"""管理员用户表"""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(200), nullable=False)
role = db.Column(db.String(20), default="user") # admin / user
created_at = db.Column(db.DateTime, default=datetime.now)
- Step 2: 在 User.to_dict() 中添加 role 字段
def to_dict(self):
return {
"id": self.id,
"username": self.username,
"role": self.role,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
}
- Step 3: 新建 Class 模型
在 User 类之后、Student 类之前添加:
class Class(db.Model):
"""班级表"""
__tablename__ = "classes"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.now)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"student_count": self.students.count(),
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
}
- Step 4: 修改 Student 模型,添加 class_id 字段
在 Student 类中找到 notes 字段后添加:
class_id = db.Column(db.Integer, db.ForeignKey("classes.id")) # 班级
- Step 5: 更新 Student.to_dict() 添加班级信息
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"phone": self.phone,
"wechat_nickname": self.wechat_nickname,
"practice_time": self.practice_time,
"notes": self.notes,
"class_id": self.class_id,
"class_name": self.class_obj.name if self.class_obj else None,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
"problem_count": self.problems.count(),
"plan_count": self.plans.count(),
}
- Step 6: 添加 ROLE_OPTIONS 常量
在文件末尾添加:
ROLE_OPTIONS = ["admin", "user"]
Task 2: 权限装饰器
Files:
-
Modify:
app/routes/auth.py:94-105 -
Step 1: 修改登录状态检查,返回 role 信息
修改 check_login 函数:
@main_bp.route("/api/check-login", methods=["GET"])
def check_login():
"""检查登录状态"""
if session.get("user_id"):
user = User.query.get(session.get("user_id"))
return jsonify({
"logged_in": True,
"username": session.get("username"),
"role": user.role if user else "user"
})
return jsonify({"logged_in": False})
- Step 2: 添加 admin_required 装饰器
在文件末尾添加:
def admin_required(f):
"""管理员权限装饰器"""
from functools import wraps
@wraps(f)
def decorated(*args, **kwargs):
if not session.get("user_id"):
return jsonify({"error": "请先登录"}), 401
user = User.query.get(session.get("user_id"))
if not user or user.role != "admin":
return jsonify({"error": "权限不足"}), 403
return f(*args, **kwargs)
return decorated
def login_required_json(f):
"""登录验证装饰器(返回JSON)"""
from functools import wraps
@wraps(f)
def decorated(*args, **kwargs):
if not session.get("user_id"):
return jsonify({"error": "请先登录"}), 401
return f(*args, **kwargs)
return decorated
- Step 3: 添加获取当前用户角色的函数
在文件末尾添加:
def get_current_user():
"""获取当前登录用户"""
user_id = session.get("user_id")
if user_id:
return User.query.get(user_id)
return None
def is_admin():
"""判断当前用户是否是管理员"""
user = get_current_user()
return user and user.role == "admin"
Task 3: 用户管理 API
Files:
-
Create:
app/routes/users.py -
Modify:
app/routes/__init__.py -
Step 1: 创建 users.py
新建文件 app/routes/users.py:
# 用户管理路由
from flask import request, jsonify, render_template, session
from app.routes import main_bp
from app.models import db, User, login_required_json, admin_required
@main_bp.route("/users")
@admin_required
def users_page():
"""用户管理页面"""
return render_template("users.html")
@main_bp.route("/api/users", methods=["GET"])
@admin_required
def api_users_list():
"""用户列表"""
users = User.query.order_by(User.created_at.desc()).all()
return jsonify([u.to_dict() for u in users])
@main_bp.route("/api/users", methods=["POST"])
@admin_required
def api_users_create():
"""新增用户"""
data = request.get_json()
username = data.get("username", "").strip()
password = data.get("password", "")
role = data.get("role", "user")
if not username or not password:
return jsonify({"error": "请输入用户名和密码"}), 400
if User.query.filter_by(username=username).first():
return jsonify({"error": "用户名已存在"}), 400
if role not in ["admin", "user"]:
return jsonify({"error": "无效的角色"}), 400
try:
user = User(username=username, role=role)
user.set_password(password)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict())
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
db.session.rollback()
return jsonify({"error": "创建失败: " + str(e)}), 500
@main_bp.route("/api/users/<int:user_id>", methods=["PUT"])
@admin_required
def api_users_update(user_id):
"""编辑用户(仅管理员可改角色)"""
user = User.query.get_or_404(user_id)
data = request.get_json()
if "role" in data:
if data["role"] in ["admin", "user"]:
user.role = data["role"]
try:
db.session.commit()
return jsonify(user.to_dict())
except Exception as e:
db.session.rollback()
return jsonify({"error": "更新失败: " + str(e)}), 500
@main_bp.route("/api/users/<int:user_id>", methods=["DELETE"])
@admin_required
def api_users_delete(user_id):
"""删除用户"""
if user_id == session.get("user_id"):
return jsonify({"error": "不能删除自己"}), 400
user = User.query.get_or_404(user_id)
try:
db.session.delete(user)
db.session.commit()
return jsonify({"message": "删除成功"})
except Exception as e:
db.session.rollback()
return jsonify({"error": "删除失败: " + str(e)}), 500
@main_bp.route("/api/users/<int:user_id>/reset-password", methods=["POST"])
@admin_required
def api_users_reset_password(user_id):
"""重置用户密码(管理员无需知道原密码)"""
user = User.query.get_or_404(user_id)
data = request.get_json()
new_password = data.get("new_password", "")
if not new_password:
return jsonify({"error": "请输入新密码"}), 400
try:
user.set_password(new_password)
db.session.commit()
return jsonify({"message": "密码重置成功"})
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
db.session.rollback()
return jsonify({"error": "重置失败: " + str(e)}), 500
@main_bp.route("/api/users/change-password", methods=["POST"])
@login_required_json
def api_users_change_password():
"""修改当前用户密码"""
user = User.query.get(session.get("user_id"))
data = request.get_json()
old_password = data.get("old_password", "")
new_password = data.get("new_password", "")
confirm_password = data.get("confirm_password", "")
if not old_password or not new_password:
return jsonify({"error": "请填写完整"}), 400
if not user.check_password(old_password):
return jsonify({"error": "原密码错误"}), 400
if new_password != confirm_password:
return jsonify({"error": "两次密码输入不一致"}), 400
try:
user.set_password(new_password)
db.session.commit()
return jsonify({"message": "密码修改成功"})
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
db.session.rollback()
return jsonify({"error": "修改失败: " + str(e)}), 500
- Step 2: 注册蓝图
修改 app/routes/__init__.py,确保 users 蓝图已注册:
from flask import Blueprint
main_bp = Blueprint("main", __name__)
# 导入所有路由
from app.routes import students, problems, plans, settings, auth, classes, users
Task 4: 班级管理 API
Files:
-
Create:
app/routes/classes.py -
Step 1: 创建 classes.py
新建文件 app/routes/classes.py:
# 班级管理路由
from flask import request, jsonify, render_template, session
from app.routes import main_bp
from app.models import db, Class, Student, User, login_required_json, admin_required
@main_bp.route("/classes")
@login_required_json
def classes_page():
"""班级管理页面"""
return render_template("classes.html")
@main_bp.route("/api/classes", methods=["GET"])
@login_required_json
def api_classes_list():
"""班级列表"""
classes = Class.query.order_by(Class.created_at.desc()).all()
return jsonify([c.to_dict() for c in classes])
@main_bp.route("/api/classes", methods=["POST"])
@admin_required
def api_classes_create():
"""新增班级"""
data = request.get_json()
name = data.get("name", "").strip()
description = data.get("description", "")
if not name:
return jsonify({"error": "请输入班级名称"}), 400
if Class.query.filter_by(name=name).first():
return jsonify({"error": "班级名称已存在"}), 400
try:
cls = Class(name=name, description=description)
db.session.add(cls)
db.session.commit()
return jsonify(cls.to_dict())
except Exception as e:
db.session.rollback()
return jsonify({"error": "创建失败: " + str(e)}), 500
@main_bp.route("/api/classes/<int:class_id>", methods=["PUT"])
@admin_required
def api_classes_update(class_id):
"""编辑班级"""
cls = Class.query.get_or_404(class_id)
data = request.get_json()
if "name" in data and data["name"]:
# 检查名称是否与其他班级冲突
existing = Class.query.filter_by(name=data["name"]).first()
if existing and existing.id != class_id:
return jsonify({"error": "班级名称已存在"}), 400
cls.name = data["name"]
if "description" in data:
cls.description = data["description"]
try:
db.session.commit()
return jsonify(cls.to_dict())
except Exception as e:
db.session.rollback()
return jsonify({"error": "更新失败: " + str(e)}), 500
@main_bp.route("/api/classes/<int:class_id>", methods=["DELETE"])
@admin_required
def api_classes_delete(class_id):
"""删除班级(不删除学员,仅解除关联)"""
cls = Class.query.get_or_404(class_id)
try:
# 解除学员与班级的关联
Student.query.filter_by(class_id=class_id).update({"class_id": None})
db.session.delete(cls)
db.session.commit()
return jsonify({"message": "删除成功"})
except Exception as e:
db.session.rollback()
return jsonify({"error": "删除失败: " + str(e)}), 500
@main_bp.route("/api/classes/<int:class_id>/students", methods=["GET"])
@login_required_json
def api_classes_students(class_id):
"""班级学员列表"""
cls = Class.query.get_or_404(class_id)
students = cls.students.all()
return jsonify([{
"id": s.id,
"name": s.name,
"phone": s.phone,
"practice_time": s.practice_time
} for s in students])
@main_bp.route("/api/classes/<int:class_id>/assign", methods=["POST"])
@login_required_json
def api_classes_assign(class_id):
"""分配学员到班级"""
cls = Class.query.get_or_404(class_id)
data = request.get_json()
student_ids = data.get("student_ids", [])
try:
# 先清除这些学员的班级关联
Student.query.filter(Student.id.in_(student_ids)).update({"class_id": None})
# 再设置新的班级关联
Student.query.filter(Student.id.in_(student_ids)).update({"class_id": class_id})
db.session.commit()
return jsonify({"message": "分配成功"})
except Exception as e:
db.session.rollback()
return jsonify({"error": "分配失败: " + str(e)}), 500
- Step 2: 导出 Class 模型
在 app/models.py 文件末尾确保导出 Class:
__all__ = ["db", "User", "Student", "StudentProblem", "PracticePlan", "Class"]
Task 5: 修改现有路由添加权限控制
Files:
-
Modify:
app/routes/students.py -
Modify:
app/routes/problems.py -
Modify:
app/routes/plans.py -
Modify:
app/routes/settings.py -
Step 1: 修改 students.py 添加登录验证
在 app/routes/students.py 顶部导入装饰器,并在每个 API 添加装饰器:
from app.models import db, Student, StudentProblem, login_required_json
为以下路由添加 @login_required_json:
-
api_students_list -
api_students_create -
api_students_detail -
api_students_update -
api_students_delete -
Step 2: 修改 problems.py 添加登录验证
from app.models import db, StudentProblem, login_required_json
为以下路由添加 @login_required_json:
-
api_problems_list -
api_problems_add -
api_problems_delete -
Step 3: 修改 plans.py 添加登录验证
from app.models import db, PracticePlan, login_required_json
为以下路由添加 @login_required_json:
-
api_generate_plan -
api_plans_detail -
api_plans_delete -
Step 4: 修改 settings.py 添加管理员权限
from app.models import db, admin_required
将以下路由的装饰器从 @login_required 改为 @admin_required:
settings_pageapi_problems_list(创建/编辑/删除需要管理员)api_problems_createapi_problems_updateapi_problems_deleteapi_config_getapi_config_setapi_config_test
Task 6: 前端 - 用户管理页面
Files:
-
Create:
app/templates/users.html -
Step 1: 创建用户管理页面
新建 app/templates/users.html:
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">钢琴练习方案系统</a>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="currentUser"></span>
<a class="nav-link" href="#" id="logoutBtn">退出</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>用户管理</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal">
<i class="bi bi-plus-circle"></i> 新增用户
</button>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover" id="usersTable">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!-- 新增/编辑用户弹窗 -->
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalTitle">新增用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="userForm">
<input type="hidden" id="userId">
<div class="mb-3">
<label class="form-label">用户名</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label class="form-label" id="passwordLabel">初始密码</label>
<input type="password" class="form-control" id="password">
<div class="form-text">8位以上,包含大小写字母、数字和特殊字符</div>
</div>
<div class="mb-3">
<label class="form-label">角色</label>
<select class="form-select" id="role">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveUserBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- 重置密码弹窗 -->
<div class="modal fade" id="resetPwdModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">重置密码</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="resetPwdForm">
<input type="hidden" id="resetUserId">
<div class="mb-3">
<label class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" required>
<div class="form-text">8位以上,包含大小写字母、数字和特殊字符</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="confirmResetBtn">重置密码</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const ROLE_LABELS = { 'admin': '管理员', 'user': '普通用户' };
// 检查登录状态
fetch('/api/check-login').then(r => r.json()).then(data => {
if (!data.logged_in || data.role !== 'admin') {
window.location.href = '/login';
return;
}
document.getElementById('currentUser').textContent = data.username + ' (' + ROLE_LABELS[data.role] + ')';
loadUsers();
});
// 加载用户列表
function loadUsers() {
fetch('/api/users').then(r => r.json()).then(users => {
const tbody = document.querySelector('#usersTable tbody');
tbody.innerHTML = users.map(u => `
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td><span class="badge ${u.role === 'admin' ? 'bg-danger' : 'bg-secondary'}">${ROLE_LABELS[u.role]}</span></td>
<td>${u.created_at}</td>
<td>
<button class="btn btn-sm btn-warning me-1" onclick="openResetPwd(${u.id})">重置密码</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser(${u.id})">删除</button>
</td>
</tr>
`).join('');
});
}
// 保存用户
document.getElementById('saveUserBtn').onclick = () => {
const id = document.getElementById('userId').value;
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const role = document.getElementById('role').value;
if (!username || !password) {
alert('请填写完整');
return;
}
const method = id ? 'PUT' : 'POST';
fetch('/api/users' + (id ? '/' + id : ''), {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, role })
}).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
bootstrap.Modal.getInstance(document.getElementById('userModal')).hide();
loadUsers();
}
});
};
// 重置密码
function openResetPwd(id) {
document.getElementById('resetUserId').value = id;
document.getElementById('newPassword').value = '';
new bootstrap.Modal(document.getElementById('resetPwdModal')).show();
}
document.getElementById('confirmResetBtn').onclick = () => {
const id = document.getElementById('resetUserId').value;
const newPassword = document.getElementById('newPassword').value;
if (!newPassword) {
alert('请输入新密码');
return;
}
fetch('/api/users/' + id + '/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ new_password: newPassword })
}).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
alert('密码重置成功');
bootstrap.Modal.getInstance(document.getElementById('resetPwdModal')).hide();
}
});
};
// 删除用户
function deleteUser(id) {
if (!confirm('确定要删除该用户吗?')) return;
fetch('/api/users/' + id, { method: 'DELETE' }).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
loadUsers();
}
});
}
// 退出登录
document.getElementById('logoutBtn').onclick = () => {
fetch('/api/logout', { method: 'POST' }).then(() => window.location.href = '/login');
};
</script>
</body>
</html>
Task 7: 前端 - 班级管理页面
Files:
-
Create:
app/templates/classes.html -
Step 1: 创建班级管理页面
新建 app/templates/classes.html:
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">钢琴练习方案系统</a>
<div class="navbar-nav ms-auto">
<span class="nav-item nav-link text-light" id="currentUser"></span>
<a class="nav-link" href="#" id="logoutBtn">退出</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>班级管理</h2>
<button class="btn btn-primary" id="addClassBtn" style="display:none;">
<i class="bi bi-plus-circle"></i> 新增班级
</button>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover" id="classesTable">
<thead>
<tr>
<th>ID</th>
<th>班级名称</th>
<th>描述</th>
<th>学员数</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!-- 新增/编辑班级弹窗 -->
<div class="modal fade" id="classModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="classModalTitle">新增班级</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="classForm">
<input type="hidden" id="classId">
<div class="mb-3">
<label class="form-label">班级名称</label>
<input type="text" class="form-control" id="className" required>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea class="form-control" id="classDesc" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveClassBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- 班级学员弹窗 -->
<div class="modal fade" id="classStudentsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">班级学员</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<button class="btn btn-sm btn-success" id="assignStudentsBtn">分配学员</button>
</div>
<table class="table" id="classStudentsTable">
<thead>
<tr>
<th>姓名</th>
<th>手机</th>
<th>练习时间</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 分配学员弹窗 -->
<div class="modal fade" id="assignModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">分配学员到班级</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="studentCheckboxes"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmAssignBtn">确认分配</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentUserRole = 'user';
let currentClassId = null;
let allStudents = [];
// 检查登录状态
fetch('/api/check-login').then(r => r.json()).then(data => {
if (!data.logged_in) {
window.location.href = '/login';
return;
}
currentUserRole = data.role;
const ROLE_LABELS = { 'admin': '管理员', 'user': '普通用户' };
document.getElementById('currentUser').textContent = data.username + ' (' + ROLE_LABELS[data.role] + ')';
if (data.role === 'admin') {
document.getElementById('addClassBtn').style.display = 'inline-block';
}
loadClasses();
});
// 加载班级列表
function loadClasses() {
fetch('/api/classes').then(r => r.json()).then(classes => {
const tbody = document.querySelector('#classesTable tbody');
const isAdmin = currentUserRole === 'admin';
tbody.innerHTML = classes.map(c => `
<tr>
<td>${c.id}</td>
<td>${c.name}</td>
<td>${c.description || '-'}</td>
<td><a href="#" onclick="viewClassStudents(${c.id})">${c.student_count}</a></td>
<td>${c.created_at}</td>
<td>
${isAdmin ? `<button class="btn btn-sm btn-primary me-1" onclick="editClass(${c.id}, '${c.name}', '${c.description || ''}')">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteClass(${c.id})">删除</button>` : ''}
</td>
</tr>
`).join('');
});
}
// 保存班级
document.getElementById('saveClassBtn').onclick = () => {
const id = document.getElementById('classId').value;
const name = document.getElementById('className').value.trim();
const description = document.getElementById('classDesc').value;
if (!name) {
alert('请输入班级名称');
return;
}
const method = id ? 'PUT' : 'POST';
fetch('/api/classes' + (id ? '/' + id : ''), {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
}).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
bootstrap.Modal.getInstance(document.getElementById('classModal')).hide();
loadClasses();
}
});
};
// 编辑班级
function editClass(id, name, desc) {
document.getElementById('classId').value = id;
document.getElementById('className').value = name;
document.getElementById('classDesc').value = desc;
document.getElementById('classModalTitle').textContent = '编辑班级';
new bootstrap.Modal(document.getElementById('classModal')).show();
}
// 删除班级
function deleteClass(id) {
if (!confirm('确定要删除该班级吗?学员不会被删除。')) return;
fetch('/api/classes/' + id, { method: 'DELETE' }).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
loadClasses();
}
});
}
// 查看班级学员
function viewClassStudents(classId) {
currentClassId = classId;
fetch('/api/classes/' + classId + '/students').then(r => r.json()).then(students => {
const tbody = document.querySelector('#classStudentsTable tbody');
tbody.innerHTML = students.length ? students.map(s => `
<tr><td>${s.name}</td><td>${s.phone || '-'}</td><td>${s.practice_time}</td></tr>
`).join('') : '<tr><td colspan="3" class="text-center">暂无学员</td></tr>';
new bootstrap.Modal(document.getElementById('classStudentsModal')).show();
});
}
// 分配学员
document.getElementById('assignStudentsBtn').onclick = () => {
fetch('/api/students').then(r => r.json()).then(students => {
allStudents = students;
const container = document.getElementById('studentCheckboxes');
container.innerHTML = students.map(s => `
<div class="form-check">
<input class="form-check-input" type="checkbox" value="${s.id}" ${s.class_id == currentClassId ? 'checked' : ''}>
<label class="form-check-label">${s.name} (${s.practice_time})</label>
</div>
`).join('');
new bootstrap.Modal(document.getElementById('assignModal')).show();
});
};
document.getElementById('confirmAssignBtn').onclick = () => {
const checked = Array.from(document.querySelectorAll('#studentCheckboxes input:checked')).map(c => parseInt(c.value));
fetch('/api/classes/' + currentClassId + '/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ student_ids: checked })
}).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
bootstrap.Modal.getInstance(document.getElementById('assignModal')).hide();
loadClasses();
viewClassStudents(currentClassId);
}
});
};
// 新增班级按钮
document.getElementById('addClassBtn').onclick = () => {
document.getElementById('classId').value = '';
document.getElementById('className').value = '';
document.getElementById('classDesc').value = '';
document.getElementById('classModalTitle').textContent = '新增班级';
new bootstrap.Modal(document.getElementById('classModal')).show();
};
// 退出登录
document.getElementById('logoutBtn').onclick = () => {
fetch('/api/logout', { method: 'POST' }).then(() => window.location.href = '/login');
};
</script>
</body>
</html>
Task 8: 前端 - 导航栏动态渲染
Files:
-
Modify:
app/templates/index.html -
Step 1: 修改导航栏,动态显示菜单
找到 <nav class="navbar"> 部分,修改为:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">钢琴练习方案系统</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">学员管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">问题配置</a>
</li>
<li class="nav-item" id="classesNav" style="display:none;">
<a class="nav-link" href="/classes">班级管理</a>
</li>
<li class="nav-item" id="usersNav" style="display:none;">
<a class="nav-link" href="/users">用户管理</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<span id="currentUserName"></span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#changePwdModal">修改密码</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" id="logoutBtn">退出</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
- Step 2: 添加修改密码弹窗
在 </nav> 后添加:
<!-- 修改密码弹窗 -->
<div class="modal fade" id="changePwdModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">修改密码</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="changePwdForm">
<div class="mb-3">
<label class="form-label">原密码</label>
<input type="password" class="form-control" id="oldPassword" required>
</div>
<div class="mb-3">
<label class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" required>
<div class="form-text">8位以上,包含大小写字母、数字和特殊字符</div>
</div>
<div class="mb-3">
<label class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirmPassword" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="changePwdBtn">确认修改</button>
</div>
</div>
</div>
</div>
- Step 3: 修改 JavaScript,动态控制菜单显示
修改 <script> 部分:
// 检查登录状态并初始化
fetch('/api/check-login').then(r => r.json()).then(data => {
if (!data.logged_in) {
window.location.href = '/login';
return;
}
const ROLE_LABELS = { 'admin': '管理员', 'user': '普通用户' };
document.getElementById('currentUserName').textContent = data.username + ' (' + ROLE_LABELS[data.role] + ')';
// 根据角色显示菜单
if (data.role === 'admin') {
document.getElementById('usersNav').style.display = '';
}
document.getElementById('classesNav').style.display = '';
// 加载数据
loadStudents();
});
- Step 4: 添加修改密码功能
在 <script> 中添加:
// 修改密码
document.getElementById('changePwdBtn').onclick = () => {
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (!oldPassword || !newPassword || !confirmPassword) {
alert('请填写完整');
return;
}
fetch('/api/users/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword, confirm_password: confirmPassword })
}).then(r => r.json()).then(data => {
if (data.error) {
alert(data.error);
} else {
alert('密码修改成功,请重新登录');
fetch('/api/logout', { method: 'POST' }).then(() => window.location.href = '/login');
}
});
};
// 退出登录
document.getElementById('logoutBtn').onclick = (e) => {
e.preventDefault();
fetch('/api/logout', { method: 'POST' }).then(() => window.location.href = '/login');
};
- Step 5: 学员列表增加班级列
在学员表格的 <thead> 中添加:
<th>班级</th>
在 <tbody> 渲染时添加:
// 在 renderStudents 函数中
`<td>${s.class_name || '-'}</td>`
Task 9: 测试与验证
Files:
-
Test: 启动应用测试所有功能
-
Step 1: 测试登录和初始设置
- 启动应用
python run.py - 访问
/setup创建管理员 - 登录验证
- Step 2: 测试用户管理
- 访问
/users(仅管理员可见) - 新增普通用户
- 重置用户密码
- 删除用户
- Step 3: 测试班级管理
- 访问
/classes - 管理员:新增/编辑/删除班级
- 分配学员到班级
- Step 4: 测试权限控制
- 用普通用户登录
- 验证无法访问
/users - 验证无法访问
/settings - 验证可以访问
/classes但无法增删
- Step 5: 测试修改密码
- 管理员修改自己密码
- 普通用户修改自己密码