From 29009ef7debf970a0da78dd0101fdafd7e34af93 Mon Sep 17 00:00:00 2001 From: superlishunqin <852326703@qq.com> Date: Thu, 1 May 2025 04:52:53 +0800 Subject: [PATCH] user --- app/__init__.py | 18 +- app/controllers/book.py | 3 +- app/controllers/user.py | 237 +- app/models/user.py | 7 +- app/services/user_service.py | 163 + app/static/css/user-edit.css | 240 ++ app/static/css/user-list.css | 244 ++ app/static/css/user-profile.css | 375 ++ app/static/css/user-roles.css | 253 ++ app/static/js/book-list.js | 15 +- app/static/js/user-edit.js | 91 + app/static/js/user-list.js | 124 + app/static/js/user-profile.js | 275 ++ app/static/js/user-roles.js | 301 ++ app/templates/404.html | 146 +- app/templates/base.html | 39 +- app/templates/book/list.html | 26 +- app/templates/user/edit.html | 148 + app/templates/user/list.html | 190 + app/templates/user/profile.html | 188 + app/templates/user/roles.html | 194 + code_collection.txt | 6205 +++++++++++++++++++++++++------ requirements.txt | 3 +- 23 files changed, 8207 insertions(+), 1278 deletions(-) create mode 100644 app/static/css/user-edit.css create mode 100644 app/static/css/user-list.css create mode 100644 app/static/css/user-profile.css create mode 100644 app/static/css/user-roles.css create mode 100644 app/static/js/user-edit.js create mode 100644 app/static/js/user-list.js create mode 100644 app/static/js/user-profile.js create mode 100644 app/static/js/user-roles.js create mode 100644 app/templates/user/edit.html create mode 100644 app/templates/user/list.html create mode 100644 app/templates/user/profile.html create mode 100644 app/templates/user/roles.html diff --git a/app/__init__.py b/app/__init__.py index da3507f..6408655 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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_user,Flask-Login自动提供 @app.errorhandler(404) def page_not_found(e): diff --git a/app/controllers/book.py b/app/controllers/book.py index 51eacbb..93ed5e2 100644 --- a/app/controllers/book.py +++ b/app/controllers/book.py @@ -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', '') diff --git a/app/controllers/user.py b/app/controllers/user.py index a0561f4..bdecc6f 100644 --- a/app/controllers/user.py +++ b/app/controllers/user.py @@ -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/', 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/', 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/', 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//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//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 \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index d0182cc..4dbfa1c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -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) diff --git a/app/services/user_service.py b/app/services/user_service.py index e69de29..c5b237e 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -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)}" diff --git a/app/static/css/user-edit.css b/app/static/css/user-edit.css new file mode 100644 index 0000000..1e0f41b --- /dev/null +++ b/app/static/css/user-edit.css @@ -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; + } +} diff --git a/app/static/css/user-list.css b/app/static/css/user-list.css new file mode 100644 index 0000000..b782310 --- /dev/null +++ b/app/static/css/user-list.css @@ -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; + } +} + diff --git a/app/static/css/user-profile.css b/app/static/css/user-profile.css new file mode 100644 index 0000000..959296d --- /dev/null +++ b/app/static/css/user-profile.css @@ -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; + } +} diff --git a/app/static/css/user-roles.css b/app/static/css/user-roles.css new file mode 100644 index 0000000..dffd7a1 --- /dev/null +++ b/app/static/css/user-roles.css @@ -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; + } +} diff --git a/app/static/js/book-list.js b/app/static/js/book-list.js index ddc013d..605dda9 100644 --- a/app/static/js/book-list.js +++ b/app/static/js/book-list.js @@ -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'); diff --git a/app/static/js/user-edit.js b/app/static/js/user-edit.js new file mode 100644 index 0000000..8c6e021 --- /dev/null +++ b/app/static/js/user-edit.js @@ -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); + } +}); diff --git a/app/static/js/user-list.js b/app/static/js/user-list.js new file mode 100644 index 0000000..236d8a3 --- /dev/null +++ b/app/static/js/user-list.js @@ -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); +} diff --git a/app/static/js/user-profile.js b/app/static/js/user-profile.js new file mode 100644 index 0000000..e2c8ccf --- /dev/null +++ b/app/static/js/user-profile.js @@ -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 = ` +
+
+ Loading... +
+

加载中...

+
+ `; + + // 实际应用中应当从后端获取 + // 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 = '
暂无活动记录
'; + 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 += ` +
+
+ +
+
+
+
${activity.title}
+
${activity.time}
+
+
+ ${activity.details} + ${activity.ip ? `
IP: ${activity.ip}
` : ''} +
+
+
+ `; + }); + + container.innerHTML = timelineHTML; + } +}); diff --git a/app/static/js/user-roles.js b/app/static/js/user-roles.js new file mode 100644 index 0000000..26cd665 --- /dev/null +++ b/app/static/js/user-roles.js @@ -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 = ' 保存中...'; + 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 = ' 删除中...'; + 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 = '加载中...'; + + // 定义默认的角色用户数量 (用于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(); +}); diff --git a/app/templates/404.html b/app/templates/404.html index 1a08b9c..a8b5ff6 100644 --- a/app/templates/404.html +++ b/app/templates/404.html @@ -5,45 +5,157 @@ 页面未找到 - 图书管理系统 + -
-
-
404
-
页面未找到
-

抱歉,您访问的页面不存在或已被移除。

-

请检查URL是否正确,或返回首页。

- 返回首页 +
+
+
+
+ +
+ + + +
+ +
404
+
噢!页面不见了~
+
+

抱歉,您要找的页面似乎藏起来了,或者从未存在过。

+

请检查您输入的网址是否正确,或者回到首页继续浏览吧!

+
+ 返回首页
diff --git a/app/templates/base.html b/app/templates/base.html index af5c51b..529d51e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -32,10 +32,13 @@
  • 通知公告
  • - {% if current_user.role_id == 1 %} + {% if current_user.is_authenticated and current_user.role_id == 1 %}
  • - 用户管理 + 用户管理 +
  • +
  • + 角色管理
  • 图书管理 @@ -69,6 +72,7 @@ 3
  • + {% if current_user.is_authenticated %} + {% else %} + + {% endif %}
    @@ -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'); + } + }); + } }); diff --git a/app/templates/book/list.html b/app/templates/book/list.html index da0e557..a20dfbe 100644 --- a/app/templates/book/list.html +++ b/app/templates/book/list.html @@ -73,7 +73,8 @@
    {% for book in books %} -
    + +
    {% if book.cover_url %} {{ book.title }} @@ -179,10 +180,31 @@ {% endif %}
    + + {% endblock %} {% block scripts %} {{ super() }} -{% endblock %} \ No newline at end of file +{% endblock %} + diff --git a/app/templates/user/edit.html b/app/templates/user/edit.html new file mode 100644 index 0000000..4669c03 --- /dev/null +++ b/app/templates/user/edit.html @@ -0,0 +1,148 @@ +{% extends "base.html" %} + +{% block title %}编辑用户 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + +
    +
    + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
    {{ message }}
    + {% endfor %} + {% endif %} + {% endwith %} + +
    +
    + +
    +
    + + + 用户名不可修改 +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + +
    + + + 留空表示不修改密码 +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    + + + 取消 + +
    +
    +
    +
    +
    + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/user/list.html b/app/templates/user/list.html new file mode 100644 index 0000000..35a6750 --- /dev/null +++ b/app/templates/user/list.html @@ -0,0 +1,190 @@ +{% extends "base.html" %} + +{% block title %}用户管理 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + + + +
    +
    +
    + + +
    + + + + + + 重置 +
    +
    +
    +
    + + +
    + + + + + + + + + + + + + + + + {% for user in pagination.items %} + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
    ID用户名昵称邮箱手机号角色状态注册时间操作
    {{ user.id }}{{ user.username }}{{ user.nickname or '-' }}{{ user.email or '-' }}{{ user.phone or '-' }} + {% for role in roles %} + {% if role.id == user.role_id %} + {{ role.role_name }} + {% endif %} + {% endfor %} + + + {{ '正常' if user.status == 1 else '禁用' }} + + {{ user.created_at }} + + + + + {% if user.id != session.get('user_id') %} + {% if user.status == 1 %} + + {% else %} + + {% endif %} + + + {% else %} + (当前用户) + {% endif %} +
    暂无用户数据
    +
    + + + {% if pagination.pages > 1 %} +
    +
      + {% if pagination.has_prev %} +
    • + + + +
    • + {% 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 %} +
    • + {{ page }} +
    • + {% else %} +
    • + {{ page }} +
    • + {% endif %} + {% else %} +
    • + ... +
    • + {% endif %} + {% endfor %} + + {% if pagination.has_next %} +
    • + + + +
    • + {% endif %} +
    +
    + {% endif %} +
    + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/app/templates/user/profile.html b/app/templates/user/profile.html new file mode 100644 index 0000000..113df1d --- /dev/null +++ b/app/templates/user/profile.html @@ -0,0 +1,188 @@ +{% extends "base.html" %} + +{% block title %}个人中心 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + +
    + +
    +
    +
    + {{ user.username[0] }} +
    +

    {{ user.nickname or user.username }}

    +

    {{ '管理员' if user.role_id == 1 else '普通用户' }}

    +
    + +
    +
    +
    --
    +
    借阅中
    +
    +
    +
    --
    +
    已归还
    +
    +
    +
    --
    +
    已逾期
    +
    +
    + + +
    + + +
    + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
    {{ message }}
    + {% endfor %} + {% endif %} + {% endwith %} + + + + + +
    + +
    +
    +
    +

    个人信息

    + +
    + + +
    + +
    + + + 用于接收系统通知和找回密码 +
    + +
    + + + 用于接收借阅提醒和系统通知 +
    +
    + +
    + +
    +
    +
    + + +
    +
    +
    +

    修改密码

    + +
    + + +
    + +
    + + + 密码长度至少为6个字符 +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    +
    +

    最近活动

    +
    + + +
    +
    + +
    +
    +
    + Loading... +
    +

    加载中...

    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/user/roles.html b/app/templates/user/roles.html new file mode 100644 index 0000000..910d550 --- /dev/null +++ b/app/templates/user/roles.html @@ -0,0 +1,194 @@ +{% extends "base.html" %} + +{% block title %}角色管理 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + + + +
    + {% for role in roles %} +
    +
    +

    {{ role.role_name }}

    +
    + + {% if role.id not in [1, 2] %} + + {% endif %} +
    +
    +
    + {% if role.description %} + {{ role.description }} + {% else %} + 暂无描述 + {% endif %} +
    +
    +
    + -- 用户 +
    + {% if role.id == 1 %} +
    管理员
    + {% elif role.id == 2 %} +
    普通用户
    + {% else %} +
    自定义
    + {% endif %} +
    +
    + {% else %} +
    + +

    暂无角色数据

    +
    + {% endfor %} +
    + + +
    +

    角色权限说明

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    功能模块管理员普通用户自定义角色
    图书浏览
    借阅图书
    图书管理可配置
    用户管理可配置
    借阅管理可配置
    库存管理可配置
    统计分析可配置
    系统设置可配置
    +
    +
    +
    +
    + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/code_collection.txt b/code_collection.txt index 986d1db..8982c59 100644 --- a/code_collection.txt +++ b/code_collection.txt @@ -134,14 +134,18 @@ if __name__ == '__main__': File: ./app/__init__.py ================================================================================ -from flask import Flask, render_template, session, g +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.book import book_bp +from app.controllers.borrow import borrow_bp import os +login_manager = LoginManager() -def create_app(): + +def create_app(config=None): app = Flask(__name__) # 配置应用 @@ -156,7 +160,7 @@ def create_app(): EMAIL_PORT=587, EMAIL_ENCRYPTION='starttls', EMAIL_USERNAME='3399560459@qq.com', - EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码,不是邮箱密码 + EMAIL_PASSWORD='fzwhyirhbqdzcjgf', EMAIL_FROM='3399560459@qq.com', EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL' ) @@ -167,9 +171,18 @@ 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') # 注册图书蓝图 + app.register_blueprint(book_bp, url_prefix='/book') + app.register_blueprint(borrow_bp, url_prefix='/borrow') # 创建数据库表 with app.app_context(): @@ -180,15 +193,14 @@ def create_app(): # 创建表 db.create_all() - # 再导入依赖模型 + # 再导入依赖模型 - 但不在这里定义关系 from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog - # 现在添加反向关系 - # 这样可以确保所有类都已经定义好 - Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic') - Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') - Category.books = db.relationship('Book', backref='category', lazy='dynamic') + # 移除这些重复的关系定义 + # Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic') + # Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') + # Category.books = db.relationship('Book', backref='category', lazy='dynamic') # 创建默认角色 from app.models.user import Role @@ -228,7 +240,7 @@ def create_app(): db.session.commit() - # 请求前处理 + # 其余代码保持不变... @app.before_request def load_logged_in_user(): user_id = session.get('user_id') @@ -238,24 +250,21 @@ def create_app(): else: g.user = User.query.get(user_id) - # 首页路由 @app.route('/') def index(): if not g.user: return render_template('login.html') return render_template('index.html', current_user=g.user) - # 错误处理 @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 - # 模板过滤器 @app.template_filter('nl2br') def nl2br_filter(s): - if not s: - return s - return s.replace('\n', '
    ') + if s: + return Markup(s.replace('\n', '
    ')) + return s return app @@ -405,13 +414,13 @@ File: ./app/models/user.py 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) @@ -431,6 +440,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) @@ -584,6 +596,7 @@ class BorrowRecord(db.Model): # 添加反向关系引用 user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic')) + book = db.relationship('Book', backref=db.backref('borrow_records', lazy='dynamic')) # book 关系会在后面步骤添加 @@ -970,6 +983,255 @@ footer a { } } +================================================================================ +File: ./app/static/css/user-list.css +================================================================================ + +/* 用户列表页面样式 */ +.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; + } +} + + ================================================================================ File: ./app/static/css/book-detail.css ================================================================================ @@ -3242,29 +3504,60 @@ body { File: ./app/static/css/book-form.css ================================================================================ -/* ========== 基础样式 ========== */ +/* ========== 基础重置和变量 ========== */ +:root { + --primary-color: #3b82f6; + --primary-hover: #2563eb; + --primary-light: #eff6ff; + --danger-color: #ef4444; + --success-color: #10b981; + --warning-color: #f59e0b; + --info-color: #3b82f6; + --text-dark: #1e293b; + --text-medium: #475569; + --text-light: #64748b; + --text-muted: #94a3b8; + --border-color: #e2e8f0; + --border-focus: #bfdbfe; + --bg-white: #ffffff; + --bg-light: #f8fafc; + --bg-lightest: #f1f5f9; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --transition-fast: 0.15s ease; + --transition-base: 0.3s ease; + --transition-slow: 0.5s ease; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +/* ========== 全局样式 ========== */ .book-form-container { - padding: 30px; + padding: 24px; max-width: 1400px; margin: 0 auto; - background-color: #f8f9fa; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03); + font-family: var(--font-sans); + color: var(--text-dark); } /* ========== 页头样式 ========== */ .page-header-wrapper { - margin-bottom: 30px; - background: linear-gradient(135deg, #ffffff 0%, #f0f4f8 100%); - border-radius: 8px; - padding: 20px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + margin-bottom: 24px; + background-color: var(--bg-white); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + overflow: hidden; } .page-header { display: flex; justify-content: space-between; align-items: center; + padding: 24px; } .header-title-section { @@ -3273,43 +3566,46 @@ File: ./app/static/css/book-form.css } .page-title { - font-size: 28px; - font-weight: 700; - color: #2c3e50; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-dark); margin: 0; - display: flex; - align-items: center; -} - -.page-title i { - margin-right: 12px; - color: #3498db; - font-size: 24px; } .subtitle { - margin: 5px 0 0 0; - color: #7f8c8d; - font-size: 16px; + margin: 8px 0 0 0; + color: var(--text-medium); + font-size: 0.9rem; } .header-actions { display: flex; align-items: center; - gap: 20px; + gap: 16px; } -.btn-icon-text { +.btn-back { display: flex; align-items: center; gap: 8px; + color: var(--text-medium); + background-color: var(--bg-lightest); + border-radius: var(--radius-md); padding: 8px 16px; - border-radius: 6px; + font-size: 0.875rem; font-weight: 500; - transition: all 0.3s ease; + transition: all var(--transition-fast); + text-decoration: none; + box-shadow: var(--shadow-sm); } -.btn-icon-text i { +.btn-back:hover { + background-color: var(--border-color); + color: var(--text-dark); + text-decoration: none; +} + +.btn-back i { font-size: 14px; } @@ -3318,74 +3614,76 @@ File: ./app/static/css/book-form.css min-width: 180px; } -.progress { - height: 8px; - border-radius: 4px; - background-color: #e9ecef; - margin-bottom: 5px; +.progress-bar-container { + height: 6px; + background-color: var(--bg-lightest); + border-radius: 3px; overflow: hidden; } .progress-bar { - background: linear-gradient(45deg, #3498db, #2ecc71); - transition: width 0.5s ease; + height: 100%; + background-color: var(--primary-color); + border-radius: 3px; + transition: width var(--transition-base); } .progress-text { - font-size: 12px; - color: #6c757d; + font-size: 0.75rem; + color: var(--text-light); text-align: right; display: block; + margin-top: 4px; +} + +/* ========== 表单布局 ========== */ +.form-grid { + display: grid; + grid-template-columns: 1fr 360px; + gap: 24px; +} + +.form-main-content { + display: flex; + flex-direction: column; + gap: 24px; +} + +.form-sidebar { + display: flex; + flex-direction: column; + gap: 24px; } /* ========== 表单卡片样式 ========== */ .form-card { - margin-bottom: 25px; - border: none; - border-radius: 10px; - box-shadow: 0 3px 12px rgba(0, 0, 0, 0.05); - background-color: #ffffff; - transition: all 0.3s ease; + background-color: var(--bg-white); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); overflow: hidden; + transition: box-shadow var(--transition-base); } .form-card:hover { - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); + box-shadow: var(--shadow-md); } .card-header { padding: 16px 20px; - background: linear-gradient(90deg, rgba(248,249,250,1) 0%, rgba(255,255,255,1) 100%); - border-bottom: 1px solid #edf2f7; + background-color: var(--bg-white); + border-bottom: 1px solid var(--border-color); display: flex; align-items: center; } -.card-header-icon { - width: 32px; - height: 32px; - border-radius: 8px; - background-color: rgba(52, 152, 219, 0.1); - display: flex; - align-items: center; - justify-content: center; - margin-right: 12px; -} - -.card-header-icon i { - color: #3498db; - font-size: 16px; -} - -.card-header-title { +.card-title { font-weight: 600; - color: #2c3e50; - font-size: 16px; + color: var(--text-dark); + font-size: 0.9375rem; } .card-body { - padding: 25px; + padding: 20px; } .form-section { @@ -3393,151 +3691,298 @@ File: ./app/static/css/book-form.css } /* ========== 表单元素样式 ========== */ -.form-label { - font-weight: 500; - color: #34495e; - margin-bottom: 8px; - display: flex; - align-items: center; -} - -.label-icon { - color: #3498db; - margin-right: 8px; - font-size: 14px; +.form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 16px; } .form-group { margin-bottom: 20px; - position: relative; } -.custom-input, .custom-select, .custom-textarea { - height: calc(2.8rem + 2px); - padding: 0.75rem 1rem; - border: 1px solid #e0e6ed; - border-radius: 8px; - font-size: 15px; - transition: all 0.3s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.02); +.form-group:last-child { + margin-bottom: 0; } -.custom-input:focus, .custom-select:focus, .custom-textarea:focus { - border-color: #3498db; - box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); - outline: none; +.form-label { + display: block; + font-weight: 500; + color: var(--text-dark); + margin-bottom: 8px; + font-size: 0.9375rem; } -.custom-textarea { - height: auto; - min-height: 200px; - resize: vertical; - line-height: 1.6; +.form-control { + display: block; + width: 100%; + padding: 10px 14px; + font-size: 0.9375rem; + line-height: 1.5; + color: var(--text-dark); + background-color: var(--bg-white); + background-clip: padding-box; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); } -.input-group-text { - background-color: #f8f9fa; - border: 1px solid #e0e6ed; - border-radius: 8px; - color: #6c757d; +.form-control:focus { + border-color: var(--border-focus); + outline: 0; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } -.form-text { +.form-control::placeholder { + color: var(--text-muted); +} + +.form-control:disabled, .form-control[readonly] { + background-color: var(--bg-lightest); + opacity: 0.6; +} + +.form-help { margin-top: 6px; - font-size: 13px; - color: #6c757d; + font-size: 0.8125rem; + color: var(--text-light); } -/* 浮动标签效果 */ -.form-group.focused .floating-label { - transform: translateY(-22px) scale(0.85); - color: #3498db; - opacity: 1; -} - -.floating-label { - position: absolute; - pointer-events: none; - left: 1rem; - top: 0.75rem; - transition: 0.2s ease all; - color: #95a5a6; - opacity: 0.8; -} - -.floating-input:focus + .floating-label, -.floating-input:not(:placeholder-shown) + .floating-label { - transform: translateY(-22px) scale(0.85); - color: #3498db; - opacity: 1; -} - -.floating-input { - padding-top: 1.1rem; - padding-bottom: 0.4rem; -} - -/* 数字输入组 */ -.input-number-group { +.form-footer { display: flex; - width: 100%; + justify-content: space-between; + align-items: center; + margin-top: 8px; } -.input-number-group input { - text-align: center; - border-radius: 0; - border-left: none; - border-right: none; +.char-counter { + font-size: 0.8125rem; + color: var(--text-muted); } -.input-number-group button { - border-radius: 8px 0 0 8px; -} - -.input-number-group button:last-child { - border-radius: 0 8px 8px 0; -} - -/* 价格滑块 */ -.custom-range { - -webkit-appearance: none; - width: 100%; - height: 6px; - border-radius: 3px; - background: #e0e6ed; - outline: none; - margin: 10px 0; -} - -.custom-range::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: #3498db; - cursor: pointer; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - transition: background 0.3s ease; -} - -.custom-range::-webkit-slider-thumb:hover { - background: #2980b9; -} - -/* 标签输入 */ -.tag-input-container { +/* 带按钮输入框 */ +.input-with-button { display: flex; - gap: 10px; + align-items: center; } -.tag-input { +.input-with-button .form-control { + border-top-right-radius: 0; + border-bottom-right-radius: 0; flex-grow: 1; } -.add-tag-btn { - padding: 0.5rem 0.75rem; - border-radius: 8px; +.btn-append { + height: 42px; + padding: 0 14px; + background-color: var(--bg-lightest); + border: 1px solid var(--border-color); + border-left: none; + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); + color: var(--text-medium); + cursor: pointer; + transition: background-color var(--transition-fast); +} + +.btn-append:hover { + background-color: var(--border-color); + color: var(--text-dark); +} + +/* 文本域 */ +textarea.form-control { + min-height: 150px; + resize: vertical; +} + +/* 数字输入控件 */ +.number-control { + display: flex; + align-items: center; + width: 100%; + border-radius: var(--radius-md); + overflow: hidden; +} + +.number-btn { + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--bg-lightest); + border: 1px solid var(--border-color); + color: var(--text-medium); + cursor: pointer; + transition: all var(--transition-fast); + font-size: 1rem; + user-select: none; +} + +.number-btn:hover { + background-color: var(--border-color); + color: var(--text-dark); +} + +.decrement { + border-top-left-radius: var(--radius-md); + border-bottom-left-radius: var(--radius-md); +} + +.increment { + border-top-right-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); +} + +.number-control .form-control { + flex: 1; + border-radius: 0; + border-left: none; + border-right: none; + text-align: center; + padding: 10px 0; +} + +/* 价格输入 */ +.price-input { + position: relative; +} + +.currency-symbol { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--text-medium); +} + +.price-input .form-control { + padding-left: 30px; +} + +.price-slider { + margin-top: 16px; +} + +.range-slider { + -webkit-appearance: none; + width: 100%; + height: 4px; + border-radius: 2px; + background-color: var(--border-color); + outline: none; + margin: 14px 0; +} + +.range-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--primary-color); + cursor: pointer; + border: 2px solid var(--bg-white); + box-shadow: var(--shadow-sm); +} + +.slider-marks { + display: flex; + justify-content: space-between; + font-size: 0.75rem; + color: var(--text-light); +} + +/* ========== 按钮样式 ========== */ +.btn-primary { + padding: 12px 16px; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: var(--radius-md); + font-weight: 500; + font-size: 0.9375rem; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all var(--transition-fast); + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover { + background-color: var(--primary-hover); + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn-primary:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); +} + +.btn-primary:active { + transform: translateY(1px); +} + +.btn-secondary { + padding: 10px 16px; + background-color: var(--bg-white); + color: var(--text-medium); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + font-weight: 500; + font-size: 0.9375rem; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all var(--transition-fast); +} + +.btn-secondary:hover { + background-color: var(--bg-lightest); + color: var(--text-dark); +} + +.btn-secondary:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(226, 232, 240, 0.5); +} + +/* ========== 标签输入样式 ========== */ +.tag-input-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.tag-input-wrapper .form-control { + flex-grow: 1; +} + +.btn-tag-add { + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-color); + border: none; + border-radius: var(--radius-md); + color: white; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-tag-add:hover { + background-color: var(--primary-hover); } .tags-container { @@ -3545,76 +3990,78 @@ File: ./app/static/css/book-form.css flex-wrap: wrap; gap: 8px; margin-top: 12px; + min-height: 32px; } -.tag-item { +.tag { display: inline-flex; align-items: center; - background-color: rgba(52, 152, 219, 0.1); - color: #3498db; - padding: 6px 12px; - border-radius: 20px; - font-size: 14px; - transition: all 0.3s ease; + background-color: var(--primary-light); + border-radius: 50px; + padding: 6px 10px 6px 14px; + font-size: 0.8125rem; + color: var(--primary-color); + transition: all var(--transition-fast); } -.tag-item:hover { - background-color: rgba(52, 152, 219, 0.2); +.tag:hover { + background-color: rgba(59, 130, 246, 0.2); } -.tag-item .remove-tag { - margin-left: 8px; +.tag-text { + margin-right: 6px; +} + +.tag-remove { + background: none; + border: none; + color: var(--primary-color); cursor: pointer; - font-size: 12px; - color: #3498db; + padding: 0; + width: 16px; + height: 16px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + transition: all var(--transition-fast); } -.tag-item .remove-tag:hover { - color: #e74c3c; -} - -/* 文本计数 */ -.text-count { - font-size: 12px; - color: #7f8c8d; -} - -.text-count.text-danger { - color: #e74c3c; +.tag-remove:hover { + background-color: rgba(59, 130, 246, 0.3); + color: white; } /* ========== 封面上传区域 ========== */ .cover-preview-container { display: flex; flex-direction: column; - align-items: center; - gap: 20px; + gap: 16px; } .cover-preview { width: 100%; - height: 320px; - border: 2px dashed #e0e6ed; - border-radius: 10px; + aspect-ratio: 5/7; + background-color: var(--bg-lightest); + border-radius: var(--radius-md); overflow: hidden; - background-color: #f8f9fa; - margin-bottom: 10px; - transition: all 0.3s ease; - display: flex; - justify-content: center; - align-items: center; cursor: pointer; + transition: all var(--transition-fast); +} + +.cover-preview:hover { + background-color: var(--bg-light); } .cover-preview.dragover { - border-color: #3498db; - background-color: rgba(52, 152, 219, 0.05); + background-color: var(--primary-light); } .cover-image { width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; } .no-cover-placeholder { @@ -3624,165 +4071,188 @@ File: ./app/static/css/book-form.css flex-direction: column; justify-content: center; align-items: center; - color: #95a5a6; - padding: 20px; + color: var(--text-light); + padding: 24px; text-align: center; } .no-cover-placeholder i { - font-size: 64px; - margin-bottom: 20px; - color: #d0d0d0; + font-size: 48px; + margin-bottom: 16px; + color: var(--text-muted); } .placeholder-tip { - font-size: 13px; - margin-top: 10px; - color: #7f8c8d; + font-size: 0.8125rem; + margin-top: 8px; + color: var(--text-muted); } .upload-options { - width: 100%; - padding: 0 20px; + display: flex; + flex-direction: column; + gap: 12px; } .upload-btn-group { display: flex; - gap: 10px; + gap: 8px; } -.custom-upload-btn { +.btn-upload { flex-grow: 1; - padding: 10px 20px; - border-radius: 8px; - font-weight: 500; - transition: all 0.3s ease; - background-color: #3498db; - border-color: #3498db; + padding: 10px 16px; + background-color: var(--primary-color); color: white; -} - -.custom-upload-btn:hover { - background-color: #2980b9; - border-color: #2980b9; - transform: translateY(-2px); - box-shadow: 0 4px 10px rgba(52, 152, 219, 0.3); -} - -.btn-icon { - width: 42px; - padding: 0; + border: none; + border-radius: var(--radius-md); + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; display: flex; align-items: center; justify-content: center; + gap: 8px; + transition: all var(--transition-fast); +} + +.btn-upload:hover { + background-color: var(--primary-hover); +} + +.btn-remove { + width: 42px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--bg-white); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: var(--text-medium); + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-remove:hover { + background-color: #fee2e2; + border-color: #fca5a5; + color: #ef4444; +} + +.upload-tips { + text-align: center; + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.5; } /* ========== 表单提交区域 ========== */ -.form-submit-container { - margin-top: 30px; -} - -.action-buttons { +.form-actions { display: flex; flex-direction: column; - gap: 15px; + gap: 16px; } -.secondary-buttons { - display: flex; - gap: 10px; -} - -.submit-btn { - padding: 15px; - border-radius: 8px; - font-weight: 600; - background: linear-gradient(45deg, #3498db, #2980b9); - border: none; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.submit-btn:hover { - transform: translateY(-3px); - box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4); -} - -.submit-btn:active { - transform: translateY(-1px); -} - -.reset-btn { - padding: 12px; - border-radius: 8px; - font-weight: 500; +.secondary-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; } .form-tip { - margin-top: 15px; - font-size: 13px; - color: #7f8c8d; + margin-top: 8px; + font-size: 0.8125rem; + color: var(--text-muted); text-align: center; } .form-tip i { - color: #3498db; - margin-right: 5px; + color: var(--info-color); + margin-right: 4px; } /* 必填项标记 */ .required { - color: #e74c3c; + color: var(--danger-color); margin-left: 4px; - font-weight: bold; } /* 无效输入状态 */ .is-invalid { - border-color: #e74c3c !important; + border-color: var(--danger-color) !important; } .invalid-feedback { display: block; - color: #e74c3c; - font-size: 13px; - margin-top: 5px; + color: var(--danger-color); + font-size: 0.8125rem; + margin-top: 6px; +} + +/* ========== Select2 定制 ========== */ +.select2-container--classic .select2-selection--single { + height: 42px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background-color: var(--bg-white); +} + +.select2-container--classic .select2-selection--single .select2-selection__rendered { + line-height: 40px; + color: var(--text-dark); + padding-left: 14px; +} + +.select2-container--classic .select2-selection--single .select2-selection__arrow { + height: 40px; + border-left: 1px solid var(--border-color); +} + +.select2-container--classic .select2-selection--single:focus { + border-color: var(--border-focus); + outline: 0; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } /* ========== 模态框样式 ========== */ .modal-content { border: none; - border-radius: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); overflow: hidden; } .modal-header { - background-color: #f8f9fa; - border-bottom: 1px solid #edf2f7; - padding: 15px 20px; + background-color: var(--bg-white); + border-bottom: 1px solid var(--border-color); + padding: 16px 20px; } .modal-title { font-weight: 600; - color: #2c3e50; + color: var(--text-dark); + font-size: 1.125rem; } .modal-body { - padding: 25px; + padding: 20px; } .modal-footer { - border-top: 1px solid #edf2f7; - padding: 15px 20px; - justify-content: space-between; + border-top: 1px solid var(--border-color); + padding: 16px 20px; +} + +.modal-btn { + min-width: 100px; } /* 裁剪模态框 */ .img-container { max-height: 500px; overflow: hidden; + margin-bottom: 20px; } #cropperImage { @@ -3792,106 +4262,232 @@ File: ./app/static/css/book-form.css .cropper-controls { display: flex; - gap: 10px; + justify-content: center; + gap: 20px; + margin-top: 16px; +} + +.control-group { + display: flex; + gap: 8px; +} + +.control-btn { + width: 40px; + height: 40px; + border-radius: var(--radius-md); + background-color: var(--bg-lightest); + border: 1px solid var(--border-color); + color: var(--text-medium); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--transition-fast); +} + +.control-btn:hover { + background-color: var(--border-color); + color: var(--text-dark); } /* 图书预览模态框 */ -.book-preview-container { - padding: 10px; +.preview-header { + background-color: var(--bg-white); + border-bottom: 1px solid var(--border-color); +} + +.preview-body { + padding: 0; + background-color: var(--bg-lightest); +} + +/* 添加到你的CSS文件中 */ +.book-preview { + display: flex; + flex-direction: row; + gap: 20px; +} + +.preview-cover-section { + flex: 0 0 200px; +} + +.preview-details-section { + flex: 1; } .book-preview-cover { - width: 100%; - height: 300px; - border-radius: 8px; + height: 280px; + width: 200px; overflow: hidden; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); - background-color: #f8f9fa; - margin-bottom: 20px; -} - -.book-preview-cover img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.book-preview-details { - padding: 10px 0; -} - -.book-preview-details h3 { - margin: 0 0 5px 0; - font-weight: 600; - color: #2c3e50; -} - -.book-author { - color: #7f8c8d; - font-size: 16px; - margin-bottom: 20px; -} - -.book-info-section { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - margin-bottom: 20px; -} - -.book-info-item { + border-radius: 4px; + border: 1px solid #ddd; display: flex; - flex-direction: column; + align-items: center; + justify-content: center; + background: #f8f9fa; } -.info-label { - font-size: 13px; - color: #7f8c8d; - margin-bottom: 3px; -} - -.info-value { - font-weight: 500; - color: #34495e; -} - -.book-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 20px; +.preview-cover-img { + max-width: 100%; + max-height: 100%; + object-fit: contain; } .preview-tag { display: inline-block; - background-color: rgba(52, 152, 219, 0.1); - color: #3498db; - padding: 5px 10px; - border-radius: 20px; - font-size: 13px; + background: #e9ecef; + color: #495057; + padding: 3px 8px; + border-radius: 12px; + font-size: 12px; + margin-right: 5px; + margin-bottom: 5px; } -.book-description { - background-color: #f8f9fa; - border-radius: 8px; - padding: 20px; +.book-tags-preview { + margin: 15px 0; +} + +.book-description-preview { margin-top: 20px; } -.book-description h4 { - margin-top: 0; +.section-title { + font-size: 16px; + margin-bottom: 10px; + color: #495057; + border-bottom: 1px solid #dee2e6; + padding-bottom: 5px; +} + +.book-meta { + margin-top: 10px; + text-align: center; +} + +.book-price { + font-size: 18px; + font-weight: bold; + color: #dc3545; +} + +.book-stock { + font-size: 14px; + color: #6c757d; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .book-preview { + flex-direction: column; + } + + .preview-cover-section { + margin: 0 auto; + } +} + +.preview-details-section { + padding: 24px; +} + +.book-title { + font-size: 1.5rem; font-weight: 600; - color: #2c3e50; - margin-bottom: 15px; + color: var(--text-dark); + margin: 0 0 8px 0; } -.book-description p { - color: #34495e; +.book-author { + color: var(--text-medium); + font-size: 1rem; + margin-bottom: 24px; +} + +.book-info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + background-color: var(--bg-white); + border-radius: var(--radius-md); + padding: 16px; + box-shadow: var(--shadow-sm); + margin-bottom: 24px; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.info-label { + font-size: 0.75rem; + color: var(--text-light); + text-transform: uppercase; +} + +.info-value { + font-weight: 500; + color: var(--text-dark); +} + +.book-tags-preview { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 24px; +} + +.preview-tag { + display: inline-block; + background-color: var(--primary-light); + color: var(--primary-color); + padding: 4px 12px; + border-radius: 50px; + font-size: 0.8125rem; +} + +.no-tags { + font-size: 0.875rem; + color: var(--text-muted); +} + +.book-description-preview { + background-color: var(--bg-white); + border-radius: var(--radius-md); + padding: 16px; + box-shadow: var(--shadow-sm); +} + +.section-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-dark); + margin: 0 0 12px 0; +} + +.description-content { + font-size: 0.9375rem; + color: var(--text-medium); line-height: 1.6; - margin: 0; } -/* ========== 自定义通知样式 ========== */ +.placeholder-text { + color: var(--text-muted); + font-style: italic; +} + +.preview-footer { + background-color: var(--bg-white); + display: flex; + justify-content: flex-end; + gap: 12px; +} + +/* ========== 通知样式 ========== */ .notification-container { position: fixed; top: 20px; @@ -3899,61 +4495,55 @@ File: ./app/static/css/book-form.css z-index: 9999; display: flex; flex-direction: column; - gap: 10px; - max-width: 350px; + gap: 12px; + max-width: 320px; } -.custom-notification { - background: white; - border-radius: 10px; - padding: 15px; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +.notification { + background-color: var(--bg-white); + border-radius: var(--radius-md); + padding: 12px 16px; + box-shadow: var(--shadow-md); display: flex; align-items: center; + gap: 12px; animation-duration: 0.5s; - border-left: 4px solid #3498db; } -.notification-success { - border-color: #2ecc71; +.success-notification { + border-left: 4px solid var(--success-color); } -.notification-error { - border-color: #e74c3c; +.error-notification { + border-left: 4px solid var(--danger-color); } -.notification-warning { - border-color: #f39c12; +.warning-notification { + border-left: 4px solid var(--warning-color); } -.notification-info { - border-color: #3498db; +.info-notification { + border-left: 4px solid var(--info-color); } .notification-icon { - width: 30px; - height: 30px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-right: 15px; + color: var(--text-light); } -.notification-success .notification-icon i { - color: #2ecc71; +.success-notification .notification-icon { + color: var(--success-color); } -.notification-error .notification-icon i { - color: #e74c3c; +.error-notification .notification-icon { + color: var(--danger-color); } -.notification-warning .notification-icon i { - color: #f39c12; +.warning-notification .notification-icon { + color: var(--warning-color); } -.notification-info .notification-icon i { - color: #3498db; +.info-notification .notification-icon { + color: var(--info-color); } .notification-content { @@ -3962,33 +4552,33 @@ File: ./app/static/css/book-form.css .notification-content p { margin: 0; - font-size: 14px; - color: #2c3e50; + font-size: 0.875rem; + color: var(--text-dark); } .notification-close { background: none; border: none; - color: #bdc3c7; + color: var(--text-muted); cursor: pointer; padding: 5px; - transition: color 0.3s ease; + transition: color var(--transition-fast); } .notification-close:hover { - color: #7f8c8d; + color: var(--text-medium); } /* ========== 动画效果 ========== */ @keyframes pulse { 0% { - box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.4); + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } 70% { - box-shadow: 0 0 0 10px rgba(52, 152, 219, 0); + box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); } 100% { - box-shadow: 0 0 0 0 rgba(52, 152, 219, 0); + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } } @@ -3996,18 +4586,11 @@ File: ./app/static/css/book-form.css animation: pulse 2s infinite; } -.pulse-icon { - animation: pulse 2s infinite; -} - /* ========== 响应式样式 ========== */ @media (max-width: 1200px) { - .book-form-container { - padding: 20px; - } - - .cover-preview { - height: 280px; + .form-grid { + grid-template-columns: 1fr 320px; + gap: 20px; } } @@ -4015,7 +4598,7 @@ File: ./app/static/css/book-form.css .page-header { flex-direction: column; align-items: flex-start; - gap: 15px; + gap: 16px; } .header-actions { @@ -4023,47 +4606,76 @@ File: ./app/static/css/book-form.css justify-content: space-between; } - .book-info-section { + .form-grid { grid-template-columns: 1fr; } + + .book-preview { + grid-template-columns: 1fr; + } + + .preview-cover-section { + border-right: none; + border-bottom: 1px solid var(--border-color); + padding-bottom: 24px; + } + + .book-preview-cover { + max-width: 240px; + margin: 0 auto; + } } @media (max-width: 768px) { .book-form-container { - padding: 15px 10px; + padding: 16px 12px; } - .page-title { - font-size: 24px; - } - - .subtitle { - font-size: 14px; - } - - .card-body { - padding: 20px 15px; - } - - .cover-preview { - height: 240px; - } - - .secondary-buttons { - flex-direction: column; + .page-header { + padding: 20px; } .form-row { - margin-right: -5px; - margin-left: -5px; + grid-template-columns: 1fr; + gap: 12px; } - .form-group { - padding-right: 5px; - padding-left: 5px; + .secondary-actions { + grid-template-columns: 1fr; + } + + .card-body { + padding: 16px; + } + + .book-info-grid { + grid-template-columns: 1fr; } } - +.cover-preview { + min-height: 250px; + width: 100%; + border: 1px dashed #ccc; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} +.cover-preview img.cover-image { + max-width: 100%; + max-height: 300px; + object-fit: contain; +} +.img-container { + max-height: 500px; + overflow: auto; +} +#cropperImage { + max-width: 100%; + display: block; +} ================================================================================ File: ./app/static/css/categories.css ================================================================================ @@ -4137,6 +4749,1332 @@ File: ./app/static/css/categories.css } } +================================================================================ +File: ./app/static/css/user-edit.css +================================================================================ + +/* 用户编辑页面样式 */ +.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; + } +} + +================================================================================ +File: ./app/static/css/user-profile.css +================================================================================ + +/* 用户个人中心页面样式 */ +.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; + } +} + +================================================================================ +File: ./app/static/css/user-roles.css +================================================================================ + +/* 角色管理页面样式 */ +.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; + } +} + +================================================================================ +File: ./app/static/js/user-edit.js +================================================================================ + +// 用户编辑页面交互 +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); + } +}); + +================================================================================ +File: ./app/static/js/user-roles.js +================================================================================ + +// 角色管理页面交互 +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) { + // 实际应用中应从后端保存 + // fetch('/user/role/save', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // 'X-Requested-With': 'XMLHttpRequest' + // }, + // body: JSON.stringify(roleData) + // }) + // .then(response => response.json()) + // .then(data => { + // if (data.success) { + // showAlert(data.message, 'success'); + // setTimeout(() => { + // window.location.reload(); + // }, 1500); + // } else { + // showAlert(data.message, 'error'); + // } + // }); + + // 模拟成功响应 + setTimeout(() => { + showAlert('角色保存成功!', 'success'); + setTimeout(() => { + window.location.reload(); + }, 1500); + }, 500); + } + + // 删除角色 + function deleteRole(roleId) { + // 实际应用中应从后端删除 + // fetch(`/user/role/delete/${roleId}`, { + // method: 'POST', + // headers: { + // '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'); + // } + // }); + + // 模拟成功响应 + setTimeout(() => { + showAlert('角色删除成功!', 'success'); + setTimeout(() => { + window.location.reload(); + }, 1500); + }, 500); + } + + // 获取角色用户数量 + function fetchRoleUserCounts() { + const roleCards = document.querySelectorAll('.role-card'); + + roleCards.forEach(card => { + const roleId = card.getAttribute('data-id'); + const countElement = document.getElementById(`userCount-${roleId}`); + + // 实际应用中应从后端获取 + // fetch(`/api/role/${roleId}/user-count`) + // .then(response => response.json()) + // .then(data => { + // countElement.textContent = data.count; + // }); + + // 模拟数据 + setTimeout(() => { + const count = roleId == 1 ? 1 : (roleId == 2 ? 42 : Math.floor(Math.random() * 10)); + if (countElement) countElement.textContent = count; + }, 300); + }); + } + + // 显示通知 + 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); + } +}); + +================================================================================ +File: ./app/static/js/user-list.js +================================================================================ + +// 用户列表页面交互 +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); +} + ================================================================================ File: ./app/static/js/main.js ================================================================================ @@ -4481,6 +6419,935 @@ document.addEventListener('DOMContentLoaded', function() { }, 3000); } }); +================================================================================ +File: ./app/static/js/book-add.js +================================================================================ + +/** + * 图书添加页面脚本 + * 处理图书表单的交互、验证和预览功能 + */ +let isSubmitting = false; +$(document).ready(function() { + // 全局变量 + let cropper; + let coverBlob; + let tags = []; + const coverPreview = $('#coverPreview'); + const coverInput = $('#cover'); + const tagInput = $('#tagInput'); + const tagsContainer = $('#tagsContainer'); + const tagsHiddenInput = $('#tags'); + + // 初始化函数 + function initialize() { + initSelect2(); + initFormProgress(); + initTagsFromInput(); + initCoverHandlers(); + initNumberControls(); + initPriceSlider(); + initCharCounter(); + initFormValidation(); + attachEventListeners(); + } + + // ========== 组件初始化 ========== + + // 初始化Select2 + function initSelect2() { + $('.select2').select2({ + placeholder: "选择分类...", + allowClear: true, + theme: "classic", + width: '100%' + }); + } + + // 初始化表单进度条 + function initFormProgress() { + updateFormProgress(); + $('input, textarea, select').on('change keyup', function() { + updateFormProgress(); + }); + } + + // 初始化标签(从隐藏输入字段) + function initTagsFromInput() { + const tagsValue = $('#tags').val(); + if (tagsValue) { + tags = tagsValue.split(','); + renderTags(); + } + } + + // 初始化封面处理 + function initCoverHandlers() { + // 拖放上传功能 + coverPreview.on('dragover', function(e) { + e.preventDefault(); + $(this).addClass('dragover'); + }).on('dragleave drop', function(e) { + e.preventDefault(); + $(this).removeClass('dragover'); + }).on('drop', function(e) { + e.preventDefault(); + const file = e.originalEvent.dataTransfer.files[0]; + if (file && file.type.match('image.*')) { + coverInput[0].files = e.originalEvent.dataTransfer.files; + coverInput.trigger('change'); + } + }).on('click', function() { + if (!$(this).find('img').length) { + coverInput.click(); + } + }); + + // 重置页面加载完后的字符计数 + if ($('#description').val()) { + $('#charCount').text($('#description').val().length); + } + } + + // 初始化数字控制 + function initNumberControls() { + $('#stockDecrement').on('click', function() { + const input = $('#stock'); + const value = parseInt(input.val()); + if (value > parseInt(input.attr('min'))) { + input.val(value - 1).trigger('change'); + } + }); + + $('#stockIncrement').on('click', function() { + const input = $('#stock'); + const value = parseInt(input.val()); + input.val(value + 1).trigger('change'); + }); + } + + // 初始化价格滑块 + function initPriceSlider() { + $('#priceRange').on('input', function() { + $('#price').val($(this).val()); + }); + + $('#price').on('input', function() { + const value = parseFloat($(this).val()) || 0; + $('#priceRange').val(Math.min(value, 500)); + }); + } + + // 初始化字符计数器 + function initCharCounter() { + $('#description').on('input', function() { + const count = $(this).val().length; + $('#charCount').text(count); + if (count > 2000) { + $('#charCount').addClass('text-danger'); + } else { + $('#charCount').removeClass('text-danger'); + } + }); + } + + // 初始化表单验证 + ffunction initFormValidation() { + $('#bookForm').on('submit', function(e) { + // 如果表单正在提交中,阻止重复提交 + if (isSubmitting) { + e.preventDefault(); + showNotification('表单正在提交中,请勿重复点击', 'warning'); + return false; + } + let isValid = true; + $('[required]').each(function() { + if (!$(this).val().trim()) { + isValid = false; + $(this).addClass('is-invalid'); + // 添加错误提示 + if (!$(this).next('.invalid-feedback').length) { + $(this).after(`
    此字段不能为空
    `); + } + } else { + $(this).removeClass('is-invalid').next('.invalid-feedback').remove(); + } + }); + // 验证ISBN格式(如果已填写) + const isbn = $('#isbn').val().trim(); + if (isbn) { + // 移除所有非数字、X和x字符后检查 + const cleanIsbn = isbn.replace(/[^0-9Xx]/g, ''); + const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/; + if (!isbnRegex.test(cleanIsbn)) { + isValid = false; + $('#isbn').addClass('is-invalid'); + if (!$('#isbn').next('.invalid-feedback').length) { + $('#isbn').after(`
    ISBN格式不正确,应为10位或13位
    `); + } + } + } + if (!isValid) { + e.preventDefault(); + // 滚动到第一个错误字段 + $('html, body').animate({ + scrollTop: $('.is-invalid:first').offset().top - 100 + }, 500); + showNotification('请正确填写所有标记的字段', 'error'); + } else { + // 设置表单锁定状态 + isSubmitting = true; + + // 修改提交按钮样式 + const submitBtn = $(this).find('button[type="submit"]'); + const originalHtml = submitBtn.html(); + submitBtn.prop('disabled', true) + .html(' 保存中...'); + + // 显示提交中通知 + showNotification('表单提交中...', 'info'); + + // 如果表单提交时间过长,30秒后自动解锁 + setTimeout(function() { + if (isSubmitting) { + isSubmitting = false; + submitBtn.prop('disabled', false).html(originalHtml); + showNotification('提交超时,请重试', 'warning'); + } + }, 30000); + } + }); + // 输入时移除错误样式 + $('input, textarea, select').on('input change', function() { + $(this).removeClass('is-invalid').next('.invalid-feedback').remove(); + }); + } + // 还需要在服务端处理成功后重置状态 + // 在页面加载完成时,添加监听服务器重定向事件 + $(window).on('pageshow', function(event) { + if (event.originalEvent.persisted || + (window.performance && window.performance.navigation.type === 2)) { + // 如果页面是从缓存加载的或通过后退按钮回到的 + isSubmitting = false; + $('button[type="submit"]').prop('disabled', false) + .html(' 保存图书'); + } + }); + + // 绑定事件监听器 + function attachEventListeners() { + // 文件选择处理 + coverInput.on('change', handleCoverSelect); + + // 裁剪控制 + $('#rotateLeft').on('click', function() { cropper && cropper.rotate(-90); }); + $('#rotateRight').on('click', function() { cropper && cropper.rotate(90); }); + $('#zoomIn').on('click', function() { cropper && cropper.zoom(0.1); }); + $('#zoomOut').on('click', function() { cropper && cropper.zoom(-0.1); }); + $('#cropImage').on('click', applyCrop); + $('#removeCover').on('click', removeCover); + + // 标签处理 + tagInput.on('keydown', handleTagKeydown); + $('#addTagBtn').on('click', addTag); + $(document).on('click', '.tag-remove', removeTag); + + // ISBN查询 + $('#isbnLookup').on('click', lookupISBN); + + // 预览按钮 + $('#previewBtn').on('click', showPreview); + + // 表单重置 + $('#resetBtn').on('click', confirmReset); + } + + // ========== 功能函数 ========== + + // 更新表单进度条 + function updateFormProgress() { + const requiredFields = $('[required]'); + const filledFields = requiredFields.filter(function() { + return $(this).val() !== ''; + }); + + const otherFields = $('input:not([required]), textarea:not([required]), select:not([required])').not('[type="file"]'); + const filledOtherFields = otherFields.filter(function() { + return $(this).val() !== ''; + }); + + let requiredWeight = 70; // 必填字段权重70% + let otherWeight = 30; // 非必填字段权重30% + + let requiredProgress = requiredFields.length ? (filledFields.length / requiredFields.length) * requiredWeight : requiredWeight; + let otherProgress = otherFields.length ? (filledOtherFields.length / otherFields.length) * otherWeight : 0; + + let totalProgress = Math.floor(requiredProgress + otherProgress); + + $('#formProgress').css('width', totalProgress + '%').attr('aria-valuenow', totalProgress); + $('#progressText').text('完成 ' + totalProgress + '%'); + + if (totalProgress >= 100) { + $('.btn-primary').addClass('pulse'); + } else { + $('.btn-primary').removeClass('pulse'); + } + } + + // 处理封面选择 + function handleCoverSelect(e) { + const file = e.target.files[0]; + if (!file) return; + // 验证文件类型 + if (!file.type.match('image.*')) { + showNotification('请选择图片文件', 'warning'); + return; + } + // 验证文件大小(最大5MB) + if (file.size > 5 * 1024 * 1024) { + showNotification('图片大小不能超过5MB', 'warning'); + return; + } + const reader = new FileReader(); + reader.onload = function(e) { + // 先显示在预览框中,确保用户能立即看到上传的图片 + coverPreview.html(`图书封面预览`); + + // 准备裁剪图片 + $('#cropperImage').attr('src', e.target.result); + + // 确保图片加载完成后再显示模态框 + $('#cropperImage').on('load', function() { + // 打开模态框 + $('#cropperModal').modal('show'); + + // 在模态框完全显示后初始化裁剪器 + $('#cropperModal').on('shown.bs.modal', function() { + if (cropper) { + cropper.destroy(); + } + + try { + cropper = new Cropper(document.getElementById('cropperImage'), { + aspectRatio: 5 / 7, + viewMode: 2, + responsive: true, + guides: true, + background: true, + ready: function() { + console.log('Cropper初始化成功'); + } + }); + } catch (err) { + console.error('Cropper初始化失败:', err); + showNotification('图片处理工具初始化失败,请重试', 'error'); + } + }); + }); + }; + + // 处理读取错误 + reader.onerror = function() { + showNotification('读取图片失败,请重试', 'error'); + }; + + reader.readAsDataURL(file); + } + // 应用裁剪 + function applyCrop() { + if (!cropper) { + showNotification('图片处理工具未就绪,请重新上传', 'error'); + $('#cropperModal').modal('hide'); + return; + } + + try { + const canvas = cropper.getCroppedCanvas({ + width: 500, + height: 700, + fillColor: '#fff', + imageSmoothingEnabled: true, + imageSmoothingQuality: 'high', + }); + if (!canvas) { + throw new Error('无法生成裁剪后的图片'); + } + canvas.toBlob(function(blob) { + if (!blob) { + showNotification('图片处理失败,请重试', 'error'); + return; + } + + const url = URL.createObjectURL(blob); + coverPreview.html(`图书封面`); + coverBlob = blob; + // 模拟File对象 + const fileList = new DataTransfer(); + const file = new File([blob], "cover.jpg", {type: "image/jpeg"}); + fileList.items.add(file); + document.getElementById('cover').files = fileList.files; + $('#cropperModal').modal('hide'); + showNotification('封面图片已更新', 'success'); + }, 'image/jpeg', 0.95); + } catch (err) { + console.error('裁剪失败:', err); + showNotification('图片裁剪失败,请重试', 'error'); + $('#cropperModal').modal('hide'); + } + } + + // 移除封面 + function removeCover() { + coverPreview.html(` +
    + + 暂无封面 +

    点击上传或拖放图片至此处

    +
    + `); + coverInput.val(''); + coverBlob = null; + } + + // 渲染标签 + function renderTags() { + tagsContainer.empty(); + tags.forEach(tag => { + tagsContainer.append(` +
    + ${tag} + +
    + `); + }); + tagsHiddenInput.val(tags.join(',')); + } + + // 添加标签 + function addTag() { + const tag = tagInput.val().trim(); + if (tag && !tags.includes(tag)) { + tags.push(tag); + renderTags(); + tagInput.val('').focus(); + } + } + + // 处理标签输入键盘事件 + function handleTagKeydown(e) { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + addTag(); + } + } + + // 移除标签 + function removeTag() { + const tagToRemove = $(this).data('tag'); + tags = tags.filter(t => t !== tagToRemove); + renderTags(); + } + + // ISBN查询 + function lookupISBN() { + const isbn = $('#isbn').val().trim(); + if (!isbn) { + showNotification('请先输入ISBN', 'warning'); + return; + } + + // 验证ISBN格式 + const cleanIsbn = isbn.replace(/[^0-9Xx]/g, ''); + const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/; + if (!isbnRegex.test(cleanIsbn)) { + showNotification('ISBN格式不正确,应为10位或13位', 'warning'); + return; + } + + $(this).html(''); + + // 先检查ISBN是否已存在 + $.get('/book/api/check-isbn', {isbn: isbn}, function(data) { + if (data.exists) { + $('#isbnLookup').html(''); + showNotification(`ISBN "${isbn}" 已存在: 《${data.book_title}》`, 'warning'); + $('#isbn').addClass('is-invalid'); + if (!$('#isbn').next('.invalid-feedback').length) { + $('#isbn').after(`
    此ISBN已被图书《${data.book_title}》使用
    `); + } + } else { + // 继续查询外部API(模拟) + simulateISBNLookup(isbn); + } + }).fail(function() { + $('#isbnLookup').html(''); + showNotification('服务器查询失败,请稍后再试', 'error'); + }); + } + + // 模拟ISBN查询 + function simulateISBNLookup(isbn) { + // 模拟API查询延迟 + setTimeout(() => { + // 模拟查到的数据 + if (isbn === '9787020002207') { + $('#title').val('红楼梦').trigger('blur'); + $('#author').val('曹雪芹').trigger('blur'); + $('#publisher').val('人民文学出版社').trigger('blur'); + $('#publish_year').val('1996').trigger('blur'); + $('#category_id').val('1').trigger('change'); + tags = ['中国文学', '古典', '名著']; + renderTags(); + $('#description').val('《红楼梦》是中国古代章回体长篇小说,中国古典四大名著之一,通行本共120回,一般认为前80回是清代作家曹雪芹所著,后40回作者有争议。小说以贾、史、王、薛四大家族的兴衰为背景,以贾府的家庭琐事、闺阁闲情为脉络,以贾宝玉、林黛玉、薛宝钗的爱情婚姻悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋有情人的人性美和悲剧美。').trigger('input'); + $('#price').val('59.70').trigger('input'); + $('#priceRange').val('59.70'); + + showNotification('ISBN查询成功', 'success'); + } else if (isbn === '9787544270878') { + $('#title').val('挪威的森林').trigger('blur'); + $('#author').val('村上春树').trigger('blur'); + $('#publisher').val('南海出版社').trigger('blur'); + $('#publish_year').val('2017').trigger('blur'); + $('#category_id').val('2').trigger('change'); + tags = ['外国文学', '日本', '小说']; + renderTags(); + $('#description').val('《挪威的森林》是日本作家村上春树创作的长篇小说,首次出版于1987年。小说讲述了一个悲伤的爱情故事,背景设定在20世纪60年代末的日本。主人公渡边纠缠在与平静的直子和开朗的绿子两人的感情中,最终选择了生活。').trigger('input'); + $('#price').val('39.50').trigger('input'); + $('#priceRange').val('39.50'); + + showNotification('ISBN查询成功', 'success'); + } else { + showNotification('未找到相关图书信息', 'warning'); + } + + $('#isbnLookup').html(''); + updateFormProgress(); + }, 1500); + } + + // 显示预览 + function showPreview() { + // 检查必填字段 + if (!$('#title').val().trim() || !$('#author').val().trim()) { + showNotification('请至少填写书名和作者后再预览', 'warning'); + return; + } + try { + // 确保所有值都有默认值,防止undefined错误 + const title = $('#title').val() || '未填写标题'; + const author = $('#author').val() || '未填写作者'; + const publisher = $('#publisher').val() || '-'; + const isbn = $('#isbn').val() || '-'; + const publishYear = $('#publish_year').val() || '-'; + const description = $('#description').val() || ''; + const stock = $('#stock').val() || '0'; + let price = parseFloat($('#price').val()) || 0; + + // 填充预览内容 + $('#previewTitle').text(title); + $('#previewAuthor').text(author ? '作者: ' + author : '未填写作者'); + $('#previewPublisher').text(publisher); + $('#previewISBN').text(isbn); + $('#previewYear').text(publishYear); + // 获取分类文本 + const categoryId = $('#category_id').val(); + const categoryText = categoryId ? $('#category_id option:selected').text() : '-'; + $('#previewCategory').text(categoryText); + // 价格和库存 + $('#previewPrice').text(price ? '¥' + price.toFixed(2) : '¥0.00'); + $('#previewStock').text('库存: ' + stock); + // 标签 + const previewTags = $('#previewTags'); + previewTags.empty(); + if (tags && tags.length > 0) { + tags.forEach(tag => { + previewTags.append(`${tag}`); + }); + } else { + previewTags.append('暂无标签'); + } + // 描述 + if (description) { + $('#previewDescription').html(`

    ${description.replace(/\n/g, '
    ')}

    `); + } else { + $('#previewDescription').html(`

    暂无简介内容

    `); + } + // 封面 + const previewCover = $('#previewCover'); + previewCover.empty(); // 清空现有内容 + + if ($('#coverPreview img').length) { + const coverSrc = $('#coverPreview img').attr('src'); + previewCover.html(`封面预览`); + } else { + previewCover.html(` +
    + + 暂无封面 +
    + `); + } + // 显示预览模态框 + $('#previewModal').modal('show'); + + console.log('预览模态框已显示'); + } catch (err) { + console.error('生成预览时发生错误:', err); + showNotification('生成预览时出错,请重试', 'error'); + } + } + + // 确认重置表单 + function confirmReset() { + if (confirm('确定要重置表单吗?所有已填写的内容将被清空。')) { + $('#bookForm')[0].reset(); + removeCover(); + tags = []; + renderTags(); + updateFormProgress(); + $('.select2').val(null).trigger('change'); + $('#charCount').text('0'); + + showNotification('表单已重置', 'info'); + } + } + + // 通知提示函数 + function showNotification(message, type) { + // 创建通知元素 + const notification = $(` +
    +
    + +
    +
    +

    ${message}

    +
    + +
    + `); + + // 添加到页面 + if ($('.notification-container').length === 0) { + $('body').append('
    '); + } + $('.notification-container').append(notification); + + // 自动关闭 + setTimeout(() => { + notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); + setTimeout(() => { + notification.remove(); + }, 500); + }, 5000); + + // 点击关闭 + notification.find('.notification-close').on('click', function() { + notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); + setTimeout(() => { + notification.remove(); + }, 500); + }); + } + + function getIconForType(type) { + switch(type) { + case 'success': return 'fa-check-circle'; + case 'warning': return 'fa-exclamation-triangle'; + case 'error': return 'fa-times-circle'; + case 'info': + default: return 'fa-info-circle'; + } + } + + // 初始化页面 + initialize(); +}); + +================================================================================ +File: ./app/static/js/user-profile.js +================================================================================ + +// 用户个人中心页面交互 +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 = ` +
    +
    + Loading... +
    +

    加载中...

    +
    + `; + + // 实际应用中应当从后端获取 + // 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 = '
    暂无活动记录
    '; + 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 += ` +
    +
    + +
    +
    +
    +
    ${activity.title}
    +
    ${activity.time}
    +
    +
    + ${activity.details} + ${activity.ip ? `
    IP: ${activity.ip}
    ` : ''} +
    +
    +
    + `; + }); + + container.innerHTML = timelineHTML; + } +}); + ================================================================================ File: ./app/static/js/book-list.js ================================================================================ @@ -4527,7 +7394,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'); @@ -4547,21 +7414,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'); @@ -4949,6 +7823,7 @@ File: ./app/templates/base.html {% block title %}图书管理系统{% endblock %} + @@ -4978,7 +7853,10 @@ File: ./app/templates/base.html {% if current_user.role_id == 1 %}
  • - 用户管理 + 用户管理 +
  • +
  • + 角色管理
  • 图书管理 @@ -5021,7 +7899,7 @@ File: ./app/templates/base.html {{ '管理员' if current_user.role_id == 1 else '普通用户' }}
  • @@ -5039,7 +7917,8 @@ File: ./app/templates/base.html
    - + + +{% endblock %} + +================================================================================ +File: ./app/templates/user/list.html +================================================================================ + +{% extends "base.html" %} + +{% block title %}用户管理 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + + + +
    +
    +
    + + +
    + + + + + + 重置 +
    +
    +
    +
    + + +
    + + + + + + + + + + + + + + + + {% for user in pagination.items %} + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
    ID用户名昵称邮箱手机号角色状态注册时间操作
    {{ user.id }}{{ user.username }}{{ user.nickname or '-' }}{{ user.email or '-' }}{{ user.phone or '-' }} + {% for role in roles %} + {% if role.id == user.role_id %} + {{ role.role_name }} + {% endif %} + {% endfor %} + + + {{ '正常' if user.status == 1 else '禁用' }} + + {{ user.created_at }} + + + + + {% if user.id != session.get('user_id') %} + {% if user.status == 1 %} + + {% else %} + + {% endif %} + + + {% else %} + (当前用户) + {% endif %} +
    暂无用户数据
    +
    + + + {% if pagination.pages > 1 %} +
    +
      + {% if pagination.has_prev %} +
    • + + + +
    • + {% 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 %} +
    • + {{ page }} +
    • + {% else %} +
    • + {{ page }} +
    • + {% endif %} + {% else %} +
    • + ... +
    • + {% endif %} + {% endfor %} + + {% if pagination.has_next %} +
    • + + + +
    • + {% endif %} +
    +
    + {% endif %} +
    + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} + + +================================================================================ +File: ./app/templates/user/edit.html +================================================================================ + +{% extends "base.html" %} + +{% block title %}编辑用户 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + +
    +
    + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
    {{ message }}
    + {% endfor %} + {% endif %} + {% endwith %} + +
    +
    + +
    +
    + + + 用户名不可修改 +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + +
    + + + 留空表示不修改密码 +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    + + + 取消 + +
    +
    +
    +
    +
    + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +================================================================================ +File: ./app/templates/user/roles.html +================================================================================ + +{% extends "base.html" %} + +{% block title %}角色管理 - 图书管理系统{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
    + + + + +
    + {% for role in roles %} +
    +
    +

    {{ role.role_name }}

    +
    + + {% if role.id not in [1, 2] %} + + {% endif %} +
    +
    +
    + {% if role.description %} + {{ role.description }} + {% else %} + 暂无描述 + {% endif %} +
    +
    +
    + -- 用户 +
    + {% if role.id == 1 %} +
    管理员
    + {% elif role.id == 2 %} +
    普通用户
    + {% else %} +
    自定义
    + {% endif %} +
    +
    + {% else %} +
    + +

    暂无角色数据

    +
    + {% endfor %} +
    + + +
    +

    角色权限说明

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    功能模块管理员普通用户自定义角色
    图书浏览
    借阅图书
    图书管理可配置
    用户管理可配置
    借阅管理可配置
    库存管理可配置
    统计分析可配置
    系统设置可配置
    +
    +
    +
    +
    + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} + ================================================================================ File: ./app/templates/book/list.html ================================================================================ @@ -5389,7 +9120,8 @@ File: ./app/templates/book/list.html
    {% for book in books %} -
    + +
    {% if book.cover_url %} {{ book.title }} @@ -5495,6 +9227,26 @@ File: ./app/templates/book/list.html {% endif %}
    + + {% endblock %} @@ -5502,6 +9254,7 @@ File: ./app/templates/book/list.html {{ super() }} {% endblock %} + ================================================================================ File: ./app/templates/book/add.html ================================================================================ @@ -5523,16 +9276,16 @@ File: ./app/templates/book/add.html