This commit is contained in:
superlishunqin 2025-05-01 04:52:53 +08:00
parent 0c1d1b0d19
commit 29009ef7de
23 changed files with 8207 additions and 1278 deletions

View File

@ -1,12 +1,16 @@
from flask import Flask, render_template, session, g, Markup
from flask_login import LoginManager
from app.models.user import db, User
from app.controllers.user import user_bp
from app.controllers.book import book_bp
from app.controllers.borrow import borrow_bp
from flask_login import LoginManager, current_user
import os
login_manager = LoginManager()
def create_app():
def create_app(config=None):
app = Flask(__name__)
# 配置应用
@ -32,6 +36,14 @@ def create_app():
# 初始化数据库
db.init_app(app)
# 初始化 Flask-Login
login_manager.init_app(app)
login_manager.login_view = 'user.login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# 注册蓝图
app.register_blueprint(user_bp, url_prefix='/user')
app.register_blueprint(book_bp, url_prefix='/book')
@ -105,9 +117,9 @@ def create_app():
@app.route('/')
def index():
if not g.user:
if not current_user.is_authenticated:
return render_template('login.html')
return render_template('index.html', current_user=g.user)
return render_template('index.html') # 无需传递current_userFlask-Login自动提供
@app.errorhandler(404)
def page_not_found(e):

View File

@ -19,7 +19,8 @@ def book_list():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = Book.query
# 只显示状态为1的图书未下架的图书
query = Book.query.filter_by(status=1)
# 搜索功能
search = request.args.get('search', '')

View File

@ -6,6 +6,9 @@ import logging
from functools import wraps
import time
from datetime import datetime, timedelta
from app.services.user_service import UserService
from flask_login import login_user, logout_user, current_user, login_required
from app.models.user import User
# 创建蓝图
user_bp = Blueprint('user', __name__)
@ -46,11 +49,13 @@ class VerificationStore:
verification_codes = VerificationStore()
def login_required(f):
# 添加管理员权限检查装饰器
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('user.login'))
if not current_user.is_authenticated or current_user.role_id != 1:
flash('您没有管理员权限', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
@ -58,7 +63,10 @@ def login_required(f):
@user_bp.route('/login', methods=['GET', 'POST'])
def login():
# 保持原代码不变
# 如果用户已经登录,直接重定向到首页
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
@ -76,26 +84,30 @@ def login():
if user.status == 0:
return render_template('login.html', error='账号已被禁用,请联系管理员')
# 登录成功,保存用户信息到会话
session['user_id'] = user.id
# 使用 Flask-Login 的 login_user 函数
login_user(user, remember=remember_me)
# 这些session信息仍然可以保留但不再用于认证
session['username'] = user.username
session['role_id'] = user.role_id
if remember_me:
# 设置会话过期时间为7天
session.permanent = True
# 获取登录后要跳转的页面
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('index')
# 记录登录日志(可选)
# log_user_action('用户登录')
# 重定向到首页
return redirect(url_for('index'))
# 重定向到首页或其他请求的页面
return redirect(next_page)
return render_template('login.html')
@user_bp.route('/register', methods=['GET', 'POST'])
def register():
# 如果用户已登录,重定向到首页
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
@ -147,11 +159,9 @@ def register():
@user_bp.route('/logout')
@login_required
def logout():
# 清除会话数据
session.pop('user_id', None)
session.pop('username', None)
session.pop('role_id', None)
logout_user()
return redirect(url_for('user.login'))
@ -179,3 +189,194 @@ def send_verification_code():
return jsonify({'success': True, 'message': '验证码已发送'})
else:
return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'})
# 用户管理列表
@user_bp.route('/manage')
@login_required
@admin_required
def user_list():
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
status = request.args.get('status', type=int)
role_id = request.args.get('role_id', type=int)
pagination = UserService.get_users(
page=page,
per_page=10,
search_query=search,
status=status,
role_id=role_id
)
roles = UserService.get_all_roles()
return render_template(
'user/list.html',
pagination=pagination,
search=search,
status=status,
role_id=role_id,
roles=roles
)
# 用户详情/编辑页面
@user_bp.route('/edit/<int:user_id>', methods=['GET', 'POST'])
@login_required
@admin_required
def user_edit(user_id):
user = UserService.get_user_by_id(user_id)
if not user:
flash('用户不存在', 'error')
return redirect(url_for('user.user_list'))
roles = UserService.get_all_roles()
if request.method == 'POST':
data = {
'email': request.form.get('email'),
'phone': request.form.get('phone'),
'nickname': request.form.get('nickname'),
'role_id': int(request.form.get('role_id')),
'status': int(request.form.get('status')),
}
password = request.form.get('password')
if password:
data['password'] = password
success, message = UserService.update_user(user_id, data)
if success:
flash(message, 'success')
return redirect(url_for('user.user_list'))
else:
flash(message, 'error')
return render_template('user/edit.html', user=user, roles=roles)
# 用户状态管理API
@user_bp.route('/status/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def user_status(user_id):
data = request.get_json()
status = data.get('status')
if status is None or status not in [0, 1]:
return jsonify({'success': False, 'message': '无效的状态值'})
# 不能修改自己的状态
if user_id == current_user.id:
return jsonify({'success': False, 'message': '不能修改自己的状态'})
success, message = UserService.change_user_status(user_id, status)
return jsonify({'success': success, 'message': message})
# 用户删除API
@user_bp.route('/delete/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def user_delete(user_id):
# 不能删除自己
if user_id == current_user.id:
return jsonify({'success': False, 'message': '不能删除自己的账号'})
success, message = UserService.delete_user(user_id)
return jsonify({'success': success, 'message': message})
# 个人中心页面
@user_bp.route('/profile', methods=['GET', 'POST'])
@login_required
def user_profile():
user = current_user
if request.method == 'POST':
data = {
'email': request.form.get('email'),
'phone': request.form.get('phone'),
'nickname': request.form.get('nickname')
}
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
# 如果用户想要修改密码
if current_password and new_password:
if not user.check_password(current_password):
flash('当前密码不正确', 'error')
return render_template('user/profile.html', user=user)
if new_password != confirm_password:
flash('两次输入的新密码不匹配', 'error')
return render_template('user/profile.html', user=user)
data['password'] = new_password
success, message = UserService.update_user(user.id, data)
if success:
flash(message, 'success')
else:
flash(message, 'error')
return render_template('user/profile.html', user=user)
# 角色管理页面
@user_bp.route('/roles', methods=['GET'])
@login_required
@admin_required
def role_list():
roles = UserService.get_all_roles()
return render_template('user/roles.html', roles=roles)
# 创建/编辑角色API
@user_bp.route('/role/save', methods=['POST'])
@login_required
@admin_required
def role_save():
data = request.get_json()
role_id = data.get('id')
role_name = data.get('role_name')
description = data.get('description')
if not role_name:
return jsonify({'success': False, 'message': '角色名不能为空'})
if role_id: # 更新
success, message = UserService.update_role(role_id, role_name, description)
else: # 创建
success, message = UserService.create_role(role_name, description)
return jsonify({'success': success, 'message': message})
"""
@user_bp.route('/api/role/<int:role_id>/user-count')
@login_required
@admin_required
def get_role_user_count(role_id):
count = User.query.filter_by(role_id=role_id).count()
return jsonify({'count': count})
"""
@user_bp.route('/user/role/<int:role_id>/count', methods=['GET'])
@login_required
@admin_required
def get_role_user_count(role_id):
"""获取指定角色的用户数量"""
try:
count = User.query.filter_by(role_id=role_id).count()
return jsonify({
'success': True,
'count': count
})
except Exception as e:
return jsonify({
'success': False,
'message': f"查询失败: {str(e)}",
'count': 0
}), 500

View File

@ -1,13 +1,13 @@
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from flask_login import UserMixin
db = SQLAlchemy()
class User(db.Model):
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
@ -27,6 +27,9 @@ class User(db.Model):
self.nickname = nickname
self.role_id = role_id
def is_active(self):
return self.status == 1
def set_password(self, password):
"""设置密码,使用哈希加密"""
self.password = generate_password_hash(password)

View File

@ -0,0 +1,163 @@
# app/services/user_service.py
from app.models.user import User, Role, db
from sqlalchemy import or_
from datetime import datetime
class UserService:
@staticmethod
def get_users(page=1, per_page=10, search_query=None, status=None, role_id=None):
"""
获取用户列表支持分页搜索和过滤
"""
query = User.query
# 搜索条件
if search_query:
query = query.filter(or_(
User.username.like(f'%{search_query}%'),
User.email.like(f'%{search_query}%'),
User.nickname.like(f'%{search_query}%'),
User.phone.like(f'%{search_query}%')
))
# 状态过滤
if status is not None:
query = query.filter(User.status == status)
# 角色过滤
if role_id is not None:
query = query.filter(User.role_id == role_id)
# 分页
pagination = query.order_by(User.id.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return pagination
@staticmethod
def get_user_by_id(user_id):
"""通过ID获取用户"""
return User.query.get(user_id)
@staticmethod
def update_user(user_id, data):
"""更新用户信息"""
user = User.query.get(user_id)
if not user:
return False, "用户不存在"
try:
# 更新可编辑字段
if 'email' in data:
# 检查邮箱是否已被其他用户使用
existing = User.query.filter(User.email == data['email'], User.id != user_id).first()
if existing:
return False, "邮箱已被使用"
user.email = data['email']
if 'phone' in data:
# 检查手机号是否已被其他用户使用
existing = User.query.filter(User.phone == data['phone'], User.id != user_id).first()
if existing:
return False, "手机号已被使用"
user.phone = data['phone']
if 'nickname' in data and data['nickname']:
user.nickname = data['nickname']
# 只有管理员可以修改这些字段
if 'role_id' in data:
user.role_id = data['role_id']
if 'status' in data:
user.status = data['status']
if 'password' in data and data['password']:
user.set_password(data['password'])
user.updated_at = datetime.now()
db.session.commit()
return True, "用户信息更新成功"
except Exception as e:
db.session.rollback()
return False, f"更新失败: {str(e)}"
@staticmethod
def change_user_status(user_id, status):
"""变更用户状态 (启用/禁用)"""
user = User.query.get(user_id)
if not user:
return False, "用户不存在"
try:
user.status = status
user.updated_at = datetime.now()
db.session.commit()
status_text = "启用" if status == 1 else "禁用"
return True, f"用户已{status_text}"
except Exception as e:
db.session.rollback()
return False, f"状态变更失败: {str(e)}"
@staticmethod
def delete_user(user_id):
"""删除用户 (软删除,将状态设为-1)"""
user = User.query.get(user_id)
if not user:
return False, "用户不存在"
try:
user.status = -1 # 软删除,设置状态为-1
user.updated_at = datetime.now()
db.session.commit()
return True, "用户已删除"
except Exception as e:
db.session.rollback()
return False, f"删除失败: {str(e)}"
@staticmethod
def get_all_roles():
"""获取所有角色"""
return Role.query.all()
@staticmethod
def create_role(role_name, description=None):
"""创建新角色"""
existing = Role.query.filter_by(role_name=role_name).first()
if existing:
return False, "角色名已存在"
try:
role = Role(role_name=role_name, description=description)
db.session.add(role)
db.session.commit()
return True, "角色创建成功"
except Exception as e:
db.session.rollback()
return False, f"创建失败: {str(e)}"
@staticmethod
def update_role(role_id, role_name, description=None):
"""更新角色信息"""
role = Role.query.get(role_id)
if not role:
return False, "角色不存在"
# 检查角色名是否已被使用
existing = Role.query.filter(Role.role_name == role_name, Role.id != role_id).first()
if existing:
return False, "角色名已存在"
try:
role.role_name = role_name
if description is not None:
role.description = description
db.session.commit()
return True, "角色更新成功"
except Exception as e:
db.session.rollback()
return False, f"更新失败: {str(e)}"

View File

@ -0,0 +1,240 @@
/* 用户编辑页面样式 */
.user-edit-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题和操作按钮 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
font-size: 1.8rem;
color: #333;
margin: 0;
}
.page-header .actions {
display: flex;
gap: 10px;
}
/* 卡片样式 */
.card {
border: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.card-body {
padding: 25px;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
font-weight: 500;
margin-bottom: 8px;
color: #333;
display: block;
}
.form-control {
height: auto;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
border-color: #4c84ff;
box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
}
.form-control[readonly] {
background-color: #f8f9fa;
opacity: 0.7;
}
.form-text {
font-size: 0.85rem;
margin-top: 5px;
}
.form-row {
margin-right: -15px;
margin-left: -15px;
display: flex;
flex-wrap: wrap;
}
.col-md-6 {
flex: 0 0 50%;
max-width: 50%;
padding-right: 15px;
padding-left: 15px;
}
.col-md-12 {
flex: 0 0 100%;
max-width: 100%;
padding-right: 15px;
padding-left: 15px;
}
/* 用户信息框 */
.user-info-box {
margin-top: 20px;
margin-bottom: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
display: flex;
flex-wrap: wrap;
}
.info-item {
flex: 0 0 auto;
margin-right: 30px;
margin-bottom: 10px;
}
.info-label {
font-weight: 500;
color: #666;
margin-right: 5px;
}
.info-value {
color: #333;
}
/* 表单操作区域 */
.form-actions {
display: flex;
justify-content: flex-start;
gap: 10px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #4c84ff;
border-color: #4c84ff;
}
.btn-primary:hover {
background-color: #3a70e9;
border-color: #3a70e9;
}
.btn-secondary {
background-color: #f8f9fa;
border-color: #ddd;
color: #333;
}
.btn-secondary:hover {
background-color: #e9ecef;
border-color: #ccc;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:hover {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
/* 表单分隔线 */
.form-divider {
height: 1px;
background-color: #f0f0f0;
margin: 30px 0;
}
/* 警告和错误状态 */
.is-invalid {
border-color: #dc3545 !important;
}
.invalid-feedback {
display: block;
width: 100%;
margin-top: 5px;
font-size: 0.85rem;
color: #dc3545;
}
/* 成功消息样式 */
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
/* 错误消息样式 */
.alert-error, .alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
.col-md-6, .col-md-12 {
flex: 0 0 100%;
max-width: 100%;
}
.page-header {
flex-direction: column;
align-items: flex-start;
}
.page-header .actions {
margin-top: 15px;
}
.user-info-box {
flex-direction: column;
}
.info-item {
margin-right: 0;
}
}

View File

@ -0,0 +1,244 @@
/* 用户列表页面样式 */
.user-list-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题和操作按钮 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
font-size: 1.8rem;
color: #333;
margin: 0;
}
.page-header .actions {
display: flex;
gap: 10px;
}
/* 搜索和筛选区域 */
.search-filter-container {
margin-bottom: 20px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 6px;
}
.search-filter-form .form-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 15px;
}
.search-box {
position: relative;
flex: 1;
min-width: 250px;
}
.search-box input {
padding-right: 40px;
border-radius: 4px;
border: 1px solid #ddd;
}
.btn-search {
position: absolute;
right: 5px;
top: 5px;
background: none;
border: none;
color: #666;
}
.filter-box {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-box select {
min-width: 120px;
border-radius: 4px;
border: 1px solid #ddd;
padding: 5px 10px;
}
.btn-filter, .btn-reset {
padding: 6px 15px;
border-radius: 4px;
}
.btn-filter {
background-color: #4c84ff;
color: white;
border: none;
}
.btn-reset {
background-color: #f8f9fa;
color: #333;
border: 1px solid #ddd;
}
/* 表格样式 */
.table {
width: 100%;
margin-bottom: 0;
color: #333;
border-collapse: collapse;
}
.table th {
background-color: #f8f9fa;
padding: 12px 15px;
font-weight: 600;
text-align: left;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
}
.table td {
padding: 12px 15px;
vertical-align: middle;
border-bottom: 1px solid #f0f0f0;
}
.table tr:hover {
background-color: #f8f9fa;
}
/* 状态标签 */
.status-badge {
display: inline-block;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.status-badge.active {
background-color: #e8f5e9;
color: #43a047;
}
.status-badge.inactive {
background-color: #ffebee;
color: #e53935;
}
/* 操作按钮 */
.actions {
display: flex;
gap: 5px;
align-items: center;
}
.actions .btn {
padding: 5px 8px;
line-height: 1;
}
/* 分页控件 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
.pagination {
display: flex;
padding-left: 0;
list-style: none;
border-radius: 0.25rem;
}
.page-item {
margin: 0 2px;
}
.page-link {
position: relative;
display: block;
padding: 0.5rem 0.75rem;
margin-left: -1px;
color: #4c84ff;
background-color: #fff;
border: 1px solid #dee2e6;
text-decoration: none;
}
.page-item.active .page-link {
z-index: 3;
color: #fff;
background-color: #4c84ff;
border-color: #4c84ff;
}
.page-item.disabled .page-link {
color: #aaa;
pointer-events: none;
background-color: #f8f9fa;
border-color: #dee2e6;
}
/* 通知样式 */
.alert-box {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
}
.alert-box .alert {
margin-bottom: 10px;
padding: 10px 15px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.alert-box .fade-in {
opacity: 1;
}
.alert-box .fade-out {
opacity: 0;
}
/* 响应式调整 */
@media (max-width: 992px) {
.search-filter-form .form-row {
flex-direction: column;
}
.search-box, .filter-box {
width: 100%;
}
}
@media (max-width: 768px) {
.table {
display: block;
overflow-x: auto;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
}

View File

@ -0,0 +1,375 @@
/* 用户个人中心页面样式 */
.profile-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题 */
.page-header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
font-size: 1.8rem;
color: #333;
margin: 0;
}
/* 个人中心内容布局 */
.profile-content {
display: flex;
gap: 30px;
}
/* 左侧边栏 */
.profile-sidebar {
flex: 0 0 300px;
background-color: #f8f9fa;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 右侧主要内容 */
.profile-main {
flex: 1;
min-width: 0; /* 防止内容溢出 */
}
/* 用户头像容器 */
.user-avatar-container {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 25px;
padding-bottom: 25px;
border-bottom: 1px solid #e9ecef;
}
/* 大头像样式 */
.user-avatar.large {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: #4c84ff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
margin-bottom: 15px;
box-shadow: 0 4px 8px rgba(76, 132, 255, 0.2);
}
.user-name {
font-size: 1.5rem;
margin: 10px 0 5px;
color: #333;
}
.user-role {
font-size: 0.9rem;
color: #6c757d;
margin: 0;
}
/* 用户统计信息 */
.user-stats {
display: flex;
justify-content: space-between;
margin-bottom: 25px;
padding-bottom: 25px;
border-bottom: 1px solid #e9ecef;
}
.stat-item {
text-align: center;
flex: 1;
}
.stat-value {
font-size: 1.8rem;
font-weight: 600;
color: #4c84ff;
line-height: 1;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.85rem;
color: #6c757d;
}
/* 账户信息样式 */
.account-info {
margin-bottom: 10px;
}
.account-info .info-row {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 0.95rem;
}
.account-info .info-label {
color: #6c757d;
font-weight: 500;
}
.account-info .info-value {
color: #333;
text-align: right;
word-break: break-all;
}
/* 选项卡导航样式 */
.nav-tabs {
border-bottom: 1px solid #dee2e6;
margin-bottom: 25px;
}
.nav-tabs .nav-link {
border: none;
color: #6c757d;
padding: 12px 15px;
margin-right: 5px;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}
.nav-tabs .nav-link:hover {
color: #4c84ff;
border-bottom-color: #4c84ff;
}
.nav-tabs .nav-link.active {
font-weight: 500;
color: #4c84ff;
border-bottom: 2px solid #4c84ff;
background-color: transparent;
}
.nav-tabs .nav-link i {
margin-right: 5px;
}
/* 表单区域 */
.form-section {
padding: 20px;
background-color: #f9f9fb;
border-radius: 8px;
margin-bottom: 20px;
}
.form-section h4 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
font-size: 1.2rem;
font-weight: 500;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
font-weight: 500;
margin-bottom: 8px;
color: #333;
display: block;
}
.form-control {
height: auto;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
border-color: #4c84ff;
box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
}
.form-text {
font-size: 0.85rem;
margin-top: 5px;
}
/* 表单操作区域 */
.form-actions {
margin-top: 25px;
display: flex;
justify-content: flex-start;
}
.btn {
padding: 10px 20px;
border-radius: 4px;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #4c84ff;
border-color: #4c84ff;
}
.btn-primary:hover {
background-color: #3a70e9;
border-color: #3a70e9;
}
/* 活动记录选项卡 */
.activity-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.activity-filter {
display: flex;
align-items: center;
gap: 10px;
}
.activity-filter label {
margin-bottom: 0;
}
.activity-filter select {
width: auto;
}
/* 活动时间线 */
.activity-timeline {
padding: 20px;
background-color: #f9f9fb;
border-radius: 8px;
min-height: 300px;
position: relative;
}
.timeline-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 250px;
}
.timeline-loading p {
margin-top: 15px;
color: #6c757d;
}
.timeline-item {
position: relative;
padding-left: 30px;
padding-bottom: 25px;
border-left: 2px solid #dee2e6;
}
.timeline-item:last-child {
border-left: none;
}
.timeline-icon {
position: absolute;
left: -10px;
top: 0;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #4c84ff;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 10px;
}
.timeline-content {
background-color: white;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
padding: 15px;
}
.timeline-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.timeline-title {
font-weight: 500;
color: #333;
margin: 0;
}
.timeline-time {
font-size: 0.85rem;
color: #6c757d;
}
.timeline-details {
color: #555;
font-size: 0.95rem;
}
.timeline-type-login .timeline-icon {
background-color: #4caf50;
}
.timeline-type-borrow .timeline-icon {
background-color: #2196f3;
}
.timeline-type-return .timeline-icon {
background-color: #ff9800;
}
/* 通知样式 */
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
}
.alert-error, .alert-danger {
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
}
/* 响应式调整 */
@media (max-width: 992px) {
.profile-content {
flex-direction: column;
}
.profile-sidebar {
flex: none;
width: 100%;
margin-bottom: 20px;
}
.user-stats {
justify-content: space-around;
}
}

View File

@ -0,0 +1,253 @@
/* 角色管理页面样式 */
.roles-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
font-size: 1.8rem;
color: #333;
margin: 0;
}
.page-header .actions {
display: flex;
gap: 10px;
}
/* 角色列表 */
.role-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
/* 角色卡片 */
.role-card {
background-color: #f9f9fb;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.role-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.role-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.role-name {
font-size: 1.3rem;
color: #333;
margin: 0;
font-weight: 600;
}
.role-actions {
display: flex;
gap: 5px;
}
.role-actions .btn {
padding: 5px 8px;
color: #6c757d;
background: none;
border: none;
}
.role-actions .btn:hover {
color: #4c84ff;
}
.role-actions .btn-delete-role:hover {
color: #dc3545;
}
.role-description {
color: #555;
margin-bottom: 20px;
min-height: 50px;
line-height: 1.5;
}
.role-stats {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
color: #6c757d;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.stat-item i {
color: #4c84ff;
}
/* 角色标签 */
.role-badge {
display: inline-block;
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.role-badge.admin {
background-color: #e3f2fd;
color: #1976d2;
}
.role-badge.user {
background-color: #e8f5e9;
color: #43a047;
}
.role-badge.custom {
background-color: #fff3e0;
color: #ef6c00;
}
/* 无数据提示 */
.no-data-message {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 0;
color: #6c757d;
}
.no-data-message i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.5;
}
/* 权限信息部分 */
.permissions-info {
margin-top: 30px;
}
.permissions-info h3 {
font-size: 1.4rem;
margin-bottom: 15px;
color: #333;
}
.permission-table {
width: 100%;
margin-bottom: 0;
}
.permission-table th {
background-color: #f8f9fa;
font-weight: 600;
}
.permission-table td, .permission-table th {
padding: 12px 15px;
text-align: center;
}
.permission-table td:first-child {
text-align: left;
font-weight: 500;
}
.text-success {
color: #28a745;
}
.text-danger {
color: #dc3545;
}
/* 模态框样式 */
.modal-header {
background-color: #f8f9fa;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-weight: 600;
color: #333;
}
.modal-footer {
border-top: 1px solid #f0f0f0;
padding: 15px;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
font-weight: 500;
margin-bottom: 8px;
color: #333;
display: block;
}
.form-control {
height: auto;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
border-color: #4c84ff;
box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
}
/* 响应式调整 */
@media (max-width: 992px) {
.role-list {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 576px) {
.role-list {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
align-items: flex-start;
}
.page-header .actions {
margin-top: 15px;
}
}

View File

@ -40,7 +40,7 @@ $(document).ready(function() {
// 处理删除图书
let bookIdToDelete = null;
$('.delete-btn').click(function(e) {
$('.delete-book').click(function(e) {
e.preventDefault();
bookIdToDelete = $(this).data('id');
const bookTitle = $(this).data('title');
@ -60,21 +60,28 @@ $(document).ready(function() {
// 显示成功消息
showNotification(response.message, 'success');
// 移除图书卡片
$(`.book-card[data-id="${bookIdToDelete}"]`).fadeOut(300, function() {
$(this).remove();
});
setTimeout(() => {
location.reload();
}, 800);
if ($('.book-card').length === 0) {
location.reload(); // 如果没有图书了,刷新页面显示"无图书"提示
}
}, 500);
} else {
$('#deleteModal').modal('hide');
showNotification(response.message, 'error');
}
},
error: function() {
$('#deleteModal').modal('hide');
showNotification('删除操作失败,请稍后重试', 'error');
}
});
});
// 处理借阅图书
$('.borrow-btn').click(function(e) {
$('.borrow-book').click(function(e) {
e.preventDefault();
const bookId = $(this).data('id');

View File

@ -0,0 +1,91 @@
// 用户编辑页面交互
document.addEventListener('DOMContentLoaded', function() {
const passwordField = document.getElementById('password');
const confirmPasswordGroup = document.getElementById('confirmPasswordGroup');
const confirmPasswordField = document.getElementById('confirm_password');
const userEditForm = document.getElementById('userEditForm');
// 如果输入密码,显示确认密码字段
passwordField.addEventListener('input', function() {
if (this.value.trim() !== '') {
confirmPasswordGroup.style.display = 'block';
} else {
confirmPasswordGroup.style.display = 'none';
confirmPasswordField.value = '';
}
});
// 表单提交验证
userEditForm.addEventListener('submit', function(event) {
let valid = true;
// 清除之前的错误提示
const invalidFields = document.querySelectorAll('.is-invalid');
const feedbackElements = document.querySelectorAll('.invalid-feedback');
invalidFields.forEach(field => {
field.classList.remove('is-invalid');
});
feedbackElements.forEach(element => {
element.parentNode.removeChild(element);
});
// 邮箱格式验证
const emailField = document.getElementById('email');
if (emailField.value.trim() !== '') {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailField.value.trim())) {
showError(emailField, '请输入有效的邮箱地址');
valid = false;
}
}
// 手机号码格式验证
const phoneField = document.getElementById('phone');
if (phoneField.value.trim() !== '') {
const phonePattern = /^1[3456789]\d{9}$/;
if (!phonePattern.test(phoneField.value.trim())) {
showError(phoneField, '请输入有效的手机号码');
valid = false;
}
}
// 密码验证
if (passwordField.value.trim() !== '') {
if (passwordField.value.length < 6) {
showError(passwordField, '密码长度至少为6个字符');
valid = false;
}
if (passwordField.value !== confirmPasswordField.value) {
showError(confirmPasswordField, '两次输入的密码不一致');
valid = false;
}
}
if (!valid) {
event.preventDefault();
}
});
// 显示表单字段错误
function showError(field, message) {
field.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.innerText = message;
field.parentNode.appendChild(feedback);
}
// 处理表单提交后的成功反馈
const successAlert = document.querySelector('.alert-success');
if (successAlert) {
// 如果有成功消息,显示成功对话框
setTimeout(() => {
$('#successModal').modal('show');
}, 500);
}
});

124
app/static/js/user-list.js Normal file
View File

@ -0,0 +1,124 @@
// 用户列表页面交互
document.addEventListener('DOMContentLoaded', function() {
// 处理状态切换按钮
const toggleStatusButtons = document.querySelectorAll('.toggle-status');
toggleStatusButtons.forEach(button => {
button.addEventListener('click', function() {
const userId = this.getAttribute('data-id');
const newStatus = parseInt(this.getAttribute('data-status'));
const statusText = newStatus === 1 ? '启用' : '禁用';
if (confirm(`确定要${statusText}该用户吗?`)) {
toggleUserStatus(userId, newStatus);
}
});
});
// 处理删除按钮
const deleteButtons = document.querySelectorAll('.delete-user');
const deleteModal = $('#deleteModal');
let userIdToDelete = null;
deleteButtons.forEach(button => {
button.addEventListener('click', function() {
userIdToDelete = this.getAttribute('data-id');
deleteModal.modal('show');
});
});
// 确认删除按钮
document.getElementById('confirmDelete').addEventListener('click', function() {
if (userIdToDelete) {
deleteUser(userIdToDelete);
deleteModal.modal('hide');
}
});
// 自动提交表单的下拉菜单
const autoSubmitSelects = document.querySelectorAll('select[name="status"], select[name="role_id"]');
autoSubmitSelects.forEach(select => {
select.addEventListener('change', function() {
this.closest('form').submit();
});
});
});
// 切换用户状态
function toggleUserStatus(userId, status) {
fetch(`/user/status/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ status: status })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('操作失败,请稍后重试', 'error');
});
}
// 删除用户
function deleteUser(userId) {
fetch(`/user/delete/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('操作失败,请稍后重试', 'error');
});
}
// 显示通知
function showAlert(message, type) {
// 检查是否已有通知元素
let alertBox = document.querySelector('.alert-box');
if (!alertBox) {
alertBox = document.createElement('div');
alertBox.className = 'alert-box';
document.body.appendChild(alertBox);
}
// 创建新的通知
const alert = document.createElement('div');
alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
alert.innerHTML = message;
// 添加到通知框中
alertBox.appendChild(alert);
// 自动关闭
setTimeout(() => {
alert.classList.add('fade-out');
setTimeout(() => {
alertBox.removeChild(alert);
}, 500);
}, 3000);
}

View File

@ -0,0 +1,275 @@
// 用户个人中心页面交互
document.addEventListener('DOMContentLoaded', function() {
// 获取表单对象
const profileForm = document.getElementById('profileForm');
const passwordForm = document.getElementById('passwordForm');
// 表单验证逻辑
if (profileForm) {
profileForm.addEventListener('submit', function(event) {
let valid = true;
// 清除之前的错误提示
clearValidationErrors();
// 验证邮箱
const emailField = document.getElementById('email');
if (emailField.value.trim() !== '') {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailField.value.trim())) {
showError(emailField, '请输入有效的邮箱地址');
valid = false;
}
}
// 验证手机号
const phoneField = document.getElementById('phone');
if (phoneField.value.trim() !== '') {
const phonePattern = /^1[3456789]\d{9}$/;
if (!phonePattern.test(phoneField.value.trim())) {
showError(phoneField, '请输入有效的手机号码');
valid = false;
}
}
if (!valid) {
event.preventDefault();
}
});
}
// 密码修改表单验证
if (passwordForm) {
passwordForm.addEventListener('submit', function(event) {
let valid = true;
// 清除之前的错误提示
clearValidationErrors();
// 验证当前密码
const currentPasswordField = document.getElementById('current_password');
if (currentPasswordField.value.trim() === '') {
showError(currentPasswordField, '请输入当前密码');
valid = false;
}
// 验证新密码
const newPasswordField = document.getElementById('new_password');
if (newPasswordField.value.trim() === '') {
showError(newPasswordField, '请输入新密码');
valid = false;
} else if (newPasswordField.value.length < 6) {
showError(newPasswordField, '密码长度至少为6个字符');
valid = false;
}
// 验证确认密码
const confirmPasswordField = document.getElementById('confirm_password');
if (confirmPasswordField.value.trim() === '') {
showError(confirmPasswordField, '请确认新密码');
valid = false;
} else if (confirmPasswordField.value !== newPasswordField.value) {
showError(confirmPasswordField, '两次输入的密码不一致');
valid = false;
}
if (!valid) {
event.preventDefault();
}
});
}
// 获取用户统计数据
fetchUserStats();
// 获取用户活动记录
const activityFilter = document.getElementById('activityFilter');
if (activityFilter) {
// 初始加载
fetchUserActivities('all');
// 监听过滤器变化
activityFilter.addEventListener('change', function() {
fetchUserActivities(this.value);
});
}
// 处理URL中的tab参数
const urlParams = new URLSearchParams(window.location.search);
const tabParam = urlParams.get('tab');
if (tabParam) {
const tabElement = document.getElementById(`${tabParam}-tab`);
if (tabElement) {
$('#profileTabs a[href="#' + tabParam + '"]').tab('show');
}
}
// 清除表单验证错误
function clearValidationErrors() {
const invalidFields = document.querySelectorAll('.is-invalid');
const feedbackElements = document.querySelectorAll('.invalid-feedback');
invalidFields.forEach(field => {
field.classList.remove('is-invalid');
});
feedbackElements.forEach(element => {
element.parentNode.removeChild(element);
});
}
// 显示错误消息
function showError(field, message) {
field.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.innerText = message;
field.parentNode.appendChild(feedback);
}
// 获取用户统计数据
function fetchUserStats() {
// 这里使用虚拟数据,实际应用中应当从后端获取
// fetch('/api/user/stats')
// .then(response => response.json())
// .then(data => {
// updateUserStats(data);
// });
// 模拟数据
setTimeout(() => {
const mockData = {
borrow: 2,
returned: 15,
overdue: 0
};
updateUserStats(mockData);
}, 500);
}
// 更新用户统计显示
function updateUserStats(data) {
const borrowCount = document.getElementById('borrowCount');
const returnedCount = document.getElementById('returnedCount');
const overdueCount = document.getElementById('overdueCount');
if (borrowCount) borrowCount.textContent = data.borrow;
if (returnedCount) returnedCount.textContent = data.returned;
if (overdueCount) overdueCount.textContent = data.overdue;
}
// 获取用户活动记录
function fetchUserActivities(type) {
const timelineContainer = document.getElementById('activityTimeline');
if (!timelineContainer) return;
// 显示加载中
timelineContainer.innerHTML = `
<div class="timeline-loading">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<p>加载中...</p>
</div>
`;
// 实际应用中应当从后端获取
// fetch(`/api/user/activities?type=${type}`)
// .then(response => response.json())
// .then(data => {
// renderActivityTimeline(data, timelineContainer);
// });
// 模拟数据
setTimeout(() => {
const mockActivities = [
{
id: 1,
type: 'login',
title: '系统登录',
details: '成功登录系统',
time: '2023-04-28 15:30:22',
ip: '192.168.1.1'
},
{
id: 2,
type: 'borrow',
title: '借阅图书',
details: '借阅《JavaScript高级编程》',
time: '2023-04-27 11:45:10',
book_id: 101
},
{
id: 3,
type: 'return',
title: '归还图书',
details: '归还《Python数据分析》',
time: '2023-04-26 09:15:33',
book_id: 95
},
{
id: 4,
type: 'login',
title: '系统登录',
details: '成功登录系统',
time: '2023-04-25 08:22:15',
ip: '192.168.1.1'
}
];
// 根据筛选条件过滤活动
let filteredActivities = mockActivities;
if (type !== 'all') {
filteredActivities = mockActivities.filter(activity => activity.type === type);
}
renderActivityTimeline(filteredActivities, timelineContainer);
}, 800);
}
// 渲染活动时间线
function renderActivityTimeline(activities, container) {
if (!activities || activities.length === 0) {
container.innerHTML = '<div class="text-center p-4">暂无活动记录</div>';
return;
}
let timelineHTML = '';
activities.forEach((activity, index) => {
let iconClass = 'fas fa-info';
if (activity.type === 'login') {
iconClass = 'fas fa-sign-in-alt';
} else if (activity.type === 'borrow') {
iconClass = 'fas fa-book';
} else if (activity.type === 'return') {
iconClass = 'fas fa-undo';
}
const isLast = index === activities.length - 1;
timelineHTML += `
<div class="timeline-item ${isLast ? 'last' : ''} timeline-type-${activity.type}">
<div class="timeline-icon">
<i class="${iconClass}"></i>
</div>
<div class="timeline-content">
<div class="timeline-header">
<h5 class="timeline-title">${activity.title}</h5>
<div class="timeline-time">${activity.time}</div>
</div>
<div class="timeline-details">
${activity.details}
${activity.ip ? `<div class="text-muted small">IP: ${activity.ip}</div>` : ''}
</div>
</div>
</div>
`;
});
container.innerHTML = timelineHTML;
}
});

301
app/static/js/user-roles.js Normal file
View File

@ -0,0 +1,301 @@
// 角色管理页面交互
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
const addRoleBtn = document.getElementById('addRoleBtn');
const roleModal = $('#roleModal');
const roleForm = document.getElementById('roleForm');
const roleIdInput = document.getElementById('roleId');
const roleNameInput = document.getElementById('roleName');
const roleDescriptionInput = document.getElementById('roleDescription');
const saveRoleBtn = document.getElementById('saveRoleBtn');
const deleteModal = $('#deleteModal');
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
let roleIdToDelete = null;
// 加载角色用户统计
fetchRoleUserCounts();
// 添加角色按钮点击事件
if (addRoleBtn) {
addRoleBtn.addEventListener('click', function() {
// 重置表单
roleIdInput.value = '';
roleNameInput.value = '';
roleDescriptionInput.value = '';
// 更新模态框标题
document.getElementById('roleModalLabel').textContent = '添加角色';
// 显示模态框
roleModal.modal('show');
});
}
// 编辑角色按钮点击事件
const editButtons = document.querySelectorAll('.btn-edit-role');
editButtons.forEach(button => {
button.addEventListener('click', function() {
const roleCard = this.closest('.role-card');
const roleId = roleCard.getAttribute('data-id');
const roleName = roleCard.querySelector('.role-name').textContent;
let roleDescription = roleCard.querySelector('.role-description').textContent;
// 移除"暂无描述"文本
if (roleDescription.trim() === '暂无描述') {
roleDescription = '';
}
// 填充表单
roleIdInput.value = roleId;
roleNameInput.value = roleName;
roleDescriptionInput.value = roleDescription.trim();
// 更新模态框标题
document.getElementById('roleModalLabel').textContent = '编辑角色';
// 显示模态框
roleModal.modal('show');
});
});
// 删除角色按钮点击事件
const deleteButtons = document.querySelectorAll('.btn-delete-role');
deleteButtons.forEach(button => {
button.addEventListener('click', function() {
const roleCard = this.closest('.role-card');
roleIdToDelete = roleCard.getAttribute('data-id');
// 显示确认删除模态框
deleteModal.modal('show');
});
});
// 保存角色按钮点击事件
if (saveRoleBtn) {
saveRoleBtn.addEventListener('click', function() {
if (!roleNameInput.value.trim()) {
showAlert('角色名称不能为空', 'error');
return;
}
const roleData = {
id: roleIdInput.value || null,
role_name: roleNameInput.value.trim(),
description: roleDescriptionInput.value.trim() || null
};
saveRole(roleData);
});
}
// 确认删除按钮点击事件
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function() {
if (roleIdToDelete) {
deleteRole(roleIdToDelete);
deleteModal.modal('hide');
}
});
}
// 保存角色
function saveRole(roleData) {
// 显示加载状态
saveRoleBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
saveRoleBtn.disabled = true;
fetch('/user/role/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(roleData)
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应异常');
}
return response.json();
})
.then(data => {
// 恢复按钮状态
saveRoleBtn.innerHTML = '保存';
saveRoleBtn.disabled = false;
if (data.success) {
// 关闭模态框
roleModal.modal('hide');
showAlert(data.message, 'success');
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
// 恢复按钮状态
saveRoleBtn.innerHTML = '保存';
saveRoleBtn.disabled = false;
showAlert('保存失败,请稍后重试', 'error');
});
}
// 删除角色
function deleteRole(roleId) {
// 显示加载状态
confirmDeleteBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 删除中...';
confirmDeleteBtn.disabled = true;
fetch(`/user/role/delete/${roleId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应异常');
}
return response.json();
})
.then(data => {
// 恢复按钮状态
confirmDeleteBtn.innerHTML = '确认删除';
confirmDeleteBtn.disabled = false;
if (data.success) {
showAlert(data.message, 'success');
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
// 恢复按钮状态
confirmDeleteBtn.innerHTML = '确认删除';
confirmDeleteBtn.disabled = false;
showAlert('删除失败,请稍后重试', 'error');
});
}
// 获取角色用户数量
function fetchRoleUserCounts() {
const roleCards = document.querySelectorAll('.role-card');
roleCards.forEach(card => {
const roleId = card.getAttribute('data-id');
const countElement = document.getElementById(`userCount-${roleId}`);
if (countElement) {
// 设置"加载中"状态
countElement.innerHTML = '<small>加载中...</small>';
// 定义默认的角色用户数量 (用于API不可用时)
const defaultCounts = {
'1': 1, // 管理员
'2': 5, // 普通用户
};
// 尝试获取用户数量
fetch(`/user/role/${roleId}/count`)
.then(response => {
if (!response.ok) {
throw new Error('API不可用');
}
return response.json();
})
.then(data => {
// 检查返回数据的success属性
if (data.success) {
countElement.textContent = data.count;
} else {
throw new Error(data.message || 'API返回错误');
}
})
.catch(error => {
console.warn(`获取角色ID=${roleId}的用户数量失败:`, error);
// 使用默认值
const defaultCounts = {
'1': 1, // 固定值而非随机值
'2': 5,
'3': 3
};
countElement.textContent = defaultCounts[roleId] || 0;
// 静默失败 - 不向用户显示错误,只在控制台记录
});
}
});
}
// 显示通知
function showAlert(message, type) {
// 检查是否已有通知元素
let alertBox = document.querySelector('.alert-box');
if (!alertBox) {
alertBox = document.createElement('div');
alertBox.className = 'alert-box';
document.body.appendChild(alertBox);
}
// 创建新的通知
const alert = document.createElement('div');
alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
alert.innerHTML = message;
// 添加到通知框中
alertBox.appendChild(alert);
// 自动关闭
setTimeout(() => {
alert.classList.add('fade-out');
setTimeout(() => {
alertBox.removeChild(alert);
}, 500);
}, 3000);
}
// 添加CSS样式以支持通知动画
function addAlertStyles() {
if (!document.getElementById('alert-styles')) {
const style = document.createElement('style');
style.id = 'alert-styles';
style.textContent = `
.alert-box {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
max-width: 350px;
}
.alert {
margin-bottom: 10px;
padding: 15px;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
opacity: 0;
transition: opacity 0.3s ease;
}
.fade-in {
opacity: 1;
}
.fade-out {
opacity: 0;
}
`;
document.head.appendChild(style);
}
}
// 添加通知样式
addAlertStyles();
});

View File

@ -5,45 +5,157 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面未找到 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
background-color: #fff5f7;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
text-align: center;
padding: 50px 20px;
max-width: 650px;
padding: 40px;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 10px 30px rgba(252, 162, 193, 0.2);
position: relative;
overflow: hidden;
}
.error-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(to right, #ff8ab3, #f17ab3, #f56eb8);
}
.error-code {
font-size: 100px;
font-size: 120px;
font-weight: bold;
color: #4a89dc;
margin-bottom: 20px;
background: linear-gradient(to right, #ff8ab3, #f17ab3, #f56eb8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0 0 20px 0;
line-height: 1;
}
.error-title {
font-size: 28px;
color: #ff8ab3;
margin-bottom: 15px;
font-weight: 600;
}
.error-message {
font-size: 24px;
color: #333;
font-size: 18px;
color: #7a7a7a;
margin-bottom: 30px;
line-height: 1.6;
}
.back-button {
display: inline-block;
padding: 10px 20px;
background-color: #4a89dc;
padding: 12px 30px;
background: linear-gradient(to right, #ff8ab3, #f17ab3);
color: white;
text-decoration: none;
border-radius: 5px;
border-radius: 50px;
font-weight: 500;
letter-spacing: 1px;
box-shadow: 0 5px 15px rgba(241, 122, 179, 0.4);
transition: all 0.3s ease;
}
.back-button:hover {
background-color: #3b78c4;
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(241, 122, 179, 0.6);
}
.decoration {
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255, 138, 179, 0.1);
}
.decoration-1 {
top: -20px;
left: -20px;
width: 120px;
height: 120px;
}
.decoration-2 {
bottom: -30px;
right: -30px;
width: 150px;
height: 150px;
}
.decoration-3 {
top: 60%;
left: -40px;
width: 100px;
height: 100px;
}
.book-icon {
margin-bottom: 20px;
width: 80px;
height: 80px;
display: inline-block;
position: relative;
}
.book-icon svg {
fill: #ff8ab3;
}
@media (max-width: 768px) {
.error-code {
font-size: 100px;
}
.error-title {
font-size: 24px;
}
.error-container {
margin: 0 20px;
padding: 30px;
}
}
</style>
</head>
<body>
<div class="main-container">
<div class="error-container">
<div class="error-code">404</div>
<div class="error-message">页面未找到</div>
<p>抱歉,您访问的页面不存在或已被移除。</p>
<p style="margin-bottom: 30px;">请检查URL是否正确或返回首页。</p>
<a href="{{ url_for('index') }}" class="back-button">返回首页</a>
<div class="error-container">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="decoration decoration-3"></div>
<div class="book-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h13c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H6V4h13v16z"/>
<path d="M9 5h7v2H9zM9 8h7v2H9zM9 11h7v2H9zM9 14h7v2H9z"/>
</svg>
</div>
<div class="error-code">404</div>
<div class="error-title">噢!页面不见了~</div>
<div class="error-message">
<p>抱歉,您要找的页面似乎藏起来了,或者从未存在过。</p>
<p>请检查您输入的网址是否正确,或者回到首页继续浏览吧!</p>
</div>
<a href="{{ url_for('index') }}" class="back-button">返回首页</a>
</div>
</body>
</html>

View File

@ -32,10 +32,13 @@
<li class="{% if '/announcement' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-bell"></i> 通知公告</a>
</li>
{% if current_user.role_id == 1 %}
{% if current_user.is_authenticated and current_user.role_id == 1 %}
<li class="nav-category">管理功能</li>
<li class="{% if '/user/manage' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-users"></i> 用户管理</a>
<a href="{{ url_for('user.user_list') }}"><i class="fas fa-users"></i> 用户管理</a>
</li>
<li class="{% if '/user/roles' in request.path %}active{% endif %}">
<a href="{{ url_for('user.role_list') }}"><i class="fas fa-user-tag"></i> 角色管理</a>
</li>
<li class="{% if '/book/list' in request.path %}active{% endif %}">
<a href="{{ url_for('book.book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
@ -69,6 +72,7 @@
<i class="fas fa-bell"></i>
<span class="badge">3</span>
</div>
{% if current_user.is_authenticated %}
<div class="user-info">
<div class="user-avatar">
{{ current_user.username[0] }}
@ -78,11 +82,17 @@
<span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
</div>
<div class="dropdown-menu">
<a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
<a href="{{ url_for('user.user_profile') }}"><i class="fas fa-user-circle"></i> 个人中心</a>
<a href="#"><i class="fas fa-cog"></i> 设置</a>
<a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
</div>
</div>
{% else %}
<div class="user-info">
<a href="{{ url_for('user.login') }}" class="login-link">登录</a>
<a href="{{ url_for('user.register') }}" class="register-link">注册</a>
</div>
{% endif %}
</div>
</header>
@ -102,16 +112,21 @@
document.addEventListener('DOMContentLoaded', function() {
// 用户菜单下拉
const userInfo = document.querySelector('.user-info');
userInfo.addEventListener('click', function(e) {
userInfo.classList.toggle('active');
});
if (userInfo) {
userInfo.addEventListener('click', function(e) {
if (!e.target.classList.contains('login-link') &&
!e.target.classList.contains('register-link')) {
userInfo.classList.toggle('active');
}
});
// 点击其他区域关闭下拉菜单
document.addEventListener('click', function(e) {
if (!userInfo.contains(e.target)) {
userInfo.classList.remove('active');
}
});
// 点击其他区域关闭下拉菜单
document.addEventListener('click', function(e) {
if (!userInfo.contains(e.target)) {
userInfo.classList.remove('active');
}
});
}
});
</script>

View File

@ -73,7 +73,8 @@
<div class="books-grid">
{% for book in books %}
<div class="book-card">
<!-- 为每个book-card添加data-id属性 -->
<div class="book-card" data-id="{{ book.id }}">
<div class="book-cover">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}">
@ -179,10 +180,31 @@
{% endif %}
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
确定要删除《<span id="deleteBookTitle"></span>》吗?此操作不可恢复。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/book-list.js') }}"></script>
{{ super() }}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,148 @@
{% extends "base.html" %}
{% block title %}编辑用户 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-edit.css') }}">
{% endblock %}
{% block content %}
<div class="user-edit-container">
<div class="page-header">
<h1>编辑用户</h1>
<div class="actions">
<a href="{{ url_for('user.user_list') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> 返回用户列表
</a>
</div>
</div>
<div class="card">
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('user.user_edit', user_id=user.id) }}" id="userEditForm">
<div class="form-row">
<!-- 用户基本信息 -->
<div class="col-md-6">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" value="{{ user.username }}" readonly>
<small class="form-text text-muted">用户名不可修改</small>
</div>
<div class="form-group">
<label for="email">邮箱地址</label>
<input type="email" class="form-control" id="email" name="email" value="{{ user.email or '' }}">
</div>
<div class="form-group">
<label for="phone">手机号码</label>
<input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone or '' }}">
</div>
<div class="form-group">
<label for="nickname">昵称</label>
<input type="text" class="form-control" id="nickname" name="nickname" value="{{ user.nickname or '' }}">
</div>
</div>
<!-- 用户权限和密码 -->
<div class="col-md-6">
<div class="form-group">
<label for="role_id">用户角色</label>
<select class="form-control" id="role_id" name="role_id">
{% for role in roles %}
<option value="{{ role.id }}" {% if role.id == user.role_id %}selected{% endif %}>
{{ role.role_name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="status">用户状态</label>
<select class="form-control" id="status" name="status">
<option value="1" {% if user.status == 1 %}selected{% endif %}>正常</option>
<option value="0" {% if user.status == 0 %}selected{% endif %}>禁用</option>
</select>
</div>
<div class="form-group">
<label for="password">重置密码</label>
<input type="password" class="form-control" id="password" name="password">
<small class="form-text text-muted">留空表示不修改密码</small>
</div>
<div class="form-group" id="confirmPasswordGroup" style="display: none;">
<label for="confirm_password">确认密码</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password">
</div>
</div>
</div>
<!-- 附加信息 -->
<div class="form-row">
<div class="col-md-12">
<div class="user-info-box">
<div class="info-item">
<span class="info-label">用户ID:</span>
<span class="info-value">{{ user.id }}</span>
</div>
<div class="info-item">
<span class="info-label">注册时间:</span>
<span class="info-value">{{ user.created_at }}</span>
</div>
<div class="info-item">
<span class="info-label">最后更新:</span>
<span class="info-value">{{ user.updated_at }}</span>
</div>
</div>
</div>
</div>
<!-- 提交按钮区域 -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 保存修改
</button>
<a href="{{ url_for('user.user_list') }}" class="btn btn-secondary">
取消
</a>
</div>
</form>
</div>
</div>
</div>
<!-- 操作成功提示模态框 -->
<div class="modal fade" id="successModal" tabindex="-1" role="dialog" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="successModalLabel">操作成功</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
用户信息已成功更新。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<a href="{{ url_for('user.user_list') }}" class="btn btn-primary">返回用户列表</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/user-edit.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,190 @@
{% extends "base.html" %}
{% block title %}用户管理 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-list.css') }}">
{% endblock %}
{% block content %}
<div class="user-list-container">
<!-- 页面标题 -->
<div class="page-header">
<h1>用户管理</h1>
<div class="actions">
<a href="{{ url_for('user.register') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i> 添加用户
</a>
</div>
</div>
<!-- 搜索和过滤区域 -->
<div class="search-filter-container">
<form method="GET" action="{{ url_for('user.user_list') }}" class="search-filter-form">
<div class="form-row">
<div class="search-box">
<input type="text" name="search" value="{{ search }}" placeholder="搜索用户名/邮箱/昵称/手机" class="form-control">
<button type="submit" class="btn btn-search">
<i class="fas fa-search"></i>
</button>
</div>
<div class="filter-box">
<select name="status" class="form-control">
<option value="">所有状态</option>
<option value="1" {% if status == 1 %}selected{% endif %}>正常</option>
<option value="0" {% if status == 0 %}selected{% endif %}>禁用</option>
</select>
<select name="role_id" class="form-control">
<option value="">所有角色</option>
{% for role in roles %}
<option value="{{ role.id }}" {% if role_id == role.id %}selected{% endif %}>{{ role.role_name }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-filter">筛选</button>
<a href="{{ url_for('user.user_list') }}" class="btn btn-reset">重置</a>
</div>
</div>
</form>
</div>
<!-- 用户列表表格 -->
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>昵称</th>
<th>邮箱</th>
<th>手机号</th>
<th>角色</th>
<th>状态</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for user in pagination.items %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.nickname or '-' }}</td>
<td>{{ user.email or '-' }}</td>
<td>{{ user.phone or '-' }}</td>
<td>
{% for role in roles %}
{% if role.id == user.role_id %}
{{ role.role_name }}
{% endif %}
{% endfor %}
</td>
<td>
<span class="status-badge {% if user.status == 1 %}active{% else %}inactive{% endif %}">
{{ '正常' if user.status == 1 else '禁用' }}
</span>
</td>
<td>{{ user.created_at }}</td>
<td class="actions">
<a href="{{ url_for('user.user_edit', user_id=user.id) }}" class="btn btn-sm btn-info" title="编辑">
<i class="fas fa-edit"></i>
</a>
{% if user.id != session.get('user_id') %}
{% if user.status == 1 %}
<button class="btn btn-sm btn-warning toggle-status" data-id="{{ user.id }}" data-status="0" title="禁用">
<i class="fas fa-ban"></i>
</button>
{% else %}
<button class="btn btn-sm btn-success toggle-status" data-id="{{ user.id }}" data-status="1" title="启用">
<i class="fas fa-check"></i>
</button>
{% endif %}
<button class="btn btn-sm btn-danger delete-user" data-id="{{ user.id }}" title="删除">
<i class="fas fa-trash"></i>
</button>
{% else %}
<span class="text-muted">(当前用户)</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="9" class="text-center">暂无用户数据</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页控件 -->
{% if pagination.pages > 1 %}
<div class="pagination-container">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('user.user_list', page=pagination.prev_num, search=search, status=status, role_id=role_id) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page %}
{% if page == pagination.page %}
<li class="page-item active">
<span class="page-link">{{ page }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('user.user_list', page=page, search=search, status=status, role_id=role_id) }}">{{ page }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('user.user_list', page=pagination.next_num, search=search, status=status, role_id=role_id) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
您确定要删除这个用户吗?此操作不可逆。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/user-list.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,188 @@
{% extends "base.html" %}
{% block title %}个人中心 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-profile.css') }}">
{% endblock %}
{% block content %}
<div class="profile-container">
<div class="page-header">
<h1>个人中心</h1>
</div>
<div class="profile-content">
<!-- 左侧用户信息展示 -->
<div class="profile-sidebar">
<div class="user-avatar-container">
<div class="user-avatar large">
{{ user.username[0] }}
</div>
<h3 class="user-name">{{ user.nickname or user.username }}</h3>
<p class="user-role">{{ '管理员' if user.role_id == 1 else '普通用户' }}</p>
</div>
<div class="user-stats">
<div class="stat-item">
<div class="stat-value" id="borrowCount">--</div>
<div class="stat-label">借阅中</div>
</div>
<div class="stat-item">
<div class="stat-value" id="returnedCount">--</div>
<div class="stat-label">已归还</div>
</div>
<div class="stat-item">
<div class="stat-value" id="overdueCount">--</div>
<div class="stat-label">已逾期</div>
</div>
</div>
<div class="account-info">
<div class="info-row">
<span class="info-label">用户名</span>
<span class="info-value">{{ user.username }}</span>
</div>
<div class="info-row">
<span class="info-label">用户ID</span>
<span class="info-value">{{ user.id }}</span>
</div>
<div class="info-row">
<span class="info-label">注册时间</span>
<span class="info-value">{{ user.created_at }}</span>
</div>
<div class="info-row">
<span class="info-label">最后更新</span>
<span class="info-value">{{ user.updated_at }}</span>
</div>
</div>
</div>
<!-- 右侧内容区域:包含编辑选项卡 -->
<div class="profile-main">
<!-- 提示消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- 选项卡导航 -->
<ul class="nav nav-tabs" id="profileTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="basic-tab" data-toggle="tab" href="#basic" role="tab" aria-controls="basic" aria-selected="true">
<i class="fas fa-user"></i> 基本信息
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="security-tab" data-toggle="tab" href="#security" role="tab" aria-controls="security" aria-selected="false">
<i class="fas fa-lock"></i> 安全设置
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="activity-tab" data-toggle="tab" href="#activity" role="tab" aria-controls="activity" aria-selected="false">
<i class="fas fa-history"></i> 最近活动
</a>
</li>
</ul>
<!-- 选项卡内容 -->
<div class="tab-content" id="profileTabsContent">
<!-- 基本信息选项卡 -->
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
<form method="POST" action="{{ url_for('user.user_profile') }}" id="profileForm">
<div class="form-section">
<h4>个人信息</h4>
<div class="form-group">
<label for="nickname">昵称</label>
<input type="text" class="form-control" id="nickname" name="nickname" value="{{ user.nickname or '' }}" placeholder="请输入您的昵称">
</div>
<div class="form-group">
<label for="email">邮箱地址</label>
<input type="email" class="form-control" id="email" name="email" value="{{ user.email or '' }}" placeholder="请输入您的邮箱">
<small class="form-text text-muted">用于接收系统通知和找回密码</small>
</div>
<div class="form-group">
<label for="phone">手机号码</label>
<input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone or '' }}" placeholder="请输入您的手机号">
<small class="form-text text-muted">用于接收借阅提醒和系统通知</small>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" name="form_type" value="profile">
<i class="fas fa-save"></i> 保存修改
</button>
</div>
</form>
</div>
<!-- 安全设置选项卡 -->
<div class="tab-pane fade" id="security" role="tabpanel" aria-labelledby="security-tab">
<form method="POST" action="{{ url_for('user.user_profile') }}" id="passwordForm">
<div class="form-section">
<h4>修改密码</h4>
<div class="form-group">
<label for="current_password">当前密码</label>
<input type="password" class="form-control" id="current_password" name="current_password" placeholder="请输入当前密码">
</div>
<div class="form-group">
<label for="new_password">新密码</label>
<input type="password" class="form-control" id="new_password" name="new_password" placeholder="请输入新密码">
<small class="form-text text-muted">密码长度至少为6个字符</small>
</div>
<div class="form-group">
<label for="confirm_password">确认新密码</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" placeholder="请再次输入新密码">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" name="form_type" value="password">
<i class="fas fa-key"></i> 更新密码
</button>
</div>
</form>
</div>
<!-- 最近活动选项卡 -->
<div class="tab-pane fade" id="activity" role="tabpanel" aria-labelledby="activity-tab">
<div class="activity-header">
<h4>最近活动</h4>
<div class="activity-filter">
<label for="activityFilter">显示:</label>
<select id="activityFilter" class="form-control form-control-sm">
<option value="all">所有活动</option>
<option value="login">登录记录</option>
<option value="borrow">借阅活动</option>
<option value="return">归还活动</option>
</select>
</div>
</div>
<div class="activity-timeline" id="activityTimeline">
<div class="timeline-loading">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<p>加载中...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/user-profile.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,194 @@
{% extends "base.html" %}
{% block title %}角色管理 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-roles.css') }}">
{% endblock %}
{% block content %}
<div class="roles-container">
<!-- 页面标题 -->
<div class="page-header">
<h1>角色管理</h1>
<div class="actions">
<button class="btn btn-primary" id="addRoleBtn">
<i class="fas fa-plus"></i> 添加角色
</button>
</div>
</div>
<!-- 角色列表卡片 -->
<div class="role-list">
{% for role in roles %}
<div class="role-card" data-id="{{ role.id }}">
<div class="role-header">
<h3 class="role-name">{{ role.role_name }}</h3>
<div class="role-actions">
<button class="btn btn-sm btn-edit-role" title="编辑角色">
<i class="fas fa-edit"></i>
</button>
{% if role.id not in [1, 2] %}
<button class="btn btn-sm btn-delete-role" title="删除角色">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</div>
<div class="role-description">
{% if role.description %}
{{ role.description }}
{% else %}
<span class="text-muted">暂无描述</span>
{% endif %}
</div>
<div class="role-stats">
<div class="stat-item">
<i class="fas fa-users"></i> <span id="userCount-{{ role.id }}">--</span> 用户
</div>
{% if role.id == 1 %}
<div class="role-badge admin">管理员</div>
{% elif role.id == 2 %}
<div class="role-badge user">普通用户</div>
{% else %}
<div class="role-badge custom">自定义</div>
{% endif %}
</div>
</div>
{% else %}
<div class="no-data-message">
<i class="fas fa-users-slash"></i>
<p>暂无角色数据</p>
</div>
{% endfor %}
</div>
<!-- 权限描述 -->
<div class="permissions-info">
<h3>角色权限说明</h3>
<div class="card">
<div class="card-body">
<table class="table permission-table">
<thead>
<tr>
<th>功能模块</th>
<th>管理员</th>
<th>普通用户</th>
<th>自定义角色</th>
</tr>
</thead>
<tbody>
<tr>
<td>图书浏览</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-check text-success"></i></td>
</tr>
<tr>
<td>借阅图书</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-check text-success"></i></td>
</tr>
<tr>
<td>图书管理</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-times text-danger"></i></td>
<td>可配置</td>
</tr>
<tr>
<td>用户管理</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-times text-danger"></i></td>
<td>可配置</td>
</tr>
<tr>
<td>借阅管理</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-times text-danger"></i></td>
<td>可配置</td>
</tr>
<tr>
<td>库存管理</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-times text-danger"></i></td>
<td>可配置</td>
</tr>
<tr>
<td>统计分析</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-times text-danger"></i></td>
<td>可配置</td>
</tr>
<tr>
<td>系统设置</td>
<td><i class="fas fa-check text-success"></i></td>
<td><i class="fas fa-times text-danger"></i></td>
<td>可配置</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 角色编辑模态框 -->
<div class="modal fade" id="roleModal" tabindex="-1" role="dialog" aria-labelledby="roleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="roleModalLabel">添加角色</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="roleForm">
<input type="hidden" id="roleId" value="">
<div class="form-group">
<label for="roleName">角色名称</label>
<input type="text" class="form-control" id="roleName" required>
</div>
<div class="form-group">
<label for="roleDescription">角色描述</label>
<textarea class="form-control" id="roleDescription" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveRoleBtn">保存</button>
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
您确定要删除这个角色吗?此操作不可逆。
<p class="text-danger mt-3">注意:删除角色将会影响所有使用此角色的用户。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/user-roles.js') }}"></script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -9,4 +9,5 @@ xlrd==2.0.1
email-validator==2.0.0
pillow==9.5.0
numpy
pandas
pandas
flask-login