================================================================================ File: ./config.py ================================================================================ import os # 数据库配置 """ DB_HOST = os.environ.get('DB_HOST', '27.124.22.104') DB_PORT = os.environ.get('DB_PORT', '3306') DB_USER = os.environ.get('DB_USER', 'book20250428') DB_PASSWORD = os.environ.get('DB_PASSWORD', 'booksystem') DB_NAME = os.environ.get('DB_NAME', 'book_system') """ DB_HOST = os.environ.get('DB_HOST', 'rm-bp1h5oqo8ld21viftro.mysql.rds.aliyuncs.com') DB_PORT = os.environ.get('DB_PORT', '3306') DB_USER = os.environ.get('DB_USER', 'shiqi') DB_PASSWORD = os.environ.get('DB_PASSWORD', 'Shiqi1234!') DB_NAME = os.environ.get('DB_NAME', 'book_system') # 数据库连接字符串 SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}' SQLALCHEMY_TRACK_MODIFICATIONS = False # 应用密钥 SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key_replace_in_production') # 邮件配置 EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com') EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587)) EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls') EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com') EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf') EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com') EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL') # 会话配置 PERMANENT_SESSION_LIFETIME = 86400 * 7 ================================================================================ File: ./all_file_output.py ================================================================================ import os import sys def collect_code_files(output_file="code_collection.txt"): # 定义代码文件扩展名 code_extensions = [ '.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs', '.js', '.html', '.css', '.php', '.go', '.rb', '.swift', '.kt', '.ts', '.sh', '.pl', '.r' ] # 定义要排除的目录 excluded_dirs = [ 'venv', 'env', '.venv', '.env', 'virtualenv', '__pycache__', 'node_modules', '.git', '.idea', 'dist', 'build', 'target', 'bin' ] # 计数器 file_count = 0 # 打开输出文件 with open(output_file, 'w', encoding='utf-8') as out_file: # 遍历当前目录及所有子目录 for root, dirs, files in os.walk('.'): # 从dirs中移除排除的目录,这会阻止os.walk进入这些目录 dirs[:] = [d for d in dirs if d not in excluded_dirs] for file in files: # 获取文件扩展名 _, ext = os.path.splitext(file) # 检查是否为代码文件 if ext.lower() in code_extensions: file_path = os.path.join(root, file) file_count += 1 # 写入文件路径作为分隔 out_file.write(f"\n{'=' * 80}\n") out_file.write(f"File: {file_path}\n") out_file.write(f"{'=' * 80}\n\n") # 尝试读取文件内容并写入 try: with open(file_path, 'r', encoding='utf-8') as code_file: out_file.write(code_file.read()) except UnicodeDecodeError: # 尝试用不同的编码 try: with open(file_path, 'r', encoding='latin-1') as code_file: out_file.write(code_file.read()) except Exception as e: out_file.write(f"无法读取文件内容: {str(e)}\n") except Exception as e: out_file.write(f"读取文件时出错: {str(e)}\n") print(f"已成功收集 {file_count} 个代码文件到 {output_file}") if __name__ == "__main__": # 如果提供了命令行参数,则使用它作为输出文件名 output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt" collect_code_files(output_file) ================================================================================ File: ./app.py ================================================================================ from app import create_app app = create_app() if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=49666) ================================================================================ File: ./main.py ================================================================================ # 这是一个示例 Python 脚本。 # 按 ⌃R 执行或将其替换为您的代码。 # 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。 def print_hi(name): # 在下面的代码行中使用断点来调试脚本。 print(f'Hi, {name}') # 按 ⌘F8 切换断点。 # 按间距中的绿色按钮以运行脚本。 if __name__ == '__main__': print_hi('PyCharm') # 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 ================================================================================ File: ./app/__init__.py ================================================================================ from flask import Flask, render_template, session, g, Markup, redirect, url_for, request from flask_login import LoginManager from app.models.database import db from app.models.user import User from app.controllers.user import user_bp from app.controllers.book import book_bp from app.controllers.borrow import borrow_bp from app.controllers.inventory import inventory_bp from flask_login import LoginManager, current_user from app.controllers.statistics import statistics_bp from app.controllers.announcement import announcement_bp from app.models.notification import Notification from app.controllers.log import log_bp import os from datetime import datetime login_manager = LoginManager() def create_app(config=None): app = Flask(__name__) # 加载默认配置 app.config.from_object('config') # 如果提供了配置对象,则加载它 if config: if isinstance(config, dict): app.config.update(config) else: app.config.from_object(config) # 从环境变量指定的文件加载配置(如果有) app.config.from_envvar('APP_CONFIG_FILE', silent=True) # 初始化数据库 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)) from app.utils.template_helpers import register_template_helpers # 注册蓝图 register_template_helpers(app) app.register_blueprint(user_bp, url_prefix='/user') app.register_blueprint(book_bp, url_prefix='/book') app.register_blueprint(borrow_bp, url_prefix='/borrow') app.register_blueprint(statistics_bp) app.register_blueprint(inventory_bp) app.register_blueprint(log_bp) app.register_blueprint(announcement_bp, url_prefix='/announcement') # 创建数据库表 with app.app_context(): # 先导入基础模型 from app.models.user import User, Role from app.models.book import Book, Category # 创建表 db.create_all() # 再导入依赖模型 - 但不在这里定义关系 from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog from app.models.log import Log # 移除这些重复的关系定义 # 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 if not Role.query.filter_by(id=1).first(): admin_role = Role(id=1, role_name='管理员', description='系统管理员') db.session.add(admin_role) if not Role.query.filter_by(id=2).first(): user_role = Role(id=2, role_name='普通用户', description='普通用户') db.session.add(user_role) # 创建管理员账号 if not User.query.filter_by(username='admin').first(): admin = User( username='admin', password='admin123', email='admin@example.com', role_id=1, nickname='系统管理员' ) db.session.add(admin) # 创建基础分类 from app.models.book import Category if not Category.query.first(): categories = [ Category(name='文学', sort=1), Category(name='计算机', sort=2), Category(name='历史', sort=3), Category(name='科学', sort=4), Category(name='艺术', sort=5), Category(name='经济', sort=6), Category(name='哲学', sort=7), Category(name='教育', sort=8) ] db.session.add_all(categories) db.session.commit() # 添加缓存控制中间件 @app.after_request def add_cache_headers(response): # 为HTML页面和主页添加禁止缓存的头 if request.path == '/' or 'text/html' in response.headers.get('Content-Type', ''): response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" response.headers['Vary'] = 'Cookie, Authorization' return response # 其余代码保持不变... @app.before_request def load_logged_in_user(): user_id = session.get('user_id') if user_id is None: g.user = None else: g.user = User.query.get(user_id) @app.route('/') def index(): from app.models.book import Book from app.models.user import User from app.models.borrow import BorrowRecord from app.models.announcement import Announcement from app.models.notification import Notification from sqlalchemy import func, desc from flask_login import current_user # 获取统计数据 stats = { 'total_books': Book.query.count(), 'total_users': User.query.count(), 'active_borrows': BorrowRecord.query.filter(BorrowRecord.return_date.is_(None)).count(), 'user_borrows': 0 } # 如果用户已登录,获取其待还图书数量 if current_user.is_authenticated: stats['user_borrows'] = BorrowRecord.query.filter( BorrowRecord.user_id == current_user.id, BorrowRecord.return_date.is_(None) ).count() # 获取最新图书 latest_books = Book.query.filter_by(status=1).order_by(Book.created_at.desc()).limit(4).all() # 获取热门图书(根据借阅次数) try: # 这里假设你的数据库中有表记录借阅次数 popular_books_query = db.session.query( Book, func.count(BorrowRecord.id).label('borrow_count') ).join( BorrowRecord, Book.id == BorrowRecord.book_id, isouter=True ).filter( Book.status == 1 ).group_by( Book.id ).order_by( desc('borrow_count') ).limit(5) # 提取图书对象并添加借阅计数 popular_books = [] for book, count in popular_books_query: book.borrow_count = count popular_books.append(book) except Exception as e: # 如果查询有问题,使用最新的书作为备选 popular_books = latest_books.copy() if latest_books else [] print(f"获取热门图书失败: {str(e)}") # 获取最新公告 announcements = Announcement.query.filter_by(status=1).order_by( Announcement.is_top.desc(), Announcement.created_at.desc() ).limit(3).all() now = datetime.now() # 获取用户的未读通知 user_notifications = [] if current_user.is_authenticated: user_notifications = Notification.query.filter_by( user_id=current_user.id, status=0 ).order_by( Notification.created_at.desc() ).limit(5).all() return render_template('index.html', stats=stats, latest_books=latest_books, popular_books=popular_books, announcements=announcements, user_notifications=user_notifications, now=now ) @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @app.template_filter('nl2br') def nl2br_filter(s): if s: return Markup(s.replace('\n', '
')) return s @app.context_processor def utility_processor(): def get_unread_notifications_count(user_id): if user_id: return Notification.get_unread_count(user_id) return 0 def get_recent_notifications(user_id, limit=5): if user_id: # 按时间倒序获取最近的几条通知 notifications = Notification.query.filter_by(user_id=user_id) \ .order_by(Notification.created_at.desc()) \ .limit(limit) \ .all() return notifications return [] return dict( get_unread_notifications_count=get_unread_notifications_count, get_recent_notifications=get_recent_notifications ) @app.context_processor def inject_now(): return {'now': datetime.now()} return app ================================================================================ File: ./app/init_permissions.py ================================================================================ from app import create_app from app.models.database import db from app.models.user import Role from app.models.permission import Permission import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def init_permissions(): """初始化系统权限""" logger.info("开始初始化系统权限...") # 只定义管理类权限,对应现有的 @admin_required 装饰的路由 permissions = [ # 公告管理权限 {'code': 'manage_announcements', 'name': '公告管理', 'description': '允许管理系统公告(发布、编辑、删除、置顶等)'}, # 图书管理权限 {'code': 'manage_books', 'name': '图书管理', 'description': '允许管理图书(添加、编辑、删除图书)'}, {'code': 'manage_categories', 'name': '分类管理', 'description': '允许管理图书分类'}, {'code': 'import_export_books', 'name': '导入导出图书', 'description': '允许批量导入和导出图书数据'}, # 借阅管理权限 {'code': 'manage_borrows', 'name': '借阅管理', 'description': '允许管理全系统借阅记录和处理借还书操作'}, {'code': 'manage_overdue', 'name': '逾期管理', 'description': '允许查看和处理逾期借阅'}, # 库存管理权限 {'code': 'manage_inventory', 'name': '库存管理', 'description': '允许查看和调整图书库存'}, # 日志权限 {'code': 'view_logs', 'name': '查看日志', 'description': '允许查看系统操作日志'}, # 统计权限 {'code': 'view_statistics', 'name': '查看统计', 'description': '允许查看统计分析数据'}, # 用户管理权限 {'code': 'manage_users', 'name': '用户管理', 'description': '允许管理用户(添加、编辑、禁用、删除用户)'}, {'code': 'manage_roles', 'name': '角色管理', 'description': '允许管理角色和权限'}, ] # 添加权限记录 added_count = 0 updated_count = 0 for perm_data in permissions: # 检查权限是否已存在 existing_perm = Permission.query.filter_by(code=perm_data['code']).first() if existing_perm: # 更新现有权限信息 existing_perm.name = perm_data['name'] existing_perm.description = perm_data['description'] updated_count += 1 else: # 创建新权限 permission = Permission(**perm_data) db.session.add(permission) added_count += 1 # 提交所有权限 db.session.commit() logger.info(f"权限初始化完成: 新增 {added_count} 个, 更新 {updated_count} 个") # 处理角色权限分配 assign_role_permissions() def assign_role_permissions(): """为系统默认角色分配权限""" logger.info("开始分配角色权限...") # 获取所有权限 all_permissions = Permission.query.all() # 获取系统内置角色 admin_role = Role.query.get(1) # 管理员角色 user_role = Role.query.get(2) # 普通用户角色 if admin_role and user_role: # 管理员拥有所有权限 admin_role.permissions = all_permissions # 普通用户无需特殊管理权限 user_role.permissions = [] db.session.commit() logger.info(f"管理员角色分配了 {len(all_permissions)} 个权限") logger.info(f"普通用户角色无管理权限") else: logger.error("无法找到内置角色,请确保角色表已正确初始化") def main(): """主函数""" app = create_app() with app.app_context(): init_permissions() if __name__ == "__main__": main() ================================================================================ File: ./app/utils/auth.py ================================================================================ from functools import wraps from flask import redirect, url_for, flash, request from flask_login import current_user def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): print(f"DEBUG: login_required 检查 - current_user.is_authenticated = {current_user.is_authenticated}") if not current_user.is_authenticated: flash('请先登录', 'warning') return redirect(url_for('user.login', next=request.url)) return f(*args, **kwargs) return decorated_function def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): print(f"DEBUG: admin_required 检查 - current_user.is_authenticated = {current_user.is_authenticated}") if not current_user.is_authenticated: flash('请先登录', 'warning') return redirect(url_for('user.login', next=request.url)) print(f"DEBUG: admin_required 检查 - current_user.role_id = {getattr(current_user, 'role_id', None)}") if getattr(current_user, 'role_id', None) != 1: # 安全地获取role_id属性 flash('权限不足', 'danger') return redirect(url_for('index')) return f(*args, **kwargs) return decorated_function def permission_required(permission_code): """ 检查用户是否拥有特定权限的装饰器 :param permission_code: 权限代码,例如 'manage_books' """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): print( f"DEBUG: permission_required({permission_code}) 检查 - current_user.is_authenticated = {current_user.is_authenticated}") # 检查用户是否登录 if not current_user.is_authenticated: flash('请先登录', 'warning') return redirect(url_for('user.login', next=request.url)) # 管理员拥有所有权限 if getattr(current_user, 'role_id', None) == 1: return f(*args, **kwargs) # 获取用户角色并检查是否有指定权限 from app.models.user import Role role = Role.query.get(current_user.role_id) if not role: flash('用户角色异常', 'danger') return redirect(url_for('index')) # 检查角色是否有指定权限 has_permission = False for perm in role.permissions: if perm.code == permission_code: has_permission = True break if not has_permission: print(f"DEBUG: 用户 {current_user.username} 缺少权限 {permission_code}") flash('您没有执行此操作的权限', 'danger') return redirect(url_for('index')) return f(*args, **kwargs) return decorated_function return decorator ================================================================================ File: ./app/utils/db.py ================================================================================ ================================================================================ File: ./app/utils/__init__.py ================================================================================ ================================================================================ File: ./app/utils/logger.py ================================================================================ from flask import request, current_app from flask_login import current_user from app.models.log import Log def record_activity(action, target_type=None, target_id=None, description=None): """ 记录用户活动 参数: - action: 操作类型,如 'login', 'logout', 'create', 'update', 'delete', 'borrow', 'return' 等 - target_type: 操作对象类型,如 'book', 'user', 'borrow' 等 - target_id: 操作对象ID - description: 操作详细描述 """ try: # 获取当前用户ID user_id = current_user.id if current_user.is_authenticated else None # 获取客户端IP地址 ip_address = request.remote_addr if 'X-Forwarded-For' in request.headers: ip_address = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1] # 记录日志 Log.add_log( action=action, user_id=user_id, target_type=target_type, target_id=target_id, ip_address=ip_address, description=description ) return True except Exception as e: # 记录错误,但不影响主要功能 if current_app: current_app.logger.error(f"Error recording activity log: {str(e)}") return False ================================================================================ File: ./app/utils/template_helpers.py ================================================================================ from app.models.permission import Permission from flask import current_app def register_template_helpers(app): @app.context_processor def inject_permissions(): def has_permission(user, permission_code): """检查用户是否拥有指定权限""" if not user or not user.is_authenticated: return False # 管理员拥有所有权限 if user.role_id == 1: return True # 检查用户角色权限 if user.role: for perm in user.role.permissions: if perm.code == permission_code: return True return False return dict(has_permission=has_permission) # 在 create_app 函数中调用 # register_template_helpers(app) ================================================================================ File: ./app/utils/email.py ================================================================================ import smtplib import random import string from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from flask import current_app import logging # 配置日志 logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) # 配置邮件发送功能 def send_verification_email(to_email, verification_code): """ 发送验证码邮件 """ try: # 从应用配置获取邮件设置 email_host = current_app.config['EMAIL_HOST'] email_port = current_app.config['EMAIL_PORT'] email_username = current_app.config['EMAIL_USERNAME'] email_password = current_app.config['EMAIL_PASSWORD'] email_from = current_app.config['EMAIL_FROM'] email_from_name = current_app.config['EMAIL_FROM_NAME'] logger.info(f"准备发送邮件到: {to_email}, 验证码: {verification_code}") logger.debug(f"邮件配置: 主机={email_host}, 端口={email_port}") # 邮件内容 msg = MIMEMultipart() msg['From'] = f"{email_from_name} <{email_from}>" msg['To'] = to_email msg['Subject'] = "图书管理系统 - 验证码" # 邮件正文 body = f"""

图书管理系统 - 邮箱验证

您好,

感谢您注册图书管理系统,您的验证码是:

{verification_code}

该验证码将在10分钟内有效,请勿将验证码分享给他人。

如果您没有请求此验证码,请忽略此邮件。

此邮件为系统自动发送,请勿回复。

© 2025 图书管理系统

""" msg.attach(MIMEText(body, 'html')) logger.debug("尝试连接到SMTP服务器...") # 连接服务器发送邮件 server = smtplib.SMTP(email_host, email_port) server.set_debuglevel(1) # 启用详细的SMTP调试输出 logger.debug("检查是否需要STARTTLS加密...") if current_app.config.get('EMAIL_ENCRYPTION') == 'starttls': logger.debug("启用STARTTLS...") server.starttls() logger.debug(f"尝试登录邮箱: {email_username}") server.login(email_username, email_password) logger.debug("发送邮件...") server.send_message(msg) logger.debug("关闭连接...") server.quit() logger.info(f"邮件发送成功: {to_email}") return True except Exception as e: logger.error(f"邮件发送失败: {str(e)}", exc_info=True) return False def generate_verification_code(length=6): """ 生成数字验证码 """ return ''.join(random.choice(string.digits) for _ in range(length)) ================================================================================ File: ./app/utils/helpers.py ================================================================================ ================================================================================ File: ./app/models/user.py ================================================================================ from app.models.database import db from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime from flask_login import UserMixin from app.models.permission import RolePermission, Permission #db = SQLAlchemy() 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) email = db.Column(db.String(128), unique=True, nullable=True) phone = db.Column(db.String(20), unique=True, nullable=True) nickname = db.Column(db.String(64), nullable=True) status = db.Column(db.Integer, default=1) # 1: active, 0: disabled role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2) # 2: 普通用户, 1: 管理员 created_at = db.Column(db.DateTime, default=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2, status=1): self.username = username self.set_password(password) self.email = email self.phone = phone self.nickname = nickname self.role_id = role_id self.status = status # 新增 @property def is_active(self): return self.status == 1 def set_password(self, password): """设置密码,使用哈希加密""" self.password = generate_password_hash(password) def check_password(self, password): """验证密码""" return check_password_hash(self.password, password) def to_dict(self): """转换为字典格式""" return { 'id': self.id, 'username': self.username, 'email': self.email, 'phone': self.phone, 'nickname': self.nickname, 'status': self.status, 'role_id': self.role_id, 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'), 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') } @classmethod def create_user(cls, username, password, email=None, phone=None, nickname=None, role_id=2): """创建新用户""" user = User( username=username, password=password, email=email, phone=phone, nickname=nickname, role_id=role_id ) db.session.add(user) db.session.commit() return user class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True, autoincrement=True) role_name = db.Column(db.String(32), unique=True, nullable=False) description = db.Column(db.String(128)) permissions = db.relationship( 'Permission', secondary='role_permissions', backref=db.backref('roles', lazy='dynamic'), lazy='dynamic' ) users = db.relationship('User', backref='role') ================================================================================ File: ./app/models/permission.py ================================================================================ from app.models.database import db from datetime import datetime # 这是权限表 model class Permission(db.Model): __tablename__ = 'permissions' id = db.Column(db.Integer, primary_key=True) code = db.Column(db.String(64), unique=True, nullable=False, comment='权限代码,用于系统识别') name = db.Column(db.String(64), nullable=False, comment='权限名称,用于界面显示') description = db.Column(db.String(255), comment='权限描述,说明权限用途') # 角色-权限 关联表(辅助对象模式,方便ORM关系管理) class RolePermission(db.Model): __tablename__ = 'role_permissions' role_id = db.Column(db.Integer, db.ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True, comment='角色ID,关联roles表') permission_id = db.Column(db.Integer, db.ForeignKey('permissions.id', ondelete='CASCADE'), primary_key=True, comment='权限ID,关联permissions表') created_at = db.Column(db.DateTime, default=datetime.now, comment='权限分配时间') ================================================================================ File: ./app/models/log.py ================================================================================ from datetime import datetime from app.models.user import db, User # 从user模块导入db,而不是从utils导入 class Log(db.Model): __tablename__ = 'logs' id = db.Column(db.Integer, primary_key=True, autoincrement=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) action = db.Column(db.String(64), nullable=False) target_type = db.Column(db.String(32), nullable=True) target_id = db.Column(db.Integer, nullable=True) ip_address = db.Column(db.String(45), nullable=True) description = db.Column(db.String(255), nullable=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) # 关联用户 user = db.relationship('User', backref=db.backref('logs', lazy=True)) def __init__(self, action, user_id=None, target_type=None, target_id=None, ip_address=None, description=None): self.user_id = user_id self.action = action self.target_type = target_type self.target_id = target_id self.ip_address = ip_address self.description = description self.created_at = datetime.now() @staticmethod def add_log(action, user_id=None, target_type=None, target_id=None, ip_address=None, description=None): """添加一条日志记录""" try: log = Log( action=action, user_id=user_id, target_type=target_type, target_id=target_id, ip_address=ip_address, description=description ) db.session.add(log) db.session.commit() return True, "日志记录成功" except Exception as e: db.session.rollback() return False, f"日志记录失败: {str(e)}" @staticmethod def get_logs(page=1, per_page=20, user_id=None, action=None, target_type=None, start_date=None, end_date=None): """查询日志记录""" query = Log.query.order_by(Log.created_at.desc()) if user_id: query = query.filter(Log.user_id == user_id) if action: query = query.filter(Log.action == action) if target_type: query = query.filter(Log.target_type == target_type) if start_date: query = query.filter(Log.created_at >= start_date) if end_date: query = query.filter(Log.created_at <= end_date) return query.paginate(page=page, per_page=per_page) ================================================================================ File: ./app/models/notification.py ================================================================================ from datetime import datetime from app.models.user import db, User # 从user模块导入db,而不是从app.models导入 class Notification(db.Model): __tablename__ = 'notifications' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) title = db.Column(db.String(128), nullable=False) content = db.Column(db.Text, nullable=False) type = db.Column(db.String(32), nullable=False) # 通知类型:system, borrow, return, overdue, etc. status = db.Column(db.Integer, default=0) # 0-未读, 1-已读 sender_id = db.Column(db.Integer, db.ForeignKey('users.id')) created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) read_at = db.Column(db.DateTime) # 关联关系 user = db.relationship('User', foreign_keys=[user_id], backref='notifications') sender = db.relationship('User', foreign_keys=[sender_id], backref='sent_notifications') def to_dict(self): """将通知转换为字典""" return { 'id': self.id, 'user_id': self.user_id, 'title': self.title, 'content': self.content, 'type': self.type, 'status': self.status, 'sender_id': self.sender_id, 'sender_name': self.sender.username if self.sender else 'System', 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'), 'read_at': self.read_at.strftime('%Y-%m-%d %H:%M:%S') if self.read_at else None } @staticmethod def get_user_notifications(user_id, page=1, per_page=10, unread_only=False): """获取用户通知""" query = Notification.query.filter_by(user_id=user_id) if unread_only: query = query.filter_by(status=0) return query.order_by(Notification.created_at.desc()).paginate( page=page, per_page=per_page, error_out=False ) @staticmethod def get_unread_count(user_id): """获取用户未读通知数量""" return Notification.query.filter_by(user_id=user_id, status=0).count() @staticmethod def mark_as_read(notification_id, user_id=None): """将通知标记为已读""" notification = Notification.query.get(notification_id) if not notification: return False, "通知不存在" # 验证用户权限 if user_id and notification.user_id != user_id: return False, "无权操作此通知" notification.status = 1 notification.read_at = datetime.now() try: db.session.commit() return True, "已标记为已读" except Exception as e: db.session.rollback() return False, str(e) @staticmethod def create_notification(user_id, title, content, notification_type, sender_id=None): """创建新通知""" notification = Notification( user_id=user_id, title=title, content=content, type=notification_type, sender_id=sender_id ) try: db.session.add(notification) db.session.commit() return True, notification except Exception as e: db.session.rollback() return False, str(e) @staticmethod def create_system_notification(user_ids, title, content, notification_type, sender_id=None): """创建系统通知,发送给多个用户""" success_count = 0 fail_count = 0 for user_id in user_ids: success, _ = Notification.create_notification( user_id=user_id, title=title, content=content, notification_type=notification_type, sender_id=sender_id ) if success: success_count += 1 else: fail_count += 1 return success_count, fail_count ================================================================================ File: ./app/models/database.py ================================================================================ from flask_sqlalchemy import SQLAlchemy # 创建共享的SQLAlchemy实例 db = SQLAlchemy() ================================================================================ File: ./app/models/__init__.py ================================================================================ def create_app(): app = Flask(__name__) # ... 配置代码 ... # 初始化数据库 db.init_app(app) # 导入模型,确保所有模型在创建表之前被加载 from app.models.user import User, Role from app.models.book import Book, Category from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog # 创建数据库表 with app.app_context(): db.create_all() # ... 其余代码 ... ================================================================================ File: ./app/models/book.py ================================================================================ from app.models.user import db from datetime import datetime class Category(db.Model): __tablename__ = 'categories' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) sort = db.Column(db.Integer, default=0) # 关系 - 只保留与自身的关系 parent = db.relationship('Category', remote_side=[id], backref='children') def __repr__(self): return f'' class Book(db.Model): __tablename__ = 'books' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) author = db.Column(db.String(128), nullable=False) publisher = db.Column(db.String(128), nullable=True) category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) tags = db.Column(db.String(255), nullable=True) isbn = db.Column(db.String(32), unique=True, nullable=True) publish_year = db.Column(db.String(16), nullable=True) description = db.Column(db.Text, nullable=True) cover_url = db.Column(db.String(255), nullable=True) stock = db.Column(db.Integer, default=0) price = db.Column(db.Numeric(10, 2), nullable=True) status = db.Column(db.Integer, default=1) # 1:可用, 0:不可用 created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now) # 添加与 InventoryLog 的关系 inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') def __repr__(self): return f'' ================================================================================ File: ./app/models/borrow.py ================================================================================ from app.models.user import db from datetime import datetime class BorrowRecord(db.Model): __tablename__ = 'borrow_records' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False) borrow_date = db.Column(db.DateTime, nullable=False, default=datetime.now) due_date = db.Column(db.DateTime, nullable=False) return_date = db.Column(db.DateTime, nullable=True) renew_count = db.Column(db.Integer, default=0) status = db.Column(db.Integer, default=1) # 1: 借出, 0: 已归还 remark = db.Column(db.String(255), nullable=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now) # 添加反向关系引用 user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic')) book = db.relationship('Book', backref=db.backref('borrow_records', lazy='dynamic')) # book 关系会在后面步骤添加 def __repr__(self): return f'' ================================================================================ File: ./app/models/announcement.py ================================================================================ from datetime import datetime from app.models.user import db, User # 从user模块导入db,而不是从app.models导入 class Announcement(db.Model): __tablename__ = 'announcements' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(128), nullable=False) content = db.Column(db.Text, nullable=False) publisher_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) is_top = db.Column(db.Boolean, default=False) status = db.Column(db.Integer, default=1) # 1-正常, 0-已下架 created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now) # 关联关系 publisher = db.relationship('User', backref='announcements') def to_dict(self): """将公告转换为字典""" return { 'id': self.id, 'title': self.title, 'content': self.content, 'publisher_id': self.publisher_id, 'publisher_name': self.publisher.username if self.publisher else '', 'is_top': self.is_top, 'status': self.status, 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'), 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') } @staticmethod def get_active_announcements(limit=None): """获取活跃的公告""" query = Announcement.query.filter_by(status=1).order_by( Announcement.is_top.desc(), Announcement.created_at.desc() ) if limit: query = query.limit(limit) return query.all() @staticmethod def get_announcement_by_id(announcement_id): """根据ID获取公告""" return Announcement.query.get(announcement_id) @staticmethod def create_announcement(title, content, publisher_id, is_top=False): """创建新公告""" announcement = Announcement( title=title, content=content, publisher_id=publisher_id, is_top=is_top ) try: db.session.add(announcement) db.session.commit() return True, announcement except Exception as e: db.session.rollback() return False, str(e) @staticmethod def update_announcement(announcement_id, title, content, is_top=None): """更新公告内容""" announcement = Announcement.query.get(announcement_id) if not announcement: return False, "公告不存在" announcement.title = title announcement.content = content if is_top is not None: announcement.is_top = is_top try: db.session.commit() return True, announcement except Exception as e: db.session.rollback() return False, str(e) @staticmethod def change_status(announcement_id, status): """更改公告状态""" announcement = Announcement.query.get(announcement_id) if not announcement: return False, "公告不存在" announcement.status = status try: db.session.commit() return True, "状态已更新" except Exception as e: db.session.rollback() return False, str(e) @staticmethod def change_top_status(announcement_id, is_top): """更改置顶状态""" announcement = Announcement.query.get(announcement_id) if not announcement: return False, "公告不存在" announcement.is_top = is_top try: db.session.commit() return True, "置顶状态已更新" except Exception as e: db.session.rollback() return False, str(e) ================================================================================ File: ./app/models/inventory.py ================================================================================ from app.models.user import db from datetime import datetime class InventoryLog(db.Model): __tablename__ = 'inventory_logs' id = db.Column(db.Integer, primary_key=True) book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False) change_type = db.Column(db.String(32), nullable=False) # 'in' 入库, 'out' 出库 change_amount = db.Column(db.Integer, nullable=False) after_stock = db.Column(db.Integer, nullable=False) operator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) remark = db.Column(db.String(255), nullable=True) changed_at = db.Column(db.DateTime, nullable=False, default=datetime.now) # 添加反向关系引用 operator = db.relationship('User', backref=db.backref('inventory_logs', lazy='dynamic')) # book 关系会在后面步骤添加 def __repr__(self): return f'' ================================================================================ File: ./app/static/css/log-detail.css ================================================================================ /* 日志详情样式 */ .content-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .content-header h1 { margin: 0; font-size: 24px; } .log-info { padding: 10px; } .info-item { margin-bottom: 15px; display: flex; } .info-item .label { width: 100px; font-weight: 600; color: #495057; } .info-item .value { flex: 1; } .description { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 20px; display: block; } .description .label { display: block; width: 100%; margin-bottom: 10px; } .description .value { display: block; width: 100%; white-space: pre-wrap; word-break: break-word; } ================================================================================ File: ./app/static/css/register.css ================================================================================ /* register.css - 注册页面专用样式 */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; } :root { --primary-color: #4a89dc; --primary-hover: #3b78c4; --secondary-color: #5cb85c; --text-color: #333; --light-text: #666; --bg-color: #f5f7fa; --card-bg: #ffffff; --border-color: #ddd; --error-color: #e74c3c; --success-color: #2ecc71; } body.dark-mode { --primary-color: #5a9aed; --primary-hover: #4a89dc; --secondary-color: #6bc76b; --text-color: #f1f1f1; --light-text: #aaa; --bg-color: #1a1a1a; --card-bg: #2c2c2c; --border-color: #444; } body { background-color: var(--bg-color); background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background-size: cover; background-position: center; display: flex; flex-direction: column; min-height: 100vh; color: var(--text-color); transition: all 0.3s ease; } .theme-toggle { position: absolute; top: 20px; right: 20px; z-index: 10; cursor: pointer; padding: 8px; border-radius: 50%; background-color: rgba(255, 255, 255, 0.2); backdrop-filter: blur(5px); border: 1px solid rgba(255, 255, 255, 0.1); } .overlay { background-color: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px); position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; } .main-container { display: flex; justify-content: center; align-items: center; flex: 1; padding: 20px; } .login-container { background-color: var(--card-bg); border-radius: 12px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); width: 450px; padding: 35px; position: relative; overflow: hidden; animation: fadeIn 0.5s ease; } .register-container { width: 500px; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .logo { text-align: center; margin-bottom: 25px; position: relative; } .logo img { width: 90px; height: 90px; border-radius: 12px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); padding: 5px; background-color: #fff; transition: transform 0.3s ease; } h1 { text-align: center; color: var(--text-color); margin-bottom: 10px; font-weight: 600; font-size: 28px; } .subtitle { text-align: center; color: var(--light-text); margin-bottom: 30px; font-size: 14px; } .form-group { margin-bottom: 22px; position: relative; } .form-group label { display: block; margin-bottom: 8px; color: var(--text-color); font-weight: 500; font-size: 14px; } .input-with-icon { position: relative; } .input-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: var(--light-text); } .form-control { width: 100%; height: 48px; border: 1px solid var(--border-color); border-radius: 6px; padding: 0 15px 0 45px; font-size: 15px; transition: all 0.3s ease; background-color: var(--card-bg); color: var(--text-color); } .form-control:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2); outline: none; } .password-toggle { position: absolute; right: 15px; top: 50%; transform: translateY(-50%); cursor: pointer; color: var(--light-text); } .validation-message { margin-top: 6px; font-size: 12px; color: var(--error-color); display: none; } .validation-message.show { display: block; animation: shake 0.5s ease; } @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .btn-login { width: 100%; height: 48px; background-color: var(--primary-color); color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; } .btn-login:hover { background-color: var(--primary-hover); } .btn-login:active { transform: scale(0.98); } .btn-login .loading { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .btn-login.loading-state { color: transparent; } .btn-login.loading-state .loading { display: block; } .signup { text-align: center; margin-top: 25px; font-size: 14px; color: var(--light-text); } .signup a { color: var(--primary-color); text-decoration: none; font-weight: 600; transition: color 0.3s ease; } .signup a:hover { color: var(--primary-hover); text-decoration: underline; } .alert { padding: 10px; margin-bottom: 15px; border-radius: 4px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; } .verification-code-container { display: flex; gap: 10px; } .verification-input { flex: 1; height: 48px; border: 1px solid var(--border-color); border-radius: 6px; padding: 0 15px; font-size: 15px; transition: all 0.3s ease; background-color: var(--card-bg); color: var(--text-color); } .send-code-btn { padding: 0 15px; background-color: var(--primary-color); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; white-space: nowrap; transition: all 0.3s ease; } .send-code-btn:hover { background-color: var(--primary-hover); } .send-code-btn:disabled { background-color: #ccc; cursor: not-allowed; } footer { text-align: center; padding: 20px; color: rgba(255, 255, 255, 0.7); font-size: 12px; } footer a { color: rgba(255, 255, 255, 0.9); text-decoration: none; } @media (max-width: 576px) { .login-container, .register-container { width: 100%; padding: 25px; border-radius: 0; } .theme-toggle { top: 10px; } .logo img { width: 70px; height: 70px; } h1 { font-size: 22px; } .main-container { padding: 0; } .verification-code-container { flex-direction: column; } } ================================================================================ File: ./app/static/css/inventory-book-logs.css ================================================================================ /* 冰雪奇缘主题库存日志页面样式 */ /* 基础背景与字体 */ body { font-family: 'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif; background-color: #e6f2ff; color: #2c3e50; } /* 冰雪背景 */ .frozen-background { position: relative; min-height: 100vh; padding: 30px 0 50px; background: linear-gradient(135deg, #e4f1fe, #d4e6fb, #c9e0ff); overflow: hidden; } /* 雪花效果 */ .snowflakes { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; } .snowflake { position: absolute; color: #fff; font-size: 1.5em; opacity: 0.8; top: -20px; animation: snowfall linear infinite; } .snowflake:nth-child(1) { left: 10%; animation-duration: 15s; animation-delay: 0s; } .snowflake:nth-child(2) { left: 20%; animation-duration: 12s; animation-delay: 1s; } .snowflake:nth-child(3) { left: 30%; animation-duration: 13s; animation-delay: 2s; } .snowflake:nth-child(4) { left: 40%; animation-duration: 10s; animation-delay: 0s; } .snowflake:nth-child(5) { left: 50%; animation-duration: 16s; animation-delay: 3s; } .snowflake:nth-child(6) { left: 60%; animation-duration: 14s; animation-delay: 1s; } .snowflake:nth-child(7) { left: 70%; animation-duration: 12s; animation-delay: 0s; } .snowflake:nth-child(8) { left: 80%; animation-duration: 15s; animation-delay: 2s; } .snowflake:nth-child(9) { left: 90%; animation-duration: 13s; animation-delay: 1s; } .snowflake:nth-child(10) { left: 95%; animation-duration: 14s; animation-delay: 3s; } @keyframes snowfall { 0% { transform: translateY(0) rotate(0deg); } 100% { transform: translateY(100vh) rotate(360deg); } } /* 冰雪主题卡片 */ .frozen-card { position: relative; background-color: rgba(255, 255, 255, 0.85); border-radius: 20px; box-shadow: 0 10px 30px rgba(79, 149, 255, 0.2); backdrop-filter: blur(10px); border: 2px solid #e1f0ff; margin-bottom: 40px; overflow: hidden; z-index: 2; } /* 城堡装饰 */ .castle-decoration { position: absolute; top: -40px; right: 30px; width: 120px; height: 120px; background-image: url('https://i.imgur.com/KkMfwWv.png'); background-size: contain; background-repeat: no-repeat; opacity: 0.6; z-index: 1; transform: rotate(10deg); filter: hue-rotate(190deg); } /* 卡片标题栏 */ .card-header-frozen { background: linear-gradient(45deg, #7AB6FF, #94C5FF); color: #fff; padding: 1.5rem; border-radius: 18px 18px 0 0; text-align: center; position: relative; text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; } .card-header-frozen h4 { font-weight: 700; margin: 0; font-size: 1.6rem; z-index: 1; } .card-header-frozen i { margin-right: 10px; } /* 冰晶装饰 */ .ice-crystal { position: absolute; width: 50px; height: 50px; background-image: url('https://i.imgur.com/8vZuwlG.png'); background-size: contain; background-repeat: no-repeat; filter: brightness(1.2) hue-rotate(190deg); } .ice-crystal.left { left: 20px; transform: rotate(-30deg) scale(0.8); } .ice-crystal.right { right: 20px; transform: rotate(30deg) scale(0.8); } /* 卡片内容区 */ .card-body-frozen { padding: 2.5rem; position: relative; z-index: 2; } /* 书籍基本信息区域 */ .book-info-row { background: linear-gradient(to right, rgba(232, 244, 255, 0.7), rgba(216, 234, 255, 0.4)); border-radius: 15px; padding: 20px; margin-bottom: 30px !important; box-shadow: 0 5px 15px rgba(79, 149, 255, 0.1); position: relative; overflow: hidden; } /* 书籍封面 */ .book-cover-container { display: flex; justify-content: center; align-items: center; } .book-frame { position: relative; padding: 10px; background-color: white; border-radius: 10px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); transform: rotate(-3deg); transition: transform 0.5s ease; z-index: 1; } .book-frame:hover { transform: rotate(0deg) scale(1.05); } .book-cover { max-height: 250px; width: auto; object-fit: contain; border-radius: 5px; transform: rotate(3deg); transition: transform 0.5s ease; } .book-frame:hover .book-cover { transform: rotate(0deg); } .book-glow { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle at 50% 50%, rgba(173, 216, 230, 0.4), rgba(173, 216, 230, 0) 70%); opacity: 0; transition: opacity 0.5s ease; pointer-events: none; } .book-frame:hover .book-glow { opacity: 1; } /* 书籍详情 */ .book-details { display: flex; flex-direction: column; justify-content: center; } .book-title { color: #4169e1; font-weight: 700; margin-bottom: 20px; font-size: 1.8rem; position: relative; display: inline-block; } .book-title::after { content: ""; position: absolute; bottom: -10px; left: 0; width: 100%; height: 3px; background: linear-gradient(to right, #7AB6FF, transparent); border-radius: 3px; } .book-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; } .info-item { margin: 0; display: flex; align-items: center; font-size: 1.1rem; color: #34495e; } .info-item i { color: #7AB6FF; margin-right: 10px; font-size: 1.2rem; width: 24px; text-align: center; } /* 库存标签 */ .frozen-badge { display: inline-block; padding: 0.35em 0.9em; border-radius: 50px; font-weight: 600; margin-left: 8px; font-size: 0.95rem; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .high-stock { background: linear-gradient(45deg, #e0f7fa, #b3e5fc); color: #0277bd; border: 1px solid #81d4fa; } .low-stock { background: linear-gradient(45deg, #fff8e1, #ffecb3); color: #ff8f00; border: 1px solid #ffe082; } .out-stock { background: linear-gradient(45deg, #ffebee, #ffcdd2); color: #c62828; border: 1px solid #ef9a9a; } /* 历史记录区域 */ .history-section { position: relative; margin-top: 40px; } .section-title { color: #4169e1; font-weight: 700; font-size: 1.4rem; margin-bottom: 25px; position: relative; display: inline-block; } .section-title i { margin-right: 10px; color: #7AB6FF; } .magic-underline { position: absolute; bottom: -8px; left: 0; width: 100%; height: 3px; background: linear-gradient(to right, #7AB6FF, transparent); animation: sparkle 2s infinite; } @keyframes sparkle { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } } /* 自定义表格 */ .table-container { position: relative; margin-bottom: 30px; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 15px rgba(79, 149, 255, 0.1); } .table-frozen { width: 100%; background-color: white; border-collapse: collapse; } .table-header-row { display: grid; grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 2fr 1.5fr; background: linear-gradient(45deg, #5e81ac, #81a1c1); color: white; font-weight: 600; } .th-frozen { padding: 15px; text-align: center; position: relative; } .th-frozen:not(:last-child)::after { content: ""; position: absolute; right: 0; top: 20%; height: 60%; width: 1px; background-color: rgba(255, 255, 255, 0.3); } .table-body { max-height: 500px; overflow-y: auto; } .table-row { display: grid; grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 2fr 1.5fr; border-bottom: 1px solid #ecf0f1; transition: all 0.3s ease; cursor: pointer; position: relative; overflow: hidden; } .table-row:hover { background-color: #f0f8ff; transform: translateY(-2px); box-shadow: 0 5px 10px rgba(79, 149, 255, 0.1); } .table-row::before { content: ""; position: absolute; left: 0; top: 0; height: 100%; width: 4px; background: linear-gradient(to bottom, #7AB6FF, #5e81ac); opacity: 0; transition: opacity 0.3s ease; } .table-row:hover::before { opacity: 1; } .td-frozen { padding: 15px; text-align: center; display: flex; align-items: center; justify-content: center; } .remark-cell { text-align: left; justify-content: flex-start; font-style: italic; color: #7f8c8d; } /* 表格中的徽章 */ .operation-badge { display: inline-flex; align-items: center; padding: 5px 12px; border-radius: 50px; font-weight: 600; font-size: 0.9rem; } .operation-badge i { margin-left: 5px; } .in-badge { background: linear-gradient(45deg, #e0f7fa, #b3e5fc); color: #0277bd; border: 1px solid #81d4fa; } .out-badge { background: linear-gradient(45deg, #fff8e1, #ffecb3); color: #ff8f00; border: 1px solid #ffe082; } /* 奥拉夫空状态 */ .empty-log { grid-template-columns: 1fr !important; height: 250px; } .empty-message { grid-column: span 7; display: flex; align-items: center; justify-content: center; height: 100%; } .olaf-empty { text-align: center; } .olaf-image { width: 120px; height: 150px; background-image: url('https://i.imgur.com/lM0cLxb.png'); background-size: contain; background-repeat: no-repeat; background-position: center; margin: 0 auto 15px; animation: olaf-wave 3s infinite; } @keyframes olaf-wave { 0%, 100% { transform: rotate(-5deg); } 50% { transform: rotate(5deg); } } .olaf-empty p { font-size: 1.2rem; color: #7f8c8d; margin: 0; } /* 特殊的行样式 */ .log-entry[data-type="in"] { background-color: rgba(224, 247, 250, 0.2); } .log-entry[data-type="out"] { background-color: rgba(255, 248, 225, 0.2); } /* 分页容器 */ .pagination-container { margin-top: 30px; margin-bottom: 10px; } .frozen-pagination { display: flex; padding-left: 0; list-style: none; justify-content: center; gap: 5px; } .frozen-pagination .page-item { margin: 0 2px; } .frozen-pagination .page-link { display: flex; align-items: center; justify-content: center; padding: 8px 16px; color: #4169e1; background-color: white; border: 1px solid #e1f0ff; border-radius: 50px; text-decoration: none; transition: all 0.3s ease; min-width: 40px; } .frozen-pagination .page-link:hover { background-color: #e1f0ff; color: #2c3e50; transform: translateY(-2px); box-shadow: 0 5px 10px rgba(79, 149, 255, 0.1); } .frozen-pagination .page-item.active .page-link { background: linear-gradient(45deg, #7AB6FF, #5e81ac); color: white; border-color: #5e81ac; } .frozen-pagination .page-item.disabled .page-link { color: #95a5a6; background-color: #f8f9fa; cursor: not-allowed; } /* 页脚 */ .card-footer-frozen { background: linear-gradient(45deg, #ecf5ff, #d8e6ff); padding: 1.5rem; border-radius: 0 0 18px 18px; position: relative; } .footer-actions { display: flex; justify-content: space-between; position: relative; z-index: 2; } .footer-decoration { position: absolute; bottom: 0; left: 0; width: 100%; height: 15px; background-image: url('https://i.imgur.com/KkMfwWv.png'); background-size: 50px; background-repeat: repeat-x; opacity: 0.2; filter: hue-rotate(190deg); } /* 冰雪风格按钮 */ .frozen-btn { padding: 10px 20px; border-radius: 50px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; transition: all 0.3s ease; position: relative; overflow: hidden; border: none; color: white; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); } .frozen-btn i { margin-right: 8px; } .return-btn { background: linear-gradient(45deg, #81a1c1, #5e81ac); } .return-btn:hover { background: linear-gradient(45deg, #5e81ac, #4c6f94); transform: translateY(-3px); box-shadow: 0 8px 15px rgba(94, 129, 172, 0.3); color: white; } .adjust-btn { background: linear-gradient(45deg, #7AB6FF, #5d91e5); } .adjust-btn:hover { background: linear-gradient(45deg, #5d91e5, #4169e1); transform: translateY(-3px); box-shadow: 0 8px 15px rgba(65, 105, 225, 0.3); color: white; } .frozen-btn::after { content: ""; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: rgba(255, 255, 255, 0.1); transform: rotate(45deg); transition: all 0.3s ease; opacity: 0; } .frozen-btn:hover::after { opacity: 1; transform: rotate(45deg) translateY(-50%); } /* 动画类 */ .fade-in { animation: fadeIn 0.5s ease forwards; opacity: 0; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .selected-row { background-color: #e3f2fd !important; position: relative; z-index: 1; } .selected-row::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to right, rgba(122, 182, 255, 0.1), transparent); pointer-events: none; } /* 响应式调整 */ @media (max-width: 992px) { .table-header-row, .table-row { grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 1.2fr 1.2fr; } .book-info { grid-template-columns: 1fr; } } @media (max-width: 768px) { .book-cover-container { margin-bottom: 30px; } .book-frame { transform: rotate(0); max-width: 180px; } .book-cover { transform: rotate(0); max-height: 200px; } .book-title { text-align: center; font-size: 1.5rem; } .table-header-row, .table-row { display: flex; flex-direction: column; } .th-frozen:after { display: none; } .th-frozen { text-align: left; padding: 10px 15px; border-bottom: 1px solid rgba(255, 255, 255, 0.2); } .td-frozen { justify-content: flex-start; padding: 10px 15px; border-bottom: 1px solid #ecf0f1; } .td-frozen:before { content: attr(data-label); font-weight: 600; margin-right: 10px; color: #7f8c8d; } .footer-actions { flex-direction: column; gap: 15px; } .frozen-btn { width: 100%; } } ================================================================================ 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_ranking.css ================================================================================ /* app/static/css/book_ranking.css */ .table-container { margin-top: 30px; } .table-container h3 { text-align: center; margin-bottom: 20px; color: var(--accent-color); font-family: 'Ma Shan Zheng', cursive, Arial, sans-serif; font-size: 1.6em; position: relative; display: inline-block; left: 50%; transform: translateX(-50%); } .table-container h3:before, .table-container h3:after { content: ''; position: absolute; height: 2px; background: linear-gradient(to right, transparent, var(--primary-color), transparent); width: 120px; top: 50%; } .table-container h3:before { right: 100%; margin-right: 15px; } .table-container h3:after { left: 100%; margin-left: 15px; } .data-table img { width: 55px; height: 80px; object-fit: cover; border-radius: 8px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); transition: transform 0.3s ease, box-shadow 0.3s ease; border: 2px solid white; } .data-table tr:hover img { transform: scale(1.08); box-shadow: 0 5px 15px rgba(0,0,0,0.15); border-color: var(--primary-color); } .data-table .rank { font-weight: 700; text-align: center; position: relative; } /* 前三名特殊样式 */ .data-table tr:nth-child(1) .rank:before { content: '👑'; position: absolute; top: -15px; left: 50%; transform: translateX(-50%); font-size: 18px; } .data-table tr:nth-child(2) .rank:before { content: '✨'; position: absolute; top: -15px; left: 50%; transform: translateX(-50%); font-size: 16px; } .data-table tr:nth-child(3) .rank:before { content: '🌟'; position: absolute; top: -15px; left: 50%; transform: translateX(-50%); font-size: 16px; } .data-table .book-title { font-weight: 500; color: var(--accent-color); transition: color 0.3s; } .data-table tr:hover .book-title { color: #d06b9c; } .data-table .author { font-style: italic; color: var(--light-text); } .data-table .borrow-count { font-weight: 600; color: var(--accent-color); position: relative; display: block; /* 修改为block以占据整个单元格 */ text-align: center; /* 确保文本居中 */ } .data-table .borrow-count:after { content: '❤️'; font-size: 12px; margin-left: 5px; opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; transform: translateY(5px); display: inline-block; } .data-table tr:hover .borrow-count:after { opacity: 1; transform: translateY(0); } .no-data { text-align: center; padding: 40px; color: var(--light-text); background-color: var(--secondary-color); border-radius: 12px; font-style: italic; border: 1px dashed var(--border-color); } /* 书籍行动画 */ #ranking-table-body tr { transition: transform 0.3s ease, opacity 0.3s ease; } #ranking-table-body tr:hover { transform: translateX(5px); } /* 加载动画美化 */ .loading-row td { background-color: var(--secondary-color); color: var(--accent-color); font-size: 16px; } /* 书名悬停效果 */ .book-title { position: relative; text-decoration: none; display: inline-block; } .book-title:after { content: ''; position: absolute; width: 100%; height: 2px; bottom: -2px; left: 0; background-color: var(--accent-color); transform: scaleX(0); transform-origin: bottom right; transition: transform 0.3s ease-out; } tr:hover .book-title:after { transform: scaleX(1); transform-origin: bottom left; } /* 特殊效果:波浪下划线 */ @keyframes wave { 0%, 100% { background-position-x: 0%; } 50% { background-position-x: 100%; } } .page-title:after { content: ''; display: block; width: 100px; height: 5px; margin: 10px auto 0; background: linear-gradient(90deg, var(--primary-color), var(--accent-color), var(--primary-color)); background-size: 200% 100%; border-radius: 5px; animation: wave 3s infinite linear; } .book-list-title { text-align: center; margin-bottom: 25px; color: var(--accent-color); font-family: 'Ma Shan Zheng', cursive, Arial, sans-serif; font-size: 1.6em; position: relative; display: inline-block; left: 50%; transform: translateX(-50%); padding: 0 15px; } .book-icon { font-size: 0.9em; margin: 0 8px; opacity: 0.85; } .column-icon { font-size: 0.9em; margin-right: 5px; opacity: 0.8; } .book-list-title:before, .book-list-title:after { content: ''; position: absolute; height: 2px; background: linear-gradient(to right, transparent, var(--primary-color), transparent); width: 80px; top: 50%; } .book-list-title:before { right: 100%; margin-right: 15px; } .book-list-title:after { left: 100%; margin-left: 15px; } /* 表格中的图标样式 */ .data-table .borrow-count:after { content: '📚'; font-size: 12px; margin-left: 5px; opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; transform: translateY(5px); display: inline-block; } /* 前三名特殊样式 - 替换这部分代码 */ .data-table tr:nth-child(1) .rank:before, .data-table tr:nth-child(2) .rank:before, .data-table tr:nth-child(3) .rank:before { position: absolute; left: 10px; /* 调整到数字左侧 */ top: 50%; /* 垂直居中 */ transform: translateY(-50%); /* 保持垂直居中 */ opacity: 0.9; } /* 分别设置每个奖牌的内容 */ .data-table tr:nth-child(1) .rank:before { content: '🏆'; font-size: 18px; } .data-table tr:nth-child(2) .rank:before { content: '🥈'; font-size: 16px; } .data-table tr:nth-child(3) .rank:before { content: '🥉'; font-size: 16px; } /* 调整排名单元格的内边距,为图标留出空间 */ .data-table .rank { padding-left: 35px; /* 增加左内边距为图标腾出空间 */ text-align: left; /* 使数字左对齐 */ } /* 加载动画美化 */ .loading-animation { display: flex; align-items: center; justify-content: center; } .loading-animation:before { content: '📖'; margin-right: 10px; animation: bookFlip 2s infinite; display: inline-block; } @keyframes bookFlip { 0% { transform: rotateY(0deg); } 50% { transform: rotateY(180deg); } 100% { transform: rotateY(360deg); } } ================================================================================ File: ./app/static/css/book-detail.css ================================================================================ /* 图书详情页样式 */ .book-detail-container { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .actions { display: flex; gap: 10px; } .book-content { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); overflow: hidden; } .book-header { display: flex; padding: 25px; border-bottom: 1px solid #f0f0f0; background-color: #f9f9f9; } .book-cover-large { flex: 0 0 200px; height: 300px; background-color: #f0f0f0; border-radius: 5px; overflow: hidden; box-shadow: 0 4px 8px rgba(0,0,0,0.1); margin-right: 30px; } .book-cover-large img { width: 100%; height: 100%; object-fit: cover; } .no-cover-large { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #aaa; } .no-cover-large i { font-size: 48px; margin-bottom: 10px; } .book-main-info { flex: 1; } .book-title { font-size: 1.8rem; font-weight: 600; margin-bottom: 15px; color: #333; } .book-author { font-size: 1.1rem; color: #555; margin-bottom: 20px; } .book-meta-info { margin-bottom: 25px; } .meta-item { display: flex; align-items: center; margin-bottom: 12px; color: #666; } .meta-item i { width: 20px; margin-right: 10px; text-align: center; color: #555; } .meta-value { font-weight: 500; color: #444; } .tag { display: inline-block; background-color: #e9ecef; color: #495057; padding: 2px 8px; border-radius: 3px; margin-right: 5px; margin-bottom: 5px; font-size: 0.85rem; } .book-status-info { display: flex; align-items: center; gap: 20px; margin-top: 20px; } .status-badge { display: inline-block; padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 0.9rem; } .status-badge.available { background-color: #d4edda; color: #155724; } .status-badge.unavailable { background-color: #f8d7da; color: #721c24; } .stock-info { font-size: 0.95rem; color: #555; } .book-details-section { padding: 25px; } .book-details-section h3 { font-size: 1.3rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; color: #444; } .book-description { color: #555; line-height: 1.6; } .no-description { color: #888; font-style: italic; } .book-borrow-history { padding: 0 25px 25px; } .book-borrow-history h3 { font-size: 1.3rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; color: #444; } .borrow-table { border: 1px solid #eee; } .no-records { color: #888; font-style: italic; text-align: center; padding: 20px; background-color: #f9f9f9; border-radius: 4px; } /* 响应式调整 */ @media (max-width: 768px) { .book-header { flex-direction: column; } .book-cover-large { margin-right: 0; margin-bottom: 20px; max-width: 200px; align-self: center; } .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } .actions { width: 100%; } } ================================================================================ File: ./app/static/css/announcement-form.css ================================================================================ .announcement-form-container { padding: 20px; max-width: 900px; margin: 0 auto; } .page-header { margin-bottom: 25px; border-bottom: 1px solid #e3e3e3; padding-bottom: 10px; } .card { box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 30px; } .form-group { margin-bottom: 1.5rem; } .form-group label { font-weight: 500; margin-bottom: 0.5rem; display: block; } .ql-container { min-height: 200px; font-size: 16px; } .form-check { margin-top: 20px; margin-bottom: 20px; } .form-buttons { display: flex; justify-content: flex-end; gap: 15px; margin-top: 30px; } .form-buttons .btn { min-width: 100px; } /* Quill编辑器样式重写 */ .ql-toolbar.ql-snow { border-top-left-radius: 4px; border-top-right-radius: 4px; } .ql-container.ql-snow { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } ================================================================================ File: ./app/static/css/book.css ================================================================================ /* 图书列表页面样式 - 女性友好版 */ /* 背景和泡泡动画 */ .book-list-container { padding: 24px; background-color: #ffeef2; /* 淡粉色背景 */ min-height: calc(100vh - 60px); position: relative; overflow: hidden; } /* 泡泡动画 */ .book-list-container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 0; } @keyframes bubble { 0% { transform: translateY(100%) scale(0); opacity: 0; } 50% { opacity: 0.6; } 100% { transform: translateY(-100vh) scale(1); opacity: 0; } } .bubble { position: absolute; bottom: -50px; background-color: rgba(255, 255, 255, 0.5); border-radius: 50%; z-index: 1; animation: bubble 15s infinite ease-in; } /* 为页面添加15个泡泡 */ .bubble:nth-child(1) { left: 5%; width: 30px; height: 30px; animation-duration: 20s; animation-delay: 0s; } .bubble:nth-child(2) { left: 15%; width: 20px; height: 20px; animation-duration: 18s; animation-delay: 1s; } .bubble:nth-child(3) { left: 25%; width: 25px; height: 25px; animation-duration: 16s; animation-delay: 2s; } .bubble:nth-child(4) { left: 35%; width: 15px; height: 15px; animation-duration: 15s; animation-delay: 0.5s; } .bubble:nth-child(5) { left: 45%; width: 30px; height: 30px; animation-duration: 14s; animation-delay: 3s; } .bubble:nth-child(6) { left: 55%; width: 20px; height: 20px; animation-duration: 13s; animation-delay: 2.5s; } .bubble:nth-child(7) { left: 65%; width: 25px; height: 25px; animation-duration: 12s; animation-delay: 1.5s; } .bubble:nth-child(8) { left: 75%; width: 15px; height: 15px; animation-duration: 11s; animation-delay: 4s; } .bubble:nth-child(9) { left: 85%; width: 30px; height: 30px; animation-duration: 10s; animation-delay: 3.5s; } .bubble:nth-child(10) { left: 10%; width: 18px; height: 18px; animation-duration: 19s; animation-delay: 0.5s; } .bubble:nth-child(11) { left: 20%; width: 22px; height: 22px; animation-duration: 17s; animation-delay: 2.5s; } .bubble:nth-child(12) { left: 30%; width: 28px; height: 28px; animation-duration: 16s; animation-delay: 1.2s; } .bubble:nth-child(13) { left: 40%; width: 17px; height: 17px; animation-duration: 15s; animation-delay: 3.7s; } .bubble:nth-child(14) { left: 60%; width: 23px; height: 23px; animation-duration: 13s; animation-delay: 2.1s; } .bubble:nth-child(15) { left: 80%; width: 19px; height: 19px; animation-duration: 12s; animation-delay: 1.7s; } /* 页面标题部分 */ .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid rgba(233, 152, 174, 0.3); position: relative; z-index: 2; } .page-header h1 { color: #d23f6e; font-size: 1.9rem; font-weight: 600; margin: 0; text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8); } /* 更漂亮的顶部按钮 */ .action-buttons { display: flex; gap: 12px; position: relative; z-index: 2; } .action-buttons .btn { display: flex; align-items: center; justify-content: center; gap: 8px; border-radius: 50px; font-weight: 500; padding: 9px 18px; transition: all 0.3s ease; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06); border: none; font-size: 0.95rem; position: relative; overflow: hidden; } .action-buttons .btn::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), transparent); pointer-events: none; } .action-buttons .btn:hover { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08); } .action-buttons .btn:active { transform: translateY(1px); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } /* 按钮颜色 */ .btn-primary { background: linear-gradient(135deg, #5c88da, #4a73c7); color: white; } .btn-success { background: linear-gradient(135deg, #56c596, #41b384); color: white; } .btn-info { background: linear-gradient(135deg, #5bc0de, #46b8da); color: white; } .btn-secondary { background: linear-gradient(135deg, #f0ad4e, #ec971f); color: white; } .btn-danger { background: linear-gradient(135deg, #ff7676, #ff5252); color: white; } /* 过滤和搜索部分 */ .filter-section { margin-bottom: 25px; padding: 18px; background-color: rgba(255, 255, 255, 0.8); border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); position: relative; z-index: 2; backdrop-filter: blur(5px); } .search-form { display: flex; flex-direction: column; gap: 16px; } .search-row { margin-bottom: 5px; width: 100%; } .search-group { display: flex; width: 100%; max-width: 800px; } .search-group .form-control { border: 1px solid #f9c0d0; border-right: none; border-radius: 25px 0 0 25px; padding: 10px 20px; height: 42px; font-size: 0.95rem; background-color: rgba(255, 255, 255, 0.9); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s; flex: 1; } .search-group .form-control:focus { outline: none; border-color: #e67e9f; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2); } .search-group .btn { border-radius: 50%; width: 42px; height: 42px; min-width: 42px; padding: 0; background: linear-gradient(135deg, #e67e9f 60%, #ffd3e1 100%); color: white; display: flex; align-items: center; justify-content: center; margin-left: -1px; /* 防止和输入框间有缝隙 */ font-size: 1.1rem; box-shadow: 0 2px 6px rgba(230, 126, 159, 0.10); transition: background 0.2s, box-shadow 0.2s; } .search-group .btn:hover { background: linear-gradient(135deg, #d23f6e 80%, #efb6c6 100%); color: #fff; box-shadow: 0 4px 12px rgba(230, 126, 159, 0.14); } .filter-row { display: flex; flex-wrap: wrap; gap: 15px; width: 100%; } .filter-group { flex: 1; min-width: 130px; } .filter-section .form-control { border: 1px solid #f9c0d0; border-radius: 25px; height: 42px; padding: 10px 20px; background-color: rgba(255, 255, 255, 0.9); appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23e67e9f' d='M6 8.825L1.175 4 2.238 2.938 6 6.7 9.763 2.937 10.825 4z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 15px center; background-size: 12px; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); width: 100%; } .filter-section .form-control:focus { outline: none; border-color: #e67e9f; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2); } /* 图书网格布局 */ .books-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24px; margin-bottom: 30px; position: relative; z-index: 2; } /* 图书卡片样式 */ .book-card { display: flex; flex-direction: column; border-radius: 16px; overflow: hidden; background-color: white; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.06); transition: all 0.3s ease; height: 100%; position: relative; border: 1px solid rgba(233, 152, 174, 0.2); } .book-card:hover { transform: translateY(-8px); box-shadow: 0 12px 25px rgba(0, 0, 0, 0.1); } .book-cover { width: 100%; height: 180px; background-color: #faf3f5; overflow: hidden; position: relative; } .book-cover::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, transparent 60%, rgba(249, 219, 227, 0.4)); pointer-events: none; } .book-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; } .book-card:hover .book-cover img { transform: scale(1.05); } .no-cover { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background: linear-gradient(135deg, #ffeef2 0%, #ffd9e2 100%); color: #e67e9f; position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 1; pointer-events: none; } .no-cover i { font-size: 36px; margin-bottom: 10px; } .book-info { padding: 20px; display: flex; flex-direction: column; flex: 1; } .book-title { font-size: 1.1rem; font-weight: 600; margin: 0 0 10px; color: #d23f6e; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .book-author { font-size: 0.95rem; color: #888; margin-bottom: 15px; } .book-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; } .book-category { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; background-color: #ffebf0; color: #e67e9f; font-weight: 500; } .book-status { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 500; } .book-status.available { background-color: #dffff6; color: #26a69a; } .book-status.unavailable { background-color: #ffeeee; color: #e57373; } .book-details { flex: 1; display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; font-size: 0.9rem; color: #777; } .book-details p { margin: 0; display: flex; } .book-details strong { min-width: 65px; color: #999; font-weight: 600; } /* 按钮组样式 */ .book-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: auto; } .book-actions .btn { padding: 8px 0; font-size: 0.9rem; text-align: center; border-radius: 25px; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 6px; border: none; font-weight: 500; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .book-actions .btn:hover { transform: translateY(-3px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12); } .book-actions .btn i { font-size: 0.85rem; } /* 具体按钮颜色 */ .book-actions .btn-primary { background: linear-gradient(135deg, #5c88da, #4a73c7); } .book-actions .btn-info { background: linear-gradient(135deg, #5bc0de, #46b8da); } .book-actions .btn-success { background: linear-gradient(135deg, #56c596, #41b384); } .book-actions .btn-danger { background: linear-gradient(135deg, #ff7676, #ff5252); } /* 无图书状态 */ .no-books { grid-column: 1 / -1; padding: 50px 30px; text-align: center; background-color: white; border-radius: 16px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); position: relative; z-index: 2; } .no-books i { font-size: 60px; color: #f9c0d0; margin-bottom: 20px; } .no-books p { font-size: 1.1rem; color: #e67e9f; font-weight: 500; } /* 分页容器 */ .pagination-container { display: flex; flex-direction: column; align-items: center; margin-top: 30px; position: relative; z-index: 2; } .pagination { display: flex; list-style: none; padding: 0; margin: 0 0 15px 0; background-color: white; border-radius: 30px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); overflow: hidden; } .pagination .page-item { margin: 0; } .pagination .page-link { display: flex; align-items: center; justify-content: center; min-width: 40px; height: 40px; padding: 0 15px; border: none; color: #777; font-weight: 500; transition: all 0.2s; position: relative; } .pagination .page-link:hover { color: #e67e9f; background-color: #fff9fb; } .pagination .page-item.active .page-link { background-color: #e67e9f; color: white; box-shadow: none; } .pagination .page-item.disabled .page-link { color: #bbb; background-color: #f9f9f9; } .pagination-info { color: #999; font-size: 0.9rem; } /* 优化模态框样式 */ .modal-content { border-radius: 20px; border: none; box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07); overflow: hidden; } .modal-header { padding: 20px 25px; background-color: #ffeef2; border-bottom: 1px solid #ffe0e9; } .modal-title { color: #d23f6e; font-size: 1.2rem; font-weight: 600; } .modal-body { padding: 25px; } .modal-footer { padding: 15px 25px; border-top: 1px solid #ffe0e9; background-color: #ffeef2; } .modal-body p { color: #666; font-size: 1rem; line-height: 1.6; } .modal-body p.text-danger { color: #ff5252 !important; font-weight: 500; display: flex; align-items: center; gap: 8px; } .modal-body p.text-danger::before { content: "\f06a"; font-family: "Font Awesome 5 Free"; font-weight: 900; } .modal .close { font-size: 1.5rem; color: #e67e9f; opacity: 0.8; text-shadow: none; transition: all 0.2s; } .modal .close:hover { opacity: 1; color: #d23f6e; } .modal .btn { border-radius: 25px; padding: 8px 20px; font-weight: 500; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); border: none; } .modal .btn-secondary { background: linear-gradient(135deg, #a0a0a0, #808080); color: white; } .modal .btn-danger { background: linear-gradient(135deg, #ff7676, #ff5252); color: white; } /* 封面标题栏 */ .cover-title-bar { position: absolute; left: 0; right: 0; bottom: 0; background: linear-gradient(0deg, rgba(233,152,174,0.92) 0%, rgba(255,255,255,0.08) 90%); color: #fff; font-size: 1rem; font-weight: bold; padding: 10px 14px 7px 14px; text-shadow: 0 2px 6px rgba(180,0,80,0.14); line-height: 1.3; width: 100%; box-sizing: border-box; display: flex; align-items: flex-end; min-height: 38px; z-index: 2; } .book-card:hover .cover-title-bar { background: linear-gradient(0deg, #d23f6e 0%, rgba(255,255,255,0.1) 100%); font-size: 1.07rem; letter-spacing: .5px; } /* 响应式调整 */ @media (max-width: 992px) { .filter-row { flex-wrap: wrap; } .filter-group { flex: 1 0 180px; } } @media (max-width: 768px) { .book-list-container { padding: 16px; } .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } .action-buttons { width: 100%; overflow-x: auto; padding-bottom: 8px; flex-wrap: nowrap; justify-content: flex-start; } .filter-section { padding: 15px; } .search-form { flex-direction: column; gap: 12px; } .search-group { max-width: 100%; } .filter-row { gap: 12px; } .books-grid { grid-template-columns: 1fr; } .book-actions { grid-template-columns: 1fr 1fr; } } @media (max-width: 600px) { .cover-title-bar { font-size: 0.95rem; min-height: 27px; padding: 8px 8px 5px 10px; } .book-actions { grid-template-columns: 1fr; } } ================================================================================ File: ./app/static/css/login.css ================================================================================ /* login.css - 登录页面专用样式 */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; } :root { --primary-color: #4a89dc; --primary-hover: #3b78c4; --secondary-color: #5cb85c; --text-color: #333; --light-text: #666; --bg-color: #f5f7fa; --card-bg: #ffffff; --border-color: #ddd; --error-color: #e74c3c; --success-color: #2ecc71; } body.dark-mode { --primary-color: #5a9aed; --primary-hover: #4a89dc; --secondary-color: #6bc76b; --text-color: #f1f1f1; --light-text: #aaa; --bg-color: #1a1a1a; --card-bg: #2c2c2c; --border-color: #444; } body { background-color: var(--bg-color); background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background-size: cover; background-position: center; display: flex; flex-direction: column; min-height: 100vh; color: var(--text-color); transition: all 0.3s ease; } .theme-toggle { position: absolute; top: 20px; right: 20px; z-index: 10; cursor: pointer; padding: 8px; border-radius: 50%; background-color: rgba(255, 255, 255, 0.2); backdrop-filter: blur(5px); border: 1px solid rgba(255, 255, 255, 0.1); } .overlay { background-color: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px); position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; } .main-container { display: flex; justify-content: center; align-items: center; flex: 1; padding: 20px; } .login-container { background-color: var(--card-bg); border-radius: 12px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); width: 450px; padding: 35px; position: relative; overflow: hidden; animation: fadeIn 0.5s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .logo { text-align: center; margin-bottom: 25px; position: relative; } .logo img { width: 90px; height: 90px; border-radius: 12px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); padding: 5px; background-color: #fff; transition: transform 0.3s ease; } h1 { text-align: center; color: var(--text-color); margin-bottom: 10px; font-weight: 600; font-size: 28px; } .subtitle { text-align: center; color: var(--light-text); margin-bottom: 30px; font-size: 14px; } .form-group { margin-bottom: 22px; position: relative; } .form-group label { display: block; margin-bottom: 8px; color: var(--text-color); font-weight: 500; font-size: 14px; } .input-with-icon { position: relative; } .input-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: var(--light-text); } .form-control { width: 100%; height: 48px; border: 1px solid var(--border-color); border-radius: 6px; padding: 0 15px 0 45px; font-size: 15px; transition: all 0.3s ease; background-color: var(--card-bg); color: var(--text-color); } .form-control:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2); outline: none; } .password-toggle { position: absolute; right: 15px; top: 50%; transform: translateY(-50%); cursor: pointer; color: var(--light-text); } .validation-message { margin-top: 6px; font-size: 12px; color: var(--error-color); display: none; } .validation-message.show { display: block; animation: shake 0.5s ease; } @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .remember-forgot { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; } .custom-checkbox { position: relative; padding-left: 30px; cursor: pointer; font-size: 14px; user-select: none; color: var(--light-text); } .custom-checkbox input { position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; } .checkmark { position: absolute; top: 0; left: 0; height: 18px; width: 18px; background-color: var(--card-bg); border: 1px solid var(--border-color); border-radius: 3px; transition: all 0.2s ease; } .custom-checkbox:hover input ~ .checkmark { border-color: var(--primary-color); } .custom-checkbox input:checked ~ .checkmark { background-color: var(--primary-color); border-color: var(--primary-color); } .checkmark:after { content: ""; position: absolute; display: none; } .custom-checkbox input:checked ~ .checkmark:after { display: block; } .custom-checkbox .checkmark:after { left: 6px; top: 2px; width: 4px; height: 9px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); } .forgot-password a { color: var(--primary-color); text-decoration: none; font-size: 14px; transition: color 0.3s ease; } .forgot-password a:hover { color: var(--primary-hover); text-decoration: underline; } .btn-login { width: 100%; height: 48px; background-color: var(--primary-color); color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; } .btn-login:hover { background-color: var(--primary-hover); } .btn-login:active { transform: scale(0.98); } .btn-login .loading { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .btn-login.loading-state { color: transparent; } .btn-login.loading-state .loading { display: block; } .signup { text-align: center; margin-top: 25px; font-size: 14px; color: var(--light-text); } .signup a { color: var(--primary-color); text-decoration: none; font-weight: 600; transition: color 0.3s ease; } .signup a:hover { color: var(--primary-hover); text-decoration: underline; } .features { display: flex; justify-content: center; margin-top: 25px; gap: 30px; } .feature-item { text-align: center; font-size: 12px; color: var(--light-text); display: flex; flex-direction: column; align-items: center; } .feature-icon { margin-bottom: 5px; font-size: 18px; } footer { text-align: center; padding: 20px; color: rgba(255, 255, 255, 0.7); font-size: 12px; } footer a { color: rgba(255, 255, 255, 0.9); text-decoration: none; } .alert { padding: 10px; margin-bottom: 15px; border-radius: 4px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; } @media (max-width: 576px) { .login-container { width: 100%; padding: 25px; border-radius: 0; } .theme-toggle { top: 10px; } .logo img { width: 70px; height: 70px; } h1 { font-size: 22px; } .main-container { padding: 0; } } ================================================================================ File: ./app/static/css/book-edit.css ================================================================================ /* ========== 优雅粉色主题 - 图书编辑系统 ========== */ :root { --primary-pink: #FF85A2; --primary-pink-hover: #FF6D8E; --secondary-pink: #FFC0D3; --accent-pink: #FF4778; --background-pink: #FFF5F7; --border-pink: #FFD6E0; --soft-lavender: #E2D1F9; --mint-green: #D0F0C0; --dark-text: #5D4E60; --medium-text: #8A7B8F; --light-text: #BFB5C6; --white: #FFFFFF; --shadow-sm: 0 4px 6px rgba(255, 133, 162, 0.1); --shadow-md: 0 6px 12px rgba(255, 133, 162, 0.15); --shadow-lg: 0 15px 25px rgba(255, 133, 162, 0.2); --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; --transition-fast: 0.2s ease; --transition-base: 0.3s ease; --font-primary: 'Poppins', 'Helvetica Neue', sans-serif; --font-secondary: 'Playfair Display', serif; } /* ========== 全局样式 ========== */ body { background-color: var(--background-pink); color: var(--dark-text); font-family: var(--font-primary); line-height: 1.6; } h1, h2, h3, h4, h5, h6 { font-family: var(--font-secondary); color: var(--dark-text); } a { color: var(--accent-pink); transition: color var(--transition-fast); } a:hover { color: var(--primary-pink-hover); text-decoration: none; } .btn { border-radius: var(--border-radius-sm); font-weight: 500; transition: all var(--transition-base); box-shadow: var(--shadow-sm); padding: 0.5rem 1.25rem; } .btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); } .btn-primary { background-color: var(--primary-pink); border-color: var(--primary-pink); } .btn-primary:hover, .btn-primary:focus { background-color: var(--primary-pink-hover); border-color: var(--primary-pink-hover); } .btn-info { background-color: var(--soft-lavender); border-color: var(--soft-lavender); color: var(--dark-text); } .btn-info:hover, .btn-info:focus { background-color: #D4BFF0; border-color: #D4BFF0; color: var(--dark-text); } .btn-secondary { background-color: var(--white); border-color: var(--border-pink); color: var(--medium-text); } .btn-secondary:hover, .btn-secondary:focus { background-color: var(--border-pink); border-color: var(--border-pink); color: var(--dark-text); } .btn i { margin-right: 8px; } /* ========== 表单容器 ========== */ .book-form-container { max-width: 1400px; margin: 2rem auto; padding: 2rem; background-color: var(--white); border-radius: var(--border-radius-lg); box-shadow: var(--shadow-md); position: relative; overflow: hidden; } .book-form-container::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 8px; background: linear-gradient(to right, var(--primary-pink), var(--accent-pink), var(--soft-lavender)); } /* ========== 页面标题区域 ========== */ .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid var(--secondary-pink); } .page-header h1 { font-size: 2.2rem; font-weight: 700; color: var(--primary-pink); margin: 0; position: relative; font-family: var(--font-secondary); } .flower-icon { color: var(--accent-pink); margin-right: 8px; } .actions { display: flex; gap: 1rem; } /* ========== 表单元素 ========== */ .form-row { margin-bottom: 1.5rem; } .form-group { margin-bottom: 1.5rem; } .form-group label { color: var(--dark-text); font-weight: 500; font-size: 0.95rem; margin-bottom: 0.5rem; display: block; } .form-control { border: 2px solid var(--border-pink); border-radius: var(--border-radius-sm); padding: 0.75rem 1rem; color: var(--dark-text); transition: all var(--transition-fast); font-size: 0.95rem; } .form-control:focus { border-color: var(--primary-pink); box-shadow: 0 0 0 0.2rem rgba(255, 133, 162, 0.25); } .form-control::placeholder { color: var(--light-text); } .required { color: var(--accent-pink); } select.form-control { height: 42px; / 确保高度一致,内容不截断 */ line-height: 1.5; padding: 8px 12px; font-size: 0.95rem; color: var(--dark-text); background-color: var(--white); border: 1px solid var(--border-pink); border-radius: var(--border-radius-sm); appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%235D4E60' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.75rem center; background-size: 1rem; transition: border-color 0.2s ease, box-shadow 0.2s ease; } select.form-control:focus { border-color: var(--primary-pink); outline: none; box-shadow: 0 0 0 0.2rem rgba(255, 133, 162, 0.2); } /* 状态选项 / 分类样式专属修复(可选项) */ #status, #category_id { padding-top: 8px; padding-bottom: 8px; font-family: inherit; } /* iOS & Edge 下拉兼容优化 */ select.form-control::-ms-expand { display: none; } /* 浏览器优雅过渡体验 */ select.form-control:hover { border-color: var(--accent-pink); } select.form-control:disabled { background-color: var(--background-pink); color: var(--light-text); cursor: not-allowed; opacity: 0.7; } textarea.form-control { min-height: 150px; resize: vertical; } /* ========== 卡片样式 ========== */ .card { border: none; border-radius: var(--border-radius-md); box-shadow: var(--shadow-sm); overflow: hidden; transition: all var(--transition-base); margin-bottom: 1.5rem; background-color: var(--white); } .card:hover { box-shadow: var(--shadow-md); } .card-header { background-color: var(--secondary-pink); border-bottom: none; padding: 1rem 1.5rem; font-family: var(--font-secondary); font-weight: 600; color: var(--dark-text); font-size: 1.1rem; } .card-body { padding: 1.5rem; background-color: var(--white); } /* ========== 封面图片区域 ========== */ .cover-preview-container { padding: 1rem; text-align: center; } .cover-preview { min-height: 300px; background-color: var(--background-pink); border: 2px dashed var(--secondary-pink); border-radius: var(--border-radius-sm); overflow: hidden; display: flex; align-items: center; justify-content: center; margin-bottom: 1rem; position: relative; transition: all var(--transition-fast); } .cover-preview:hover { border-color: var(--primary-pink); } .cover-image { max-width: 100%; max-height: 300px; border-radius: var(--border-radius-sm); box-shadow: var(--shadow-sm); } .no-cover-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--light-text); padding: 2rem; } .no-cover-placeholder i { font-size: 3rem; margin-bottom: 1rem; } .upload-container { margin-top: 1rem; } .btn-outline-primary { color: var(--primary-pink); border-color: var(--primary-pink); background-color: transparent; transition: all var(--transition-base); } .btn-outline-primary:hover, .btn-outline-primary:focus { background-color: var(--primary-pink); color: white; } /* ========== 提交按钮区域 ========== */ .form-submit-container { margin-top: 2rem; } .btn-lg { padding: 1rem 1.5rem; font-size: 1.1rem; } .btn-block { width: 100%; } /* 输入组样式 */ .input-group-prepend .input-group-text { background-color: var(--secondary-pink); border-color: var(--border-pink); color: var(--dark-text); border-radius: var(--border-radius-sm) 0 0 var(--border-radius-sm); } /* 聚焦效果 */ .is-focused label { color: var(--primary-pink); } /* ========== 动画效果 ========== */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .book-form-container { animation: fadeIn 0.5s ease; } /* ========== 响应式样式 ========== */ @media (max-width: 992px) { .book-form-container { padding: 1.5rem; } .page-header { flex-direction: column; align-items: flex-start; gap: 1rem; } .actions { margin-top: 1rem; } } @media (max-width: 768px) { .book-form-container { padding: 1rem; } .card-header, .card-body { padding: 1rem; } .cover-preview { min-height: 250px; } .col-md-8, .col-md-4 { padding: 0 0.5rem; } } .is-invalid { border-color: #dc3545; } .is-valid { border-color: #28a745; } .invalid-feedback { display: none; color: #dc3545; font-size: 0.875rem; } .is-invalid ~ .invalid-feedback { display: block; } ================================================================================ File: ./app/static/css/borrow_management.css ================================================================================ /* borrow_management.css - Optimized for literary female audience */ /* Main typography and colors */ body { font-family: 'Georgia', serif; color: #4a3728; background-color: #fcf8f3; } .page-title { margin-bottom: 1.5rem; color: #5d3511; border-bottom: 2px solid #d9c7b8; padding-bottom: 15px; font-family: 'Playfair Display', Georgia, serif; letter-spacing: 0.5px; position: relative; } .page-title:after { content: "❦"; position: absolute; bottom: -12px; left: 50%; font-size: 18px; color: #8d6e63; background: #fcf8f3; padding: 0 10px; transform: translateX(-50%); } .container { background-color: #fff9f5; border-radius: 8px; box-shadow: 0 3px 15px rgba(113, 66, 20, 0.1); padding: 25px; margin-top: 20px; margin-bottom: 20px; border: 1px solid #e8d9cb; } /* Tabs styling */ .tabs { display: flex; border-bottom: 1px solid #d9c7b8; margin-bottom: 25px; position: relative; } .tabs:before { content: ""; position: absolute; left: 0; right: 0; bottom: -3px; height: 2px; background: linear-gradient(to right, transparent, #8d6e63, transparent); } .tab { padding: 12px 22px; text-decoration: none; color: #5d3511; margin-right: 5px; border-radius: 8px 8px 0 0; position: relative; transition: all 0.3s ease; font-family: 'Georgia', serif; } .tab:hover { background-color: #f1e6dd; color: #704214; text-decoration: none; } .tab.active { background-color: #704214; color: #f8f0e5; font-weight: 500; } .tab.overdue-tab { background-color: #f9e8e8; color: #a15950; } .tab.overdue-tab:hover { background-color: #f4d3d3; } /* 修改 count 样式,避免与顶部导航冲突 */ .tabs .count { background-color: rgba(113, 66, 20, 0.15); border-radius: 12px; padding: 2px 10px; font-size: 0.8em; margin-left: 8px; font-family: 'Arial', sans-serif; display: inline-block; position: static; width: auto; height: auto; } .tab.active .count { background-color: rgba(255, 243, 224, 0.3); } .count.overdue-count { background-color: rgba(161, 89, 80, 0.2); } /* Search and filters */ .search-card { margin-bottom: 25px; border-radius: 8px; box-shadow: 0 2px 8px rgba(113, 66, 20, 0.08); border: 1px solid #e8d9cb; background: linear-gradient(to bottom right, #fff, #fcf8f3); } .search-card .card-body { padding: 20px; } .search-form { margin-bottom: 0; } .search-card .form-control { border: 1px solid #d9c7b8; border-radius: 6px; color: #5d3511; background-color: #fff9f5; transition: all 0.3s ease; font-family: 'Georgia', serif; } .search-card .form-control:focus { border-color: #704214; box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.15); background-color: #fff; } .search-card .btn-outline-secondary { color: #704214; border-color: #d9c7b8; background-color: transparent; } .search-card .btn-outline-secondary:hover { color: #fff; background-color: #8d6e63; border-color: #8d6e63; } .clear-filters { display: block; width: 100%; text-align: center; font-style: italic; } /* Table styling */ .borrow-table { width: 100%; border-collapse: separate; border-spacing: 0; margin-bottom: 25px; box-shadow: 0 2px 10px rgba(113, 66, 20, 0.05); border-radius: 8px; overflow: hidden; border: 1px solid #e8d9cb; } .borrow-table th, .borrow-table td { padding: 15px 18px; text-align: left; border-bottom: 1px solid #e8d9cb; vertical-align: middle; } /* 调整借阅用户列向左偏移15px */ .borrow-table th:nth-child(3), .borrow-table td:nth-child(3) { padding-right: 3px; } .borrow-table th { background-color: #f1e6dd; color: #5d3511; font-weight: 600; letter-spacing: 0.5px; } /* 状态列调整 - 居中并确保内容显示 */ .borrow-table th:nth-child(6) { text-align: center; } .borrow-table td:nth-child(6) { text-align: center; } .borrow-item:hover { background-color: #f8f0e5; } .borrow-item:last-child td { border-bottom: none; } .book-cover img { width: 65px; height: 90px; object-fit: cover; border-radius: 6px; box-shadow: 0 3px 8px rgba(113, 66, 20, 0.15); border: 2px solid #fff; transition: transform 0.3s ease; } .book-cover img:hover { transform: scale(1.05); } .book-title { font-weight: 600; font-family: 'Georgia', serif; } .book-title a { color: #5d3511; text-decoration: none; transition: color 0.3s ease; } .book-title a:hover { color: #a66321; text-decoration: underline; } .book-author { color: #8d6e63; font-size: 0.9em; margin-top: 5px; font-style: italic; } /* 修改借阅用户显示方式 */ .borrow-table .user-info { text-align: center; display: table-cell; vertical-align: middle; height: 100%; } .borrow-table .user-info a { color: #5d3511; text-decoration: none; font-weight: 600; transition: color 0.3s ease; display: block; margin-bottom: 8px; } .borrow-table .user-info a:hover { color: #a66321; text-decoration: underline; } .user-nickname { color: #8d6e63; font-size: 0.9em; display: block; margin-top: 0; } /* Badges and status indicators - 修复与顶部导航栏冲突 */ .borrow-table .badge { padding: 5px 12px; border-radius: 20px; font-weight: 500; font-size: 0.85em; letter-spacing: 0.5px; display: inline-block; margin-bottom: 5px; position: static; width: auto; height: auto; top: auto; right: auto; } /* 给状态列的徽章额外的特异性 */ .borrow-table td .badge { position: static; width: auto; height: auto; display: inline-block; font-size: 0.85em; border-radius: 20px; padding: 5px 12px; } .borrow-table .badge-primary { background-color: #704214; color: white; } .borrow-table .badge-success { background-color: #5b8a72; color: white; } .borrow-table .badge-danger { background-color: #a15950; color: white; } .borrow-table .badge-info { background-color: #6a8da9; color: white; } .borrow-table .badge-warning { background-color: #d4a76a; color: #4a3728; } .return-date { color: #8d6e63; font-size: 0.9em; margin-top: 5px; } /* 确保状态显示正确 */ .borrow-item td:nth-child(6) span.badge { min-width: 80px; } /* 只为表格中的操作按钮设置样式 */ .actions .btn { margin-right: 5px; margin-bottom: 6px; border-radius: 20px; padding: 8px 16px; transition: all 0.3s ease; font-family: 'Georgia', serif; letter-spacing: 0.5px; } .actions .btn-success { background-color: #5b8a72; border-color: #5b8a72; } .actions .btn-success:hover, .actions .btn-success:focus { background-color: #4a7561; border-color: #4a7561; } .actions .btn-warning { background-color: #d4a76a; border-color: #d4a76a; color: #4a3728; } .actions .btn-warning:hover, .actions .btn-warning:focus { background-color: #c29355; border-color: #c29355; color: #4a3728; } .actions .btn-primary { background-color: #704214; border-color: #704214; } .actions .btn-primary:hover, .actions .btn-primary:focus { background-color: #5d3511; border-color: #5d3511; box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.25); } .text-danger { color: #a15950 !important; } .overdue { background-color: rgba(161, 89, 80, 0.05); } /* Empty states */ .no-records { text-align: center; padding: 60px 20px; background-color: #f8f0e5; border-radius: 8px; margin: 25px 0; border: 1px dashed #d9c7b8; } .empty-icon { font-size: 4.5em; color: #d9c7b8; margin-bottom: 25px; } .empty-text { color: #8d6e63; margin-bottom: 25px; font-style: italic; font-size: 1.1em; } /* Pagination */ .pagination-container { display: flex; justify-content: center; margin-top: 25px; } .pagination .page-link { color: #5d3511; border-color: #e8d9cb; margin: 0 3px; border-radius: 4px; } .pagination .page-item.active .page-link { background-color: #704214; border-color: #704214; } .pagination .page-link:hover { background-color: #f1e6dd; color: #5d3511; } /* Modal customization */ .modal-content { border-radius: 8px; border: 1px solid #e8d9cb; box-shadow: 0 5px 20px rgba(113, 66, 20, 0.15); background-color: #fff9f5; } .modal-header { border-bottom: 1px solid #e8d9cb; background-color: #f1e6dd; border-radius: 8px 8px 0 0; } .modal-title { color: #5d3511; font-family: 'Georgia', serif; } .modal-footer { border-top: 1px solid #e8d9cb; } /* Decorative elements */ .container:before { content: ""; position: absolute; top: 0; right: 0; width: 150px; height: 150px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cpath fill='%23d9c7b8' fill-opacity='0.2' d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/svg%3E"); opacity: 0.3; pointer-events: none; z-index: -1; } /* Responsive design */ @media (max-width: 992px) { .tabs { flex-wrap: wrap; } .tab { margin-bottom: 8px; } } @media (max-width: 768px) { .tabs { flex-direction: column; border-bottom: none; } .tab { border-radius: 8px; margin-right: 0; margin-bottom: 8px; border: 1px solid #d9c7b8; } .borrow-table { display: block; overflow-x: auto; } .book-cover img { width: 50px; height: 70px; } .search-card .row { margin-bottom: 15px; } } .container .btn-primary { background-color: #704214; border-color: #704214; color: white; border-radius: 20px; } .container .btn-primary:hover, .container .btn-primary:focus { background-color: #5d3511; border-color: #5d3511; box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.25); } ================================================================================ File: ./app/static/css/index.css ================================================================================ /* index.css - 仅用于图书管理系统首页/仪表板 */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; } body { background-color: #f5f7fa; color: #333; font-size: 16px; line-height: 1.6; } a { text-decoration: none; color: #4a89dc; } ul { list-style: none; } /* 应用容器 */ .app-container { display: flex; min-height: 100vh; } /* 侧边导航栏 */ .sidebar { width: 250px; background-color: #2c3e50; color: #ecf0f1; padding: 20px 0; box-shadow: 0 0 10px rgba(0,0,0,0.1); position: fixed; height: 100vh; overflow-y: auto; } .logo-container { padding: 0 20px 20px; display: flex; align-items: center; justify-content: center; flex-direction: column; margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.1); } .logo { width: 60px; height: auto; margin-bottom: 10px; } .logo-container h2 { font-size: 1.2rem; margin: 10px 0; color: #ecf0f1; font-weight: 500; } .nav-links li { margin-bottom: 5px; } .nav-links li a { padding: 10px 20px; display: flex; align-items: center; color: #bdc3c7; transition: all 0.3s ease; } .nav-links li a i { margin-right: 10px; font-size: 1.1rem; width: 20px; text-align: center; } .nav-links li a:hover, .nav-links li.active a { background-color: #34495e; color: #ecf0f1; border-left: 3px solid #4a89dc; } .nav-category { padding: 10px 20px; font-size: 0.85rem; text-transform: uppercase; color: #7f8c8d; margin-top: 15px; margin-bottom: 5px; } /* 主内容区 */ .main-content { flex: 1; margin-left: 250px; padding: 20px; } /* 顶部导航栏 */ .top-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 30px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 20px; } .search-container { position: relative; width: 300px; } .search-input { padding: 10px 15px 10px 40px; width: 100%; border: 1px solid #e1e4e8; border-radius: 20px; font-size: 14px; transition: all 0.3s ease; } .search-input:focus { border-color: #4a89dc; box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.1); outline: none; } .search-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #8492a6; } .user-menu { display: flex; align-items: center; } .notifications { margin-right: 20px; position: relative; cursor: pointer; } .notifications i { font-size: 1.2rem; color: #606266; } .badge { position: absolute; top: -8px; right: -8px; background-color: #f56c6c; color: white; font-size: 0.7rem; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .user-info { display: flex; align-items: center; position: relative; cursor: pointer; } .user-avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #4a89dc; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px; font-size: 1.2rem; } .user-details { display: flex; flex-direction: column; } .user-name { font-weight: 500; color: #333; } .user-role { font-size: 0.8rem; color: #8492a6; } .dropdown-menu { position: absolute; top: 100%; right: 0; background-color: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); padding: 10px 0; min-width: 150px; display: none; z-index: 10; } .user-info.active .dropdown-menu { display: block; } .dropdown-menu a { display: block; padding: 8px 15px; color: #606266; transition: all 0.3s ease; } .dropdown-menu a:hover { background-color: #f5f7fa; } .dropdown-menu a i { margin-right: 8px; width: 16px; text-align: center; } /* 欢迎区域 */ .welcome-section { background: linear-gradient(to right, #4a89dc, #5d9cec); color: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } .welcome-section h1 { font-size: 1.8rem; margin-bottom: 5px; } .welcome-section p { font-size: 1rem; opacity: 0.9; } /* 统计卡片样式 */ .stats-container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; } .stat-card { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px; display: flex; align-items: center; transition: transform 0.3s ease, box-shadow 0.3s ease; } .stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .stat-icon { font-size: 2rem; color: #4a89dc; margin-right: 15px; width: 40px; text-align: center; } .stat-info h3 { font-size: 0.9rem; color: #606266; margin-bottom: 5px; } .stat-number { font-size: 1.8rem; font-weight: 600; color: #2c3e50; } /* 主要内容区域 */ .main-sections { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; } .content-section { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #edf2f7; } .section-header h2 { font-size: 1.2rem; color: #2c3e50; } .view-all { font-size: 0.85rem; color: #4a89dc; display: flex; align-items: center; } .view-all i { margin-left: 5px; transition: transform 0.3s ease; } .view-all:hover i { transform: translateX(3px); } /* 图书卡片样式 */ .book-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; } .book-card { display: flex; border: 1px solid #edf2f7; border-radius: 8px; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; } .book-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.05); } .book-cover { width: 100px; height: 140px; min-width: 100px; background-color: #f5f7fa; display: flex; align-items: center; justify-content: center; overflow: hidden; } .book-cover img { width: 100%; height: 100%; object-fit: cover; } .book-info { padding: 15px; flex: 1; display: flex; flex-direction: column; } .book-title { font-size: 1rem; margin-bottom: 5px; color: #2c3e50; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; } .book-author { font-size: 0.85rem; color: #606266; margin-bottom: 10px; } .book-meta { display: flex; justify-content: space-between; margin-bottom: 15px; } .book-category { background-color: #e5f1ff; color: #4a89dc; padding: 3px 8px; border-radius: 4px; font-size: 0.75rem; } .book-status { font-size: 0.75rem; font-weight: 500; } .book-status.available { color: #67c23a; } .book-status.borrowed { color: #e6a23c; } .borrow-btn { background-color: #4a89dc; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85rem; margin-top: auto; transition: background-color 0.3s ease; } .borrow-btn:hover { background-color: #357bc8; } /* 通知公告样式 */ .notice-item { display: flex; padding: 15px 0; border-bottom: 1px solid #edf2f7; } .notice-item:last-child { border-bottom: none; } .notice-icon { font-size: 1.5rem; color: #4a89dc; margin-right: 15px; display: flex; align-items: flex-start; padding-top: 5px; } .notice-content h3 { font-size: 1rem; color: #2c3e50; margin-bottom: 5px; } .notice-content p { font-size: 0.9rem; color: #606266; margin-bottom: 10px; } .notice-meta { display: flex; justify-content: space-between; align-items: center; } .notice-time { font-size: 0.8rem; color: #8492a6; } .renew-btn { background-color: #ecf5ff; color: #4a89dc; border: 1px solid #d9ecff; padding: 5px 10px; border-radius: 4px; font-size: 0.8rem; cursor: pointer; transition: all 0.3s ease; } .renew-btn:hover { background-color: #4a89dc; color: white; border-color: #4a89dc; } /* 热门图书区域 */ .popular-section { margin-top: 20px; } .popular-books { display: flex; overflow-x: auto; gap: 15px; padding-bottom: 10px; } .popular-book-item { display: flex; background-color: #f8fafc; border-radius: 8px; padding: 20px 15px 15px; /* 增加顶部内边距,为角标留出空间 */ min-width: 280px; position: relative; margin-top: 10px; /* 在顶部添加一些外边距 */ box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: transform 0.2s; } .popular-book-item:hover { transform: translateY(-3px); box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .rank-badge { position: absolute; top: -8px; /* 略微调高一点 */ left: 10px; background-color: #4a89dc; color: white; width: 28px; /* 增加尺寸 */ height: 28px; /* 增加尺寸 */ display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 0.85rem; font-weight: bold; z-index: 10; /* 确保它位于其他元素之上 */ box-shadow: 0 2px 4px rgba(0,0,0,0.2); /* 添加阴影使其更突出 */ } .book-cover.small { width: 60px; height: 90px; min-width: 60px; margin-right: 15px; border-radius: 4px; overflow: hidden; /* 确保图片不会溢出容器 */ } .book-cover.small img { width: 100%; height: 100%; object-fit: cover; /* 确保图片正确填充容器 */ } .book-details { flex: 1; } .book-stats { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; } .book-stats span { font-size: 0.8rem; color: #8492a6; } .book-stats i { margin-right: 5px; } /* 响应式调整 */ @media (max-width: 1200px) { .stats-container { grid-template-columns: repeat(2, 1fr); } .main-sections { grid-template-columns: 1fr; } } @media (max-width: 768px) { .sidebar { width: 70px; overflow: hidden; } .logo-container { padding: 10px; } .logo-container h2 { display: none; } .nav-links li a span { display: none; } .nav-links li a i { margin-right: 0; } .nav-category { display: none; } .main-content { margin-left: 70px; } .search-container { width: 180px; } .book-grid { grid-template-columns: 1fr; } } @media (max-width: 576px) { .stats-container { grid-template-columns: 1fr; } .top-bar { flex-direction: column; gap: 15px; } .search-container { width: 100%; } .user-details { display: none; } } ================================================================================ File: ./app/static/css/main.css ================================================================================ /* 基础样式 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f0f2f5; color: #333; } .app-container { display: flex; min-height: 100vh; } /* 侧边栏样式 */ .sidebar { width: 250px; background-color: #2c3e50; color: white; box-shadow: 2px 0 5px rgba(0,0,0,0.1); position: fixed; height: 100vh; overflow-y: auto; z-index: 1000; } .logo-container { display: flex; align-items: center; padding: 20px 15px; border-bottom: 1px solid rgba(255,255,255,0.1); } .logo { width: 40px; height: 40px; margin-right: 10px; } .logo-container h2 { font-size: 1.2rem; font-weight: 600; } .nav-links { list-style: none; padding: 15px 0; } .nav-category { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; padding: 15px 20px 5px; color: #adb5bd; } .nav-links li { position: relative; } .nav-links li.active { background-color: rgba(255,255,255,0.1); } .nav-links li a { display: flex; align-items: center; padding: 12px 20px; color: #ecf0f1; text-decoration: none; transition: all 0.3s; } .nav-links li a:hover { background-color: rgba(255,255,255,0.05); } .nav-links li a i { margin-right: 10px; width: 20px; text-align: center; } /* 主内容区样式 */ .main-content { flex: 1; margin-left: 250px; display: flex; flex-direction: column; min-height: 100vh; } .top-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 25px; background-color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 900; } .search-container { position: relative; width: 350px; } .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: #adb5bd; } .search-input { width: 100%; padding: 10px 10px 10px 35px; border: 1px solid #dee2e6; border-radius: 20px; font-size: 0.9rem; } .search-input:focus { outline: none; border-color: #4a6cf7; } .user-menu { display: flex; align-items: center; } /* 通知图标样式 */ .notifications { position: relative; margin-right: 20px; } .notification-icon { cursor: pointer; color: #495057; position: relative; display: block; padding: 5px; } .badge { position: absolute; top: -5px; right: -5px; background-color: #e74c3c; color: white; font-size: 0.7rem; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } /* 通知下拉菜单 */ .notification-dropdown { position: absolute; top: 100%; right: -10px; width: 320px; background-color: white; border-radius: 5px; box-shadow: 0 3px 10px rgba(0,0,0,0.2); display: none; z-index: 1000; max-height: 400px; overflow-y: auto; margin-top: 10px; } .notification-dropdown.show { display: block; } .notification-dropdown::before { content: ''; position: absolute; top: -8px; right: 15px; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 8px solid white; } .notification-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; border-bottom: 1px solid #eaeaea; } .notification-header h6 { margin: 0; font-size: 0.9rem; font-weight: 600; } .mark-all-read { font-size: 0.8rem; color: #4a6cf7; text-decoration: none; } .mark-all-read:hover { text-decoration: underline; } .notification-items { max-height: 300px; overflow-y: auto; } .notification-item { display: block; padding: 12px 15px; border-bottom: 1px solid #f5f5f5; color: #333; text-decoration: none; transition: background-color 0.2s; } .notification-item:hover { background-color: #f8f9fa; text-decoration: none; } .notification-item.unread { background-color: #f0f7ff; } .notification-title { font-size: 0.9rem; margin-bottom: 5px; font-weight: 600; } .notification-text { font-size: 0.8rem; color: #666; margin-bottom: 5px; } .notification-time { font-size: 0.75rem; color: #999; display: block; } .no-notifications { padding: 25px; text-align: center; color: #999; } .dropdown-divider { height: 1px; background-color: #eaeaea; margin: 5px 0; } .view-all { display: block; text-align: center; padding: 10px; color: #4a6cf7; text-decoration: none; font-weight: 500; font-size: 0.9rem; } .view-all:hover { background-color: #f8f9fa; text-decoration: none; } /* 用户信息样式 */ .user-info { position: relative; } .user-info-toggle { display: flex; align-items: center; cursor: pointer; text-decoration: none; color: inherit; } .user-avatar { width: 40px; height: 40px; background-color: #4a6cf7; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px; } .user-details { display: flex; flex-direction: column; } .user-name { font-weight: 600; font-size: 0.9rem; } .user-role { font-size: 0.8rem; color: #6c757d; } .user-dropdown { position: absolute; top: 100%; right: 0; background-color: white; box-shadow: 0 3px 10px rgba(0,0,0,0.2); border-radius: 5px; width: 200px; padding: 5px 0; display: none; z-index: 1000; margin-top: 10px; } .user-dropdown.show { display: block; } .user-dropdown::before { content: ''; position: absolute; top: -8px; right: 15px; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 8px solid white; } .user-dropdown a { display: block; padding: 10px 15px; color: #333; text-decoration: none; transition: background-color 0.2s; } .user-dropdown a:hover { background-color: #f8f9fa; } .user-dropdown a i { width: 20px; margin-right: 10px; text-align: center; } .login-link, .register-link { color: #4a6cf7; text-decoration: none; margin-left: 10px; font-weight: 500; } .login-link:hover, .register-link:hover { text-decoration: underline; } /* 内容区域 */ .content-wrapper { flex: 1; padding: 20px; background-color: #f0f2f5; } /* 响应式适配 */ @media (max-width: 768px) { .sidebar { width: 70px; overflow: visible; } .logo-container h2 { display: none; } .nav-links li a span { display: none; } .main-content { margin-left: 70px; } .user-details { display: none; } .search-container { width: 200px; } .notification-dropdown, .user-dropdown { position: fixed; right: 10px; width: calc(100% - 80px); max-width: 320px; } } ================================================================================ File: ./app/static/css/log-list.css ================================================================================ /* 全局风格与颜色 */ :root { --primary-color: #9c88ff; --secondary-color: #f8a5c2; --accent-color: #78e08f; --light-pink: #ffeef8; --soft-blue: #e5f1ff; --soft-purple: #f3e5ff; --soft-pink: #ffeef5; --soft-red: #ffd8d8; --text-color: #4a4a4a; --light-text: #8a8a8a; --border-radius: 12px; --box-shadow: 0 6px 15px rgba(0,0,0,0.05); --transition: all 0.3s ease; } /* 整体容器 */ .content-container { padding: 20px; font-family: 'Montserrat', sans-serif; color: var(--text-color); background-image: linear-gradient(to bottom, var(--soft-blue) 0%, rgba(255,255,255,0.8) 20%, rgba(255,255,255,0.9) 100%); border-radius: var(--border-radius); box-shadow: var(--box-shadow); position: relative; overflow: hidden; } /* 头部样式 */ .content-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; background: linear-gradient(120deg, var(--soft-purple), var(--soft-pink)); padding: 15px 20px; border-radius: var(--border-radius); box-shadow: 0 4px 10px rgba(0,0,0,0.05); } .content-header h1 { margin: 0; font-size: 24px; font-weight: 500; color: #6a3093; letter-spacing: 0.5px; } .content-header .actions { display: flex; gap: 12px; } /* 闪烁星星效果 */ .sparkle { position: relative; display: inline-block; animation: sparkle 2s infinite; } @keyframes sparkle { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.9; } } /* 按钮样式 */ .btn { padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; transition: var(--transition); border: none; display: flex; align-items: center; gap: 6px; box-shadow: 0 3px 8px rgba(0,0,0,0.05); } .btn-blossom { background: linear-gradient(45deg, #ffcee0, #b5c0ff); color: #634a7a; } .btn-primary-soft { background: linear-gradient(135deg, #a1c4fd, #c2e9fb); color: #4a4a4a; } .btn-secondary-soft { background: linear-gradient(135deg, #e2c9fa, #d3f9fb); color: #4a4a4a; } .btn-danger-soft { background: linear-gradient(135deg, #ffb8c6, #ffdfd3); color: #a55; } .btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .btn-glow { animation: glow 1s ease-in-out infinite alternate; } @keyframes glow { from { box-shadow: 0 0 5px rgba(156, 136, 255, 0.3); } to { box-shadow: 0 0 15px rgba(156, 136, 255, 0.7); } } /* 筛选面板 */ .filter-panel { background: rgba(255, 255, 255, 0.9); border-radius: var(--border-radius); padding: 20px; margin-bottom: 25px; box-shadow: 0 5px 15px rgba(0,0,0,0.05); border: 1px solid rgba(248, 200, 220, 0.3); } .filter-panel-header { margin-bottom: 15px; text-align: center; } .filter-title { font-size: 18px; color: #9c88ff; font-weight: 500; font-family: 'Dancing Script', cursive; font-size: 24px; } .snowflake-divider { display: flex; justify-content: center; gap: 15px; margin: 8px 0; color: var(--primary-color); font-size: 14px; opacity: 0.7; } .filter-row { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 15px; } .filter-item { flex: 1; min-width: 200px; } .filter-item label { display: block; margin-bottom: 8px; font-weight: 500; color: #7e6d94; font-size: 14px; } .elegant-select, .elegant-input { width: 100%; padding: 10px; border: 1px solid #e0d0f0; border-radius: 8px; background-color: rgba(255, 255, 255, 0.8); color: var(--text-color); transition: var(--transition); box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); } .elegant-select:focus, .elegant-input:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(156, 136, 255, 0.2); } .filter-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } /* 日期范围样式 */ .date-range-inputs { padding-top: 15px; margin-top: 5px; border-top: 1px dashed #e0d0f0; } /* 卡片效果 */ .glass-card { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); border-radius: var(--border-radius); box-shadow: 0 8px 20px rgba(0,0,0,0.05); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; } .card-body { padding: 20px; } /* 表格样式 */ .table-container { overflow-x: auto; border-radius: 8px; } .elegant-table { width: 100%; border-collapse: separate; border-spacing: 0; color: var(--text-color); } .elegant-table th { background: linear-gradient(to right, var(--soft-purple), var(--soft-pink)); color: #6a4c93; font-weight: 500; text-align: left; padding: 12px 15px; font-size: 14px; border: none; } .elegant-table th:first-child { border-top-left-radius: 8px; } .elegant-table th:last-child { border-top-right-radius: 8px; } .elegant-table td { padding: 12px 15px; border-bottom: 1px solid rgba(224, 208, 240, 0.3); font-size: 14px; transition: var(--transition); } .elegant-table tr:last-child td { border-bottom: none; } .elegant-table tr:hover td { background-color: rgba(248, 239, 255, 0.6); } /* 用户徽章样式 */ .user-badge { background: linear-gradient(45deg, #a1c4fd, #c2e9fb); padding: 4px 10px; border-radius: 12px; font-size: 12px; color: #4a4a4a; display: inline-block; } /* 空数据提示 */ .empty-container { padding: 30px; text-align: center; color: var(--light-text); } .empty-container i { font-size: 40px; color: #d0c0e0; margin-bottom: 15px; } .empty-container p { margin: 0; font-size: 16px; } /* 分页样式 */ .pagination-wrapper { display: flex; justify-content: center; margin-top: 25px; } .pagination-container { display: flex; justify-content: space-between; align-items: center; width: 100%; background: rgba(248, 239, 255, 0.5); padding: 15px 20px; border-radius: 25px; } .page-btn { padding: 6px 15px; border-radius: 20px; background: linear-gradient(45deg, #e2bbec, #b6cefd); color: #634a7a; border: none; transition: var(--transition); text-decoration: none; display: flex; align-items: center; gap: 5px; font-size: 13px; } .page-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 10px rgba(0,0,0,0.1); text-decoration: none; color: #4a3a5a; } .page-info { color: var(--light-text); font-size: 14px; } /* 模态框样式 */ .modal-elegant { max-width: 400px; } .modal-content { border-radius: 15px; border: none; box-shadow: 0 10px 30px rgba(0,0,0,0.1); overflow: hidden; background: rgba(255, 255, 255, 0.95); } .modal-header { background: linear-gradient(135deg, #f8c8dc, #c8e7f8); border-bottom: none; padding: 15px 20px; } .modal-header .modal-title { color: #634a7a; font-weight: 500; display: flex; align-items: center; gap: 8px; } .modal-body { padding: 20px; } .modal-message { color: #7e6d94; margin-bottom: 15px; } .elegant-alert { background-color: rgba(255, 248, 225, 0.7); border: 1px solid #ffeeba; color: #856404; border-radius: 8px; padding: 12px 15px; display: flex; align-items: center; gap: 10px; } .modal-footer { background: rgba(248, 239, 255, 0.5); border-top: none; padding: 15px 20px; } /* 行动画效果 */ .fade-in-row { animation: fadeIn 0.5s ease-out forwards; opacity: 0; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* 响应式调整 */ @media (max-width: 768px) { .filter-item { min-width: 100%; } .content-header { flex-direction: column; align-items: flex-start; } .content-header .actions { margin-top: 15px; } .pagination-container { flex-direction: column; gap: 15px; } } ================================================================================ 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: 24px; max-width: 1400px; margin: 0 auto; font-family: var(--font-sans); color: var(--text-dark); } /* ========== 页头样式 ========== */ .page-header-wrapper { 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 { display: flex; flex-direction: column; } .page-title { font-size: 1.5rem; font-weight: 600; color: var(--text-dark); margin: 0; } .subtitle { margin: 8px 0 0 0; color: var(--text-medium); font-size: 0.9rem; } .header-actions { display: flex; align-items: center; gap: 16px; } .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; font-size: 0.875rem; font-weight: 500; transition: all var(--transition-fast); text-decoration: none; box-shadow: var(--shadow-sm); } .btn-back:hover { background-color: var(--border-color); color: var(--text-dark); text-decoration: none; } .btn-back i { font-size: 14px; } /* 进度条样式 */ .form-progress { min-width: 180px; } .progress-bar-container { height: 6px; background-color: var(--bg-lightest); border-radius: 3px; overflow: hidden; } .progress-bar { height: 100%; background-color: var(--primary-color); border-radius: 3px; transition: width var(--transition-base); } .progress-text { 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 { 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: var(--shadow-md); } .card-header { padding: 16px 20px; background-color: var(--bg-white); border-bottom: 1px solid var(--border-color); display: flex; align-items: center; } .card-title { font-weight: 600; color: var(--text-dark); font-size: 0.9375rem; } .card-body { padding: 20px; } .form-section { padding: 0; } /* ========== 表单元素样式 ========== */ .form-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 16px; } .form-group { margin-bottom: 20px; } .form-group:last-child { margin-bottom: 0; } .form-label { display: block; font-weight: 500; color: var(--text-dark); margin-bottom: 8px; font-size: 0.9375rem; } .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); } .form-control:focus { border-color: var(--border-focus); outline: 0; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } .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: 0.8125rem; color: var(--text-light); } .form-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; } .char-counter { font-size: 0.8125rem; color: var(--text-muted); } /* 带按钮输入框 */ .input-with-button { display: flex; align-items: center; } .input-with-button .form-control { border-top-right-radius: 0; border-bottom-right-radius: 0; flex-grow: 1; } .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 { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; min-height: 32px; } .tag { display: inline-flex; align-items: center; 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:hover { background-color: rgba(59, 130, 246, 0.2); } .tag-text { margin-right: 6px; } .tag-remove { background: none; border: none; color: var(--primary-color); cursor: pointer; 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-remove:hover { background-color: rgba(59, 130, 246, 0.3); color: white; } /* ========== 封面上传区域 ========== */ .cover-preview-container { display: flex; flex-direction: column; gap: 16px; } .cover-preview { width: 100%; aspect-ratio: 5/7; background-color: var(--bg-lightest); border-radius: var(--radius-md); overflow: hidden; cursor: pointer; transition: all var(--transition-fast); } .cover-preview:hover { background-color: var(--bg-light); } .cover-preview.dragover { background-color: var(--primary-light); } .cover-image { width: 100%; height: 100%; object-fit: cover; } .no-cover-placeholder { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-light); padding: 24px; text-align: center; } .no-cover-placeholder i { font-size: 48px; margin-bottom: 16px; color: var(--text-muted); } .placeholder-tip { font-size: 0.8125rem; margin-top: 8px; color: var(--text-muted); } .upload-options { display: flex; flex-direction: column; gap: 12px; } .upload-btn-group { display: flex; gap: 8px; } .btn-upload { flex-grow: 1; padding: 10px 16px; background-color: var(--primary-color); color: white; 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-actions { display: flex; flex-direction: column; gap: 16px; } .secondary-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .form-tip { margin-top: 8px; font-size: 0.8125rem; color: var(--text-muted); text-align: center; } .form-tip i { color: var(--info-color); margin-right: 4px; } /* 必填项标记 */ .required { color: var(--danger-color); margin-left: 4px; } /* 无效输入状态 */ .is-invalid { border-color: var(--danger-color) !important; } .invalid-feedback { display: block; 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: var(--radius-lg); box-shadow: var(--shadow-lg); overflow: hidden; } .modal-header { background-color: var(--bg-white); border-bottom: 1px solid var(--border-color); padding: 16px 20px; } .modal-title { font-weight: 600; color: var(--text-dark); font-size: 1.125rem; } .modal-body { padding: 20px; } .modal-footer { 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 { display: block; max-width: 100%; } .cropper-controls { display: flex; 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); } /* 图书预览模态框 */ .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 { height: 280px; width: 200px; overflow: hidden; border-radius: 4px; border: 1px solid #ddd; display: flex; align-items: center; justify-content: center; background: #f8f9fa; } .preview-cover-img { max-width: 100%; max-height: 100%; object-fit: contain; } .preview-tag { display: inline-block; background: #e9ecef; color: #495057; padding: 3px 8px; border-radius: 12px; font-size: 12px; margin-right: 5px; margin-bottom: 5px; } .book-tags-preview { margin: 15px 0; } .book-description-preview { margin-top: 20px; } .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: var(--text-dark); margin: 0 0 8px 0; } .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; } .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; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 12px; max-width: 320px; } .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; } .success-notification { border-left: 4px solid var(--success-color); } .error-notification { border-left: 4px solid var(--danger-color); } .warning-notification { border-left: 4px solid var(--warning-color); } .info-notification { border-left: 4px solid var(--info-color); } .notification-icon { color: var(--text-light); } .success-notification .notification-icon { color: var(--success-color); } .error-notification .notification-icon { color: var(--danger-color); } .warning-notification .notification-icon { color: var(--warning-color); } .info-notification .notification-icon { color: var(--info-color); } .notification-content { flex-grow: 1; } .notification-content p { margin: 0; font-size: 0.875rem; color: var(--text-dark); } .notification-close { background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 5px; transition: color var(--transition-fast); } .notification-close:hover { color: var(--text-medium); } /* ========== 动画效果 ========== */ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } 70% { box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); } 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } } .pulse { animation: pulse 2s infinite; } /* ========== 响应式样式 ========== */ @media (max-width: 1200px) { .form-grid { grid-template-columns: 1fr 320px; gap: 20px; } } @media (max-width: 992px) { .page-header { flex-direction: column; align-items: flex-start; gap: 16px; } .header-actions { width: 100%; justify-content: space-between; } .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: 16px 12px; } .page-header { padding: 20px; } .form-row { grid-template-columns: 1fr; gap: 12px; } .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/borrow_statistics.css ================================================================================ /* app/static/css/borrow_statistics.css */ /* 确保与 statistics.css 兼容的样式 */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 15px; margin-top: 15px; } .stats-item { background-color: var(--secondary-color); border-radius: 12px; padding: 20px 15px; text-align: center; transition: all 0.3s ease; border: 1px solid var(--border-color); box-shadow: 0 4px 12px var(--shadow-color); position: relative; overflow: hidden; } .stats-item:hover { transform: translateY(-5px); box-shadow: 0 8px 20px var(--shadow-color); background-color: white; } .stats-item::after { content: ''; position: absolute; bottom: -15px; right: -15px; width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary-color); opacity: 0.1; transition: all 0.3s ease; } .stats-item:hover::after { transform: scale(1.2); opacity: 0.2; } .stats-value { font-size: 26px; font-weight: 700; margin-bottom: 8px; color: var(--accent-color); display: flex; justify-content: center; align-items: center; height: 40px; position: relative; } .stats-value::before { content: ''; position: absolute; bottom: -2px; left: 50%; transform: translateX(-50%); width: 40px; height: 2px; background-color: var(--primary-color); border-radius: 2px; } .stats-title { font-size: 14px; color: var(--light-text); font-weight: 500; } .loading { text-align: center; padding: 40px; color: var(--light-text); display: flex; flex-direction: column; align-items: center; justify-content: center; } .loader { border: 4px solid rgba(244, 188, 204, 0.3); border-top: 4px solid var(--accent-color); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin-bottom: 15px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 修复图表容器 */ .chart-container { margin-bottom: 30px; } .chart-wrapper { position: relative; height: 300px; width: 100%; } .trend-chart .chart-wrapper { height: 330px; } /* 确保图表正确渲染 */ canvas { max-width: 100%; height: auto !important; } /* 添加一些女性化的装饰元素 */ .chart-container::before { content: ''; position: absolute; top: -15px; left: -15px; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); opacity: 0.4; z-index: 0; } .chart-container::after { content: ''; position: absolute; bottom: -15px; right: -15px; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); opacity: 0.4; z-index: 0; } /* 新增部分 */ .intro-text { text-align: center; margin-bottom: 25px; font-size: 16px; font-weight: 300; color: var(--light-text); font-style: italic; } .insights-container { background-color: var(--secondary-color); border-radius: 15px; padding: 25px; margin-top: 30px; box-shadow: 0 5px 20px var(--shadow-color); border: 1px solid var(--border-color); position: relative; overflow: hidden; } .insights-container h3 { color: var(--accent-color); font-size: 1.3rem; margin-bottom: 15px; font-weight: 600; text-align: center; position: relative; } .insights-container h3::after { content: ''; position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%); width: 60px; height: 2px; background: linear-gradient(to right, var(--secondary-color), var(--accent-color), var(--secondary-color)); border-radius: 3px; } .insights-content { line-height: 1.6; color: var(--text-color); text-align: center; position: relative; z-index: 1; } .insights-container::before { content: ''; position: absolute; top: -30px; right: -30px; width: 100px; height: 100px; border-radius: 50%; background-color: var(--primary-color); opacity: 0.1; } /* 优雅的动画效果 */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .animate-fadeInUp { animation: fadeInUp 0.8s ease forwards; } /* 确保响应式布局 */ @media (max-width: 768px) { .chart-row { flex-direction: column; } .half { width: 100%; min-width: 0; } .stats-grid { grid-template-columns: repeat(2, 1fr); } .chart-wrapper { height: 250px; } } ================================================================================ File: ./app/static/css/categories.css ================================================================================ /* 分类管理页面样式 */ .categories-container { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .card { margin-bottom: 20px; border: 1px solid rgba(0,0,0,0.125); border-radius: 0.25rem; } .card-header { padding: 0.75rem 1.25rem; background-color: rgba(0,0,0,0.03); border-bottom: 1px solid rgba(0,0,0,0.125); font-weight: 600; } .card-body { padding: 1.25rem; } .category-table { border: 1px solid #eee; } .category-table th { background-color: #f8f9fa; } .no-categories { text-align: center; padding: 30px; color: #888; } .no-categories i { font-size: 48px; color: #ddd; margin-bottom: 10px; } /* 通知弹窗 */ .notification-alert { position: fixed; top: 20px; right: 20px; min-width: 300px; z-index: 1050; } /* 响应式调整 */ @media (max-width: 768px) { .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } } ================================================================================ 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/browse.css ================================================================================ /* 图书浏览页面样式 */ /* 全局容器 */ .browse-container { padding: 24px; background-color: #f6f9fc; min-height: calc(100vh - 60px); position: relative; overflow: hidden; } /* 装饰气泡 */ .bubble { position: absolute; bottom: -50px; background-color: rgba(221, 236, 255, 0.4); border-radius: 50%; z-index: 1; animation: bubble 25s infinite ease-in; } @keyframes bubble { 0% { transform: translateY(100%) scale(0); opacity: 0; } 50% { opacity: 0.6; } 100% { transform: translateY(-100vh) scale(1); opacity: 0; } } /* 为页面添加15个泡泡 */ .bubble:nth-child(1) { left: 5%; width: 30px; height: 30px; animation-duration: 20s; animation-delay: 0s; } .bubble:nth-child(2) { left: 15%; width: 20px; height: 20px; animation-duration: 18s; animation-delay: 1s; } .bubble:nth-child(3) { left: 25%; width: 25px; height: 25px; animation-duration: 16s; animation-delay: 2s; } .bubble:nth-child(4) { left: 35%; width: 15px; height: 15px; animation-duration: 15s; animation-delay: 0.5s; } .bubble:nth-child(5) { left: 45%; width: 30px; height: 30px; animation-duration: 14s; animation-delay: 3s; } .bubble:nth-child(6) { left: 55%; width: 20px; height: 20px; animation-duration: 13s; animation-delay: 2.5s; } .bubble:nth-child(7) { left: 65%; width: 25px; height: 25px; animation-duration: 12s; animation-delay: 1.5s; } .bubble:nth-child(8) { left: 75%; width: 15px; height: 15px; animation-duration: 11s; animation-delay: 4s; } .bubble:nth-child(9) { left: 85%; width: 30px; height: 30px; animation-duration: 10s; animation-delay: 3.5s; } .bubble:nth-child(10) { left: 10%; width: 18px; height: 18px; animation-duration: 19s; animation-delay: 0.5s; } .bubble:nth-child(11) { left: 20%; width: 22px; height: 22px; animation-duration: 17s; animation-delay: 2.5s; } .bubble:nth-child(12) { left: 30%; width: 28px; height: 28px; animation-duration: 16s; animation-delay: 1.2s; } .bubble:nth-child(13) { left: 40%; width: 17px; height: 17px; animation-duration: 15s; animation-delay: 3.7s; } .bubble:nth-child(14) { left: 60%; width: 23px; height: 23px; animation-duration: 13s; animation-delay: 2.1s; } .bubble:nth-child(15) { left: 80%; width: 19px; height: 19px; animation-duration: 12s; animation-delay: 1.7s; } /* 页面标题部分 */ .page-header { margin-bottom: 25px; position: relative; z-index: 2; text-align: center; } .page-header h1 { color: #3c4858; font-size: 2.2rem; font-weight: 700; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .welcome-text { margin-top: 10px; color: #8492a6; font-size: 1.1rem; } .welcome-text strong { color: #764ba2; } /* 过滤和搜索部分 */ .filter-section { margin-bottom: 25px; padding: 20px; background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); position: relative; z-index: 2; } .search-form { display: flex; flex-direction: column; gap: 16px; } .search-row { margin-bottom: 5px; width: 100%; } .search-group { display: flex; width: 100%; max-width: 800px; margin: 0 auto; } .search-group .form-control { border: 1px solid #e4e7eb; border-right: none; border-radius: 25px 0 0 25px; padding: 10px 20px; height: 46px; font-size: 1rem; background-color: #f7fafc; box-shadow: none; transition: all 0.3s; flex: 1; } .search-group .form-control:focus { outline: none; border-color: #a3bffa; background-color: #ffffff; box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.25); } .search-group .btn { border-radius: 0 25px 25px 0; width: 46px; height: 46px; min-width: 46px; padding: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; justify-content: center; margin-left: -1px; font-size: 1.1rem; box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11); transition: all 0.3s; border: none; } .search-group .btn:hover { transform: translateY(-1px); box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); } .filter-row { display: flex; flex-wrap: wrap; gap: 15px; width: 100%; align-items: center; } .category-filters { position: relative; flex: 2; min-width: 180px; } .category-filter-toggle { display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; width: 100%; height: 42px; background: #f7fafc; border: 1px solid #e4e7eb; border-radius: 25px; cursor: pointer; font-size: 0.95rem; color: #3c4858; transition: all 0.3s; } .category-filter-toggle:hover { background: #edf2f7; } .category-filter-toggle i.fa-chevron-down { margin-left: 8px; transition: transform 0.3s; } .category-filter-toggle.active i.fa-chevron-down { transform: rotate(180deg); } .category-filter-dropdown { position: absolute; top: 100%; left: 0; right: 0; margin-top: 8px; background: white; border-radius: 12px; box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); padding: 10px; z-index: 100; display: none; max-height: 300px; overflow-y: auto; } .category-filter-dropdown.show { display: block; animation: fadeIn 0.2s; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .category-item { display: flex; align-items: center; padding: 10px 15px; color: #3c4858; border-radius: 6px; text-decoration: none; margin-bottom: 5px; transition: all 0.2s; } .category-item:hover { background: #f7fafc; color: #667eea; } .category-item.active { background: #ebf4ff; color: #667eea; font-weight: 500; } .category-item i { margin-right: 10px; } .filter-group { flex: 1; min-width: 130px; } .filter-section .form-control { border: 1px solid #e4e7eb; border-radius: 25px; height: 42px; padding: 10px 20px; background-color: #f7fafc; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 8.825L1.175 4 2.238 2.938 6 6.7 9.763 2.937 10.825 4z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 15px center; background-size: 12px; width: 100%; transition: all 0.3s; } .filter-section .form-control:focus { outline: none; border-color: #a3bffa; background-color: #ffffff; box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.25); } /* 图书统计显示 */ .browse-stats { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 25px; align-items: center; } .stat-item { display: flex; align-items: center; background: white; padding: 12px 20px; border-radius: 12px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); flex: 1; min-width: 160px; max-width: 240px; } .stat-item i { font-size: 24px; color: #667eea; margin-right: 15px; background: #ebf4ff; width: 45px; height: 45px; display: flex; align-items: center; justify-content: center; border-radius: 12px; } .stat-content { display: flex; flex-direction: column; } .stat-value { font-size: 1.25rem; font-weight: 700; color: #3c4858; } .stat-label { font-size: 0.875rem; color: #8492a6; } .search-results { flex: 2; padding: 12px 20px; background: #ebf4ff; border-radius: 12px; color: #667eea; font-weight: 500; text-align: center; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); } /* 图书网格布局 */ .books-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 25px; margin-bottom: 40px; position: relative; z-index: 2; } /* 图书卡片样式 */ .book-card { border-radius: 10px; overflow: hidden; background-color: white; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); transition: all 0.3s ease; position: relative; height: 100%; display: flex; flex-direction: column; opacity: 0; transform: translateY(20px); animation: fadeInUp 0.5s forwards; } @keyframes fadeInUp { to { opacity: 1; transform: translateY(0); } } .books-grid .book-card:nth-child(1) { animation-delay: 0.1s; } .books-grid .book-card:nth-child(2) { animation-delay: 0.15s; } .books-grid .book-card:nth-child(3) { animation-delay: 0.2s; } .books-grid .book-card:nth-child(4) { animation-delay: 0.25s; } .books-grid .book-card:nth-child(5) { animation-delay: 0.3s; } .books-grid .book-card:nth-child(6) { animation-delay: 0.35s; } .books-grid .book-card:nth-child(7) { animation-delay: 0.4s; } .books-grid .book-card:nth-child(8) { animation-delay: 0.45s; } .books-grid .book-card:nth-child(9) { animation-delay: 0.5s; } .books-grid .book-card:nth-child(10) { animation-delay: 0.55s; } .books-grid .book-card:nth-child(11) { animation-delay: 0.6s; } .books-grid .book-card:nth-child(12) { animation-delay: 0.65s; } .book-card:hover { transform: translateY(-8px); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.12); } .book-cover { height: 240px; position: relative; overflow: hidden; } .book-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; } .book-card:hover .book-cover img { transform: scale(1.08); } .cover-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to bottom, rgba(0,0,0,0) 50%, rgba(0,0,0,0.5) 100%); z-index: 1; } .book-ribbon { position: absolute; top: 10px; right: -30px; transform: rotate(45deg); width: 120px; text-align: center; z-index: 2; } .book-ribbon span { display: block; width: 100%; padding: 5px 0; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; } .book-ribbon .available { background-color: #4caf50; color: white; } .book-ribbon .unavailable { background-color: #f44336; color: white; } .no-cover { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; width: 100%; background: linear-gradient(135deg, #f6f9fc 0%, #e9ecef 100%); color: #8492a6; } .no-cover i { font-size: 40px; margin-bottom: 10px; } .book-info { padding: 20px; flex: 1; display: flex; flex-direction: column; } .book-title { font-size: 1.1rem; font-weight: 600; color: #3c4858; margin: 0 0 8px; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .book-author { font-size: 0.95rem; color: #8492a6; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .book-meta { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; } .book-category { padding: 5px 10px; background-color: #ebf4ff; color: #667eea; border-radius: 20px; font-size: 0.75rem; } .book-year { padding: 5px 10px; background-color: #f7fafc; color: #8492a6; border-radius: 20px; font-size: 0.75rem; } .book-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: auto; } .book-actions a, .book-actions button { padding: 10px 12px; border-radius: 8px; text-align: center; text-decoration: none; font-size: 0.9rem; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.3s; } .btn-detail { background-color: #e9ecef; color: #3c4858; } .btn-detail:hover { background-color: #dee2e6; color: #2d3748; } .btn-borrow { background-color: #667eea; color: white; } .btn-borrow:hover { background-color: #5a67d8; color: white; transform: translateY(-2px); box-shadow: 0 5px 10px rgba(102, 126, 234, 0.4); } .btn-borrow.disabled { background-color: #cbd5e0; color: #718096; cursor: not-allowed; } /* 无图书状态 */ .no-books { grid-column: 1 / -1; padding: 50px 30px; text-align: center; background-color: white; border-radius: 16px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; } .no-books-img { max-width: 200px; margin-bottom: 20px; } .no-books h3 { font-size: 1.25rem; color: #3c4858; margin: 0 0 10px; } .no-books p { font-size: 1rem; color: #8492a6; margin-bottom: 20px; } .btn-reset-search { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; background-color: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 500; transition: all 0.3s; } .btn-reset-search:hover { background-color: #5a67d8; color: white; transform: translateY(-2px); box-shadow: 0 5px 10px rgba(102, 126, 234, 0.4); } /* 分页容器 */ .pagination-container { display: flex; flex-direction: column; align-items: center; margin-top: 30px; margin-bottom: 20px; position: relative; z-index: 2; } .pagination { display: flex; list-style: none; padding: 0; margin: 0 0 15px 0; background-color: white; border-radius: 30px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); overflow: hidden; } .pagination .page-item { margin: 0; } .pagination .page-link { display: flex; align-items: center; justify-content: center; min-width: 40px; height: 40px; padding: 0 15px; border: none; color: #3c4858; font-weight: 500; transition: all 0.2s; text-decoration: none; } .pagination .page-link:hover { color: #667eea; background-color: #f7fafc; } .pagination .page-item.active .page-link { background-color: #667eea; color: white; box-shadow: none; } .pagination .page-item.disabled .page-link { color: #cbd5e0; background-color: #f7fafc; cursor: not-allowed; } .pagination-info { color: #8492a6; font-size: 0.9rem; } /* 模态框样式优化 */ .modal-content { border-radius: 16px; border: none; box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07); overflow: hidden; } .modal-header { padding: 20px 25px; background-color: #f7fafc; border-bottom: 1px solid #e2e8f0; } .modal-title { color: #3c4858; font-size: 1.2rem; font-weight: 600; } .modal-body { padding: 25px; } .modal-footer { padding: 15px 25px; border-top: 1px solid #e2e8f0; background-color: #f7fafc; } .modal-info { margin-top: 10px; padding: 12px 16px; background-color: #ebf8ff; border-left: 4px solid #4299e1; color: #2b6cb0; font-size: 0.9rem; border-radius: 4px; } .modal .close { font-size: 1.5rem; color: #a0aec0; opacity: 0.8; text-shadow: none; transition: all 0.2s; } .modal .close:hover { opacity: 1; color: #667eea; } .modal .btn { border-radius: 8px; padding: 10px 20px; font-weight: 500; transition: all 0.3s; } .modal .btn-secondary { background-color: #e2e8f0; color: #4a5568; border: none; } .modal .btn-secondary:hover { background-color: #cbd5e0; color: #2d3748; } .modal .btn-primary { background-color: #667eea; color: white; border: none; } .modal .btn-primary:hover { background-color: #5a67d8; box-shadow: 0 5px 10px rgba(102, 126, 234, 0.4); } /* 响应式调整 */ @media (max-width: 992px) { .filter-row { flex-wrap: wrap; } .category-filters { flex: 1 0 100%; margin-bottom: 10px; } .filter-group { flex: 1 0 180px; } } @media (max-width: 768px) { .browse-container { padding: 16px; } .page-header { text-align: left; } .filter-section { padding: 15px; } .search-form { flex-direction: column; gap: 12px; } .search-group { max-width: 100%; } .filter-row { gap: 12px; } .books-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } .stat-item { min-width: 130px; padding: 10px; } .stat-item i { width: 35px; height: 35px; font-size: 18px; } .search-results { padding: 10px; font-size: 0.9rem; } } @media (max-width: 576px) { .books-grid { grid-template-columns: 1fr 1fr; gap: 12px; } .book-cover { height: 180px; } .book-info { padding: 12px; } .book-title { font-size: 0.9rem; } .book-author { font-size: 0.8rem; } .book-actions { grid-template-columns: 1fr; gap: 8px; } .browse-stats { flex-direction: column; align-items: stretch; } .stat-item { max-width: none; } .search-results { width: 100%; } } ================================================================================ File: ./app/static/css/inventory-adjust.css ================================================================================ /* 迪士尼主题库存管理页面样式 */ /* 基础样式 */ body { background-color: #f9f7ff; font-family: 'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif; color: #3d4c65; } /* 迪士尼风格卡片 */ .disney-inventory-card { border: none; border-radius: 20px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); background-color: #ffffff; margin-bottom: 40px; position: relative; overflow: hidden; transition: all 0.3s ease; padding: 2px; border: 3px solid #f0e6fa; } .disney-inventory-card:hover { box-shadow: 0 15px 30px rgba(110, 125, 249, 0.2); transform: translateY(-5px); } /* 迪士尼装饰元素 */ .disney-decoration { position: absolute; width: 60px; height: 60px; background-size: contain; background-repeat: no-repeat; opacity: 0.8; z-index: 1; } .top-left { top: 10px; left: 10px; background-image: url('https://i.imgur.com/Vyo9IF4.png'); /* 替换为迪士尼星星图标URL */ transform: rotate(-15deg); } .top-right { top: 10px; right: 10px; background-image: url('https://i.imgur.com/pLRUYhb.png'); /* 替换为迪士尼魔法棒图标URL */ transform: rotate(15deg); } .bottom-left { bottom: 10px; left: 10px; background-image: url('https://i.imgur.com/KkMfwWv.png'); /* 替换为迪士尼城堡图标URL */ transform: rotate(-5deg); } .bottom-right { bottom: 10px; right: 10px; background-image: url('https://i.imgur.com/TcA6PL2.png'); /* 替换为迪士尼皇冠图标URL */ transform: rotate(5deg); } /* 米奇耳朵标题装饰 */ .card-header-disney { background: linear-gradient(45deg, #e4c1f9, #d4a5ff); color: #512b81; padding: 1.8rem 1.5rem 1.5rem; font-weight: 600; border-radius: 18px 18px 0 0; text-align: center; position: relative; z-index: 2; } .mickey-ears { position: absolute; top: -25px; left: 50%; transform: translateX(-50%); width: 80px; height: 40px; background-image: url('https://i.imgur.com/pCPQoZx.png'); /* 替换为米奇耳朵图标URL */ background-size: contain; background-repeat: no-repeat; background-position: center; } /* 卡片内容 */ .card-body-disney { padding: 2.5rem; background-color: #ffffff; border-radius: 0 0 18px 18px; position: relative; z-index: 2; } /* 书籍封面 */ .book-cover-container { position: relative; display: flex; justify-content: center; align-items: flex-start; } .book-cover { max-height: 300px; width: auto; object-fit: contain; border-radius: 12px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease; position: relative; z-index: 2; border: 3px solid #f9f0ff; } .book-cover:hover { transform: scale(1.03); } .disney-sparkles { position: absolute; width: 100%; height: 100%; background-image: url('https://i.imgur.com/8vZuwlG.png'); /* 替换为迪士尼闪光效果URL */ background-size: 200px; background-repeat: no-repeat; background-position: center; opacity: 0; transition: opacity 0.5s ease; pointer-events: none; } .book-cover:hover + .disney-sparkles { opacity: 0.7; } /* 书籍详情 */ .book-details { display: flex; flex-direction: column; justify-content: center; } .book-title { color: #5e35b1; font-weight: 700; margin-bottom: 1.8rem; font-size: 1.8rem; border-bottom: 3px dotted #e1bee7; padding-bottom: 1rem; } .book-info { font-size: 1.05rem; color: #424242; } .book-info p { margin-bottom: 1rem; display: flex; align-items: center; } /* 迪士尼图标 */ .disney-icon { display: inline-block; width: 28px; height: 28px; background-size: contain; background-position: center; background-repeat: no-repeat; margin-right: 10px; flex-shrink: 0; } .author-icon { background-image: url('https://i.imgur.com/2K5qpgQ.png'); /* 替换为米妮图标URL */ } .publisher-icon { background-image: url('https://i.imgur.com/YKhKVT7.png'); /* 替换为唐老鸭图标URL */ } .isbn-icon { background-image: url('https://i.imgur.com/ioaQTBM.png'); /* 替换为高飞图标URL */ } .inventory-icon { background-image: url('https://i.imgur.com/D0jRTKX.png'); /* 替换为奇奇蒂蒂图标URL */ } .type-icon { background-image: url('https://i.imgur.com/xgQriQn.png'); /* 替换为米奇图标URL */ } .amount-icon { background-image: url('https://i.imgur.com/ioaQTBM.png'); /* 替换为高飞图标URL */ } .remark-icon { background-image: url('https://i.imgur.com/2K5qpgQ.png'); /* 替换为米妮图标URL */ } /* 库存状态标签 */ .stock-badge { display: inline-block; padding: 0.35em 0.9em; border-radius: 50px; font-weight: 600; margin-left: 8px; font-size: 0.9rem; } .high-stock { background-color: #e0f7fa; color: #0097a7; border: 2px solid #80deea; } .low-stock { background-color: #fff8e1; color: #ff8f00; border: 2px solid #ffe082; } .out-stock { background-color: #ffebee; color: #c62828; border: 2px solid #ef9a9a; } /* 表单容器 */ .form-container { background-color: #f8f4ff; padding: 2rem; border-radius: 15px; margin-top: 2rem; border: 2px dashed #d1c4e9; position: relative; } .form-group { position: relative; } /* 表单标签 */ .disney-label { color: #5e35b1; font-weight: 600; margin-bottom: 0.8rem; display: flex; align-items: center; font-size: 1.1rem; } /* 自定义表单控件 */ .disney-select, .disney-input, .disney-textarea { display: block; width: 100%; padding: 0.8rem 1rem; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #495057; background-color: #fff; background-clip: padding-box; border: 2px solid #d1c4e9; border-radius: 12px; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .disney-select:focus, .disney-input:focus, .disney-textarea:focus { border-color: #9575cd; outline: 0; box-shadow: 0 0 0 0.2rem rgba(149, 117, 205, 0.25); } /* 确保下拉菜单选项可见 */ .disney-select option { background-color: #fff; color: #495057; padding: 8px; } /* 库存提示 */ .stock-hint { color: #757575; font-size: 0.95rem; margin-top: 0.6rem; font-weight: 500; } .stock-hint.warning { color: #ff8f00; font-weight: bold; } .stock-hint.danger { color: #c62828; font-weight: bold; } /* 按钮样式 */ .button-group { display: flex; justify-content: flex-end; gap: 15px; margin-top: 2rem; } .btn { padding: 0.7rem 2rem; border-radius: 50px; font-weight: 600; font-size: 1rem; letter-spacing: 0.5px; display: inline-block; text-align: center; vertical-align: middle; transition: all 0.3s ease; position: relative; overflow: hidden; } .disney-cancel-btn { background-color: #f3e5f5; color: #6a1b9a; border: 2px solid #ce93d8; } .disney-cancel-btn:hover { background-color: #e1bee7; color: #4a148c; transform: translateY(-3px); } .disney-confirm-btn { background: linear-gradient(45deg, #7e57c2, #5e35b1); color: white; border: none; } .disney-confirm-btn:hover { background: linear-gradient(45deg, #673ab7, #4527a0); transform: translateY(-3px); box-shadow: 0 7px 15px rgba(103, 58, 183, 0.3); } .disney-confirm-btn:before { content: ""; position: absolute; top: -10px; left: -20px; width: 40px; height: 40px; background-image: url('https://i.imgur.com/8vZuwlG.png'); /* 替换为迪士尼魔法效果URL */ background-size: contain; background-repeat: no-repeat; opacity: 0; transition: all 0.5s ease; transform: scale(0.5); } .disney-confirm-btn:hover:before { opacity: 0.8; transform: scale(1) rotate(45deg); top: -5px; left: 10px; } /* 响应式调整 */ @media (max-width: 768px) { .book-cover-container { margin-bottom: 30px; } .book-cover { max-height: 250px; } .book-title { text-align: center; font-size: 1.5rem; } .disney-decoration { width: 40px; height: 40px; } .button-group { flex-direction: column; } .btn { width: 100%; margin-bottom: 10px; } .card-header-disney, .card-body-disney { padding: 1.5rem; } } /* 表单元素聚焦效果 */ .form-group.focused { transform: translateY(-3px); } .form-group.focused .disney-label { color: #7e57c2; } /* 提交动画 */ .disney-inventory-card.submitting { animation: submitPulse 1s ease; } @keyframes submitPulse { 0% { transform: scale(1); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); } 50% { transform: scale(1.02); box-shadow: 0 15px 35px rgba(126, 87, 194, 0.3); } 100% { transform: scale(1); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); } } /* 确认按钮动画 */ .disney-confirm-btn.active { animation: btnPulse 0.3s ease; } @keyframes btnPulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } /* 表单过渡效果 */ .form-group { transition: transform 0.3s ease; } .disney-select, .disney-input, .disney-textarea { transition: all 0.3s ease; } /* 闪光效果持续时间 */ .disney-sparkles { transition: opacity 0.8s ease; } ================================================================================ File: ./app/static/css/overdue.css ================================================================================ /* overdue.css - 适合文艺少女的深棕色调设计 */ body { font-family: 'Georgia', 'Times New Roman', serif; color: #4a3728; background-color: #fcf8f3; } .container { background-color: #fff9f5; border-radius: 8px; box-shadow: 0 3px 15px rgba(113, 66, 20, 0.1); padding: 25px; margin-top: 20px; margin-bottom: 20px; border: 1px solid #e8d9cb; position: relative; } .page-title { margin-bottom: 0; color: #5d3511; font-family: 'Playfair Display', Georgia, 'Times New Roman', serif; font-weight: 600; letter-spacing: 0.5px; } .d-flex { position: relative; } .d-flex:after { content: ""; display: block; height: 2px; width: 100%; background: linear-gradient(to right, #d9c7b8, #8d6e63, #d9c7b8); margin-top: 15px; margin-bottom: 20px; } .alert-warning { background-color: #f9e8d0; border: 1px solid #ebd6ba; color: #8a6d3b; border-radius: 8px; padding: 15px; margin-bottom: 25px; box-shadow: 0 2px 5px rgba(138, 109, 59, 0.1); position: relative; overflow: hidden; } .alert-warning:before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(to right, #d4a76a, transparent); } /* 表格样式 */ .overdue-table { width: 100%; border-collapse: separate; border-spacing: 0; margin-bottom: 25px; box-shadow: 0 2px 10px rgba(113, 66, 20, 0.05); border-radius: 8px; overflow: hidden; border: 1px solid #e8d9cb; } .overdue-table th, .overdue-table td { padding: 15px 18px; text-align: left; border-bottom: 1px solid #e8d9cb; } .overdue-table th { background-color: #f1e6dd; color: #5d3511; font-weight: 600; letter-spacing: 0.5px; } .overdue-item:hover { background-color: #f8f0e5; } .overdue-item:last-child td { border-bottom: none; } .book-cover img { width: 65px; height: 90px; object-fit: cover; border-radius: 6px; box-shadow: 0 3px 8px rgba(113, 66, 20, 0.15); border: 2px solid #fff; transition: transform 0.3s ease; } .book-cover img:hover { transform: scale(1.05); } .book-title { font-weight: 600; font-family: 'Georgia', 'Times New Roman', serif; } .book-title a { color: #5d3511; text-decoration: none; transition: color 0.3s ease; } .book-title a:hover { color: #a66321; text-decoration: underline; } .book-author { color: #8d6e63; font-size: 0.9em; margin-top: 5px; font-style: italic; } .user-info a { color: #5d3511; text-decoration: none; font-weight: 600; transition: color 0.3s ease; } .user-info a:hover { color: #a66321; text-decoration: underline; } .user-nickname { color: #8d6e63; font-size: 0.9em; margin-top: 3px; } .user-contact { margin-top: 8px; } .user-contact a { color: #8d6e63; margin-right: 10px; text-decoration: none; } .user-contact a:hover { color: #704214; } .email-link, .phone-link { display: inline-block; padding: 4px 10px; font-size: 0.85em; background-color: #f1e6dd; border-radius: 15px; border: 1px solid #e8d9cb; transition: all 0.3s ease; } .email-link:hover, .phone-link:hover { background-color: #e8d9cb; } .text-danger { color: #a15950 !important; } .overdue-days { font-weight: 600; } /* 徽章 */ .badge { padding: 5px 12px; border-radius: 20px; font-weight: 500; font-size: 0.85em; letter-spacing: 0.5px; } .badge-danger { background-color: #a15950; color: white; } .badge-warning { background-color: #d4a76a; color: #4a3728; } .badge-info { background-color: #6a8da9; color: white; } /* 按钮 */ .btn { border-radius: 20px; padding: 8px 16px; transition: all 0.3s ease; letter-spacing: 0.3px; } .btn-outline-secondary { color: #704214; border-color: #d9c7b8; background-color: transparent; } .btn-outline-secondary:hover { color: #fff; background-color: #8d6e63; border-color: #8d6e63; } .btn-success { background-color: #5b8a72; border-color: #5b8a72; } .btn-success:hover, .btn-success:focus { background-color: #4a7561; border-color: #4a7561; } .btn-warning { background-color: #d4a76a; border-color: #d4a76a; color: #4a3728; } .btn-warning:hover, .btn-warning:focus { background-color: #c29355; border-color: #c29355; color: #4a3728; } .btn-primary { background-color: #704214; border-color: #704214; } .btn-primary:hover, .btn-primary:focus { background-color: #5d3511; border-color: #5d3511; } .actions .btn { margin-right: 5px; margin-bottom: 6px; } /* 空状态 */ .no-records { text-align: center; padding: 60px 20px; background-color: #f8f0e5; border-radius: 8px; margin: 25px 0; border: 1px dashed #d9c7b8; position: relative; } .no-records:before, .no-records:after { content: "❦"; position: absolute; color: #d9c7b8; font-size: 24px; } .no-records:before { top: 20px; left: 20px; } .no-records:after { bottom: 20px; right: 20px; } .empty-icon { font-size: 4.5em; color: #5b8a72; margin-bottom: 25px; } .empty-text { color: #5b8a72; margin-bottom: 25px; font-style: italic; font-size: 1.1em; } /* 分页 */ .pagination-container { display: flex; justify-content: center; margin-top: 25px; } .pagination .page-link { color: #5d3511; border-color: #e8d9cb; margin: 0 3px; border-radius: 4px; } .pagination .page-item.active .page-link { background-color: #704214; border-color: #704214; } .pagination .page-link:hover { background-color: #f1e6dd; color: #5d3511; } /* 模态框定制 */ .modal-content { border-radius: 8px; border: 1px solid #e8d9cb; box-shadow: 0 5px 20px rgba(113, 66, 20, 0.15); background-color: #fff9f5; } .modal-header { border-bottom: 1px solid #e8d9cb; background-color: #f1e6dd; border-radius: 8px 8px 0 0; } .modal-title { color: #5d3511; font-family: 'Georgia', 'Times New Roman', serif; font-weight: 600; } .modal-footer { border-top: 1px solid #e8d9cb; } /* 装饰元素 */ .container:before { content: ""; position: absolute; top: 0; right: 0; width: 150px; height: 150px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cpath fill='%23d9c7b8' fill-opacity='0.2' d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/svg%3E"); opacity: 0.3; pointer-events: none; z-index: -1; } /* 响应式设计 */ @media (max-width: 992px) { .actions .btn { display: block; width: 100%; margin-bottom: 8px; } .no-records:before, .no-records:after { display: none; } } @media (max-width: 768px) { .overdue-table { display: block; overflow-x: auto; } .book-cover img { width: 50px; height: 70px; } } ================================================================================ File: ./app/static/css/announcement-manage.css ================================================================================ .announcement-manage-container { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; border-bottom: 1px solid #e3e3e3; padding-bottom: 15px; } .filter-container { margin-bottom: 25px; } .filter-form { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; } .filter-form .form-group { margin-bottom: 0; min-width: 200px; } .announcement-table { background-color: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.05); border-radius: 8px; } .announcement-table th { background-color: #f8f9fa; white-space: nowrap; } .announcement-title { font-weight: 500; color: #333; text-decoration: none; display: block; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .announcement-title:hover { color: #007bff; text-decoration: underline; } .btn-group { display: flex; gap: 5px; } .pagination-container { margin-top: 30px; display: flex; justify-content: center; } .no-records { text-align: center; padding: 50px 20px; background-color: #f8f9fa; border-radius: 8px; color: #6c757d; } .no-records i { font-size: 3rem; margin-bottom: 15px; } .no-records p { font-size: 1.2rem; } ================================================================================ File: ./app/static/css/user_activity.css ================================================================================ /* app/static/css/user_activity.css */ .data-table .rank { font-weight: 700; text-align: center; } .data-table .borrow-count { font-weight: 600; color: #007bff; } ================================================================================ File: ./app/static/css/user-form.css ================================================================================ /* 用户表单样式 - 甜美风格 */ .user-form-container { max-width: 850px; margin: 25px auto; padding: 0 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 2px solid #f8e6e8; animation: slideInDown 0.6s ease-out; } .page-header h1 { margin: 0; font-size: 28px; color: #e75480; /* 粉红色调 */ font-weight: 600; letter-spacing: 0.5px; } .form-card { background-color: #fff; border-radius: 12px; box-shadow: 0 5px 20px rgba(231, 84, 128, 0.08); padding: 30px; border: 1px solid #f8e6e8; position: relative; overflow: visible; animation: fadeIn 0.7s ease-out; } .form-group { margin-bottom: 22px; animation: slideInRight 0.4s ease-out; animation-fill-mode: both; } /* 为每个表单组添加延迟,创造波浪效果 */ .form-group:nth-child(1) { animation-delay: 0.1s; } .form-group:nth-child(2) { animation-delay: 0.2s; } .form-group:nth-child(3) { animation-delay: 0.3s; } .form-group:nth-child(4) { animation-delay: 0.4s; } .form-group:nth-child(5) { animation-delay: 0.5s; } .form-group:nth-child(6) { animation-delay: 0.6s; } .form-group:nth-child(7) { animation-delay: 0.7s; } .form-group:nth-child(8) { animation-delay: 0.8s; } .form-group:nth-child(9) { animation-delay: 0.9s; } .form-group:nth-child(10) { animation-delay: 1.0s; } .form-group.required label:after { content: " *"; color: #ff6b8b; } .form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #5d5d5d; font-size: 15px; transition: all 0.3s ease; } .form-group:hover label { color: #e75480; transform: translateX(3px); } .form-control { display: block; width: 100%; padding: 12px 15px; font-size: 15px; line-height: 1.5; color: #555; background-color: #fff; background-clip: padding-box; border: 1.5px solid #ffd1dc; /* 淡粉色边框 */ border-radius: 8px; transition: all 0.3s ease; } .form-control:focus { border-color: #ff8da1; outline: 0; box-shadow: 0 0 0 3px rgba(255, 141, 161, 0.25); transform: translateY(-2px); } .form-control::placeholder { color: #bbb; font-style: italic; } .password-field { position: relative; } .toggle-password { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: #ff8da1; transition: all 0.3s ease; z-index: 2; } .toggle-password:hover { color: #e75480; transform: translateY(-50%) scale(1.2); } .input-with-button { display: flex; gap: 12px; } .input-with-button .form-control { flex: 1; } .input-with-button .btn { white-space: nowrap; } .form-text { display: block; margin-top: 6px; font-size: 13.5px; color: #888; font-style: italic; transition: all 0.3s ease; } .form-text.text-danger { color: #ff5c77; font-style: normal; animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; } .form-text.text-success { color: #7ac98f; font-style: normal; animation: pulse 0.5s ease; } .form-actions { display: flex; gap: 15px; margin-top: 35px; justify-content: center; animation: fadeInUp 0.8s ease-out; animation-delay: 1.2s; animation-fill-mode: both; } .btn { display: inline-block; font-weight: 500; text-align: center; white-space: nowrap; vertical-align: middle; user-select: none; border: 1.5px solid transparent; padding: 10px 22px; font-size: 15px; line-height: 1.5; border-radius: 25px; /* 圆润按钮 */ transition: all 0.3s ease; cursor: pointer; letter-spacing: 0.3px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); position: relative; overflow: hidden; } /* 按钮波纹效果 */ .btn:after { content: ""; position: absolute; top: 50%; left: 50%; width: 5px; height: 5px; background: rgba(255, 255, 255, 0.5); opacity: 0; border-radius: 100%; transform: scale(1, 1) translate(-50%); transform-origin: 50% 50%; } .btn:focus:not(:active)::after { animation: ripple 1s ease-out; } @keyframes ripple { 0% { transform: scale(0, 0); opacity: 0.5; } 20% { transform: scale(25, 25); opacity: 0.3; } 100% { transform: scale(50, 50); opacity: 0; } } .btn-primary { color: #fff; background-color: #ff8da1; border-color: #ff8da1; } .btn-primary:hover { color: #fff; background-color: #ff7389; border-color: #ff7389; box-shadow: 0 4px 8px rgba(255, 141, 161, 0.3); transform: translateY(-3px); } .btn-primary:active { transform: translateY(-1px); } .btn-secondary { color: #777; background-color: #f8f9fa; border-color: #e6e6e6; } .btn-secondary:hover { color: #555; background-color: #f1f1f1; border-color: #d9d9d9; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); transform: translateY(-3px); } .btn-outline-primary { color: #ff8da1; background-color: transparent; border-color: #ff8da1; } .btn-outline-primary:hover { color: #fff; background-color: #ff8da1; border-color: #ff8da1; box-shadow: 0 4px 8px rgba(255, 141, 161, 0.2); transform: translateY(-2px); } .btn i { margin-right: 6px; transition: transform 0.3s ease; } .btn:hover i { transform: translateX(-3px); } /* 禁用状态 */ .btn:disabled, .btn.disabled { opacity: 0.65; cursor: not-allowed; transform: none !important; box-shadow: none !important; } /* 提示信息 */ .alert { position: relative; padding: 14px 20px; margin-bottom: 25px; border: 1px solid transparent; border-radius: 8px; animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; } .alert-danger { color: #ff5c77; background-color: #fff0f3; border-color: #ffe0e5; } /* 装饰元素 */ .form-card::before { content: ""; position: absolute; top: -15px; right: 30px; width: 40px; height: 40px; background-color: #ffeaef; border-radius: 50%; z-index: -1; opacity: 0.8; animation: float 6s ease-in-out infinite; } .form-card::after { content: ""; position: absolute; bottom: -20px; left: 50px; width: 60px; height: 60px; background-color: #ffeaef; border-radius: 50%; z-index: -1; opacity: 0.6; animation: float 7s ease-in-out infinite reverse; } /* 修复选择框问题 */ s/* 专门修复下拉框文字显示问题 */ select.form-control { /* 保持一致的外观 */ appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23ff8da1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; /* 修正文字显示问题 */ padding: 12px 40px 12px 15px; /* 增加右侧内边距,确保文字不被箭头遮挡 */ text-overflow: ellipsis; /* 如果文字太长会显示省略号 */ white-space: nowrap; /* 防止文本换行 */ color: #555 !important; /* 强制文本颜色 */ font-weight: normal; line-height: 1.5; position: relative; z-index: 1; } /* 确保选定的选项能被完整显示 */ select.form-control option { padding: 10px 15px; color: #555; background-color: #fff; font-size: 15px; line-height: 1.5; } /* 针对特定浏览器的修复 */ @-moz-document url-prefix() { select.form-control { color: #555; text-indent: 0; text-overflow: clip; } } /* 针对Safari的修复 */ @media screen and (-webkit-min-device-pixel-ratio: 0) { select.form-control { text-indent: 1px; text-overflow: clip; } } /* 设置选中文本的样式 */ select.form-control:focus option:checked { background: #ffeaef; color: #555; } /* 修复IE特定问题 */ select::-ms-expand { display: none; } /* 确保选项在下拉框中正确展示 */ select.form-control option { font-weight: normal; } /* 解决Chrome中的问题 */ @media screen and (-webkit-min-device-pixel-ratio: 0) { select.form-control { border-radius: 8px; } } /* 更明确地设置选择状态的样式 */ select.form-control { border: 1.5px solid #ffd1dc; background-color: #fff; } select.form-control:focus { border-color: #ff8da1; outline: 0; box-shadow: 0 0 0 3px rgba(255, 141, 161, 0.25); } /* 尝试不同的方式设置下拉箭头 */ .select-wrapper { position: relative; display: block; width: 100%; } .select-wrapper::after { content: '⌄'; font-size: 24px; color: #ff8da1; position: absolute; right: 15px; top: 50%; transform: translateY(-50%); pointer-events: none; } /* 移除自定义背景图,改用伪元素作为箭头 */ select.form-control { background-image: none; } /* 美化表单分组 */ .form-card { position: relative; overflow: hidden; } .form-group { position: relative; z-index: 1; transition: transform 0.3s ease; } .form-group:hover { transform: translateX(5px); } /* 甜美风格的表单组分隔线 */ .form-group:not(:last-child):after { content: ""; display: block; height: 1px; width: 0; background: linear-gradient(to right, transparent, #ffe0e8, transparent); margin-top: 22px; transition: width 0.5s ease; } .form-group:not(:last-child):hover:after { width: 100%; } /* 必填项标记美化 */ .form-group.required label { position: relative; } .form-group.required label:after { content: " *"; color: #ff6b8b; font-size: 18px; line-height: 0; position: relative; top: 5px; transition: all 0.3s ease; } .form-group.required:hover label:after { color: #ff3958; transform: scale(1.2); } /* 美化滚动条 */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #fff; } ::-webkit-scrollbar-thumb { background-color: #ffc0cb; border-radius: 20px; border: 2px solid #fff; } ::-webkit-scrollbar-thumb:hover { background-color: #ff8da1; } /* 添加动画 */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } @keyframes slideInDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-15px); } 100% { transform: translateY(0px); } } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } @keyframes shake { 10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); } 30%, 50%, 70% { transform: translateX(-3px); } 40%, 60% { transform: translateX(3px); } } /* 响应式设计 */ @media (max-width: 768px) { .form-actions { flex-direction: column; align-items: center; } .input-with-button { flex-direction: column; } .page-header { flex-direction: column; align-items: flex-start; } .page-header .actions { margin-top: 12px; } .btn { width: 100%; } } /* 表单光影效果 */ .form-card { position: relative; overflow: hidden; } .form-card:before, .form-card:after { content: ""; position: absolute; z-index: -1; } /* 移入表单时添加光晕效果 */ .form-card:hover:before { content: ""; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,232,238,0.3) 0%, rgba(255,255,255,0) 70%); animation: glowEffect 2s infinite linear; } @keyframes glowEffect { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 输入焦点时的动画 */ .form-control:focus { animation: focusPulse 1s infinite alternate; } @keyframes focusPulse { from { box-shadow: 0 0 0 3px rgba(255, 141, 161, 0.25); } to { box-shadow: 0 0 0 5px rgba(255, 141, 161, 0.15); } } ================================================================================ File: ./app/static/css/book-import.css ================================================================================ /* 图书批量导入页面样式 - 女性风格优化版 */ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500&family=Playfair+Display:wght@400;700&display=swap'); :root { --primary-color: #e083b8; --primary-light: #f8d7e9; --secondary-color: #89c2d9; --accent-color: #a76eb8; --text-color: #555; --light-text: #888; --dark-text: #333; --border-radius: 12px; --box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); } body { background-color: #fff6f9; font-family: 'Montserrat', sans-serif; color: var(--text-color); } .import-container { padding: 30px; position: relative; overflow: hidden; } /* 页眉样式 */ .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #f0d3e6; } .fancy-title { font-family: 'Playfair Display', serif; font-size: 2.5rem; color: var(--accent-color); text-shadow: 1px 1px 2px rgba(167, 110, 184, 0.2); letter-spacing: 1px; margin: 0; position: relative; } .fancy-title::after { content: ""; position: absolute; bottom: -10px; left: 0; width: 60px; height: 3px; background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); border-radius: 3px; } .subtitle { font-size: 1.5rem; font-weight: 300; color: var(--light-text); margin-left: 10px; } .btn-return { padding: 8px 20px; background-color: transparent; color: var(--accent-color); border: 2px solid var(--primary-light); border-radius: 25px; transition: all 0.3s ease; font-weight: 500; box-shadow: 0 3px 8px rgba(167, 110, 184, 0.1); } .btn-return:hover { background-color: var(--primary-light); color: var(--accent-color); transform: translateY(-3px); box-shadow: 0 5px 12px rgba(167, 110, 184, 0.2); } /* 卡片样式 */ .card { border: none; border-radius: var(--border-radius); box-shadow: var(--box-shadow); overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; background-color: #ffffff; margin-bottom: 30px; } .card:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12); } .card-header { background: linear-gradient(135deg, #f9f1f7, #fcf6fa); padding: 20px 25px; border-bottom: 1px solid #f0e1ea; } .card-header h4 { font-family: 'Playfair Display', serif; color: var(--accent-color); margin: 0; font-size: 1.5rem; } .sparkle { color: var(--primary-color); margin-right: 8px; animation: sparkle 2s infinite; } @keyframes sparkle { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .card-body { padding: 30px; } /* 表单样式 */ .elegant-label { font-weight: 500; color: var(--dark-text); margin-bottom: 12px; font-size: 1.1rem; display: block; } .custom-file { position: relative; display: inline-block; width: 100%; margin-bottom: 15px; } .custom-file-input { position: absolute; left: 0; top: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; z-index: 2; } .custom-file-label { padding: 15px 20px; background-color: #f9f2f7; color: var(--light-text); border: 2px dashed #e9d6e5; border-radius: var(--border-radius); text-align: center; transition: all 0.3s ease; cursor: pointer; } .custom-file-label:hover { background-color: #f4e8f0; border-color: var(--primary-color); } .has-file .custom-file-label { background-color: #e6f3ff; border-color: var(--secondary-color); color: var(--secondary-color); font-weight: 500; } .import-btn { background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); border: none; padding: 15px 30px; color: white; font-size: 1.1rem; font-weight: 500; border-radius: 30px; margin-top: 15px; transition: all 0.3s ease; box-shadow: 0 8px 15px rgba(167, 110, 184, 0.3); } .import-btn:hover { transform: translateY(-3px); box-shadow: 0 12px 20px rgba(167, 110, 184, 0.4); background: linear-gradient(45deg, var(--accent-color), var(--primary-color)); } /* 分隔线 */ .divider { display: flex; align-items: center; margin: 30px 0; color: var(--light-text); } .divider:before, .divider:after { content: ""; flex: 1; border-bottom: 1px solid #f0d3e6; } .divider-content { padding: 0 10px; color: var(--primary-color); font-size: 1.2rem; } /* 导入说明样式 */ .import-instructions { margin-top: 10px; padding: 25px; background: linear-gradient(to bottom right, #fff, #fafafa); border-radius: var(--border-radius); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.03); } .instruction-title { font-family: 'Playfair Display', serif; color: var(--accent-color); margin-bottom: 20px; font-size: 1.4rem; border-bottom: 2px solid var(--primary-light); padding-bottom: 10px; display: inline-block; } .instruction-content { color: var(--text-color); line-height: 1.6; } .elegant-list { list-style-type: none; padding-left: 5px; margin-top: 15px; } .elegant-list li { margin-bottom: 12px; position: relative; padding-left: 25px; line-height: 1.5; } .elegant-list li:before { content: "\f054"; font-family: "Font Awesome 5 Free"; font-weight: 900; color: var(--primary-color); position: absolute; left: 0; top: 2px; font-size: 12px; } .field-name { font-family: 'Courier New', monospace; background-color: #f6f6f6; padding: 2px 8px; border-radius: 4px; color: #9c5bb5; font-weight: 600; font-size: 0.9rem; } .required-field { color: var(--dark-text); } .required-badge { background-color: #fce1e9; color: #e25a86; font-size: 0.7rem; padding: 2px 8px; border-radius: 12px; margin-left: 5px; vertical-align: middle; font-weight: 600; } /* 模板下载样式 */ .template-download { margin-top: 30px; text-align: center; padding: 20px; background: linear-gradient(135deg, #f0f9ff, #f5f0ff); border-radius: var(--border-radius); border: 1px solid #e0f0ff; } .template-download p { color: var(--dark-text); margin-bottom: 15px; font-weight: 500; } .download-btn { background-color: white; color: var(--accent-color); border: 2px solid var(--primary-light); padding: 10px 25px; border-radius: 25px; font-weight: 500; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); } .download-btn:hover { background-color: var(--accent-color); color: white; border-color: var(--accent-color); transform: translateY(-3px); box-shadow: 0 8px 20px rgba(167, 110, 184, 0.2); } /* 悬浮元素 - 冰雪奇缘和天空之城风格 */ .floating-elements { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; z-index: -1; } .snowflake { position: absolute; opacity: 0.7; border-radius: 50%; background: radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, rgba(230,240,255,0.3) 70%, transparent 100%); animation: float 20s linear infinite; } .snowflake-1 { width: 20px; height: 20px; top: 10%; left: 10%; } .snowflake-2 { width: 15px; height: 15px; top: 20%; right: 20%; } .snowflake-3 { width: 25px; height: 25px; bottom: 30%; left: 30%; } .snowflake-4 { width: 18px; height: 18px; bottom: 15%; right: 15%; } .flower { position: absolute; width: 30px; height: 30px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%23e083b8' d='M50 15c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm-25 25c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm50 0c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm-25 25c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10z'/%3E%3Ccircle fill='%23f8d7e9' cx='50' cy='50' r='10'/%3E%3C/svg%3E"); background-size: contain; background-repeat: no-repeat; opacity: 0.5; animation: rotate 25s linear infinite, float 20s ease-in-out infinite; } .flower-1 { top: 70%; left: 5%; } .flower-2 { top: 15%; right: 5%; } @keyframes float { 0% { transform: translateY(0) translateX(0); } 25% { transform: translateY(30px) translateX(15px); } 50% { transform: translateY(50px) translateX(-15px); } 75% { transform: translateY(20px) translateX(25px); } 100% { transform: translateY(0) translateX(0); } } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* 响应式调整 */ @media (max-width: 992px) { .import-container { padding: 20px 15px; } .fancy-title { font-size: 2rem; } .subtitle { font-size: 1.2rem; } } @media (max-width: 768px) { .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } .card-body { padding: 20px 15px; } .import-instructions { padding: 15px; } .fancy-title { font-size: 1.8rem; } .subtitle { font-size: 1rem; display: block; margin-left: 0; margin-top: 5px; } } /* 添加到book-import.css文件末尾 */ /* 导入消息样式 */ .import-message { margin-top: 15px; } .import-message .alert { border-radius: var(--border-radius); padding: 15px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); border: none; } .import-message .alert-success { background-color: #e6f7ee; color: #28a745; } .import-message .alert-warning { background-color: #fff8e6; color: #ffc107; } .import-message .alert-danger { background-color: #feecf0; color: #dc3545; } .import-message .alert-info { background-color: #e6f3f8; color: #17a2b8; } .import-message .alert i { margin-right: 8px; } /* 导入过程中的飘落元素 */ .falling-element { position: absolute; z-index: 1000; pointer-events: none; opacity: 0.8; } .falling-flower { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath fill='%23e083b8' d='M50 15c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm-25 25c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm50 0c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10zm-25 25c-5 0-10 5-10 10s5 10 10 10 10-5 10-10-5-10-10-10z'/%3E%3Ccircle fill='%23f8d7e9' cx='50' cy='50' r='10'/%3E%3C/svg%3E"); background-size: contain; background-repeat: no-repeat; animation: fallAndSpin 5s linear forwards; } .falling-snowflake { background: radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, rgba(230,240,255,0.3) 70%, transparent 100%); border-radius: 50%; animation: fall 5s linear forwards; } @keyframes fall { 0% { transform: translateY(-50px) rotate(0deg); opacity: 0; } 10% { opacity: 1; } 100% { transform: translateY(calc(100vh - 100px)) rotate(359deg); opacity: 0; } } @keyframes fallAndSpin { 0% { transform: translateY(-50px) rotate(0deg); opacity: 0; } 10% { opacity: 1; } 100% { transform: translateY(calc(100vh - 100px)) rotate(720deg); opacity: 0; } } /* 导入过程中按钮样式 */ .import-btn:disabled { background: linear-gradient(45deg, #f089b7, #b989d9); opacity: 0.7; cursor: not-allowed; } .import-btn:disabled .fa-spinner { margin-right: 10px; } /* 文件上传成功状态样式 */ .has-file .custom-file-label { background-color: #e6f7ee; border-color: #28a745; color: #28a745; } /* 添加文件类型图标 */ .has-file .custom-file-label::before { content: "\f56f"; /* Excel文件图标 */ font-family: "Font Awesome 5 Free"; font-weight: 900; margin-right: 8px; } ================================================================================ File: ./app/static/css/statistics.css ================================================================================ /* app/static/css/statistics.css */ :root { /* Soft & Elegant Palette */ --color-primary-light-pink: #FCE4EC; /* 淡粉色 */ --color-primary-milk-white: #FFF8F0; /* 奶白色 */ --color-primary-apricot: #FFDAB9; /* 浅杏色 */ --color-aux-rose-gold: #B76E79; /* 玫瑰金 */ --color-aux-light-purple: #E6E6FA; /* 淡紫色 */ --color-aux-soft-gray: #D3D3D3; /* 柔和的灰色 */ --color-accent-berry-red: #8C2D5A; /* 深一点的浆果红 */ --font-serif-elegant: 'Playfair Display', serif; --font-serif-lora: 'Lora', serif; --font-sans-clean: 'Open Sans', sans-serif; --font-script-delicate: 'Sacramento', cursive; --font-serif-garamond: 'EB Garamond', serif; /* Derived/General Usage */ --background-main: var(--color-primary-milk-white); --background-container: #FFFFFF; --text-main: #5D5053; /* A darker, softer, slightly desaturated rose-brown */ --text-soft: #8A797C; --text-heading: var(--color-aux-rose-gold); --text-accent: var(--color-accent-berry-red); --border-soft: var(--color-aux-soft-gray); --border-decorative: var(--color-primary-light-pink); --shadow-soft: rgba(183, 110, 121, 0.1); /* Soft shadow based on rose gold */ --shadow-subtle: rgba(0, 0, 0, 0.05); /* Fallback for old variables - some might still be used by unchanged CSS */ --primary-color: var(--color-primary-light-pink); --secondary-color: var(--color-primary-apricot); /* Or #FFF8F0 for a lighter secondary */ --accent-color: var(--color-aux-rose-gold); --text-color: var(--text-main); --light-text: var(--text-soft); --border-color: var(--border-soft); --shadow-color: var(--shadow-soft); --hover-color: #F8E0E6; /* Lighter pink for hover */ } body { background-color: var(--background-main); color: var(--text-main); font-family: var(--font-sans-clean); font-weight: 300; /* Lighter default font weight */ line-height: 1.7; /* Increased line height */ } .statistics-container { padding: 40px 30px; /* Increased padding */ max-width: 1100px; /* Slightly adjusted max-width */ margin: 40px auto; /* More margin for breathing room */ background-color: var(--background-container); border-radius: 16px; /* Softer, larger border-radius */ box-shadow: 0 8px 25px var(--shadow-soft); /* Softer shadow */ position: relative; overflow: hidden; } .page-title { color: var(--text-heading); margin-bottom: 35px; padding-bottom: 15px; border-bottom: 1px solid var(--border-decorative); /* Thinner, delicate line */ text-align: center; font-family: var(--font-serif-elegant); font-size: 2.8em; /* Larger, more prominent */ font-weight: 700; letter-spacing: 0.5px; } /* Simplified page title decoration */ .page-title:after { content: ''; display: block; width: 80px; /* Shorter line */ height: 2px; /* Thinner line */ margin: 12px auto 0; background: var(--color-aux-rose-gold); /* Solid accent color */ border-radius: 2px; /* animation: wave 3s infinite linear; Removed wave animation for elegance */ } /* @keyframes wave { 0%, 100% { background-position-x: 0%; } 50% { background-position-x: 100%; } } */ /* Quote Banner - Styled for elegance */ .quote-banner { background-color: var(--color-primary-light-pink); /* Soft pink background */ border-radius: 12px; /* Softer radius */ padding: 25px 35px; /* Ample padding */ margin: 0 auto 40px; /* Increased bottom margin */ max-width: 75%; text-align: center; box-shadow: 0 4px 15px rgba(183, 110, 121, 0.08); /* Very subtle shadow */ border-left: 3px solid var(--color-aux-rose-gold); border-right: 3px solid var(--color-aux-rose-gold); position: relative; } .quote-banner p { font-family: var(--font-serif-garamond), serif; /* Elegant serif for quote */ font-style: italic; color: var(--color-accent-berry-red); /* Berry red for emphasis */ font-size: 1.1em; /* Slightly larger */ margin: 0; letter-spacing: 0.2px; line-height: 1.6; } .quote-banner:before, .quote-banner:after { content: """; /* Using """ for opening */ font-family: var(--font-serif-elegant), serif; /* Consistent elegant font */ font-size: 50px; /* Adjusted size */ color: var(--color-aux-rose-gold); /* Rose gold for quotes */ opacity: 0.4; /* Softer opacity */ position: absolute; top: 0px; } .quote-banner:before { left: 15px; } .quote-banner:after { content: """; /* Using """ for closing */ right: 15px; top: auto; /* Adjust position for closing quote mark */ bottom: -20px; } /* Stats Grid - main navigation cards container */ .stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); grid-gap: 30px; /* Increased gap for more whitespace */ margin: 40px auto; /* Adjusted margin */ max-width: 900px; /* Adjusted max-width */ } .stats-grid .stats-card { position: relative; background-color: var(--background-container); border-radius: 12px; /* Softer radius */ overflow: hidden; box-shadow: 0 6px 18px var(--shadow-subtle); /* More subtle shadow */ transition: transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 0.35s cubic-bezier(0.25, 0.8, 0.25, 1); text-decoration: none; color: var(--text-main); border: 1px solid #F0E8E9; /* Very light, almost invisible border */ min-height: 260px; /* Ensure cards have enough height */ padding: 0; } .card-inner { /* This class is directly inside stats-card links */ display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 25px; /* Ample padding */ height: 100%; position: relative; z-index: 2; background: transparent; /* Make it transparent to show card background */ transition: background-color 0.3s ease; } .stats-grid .stats-card:hover { transform: translateY(-6px); /* Slightly less aggressive transform */ box-shadow: 0 10px 25px var(--shadow-soft); /* Enhanced shadow on hover */ border-color: var(--color-primary-light-pink); } .stats-grid .stats-card:hover .card-inner { /* background: rgba(255, 248, 240, 0.5); */ /* Optional: very subtle hover background on inner part */ } .stats-grid .card-icon { font-size: 36px; /* Slightly smaller icon */ margin-bottom: 18px; color: var(--color-aux-rose-gold); background-color: var(--color-primary-milk-white); /* Milk white for icon background */ width: 70px; /* Adjusted size */ height: 70px; display: flex; align-items: center; justify-content: center; border-radius: 50%; box-shadow: 0 3px 8px rgba(183, 110, 121, 0.15); /* Subtle shadow for icon */ transition: transform 0.3s ease, color 0.3s ease; } .stats-grid .stats-card:hover .card-icon { transform: scale(1.08) rotate(3deg); color: var(--color-accent-berry-red); /* Icon color change on hover */ } .stats-grid .card-title { font-family: var(--font-serif-lora); font-size: 1.45em; /* Adjusted size */ font-weight: 600; margin-bottom: 12px; color: var(--text-heading); position: relative; display: inline-block; } .stats-grid .card-title:after { /* Decorative line under card title */ content: ''; position: absolute; bottom: -6px; /* Positioned slightly below */ left: 50%; transform: translateX(-50%) scaleX(0); /* Start scaled to 0 */ width: 60%; /* Line width relative to title */ height: 1.5px; background-color: var(--color-primary-light-pink); /* Light pink line */ transition: transform 0.35s ease-out; transform-origin: center; } .stats-grid .stats-card:hover .card-title:after { transform: translateX(-50%) scaleX(1); /* Scale to full on hover */ } .stats-grid .card-description { font-family: var(--font-sans-clean); font-size: 0.9em; color: var(--text-soft); line-height: 1.5; max-width: 90%; /* Prevent text from touching edges */ } /* Card Decoration - Subtle background elements */ .card-decoration { position: absolute; bottom: -40px; /* Adjusted position */ right: -40px; width: 120px; /* Smaller decoration */ height: 120px; border-radius: 50%; background-color: var(--color-primary-light-pink); /* Light pink base */ opacity: 0.15; /* More subtle opacity */ transition: all 0.5s ease; z-index: 1; } .stats-card:hover .card-decoration { /* Use stats-card hover for decoration */ transform: scale(1.4); opacity: 0.25; } /* Specific card decorations with more subtle emoji styling */ .card-decoration:before { /* General style for emoji if used */ position: absolute; font-size: 24px; /* Smaller emoji */ top: 50%; /* Centered better */ left: 50%; transform: translate(-50%, -50%); opacity: 0.3; /* Very subtle */ color: var(--color-aux-rose-gold); /* Themed color */ } .book-decoration:before { content: '📚'; } .trend-decoration:before { content: '📈'; } .user-decoration:before { content: '👥'; } .overdue-decoration:before { content: '⏰'; } /* Page Decoration - Floating elements */ .page-decoration { position: absolute; width: 180px; /* Slightly smaller */ height: 180px; border-radius: 50%; background: linear-gradient(45deg, var(--color-primary-apricot), var(--color-aux-light-purple), var(--color-primary-light-pink)); /* New gradient */ opacity: 0.15; /* More subtle */ z-index: -1; /* Ensure it's behind content */ } .page-decoration.left { top: -80px; /* Adjusted position */ left: -80px; animation: floatLeft 18s ease-in-out infinite; } .page-decoration.right { bottom: -80px; right: -80px; animation: floatRight 20s ease-in-out infinite; } @keyframes floatLeft { 0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); } 25% { transform: translate(15px, 20px) rotate(8deg) scale(1.05); } 50% { transform: translate(5px, 35px) rotate(15deg) scale(1); } 75% { transform: translate(25px, 10px) rotate(5deg) scale(1.05); } } @keyframes floatRight { 0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); } 25% { transform: translate(-15px, -18px) rotate(-7deg) scale(1.05); } 50% { transform: translate(-10px, -30px) rotate(-12deg) scale(1); } 75% { transform: translate(-22px, -12px) rotate(-6deg) scale(1.05); } } /* --- Unchanged CSS from this point onwards as per request for elements not in index.html --- */ /* --- (Or elements whose styling should largely be preserved unless overridden by above general styles) --- */ .breadcrumb { margin-bottom: 20px; font-size: 14px; color: var(--light-text); /* Will use new --light-text */ } .breadcrumb a { color: var(--accent-color); /* Will use new --accent-color */ text-decoration: none; transition: all 0.3s ease; } .breadcrumb a:hover { text-decoration: underline; color: var(--color-accent-berry-red); /* More specific hover */ } .breadcrumb .current-page { color: var(--text-color); /* Will use new --text-color */ font-weight: 500; } /* 原始卡片菜单 - Unchanged as it's not used in the provided HTML */ .stats-menu { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 30px; } /* 原始卡片样式 - This .stats-card is different from .stats-grid .stats-card. Keeping for other pages. */ /* However, some properties might be inherited if not specific enough. */ /* Adding a more specific selector to avoid conflict if this old style is needed elsewhere */ .stats-menu > .stats-card { background-color: var(--secondary-color); border-radius: 12px; padding: 25px; box-shadow: 0 4px 12px var(--shadow-color); transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.4s; text-decoration: none; color: var(--text-color); display: flex; flex-direction: column; align-items: center; text-align: center; border: 1px solid var(--border-color); } .stats-menu > .stats-card:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 8px 20px var(--shadow-color); border-color: var(--primary-color); } /* Card icon/title/description for .stats-menu > .stats-card */ .stats-menu > .stats-card .card-icon { font-size: 40px; margin-bottom: 15px; color: var(--accent-color); /* Resetting some properties from .stats-grid .card-icon if they conflict */ background-color: transparent; width: auto; height: auto; box-shadow: none; } .stats-menu > .stats-card .card-title { font-size: 18px; font-weight: 600; margin-bottom: 10px; font-family: var(--font-sans-clean); /* Keep it simple for this version */ color: var(--text-color); /* Default text color for these */ } .stats-menu > .stats-card .card-title:after { display: none; /* No line for this version */ } .stats-menu > .stats-card .card-description { font-size: 14px; color: var(--light-text); font-family: var(--font-sans-clean); } .filter-section { margin-bottom: 25px; display: flex; align-items: center; background-color: var(--color-primary-milk-white); /* Updated bg */ padding: 12px 18px; border-radius: 10px; border: 1px dashed var(--border-decorative); /* Updated border */ } .filter-label { font-weight: 500; margin-right: 10px; color: var(--text-main); /* Updated text */ } .filter-select { padding: 8px 15px; border: 1px solid var(--border-soft); /* Updated border */ border-radius: 8px; /* Softer radius */ background-color: white; color: var(--text-main); font-size: 0.95em; font-family: var(--font-sans-clean); transition: border-color 0.3s, box-shadow 0.3s; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23B76E79' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); /* Updated arrow color */ background-repeat: no-repeat; background-position: right 10px center; padding-right: 30px; } .filter-select:focus { outline: none; border-color: var(--color-aux-rose-gold); /* Updated focus color */ box-shadow: 0 0 0 3px rgba(183, 110, 121, 0.2); /* Updated focus shadow */ } .ml-20 { margin-left: 20px; } .chart-container { background-color: white; border-radius: 12px; /* Softer radius */ padding: 25px; box-shadow: 0 4px 15px var(--shadow-soft); /* Updated shadow */ margin-bottom: 35px; position: relative; height: 400px; border: 1px solid var(--border-decorative); /* Updated border */ overflow: hidden; } .chart-container canvas { max-height: 100%; z-index: 1; position: relative; } .chart-decoration { /* These are for charts, distinct from page/card decorations */ position: absolute; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(45deg, var(--color-primary-light-pink), var(--color-primary-apricot)); /* Updated gradient */ opacity: 0.4; /* Softer opacity */ z-index: 0; } .chart-decoration.left { top: -15px; left: -15px; } .chart-decoration.right { bottom: -15px; right: -15px; } .floating { animation: floating 6s ease-in-out infinite; } @keyframes floating { 0% { transform: translate(0, 0) scale(1); } 50% { transform: translate(8px, 8px) scale(1.05); } /* Softer float */ 100% { transform: translate(0, 0) scale(1); } } .chart-container.half { height: auto; min-height: 400px; padding-bottom: 40px; } .chart-container.half .chart-wrapper { height: 340px; padding-bottom: 20px; } canvas#category-chart { max-height: 100%; margin-bottom: 20px; padding-bottom: 20px; position: relative; } .chart-container.half::before, .chart-container.half::after { width: 40px; height: 40px; opacity: 0.2; /* Softer opacity */ } .chart-container.half .chart-wrapper { position: relative; } .chart-row { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 30px; } .half { flex: 1 1 calc(50% - 10px); min-width: 300px; } /* 表格容器样式 */ .table-container { margin-bottom: 30px; position: relative; overflow: hidden; border-radius: 12px; box-shadow: 0 4px 20px var(--shadow-subtle); } .data-table { width: 100%; border-collapse: collapse; /* 修改为collapse以解决边框问题 */ border-spacing: 0; border-radius: 10px; /* 保持圆角 */ overflow: hidden; box-shadow: 0 2px 10px var(--shadow-subtle); /* 保持阴影 */ font-family: var(--font-sans-clean); /* 确保一致字体 */ } .data-table th, .data-table td { padding: 14px 18px; text-align: left; border-bottom: 1px solid var(--border-decorative); /* 保持底部边框 */ vertical-align: middle; /* 确保所有内容垂直居中 */ box-sizing: border-box; /* 确保边框计算在单元格尺寸内 */ } .data-table td { font-size: 0.95em; } .data-table th { background-color: var(--color-primary-light-pink); /* Lighter pink for header */ font-weight: 600; /* Was 600, can be 400 for softer look */ color: var(--text-heading); /* Rose gold text for header */ letter-spacing: 0.5px; font-size: 1em; border-bottom: 2px solid var(--color-aux-rose-gold); } .data-table tr { transition: background-color 0.3s; } .data-table tr:nth-child(even) { background-color: var(--color-primary-milk-white); /* Milk white for even rows */ } .data-table tr:nth-child(odd) { background-color: white; } .data-table tr:last-child td { border-bottom: none; } .data-table tr:hover { background-color: #FEF6F8; /* Very light pink on hover */ } /* 表格特定列的样式 */ .data-table th:first-child, .data-table td:first-child { text-align: center; /* 排名居中 */ position: relative; /* 确保相对定位 */ } .data-table th:nth-child(2), .data-table td:nth-child(2) { text-align: center; /* 封面图片居中 */ } .data-table th:last-child, .data-table td:last-child { text-align: center; /* 借阅次数居中显示 */ } .loading-row td { text-align: center; padding: 30px; color: var(--text-soft); /* Updated text color */ } .loading-animation { display: flex; align-items: center; justify-content: center; } .loading-animation:before { content: '📖'; margin-right: 10px; animation: bookFlip 2s infinite; display: inline-block; color: var(--color-aux-rose-gold); /* Themed color */ } @keyframes bookFlip { 0% { transform: rotateY(0deg); } 50% { transform: rotateY(180deg); } 100% { transform: rotateY(360deg); } } .dot-animation { display: inline-block; animation: dotAnimation 1.5s infinite; } @keyframes dotAnimation { 0% { opacity: 0.3; } 50% { opacity: 1; } 100% { opacity: 0.3; } } .stats-cards { /* This is for the small summary cards, different from .stats-grid */ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; } /* Style for .stats-cards > .stats-card if they exist */ .stats-cards > .stats-card { background-color: var(--background-container); border: 1px solid var(--border-decorative); padding: 20px; border-radius: 10px; box-shadow: 0 3px 10px var(--shadow-subtle); text-align: center; } .stats-cards .stats-card .card-value { /* Assuming .card-value is inside these cards */ font-size: 2em; /* Adjusted size */ font-weight: 700; margin-bottom: 8px; color: var(--color-accent-berry-red); /* Berry red for value */ font-family: var(--font-serif-elegant); } .stats-cards .stats-card .card-title { /* Title within these small cards */ font-family: var(--font-sans-clean); font-size: 0.95em; color: var(--text-soft); font-weight: 400; } .stats-cards .stats-card .card-icon { /* Icon within these small cards */ font-size: 1.8em; color: var(--color-aux-rose-gold); margin-bottom: 10px; } /* Quote Container - Appears distinct from .quote-banner, kept for other pages */ .quote-container { text-align: center; margin: 40px auto 20px; max-width: 600px; font-style: italic; color: var(--text-main); /* Updated text */ padding: 20px; background-color: var(--color-primary-apricot); /* Apricot background */ border-radius: 12px; /* Softer radius */ position: relative; font-family: var(--font-serif-garamond); box-shadow: 0 3px 10px var(--shadow-subtle); } .quote-container:before, .quote-container:after { content: """; font-size: 50px; font-family: var(--font-serif-elegant); position: absolute; color: var(--color-aux-rose-gold); /* Rose gold quotes */ opacity: 0.3; /* Softer */ } .quote-container:before { top: -5px; left: 10px; } .quote-container:after { content: """; bottom: -25px; right: 10px; } .quote-container p { position: relative; z-index: 1; margin-bottom: 10px; font-size: 1.05em; /* Adjusted */ line-height: 1.6; } .quote-author { display: block; font-size: 0.9em; font-style: normal; text-align: right; color: var(--text-soft); /* Updated text */ font-family: var(--font-sans-clean); } /* Book list title - for table pages */ .book-list-title { text-align: center; margin-bottom: 25px; color: var(--text-heading); /* Rose gold */ font-family: var(--font-serif-lora); /* Lora for this title */ font-size: 1.8em; /* Adjusted */ position: relative; display: inline-block; left: 50%; transform: translateX(-50%); padding: 0 20px; } .book-icon { /* General book icon if used with this title */ font-size: 0.9em; margin: 0 8px; opacity: 0.85; color: var(--color-aux-rose-gold); } .column-icon { font-size: 0.9em; margin-right: 5px; opacity: 0.8; color: var(--color-aux-rose-gold); } .book-list-title:before, .book-list-title:after { content: ''; position: absolute; height: 1.5px; /* Thinner line */ background: linear-gradient(to right, transparent, var(--color-primary-light-pink), transparent); /* Softer gradient */ width: 70px; top: 50%; } .book-list-title:before { right: 100%; margin-right: 15px; } .book-list-title:after { left: 100%; margin-left: 15px; } /* 表格中的图标样式 */ .data-table .borrow-count { font-weight: 600; color: var(--text-heading); position: relative; display: block; /* 修改为block以占据整个单元格 */ text-align: center; /* 确保文本居中 */ font-size: 1em; } .data-table .borrow-count:after { content: '📚'; font-size: 12px; margin-left: 5px; opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; transform: translateY(5px); display: inline-block; color: var(--color-aux-rose-gold); } .data-table tr:hover .borrow-count:after { opacity: 0.7; /* Softer opacity */ transform: translateY(0); } /* 排名列样式 */ .data-table .rank { font-weight: 700; text-align: center; position: relative; font-size: 1.1em; color: var(--text-heading); font-family: var(--font-serif-lora); padding: 5px 15px; /* 基本内边距 */ } /* 前三名奖牌样式 */ .data-table tr:nth-child(1) .rank:before, .data-table tr:nth-child(2) .rank:before, .data-table tr:nth-child(3) .rank:before { position: absolute; font-size: 1.2em; left: 5px; /* 左侧位置 */ top: 50%; transform: translateY(-50%); opacity: 0.85; } /* 分别设置每个奖牌的内容 */ .data-table tr:nth-child(1) .rank:before { content: '🏆'; } .data-table tr:nth-child(2) .rank:before { content: '🥈'; } .data-table tr:nth-child(3) .rank:before { content: '🥉'; } /* 确保所有排名单元格的对齐一致 */ .data-table td:first-child { text-align: center; } .book-title { /* In data tables */ position: relative; text-decoration: none; display: inline-block; font-weight: 600; /* Bolder for emphasis */ color: var(--text-accent); /* Berry red for book titles */ transition: color 0.3s; } .data-table tr:hover .book-title { color: var(--color-aux-rose-gold); /* Rose gold on hover */ } .book-title:after { /* Underline effect for book titles in tables */ content: ''; position: absolute; width: 100%; height: 1.5px; bottom: -3px; left: 0; background-color: var(--color-primary-light-pink); /* Light pink underline */ transform: scaleX(0); transform-origin: bottom right; transition: transform 0.3s ease-out; } tr:hover .book-title:after { transform: scaleX(1); transform-origin: bottom left; } /* Data table image styling */ .data-table img { width: 50px; /* Slightly smaller */ height: 75px; object-fit: cover; border-radius: 6px; /* Softer radius */ box-shadow: 0 2px 6px rgba(0,0,0,0.08); /* Softer shadow */ transition: transform 0.3s ease, box-shadow 0.3s ease; border: 2px solid white; } .data-table tr:hover img { transform: scale(1.1); /* Slightly more pop */ box-shadow: 0 4px 10px rgba(0,0,0,0.12); border-color: var(--color-primary-light-pink); /* Pink border on hover */ } .data-table .author { font-style: italic; color: var(--text-soft); /* Softer text for author */ font-size: 0.9em; } .no-data { text-align: center; padding: 40px; color: var(--text-soft); background-color: var(--color-primary-milk-white); /* Milk white background */ border-radius: 12px; font-style: italic; border: 1px dashed var(--border-decorative); /* Decorative dashed border */ font-family: var(--font-serif-garamond); } /* 书籍行动画 */ #ranking-table-body tr { transition: transform 0.3s ease, opacity 0.3s ease, background-color 0.3s ease; /* Added background-color */ } #ranking-table-body tr:hover { transform: translateX(3px); /* Subtle shift */ } /* Animation shared */ .fade-in { /* This is a custom class, not from animate.css */ animation: customFadeIn 0.6s ease forwards; /* Renamed to avoid conflict */ opacity: 0; transform: translateY(15px); /* Slightly more travel */ } @keyframes customFadeIn { /* Renamed */ to { opacity: 1; transform: translateY(0); } } /* Responsive adjustments */ @media (max-width: 992px) { /* Adjusted breakpoint */ .stats-grid { max-width: 95%; gap: 20px; /* Smaller gap on medium screens */ } .stats-grid .stats-card { min-height: 240px; } .page-title { font-size: 2.4em; } .quote-banner { max-width: 85%; } } @media (max-width: 768px) { .statistics-container { padding: 30px 20px; margin: 20px auto; } .page-title { font-size: 2em; } .quote-banner { max-width: 90%; padding: 20px; } .quote-banner:before, .quote-banner:after { font-size: 35px; } .quote-banner:after { bottom: -15px; } .stats-grid { grid-template-columns: 1fr; /* Single column for cards */ gap: 25px; max-width: 450px; /* Max width for single column */ } .stats-grid .stats-card { min-height: auto; /* Auto height for single column */ height: auto; /* Ensure this is not fixed */ padding-bottom: 20px; /* Ensure padding for content */ } .stats-grid .card-inner { padding: 20px; } .chart-row { flex-direction: column; } .half { width: 100%; flex-basis: 100%; /* Ensure it takes full width */ } .stats-cards { /* Small summary cards */ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } .filter-section { flex-wrap: wrap; padding: 10px 15px; } .filter-select { width: 100%; } .ml-20 { margin-left: 0; margin-top: 10px; } .page-decoration { /* Make page decorations smaller or hide on mobile */ width: 120px; height: 120px; opacity: 0.1; } .page-decoration.left { top: -60px; left: -60px; } .page-decoration.right { bottom: -60px; right: -60px; } .data-table th, .data-table td { padding: 10px 12px; font-size: 0.9em; } .data-table img { width: 40px; height: 60px; } } @media (max-width: 480px) { .page-title { font-size: 1.8em; } .quote-banner p { font-size: 1em; } .stats-grid .card-title { font-size: 1.3em; } .stats-grid .card-description { font-size: 0.85em; } .stats-grid .card-icon { width: 60px; height: 60px; font-size: 30px; } .statistics-container { margin: 15px auto; padding: 20px 15px; } .page-decoration { display: none; /* Hide complex decorations on very small screens */ } /* 移动端表格调整 */ .data-table .rank:before { left: -5px; /* 小屏幕上减少偏移量 */ font-size: 1.2em; } .data-table .rank { padding: 5px 8px; /* 减少内边距 */ } } ================================================================================ File: ./app/static/css/inventory-list.css ================================================================================ /* 全局变量设置 */ :root { --primary-color: #f2a3b3; --primary-light: #ffd6e0; --primary-dark: #e57f9a; --secondary-color: #a9d1f7; --text-color: #4a4a4a; --light-text: #6e6e6e; --success-color: #77dd77; --warning-color: #fdfd96; --danger-color: #ff9e9e; --background-color: #fff9fb; --card-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); --transition: all 0.3s ease; --border-radius: 12px; --card-padding: 20px; } /* 基础样式 */ body { background-color: var(--background-color); color: var(--text-color); font-family: 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; } .inventory-container { max-width: 1200px; margin: 0 auto; padding: 20px; } /* 页面标题 */ .page-header { background: linear-gradient(135deg, var(--primary-light), var(--secondary-color)); border-radius: var(--border-radius); margin-bottom: 30px; padding: 40px 30px; text-align: center; box-shadow: var(--card-shadow); position: relative; overflow: hidden; } .page-header::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url('data:image/svg+xml;utf8,') repeat; background-size: 80px 80px; opacity: 0.4; } .header-content { position: relative; z-index: 2; } .page-header h1 { color: #fff; margin: 0; font-size: 2.5rem; font-weight: 300; letter-spacing: 1px; text-shadow: 1px 1px 3px rgba(0,0,0,0.1); } .header-icon { margin-right: 15px; color: #fff; } .subtitle { color: #fff; margin-top: 10px; font-size: 1.1rem; font-weight: 300; opacity: 0.9; } /* 搜索框样式 */ .search-card { background: #fff; border-radius: var(--border-radius); padding: var(--card-padding); margin-bottom: 30px; box-shadow: var(--card-shadow); border-top: 4px solid var(--primary-color); } .search-form { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px; } .search-input-group { display: flex; flex: 1; min-width: 300px; } .search-input-container { position: relative; flex: 1; } .search-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: var(--light-text); } .search-input { width: 100%; padding: 12px 15px 12px 40px; border: 1px solid #e3e3e3; border-radius: var(--border-radius) 0 0 var(--border-radius); font-size: 1rem; transition: var(--transition); outline: none; } .search-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px var(--primary-light); } .search-button { background-color: var(--primary-color); color: white; border: none; padding: 12px 25px; font-size: 1rem; border-radius: 0 var(--border-radius) var(--border-radius) 0; cursor: pointer; transition: var(--transition); } .search-button:hover { background-color: var(--primary-dark); } .log-button { background-color: #fff; color: var(--primary-color); border: 1px solid var(--primary-color); padding: 11px 20px; border-radius: var(--border-radius); text-decoration: none; font-size: 0.95rem; transition: var(--transition); display: inline-flex; align-items: center; gap: 8px; } .log-button:hover { background-color: var(--primary-light); color: var(--primary-dark); } /* 表格样式 */ .table-container { background: #fff; border-radius: var(--border-radius); padding: var(--card-padding); margin-bottom: 30px; box-shadow: var(--card-shadow); overflow: hidden; } .inventory-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } .inventory-table th { background-color: var(--primary-light); color: var(--primary-dark); padding: 15px; text-align: left; font-weight: 600; text-transform: uppercase; font-size: 0.85rem; letter-spacing: 0.5px; } .inventory-table tr { border-bottom: 1px solid #f3f3f3; transition: var(--transition); } .inventory-table tr:last-child { border-bottom: none; } .inventory-table tr:hover { background-color: #f9f9f9; } .inventory-table td { padding: 15px; vertical-align: middle; } .book-title { font-weight: 500; color: var(--text-color); } .book-author { color: var(--light-text); font-style: italic; } /* 库存和状态标签样式 */ .stock-badge, .status-badge { display: inline-block; padding: 6px 12px; border-radius: 50px; font-size: 0.85rem; font-weight: 500; text-align: center; min-width: 60px; } .stock-high { background-color: var(--success-color); color: #fff; } .stock-medium { background-color: var(--warning-color); color: #8a7800; } .stock-low { background-color: var(--danger-color); color: #fff; } .status-active { background-color: #d9f5e6; color: #2a9d5c; } .status-inactive { background-color: #ffe8e8; color: #e35555; } /* 操作按钮 */ .action-buttons { display: flex; gap: 8px; } .btn-adjust, .btn-view { padding: 8px 12px; border-radius: var(--border-radius); text-decoration: none; font-size: 0.85rem; display: inline-flex; align-items: center; gap: 5px; transition: var(--transition); } .btn-adjust { background-color: var(--primary-light); color: var(--primary-dark); border: 1px solid var(--primary-color); } .btn-adjust:hover { background-color: var(--primary-color); color: white; } .btn-view { background-color: var(--secondary-color); color: #3573b5; border: 1px solid #8ab9e3; } .btn-view:hover { background-color: #8ab9e3; color: white; } /* 分页样式 */ .pagination-wrapper { display: flex; justify-content: center; margin-top: 30px; } .pagination { display: flex; list-style: none; padding: 0; margin: 0; gap: 5px; } .page-item { display: inline-block; } .page-link { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; height: 40px; padding: 0 15px; border-radius: var(--border-radius); background-color: #fff; color: var(--text-color); text-decoration: none; transition: var(--transition); border: 1px solid #e3e3e3; } .page-item.active .page-link { background-color: var(--primary-color); color: white; border-color: var(--primary-color); } .page-item:not(.active) .page-link:hover { background-color: var(--primary-light); color: var(--primary-dark); border-color: var(--primary-light); } .page-item.disabled .page-link { background-color: #f5f5f5; color: #aaa; cursor: not-allowed; } /* 响应式调整 */ @media (max-width: 992px) { .inventory-container { padding: 15px; } .page-header { padding: 30px 20px; } .page-header h1 { font-size: 2rem; } } @media (max-width: 768px) { .search-form { flex-direction: column; align-items: stretch; } .log-button { text-align: center; } .page-header h1 { font-size: 1.8rem; } .table-container { overflow-x: auto; } .inventory-table { min-width: 800px; } .action-buttons { flex-direction: column; } .btn-adjust, .btn-view { text-align: center; } } @media (max-width: 576px) { .page-header { padding: 25px 15px; } .page-header h1 { font-size: 1.5rem; } .subtitle { font-size: 1rem; } .pagination .page-link { min-width: 35px; height: 35px; padding: 0 10px; font-size: 0.9rem; } } ================================================================================ File: ./app/static/css/my_borrows.css ================================================================================ /* my_borrows.css - 少女粉色风格图书管理系统 */ :root { --primary-color: #e686a5; /* 主要粉色 */ --primary-light: #ffedf2; /* 浅粉色 */ --primary-dark: #d26a8c; /* 深粉色 */ --accent-color: #9a83c9; /* 紫色点缀 */ --text-primary: #4a4a4a; /* 主要文字颜色 */ --text-secondary: #848484; /* 次要文字颜色 */ --border-color: #f4d7e1; /* 边框颜色 */ --success-color: #7ac9a1; /* 成功色 */ --danger-color: #ff8f9e; /* 危险色 */ --white: #ffffff; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; color: var(--text-primary); background-color: #fdf6f8; } /* 容器 */ .container { width: 95% !important; max-width: 1200px !important; margin: 1.5rem auto; padding: 1.5rem; background-color: var(--white); box-shadow: 0 3px 15px rgba(230, 134, 165, 0.15); border-radius: 20px; box-sizing: border-box; position: relative; } /* 页面标题 */ .page-title { margin-bottom: 1.8rem; color: var(--primary-dark); border-bottom: 2px solid var(--border-color); padding-bottom: 12px; font-size: 1.8rem; font-weight: 600; position: relative; text-align: center; } .page-title:after { content: ""; position: absolute; width: 80px; height: 3px; background-color: var(--primary-color); bottom: -2px; left: 50%; transform: translateX(-50%); border-radius: 3px; } /* 标签页样式 */ .tabs { display: flex; width: 100%; margin-bottom: 25px; border: none; background-color: var(--primary-light); border-radius: 25px; padding: 5px; box-shadow: 0 3px 10px rgba(230, 134, 165, 0.1); } /* tab 项 */ .tab { flex: 1; padding: 10px 20px; text-decoration: none; color: var(--text-primary); margin-right: 2px; border-radius: 20px; transition: all 0.3s ease; font-size: 0.95rem; text-align: center; white-space: nowrap; } .tab:hover { background-color: rgba(230, 134, 165, 0.1); color: var(--primary-dark); text-decoration: none; } .tab.active { background-color: var(--primary-color); color: white; box-shadow: 0 3px 8px rgba(230, 134, 165, 0.3); } .count { background-color: rgba(255, 255, 255, 0.3); border-radius: 20px; padding: 2px 8px; font-size: 0.75em; display: inline-block; margin-left: 5px; font-weight: 600; } .tab.active .count { background-color: rgba(255, 255, 255, 0.4); } /* 借阅列表与表格 */ .borrow-list { margin-top: 20px; margin-bottom: 2rem; width: 100%; overflow-x: auto; } .borrow-table { width: 100%; border-collapse: separate; border-spacing: 0; margin-bottom: 25px; border-radius: 15px; overflow: hidden; box-shadow: 0 5px 20px rgba(230, 134, 165, 0.08); } /* 调整列宽 - 解决状态列和操作列问题 */ .borrow-table th:nth-child(1), .borrow-table td:nth-child(1) { width: 90px; } .borrow-table th:nth-child(2), .borrow-table td:nth-child(2) { width: 20%; } .borrow-table th:nth-child(3), .borrow-table td:nth-child(3), .borrow-table th:nth-child(4), .borrow-table td:nth-child(4) { width: 15%; } /* 状态列 */ .borrow-table th:nth-child(5), .borrow-table td:nth-child(5) { width: 15%; min-width: 120px; position: relative; overflow: visible; padding: 14px 25px; vertical-align: middle; } /* 状态表头文字微调 - 向右移动2px */ .borrow-table th:nth-child(5) { padding-left: 28px; /* 增加左内边距,使文字看起来稍微向右移动 */ } /* 操作列 */ .borrow-table th:nth-child(6), .borrow-table td:nth-child(6) { width: 18%; min-width: 140px; padding: 14px 18px; vertical-align: middle; text-align: left; padding: 14px 0 14px 15px; /* 减少右内边距,增加左内边距 */ } .borrow-table th, .borrow-table td { padding: 14px 18px; text-align: left; vertical-align: middle; } .borrow-table th { background-color: var(--primary-light); color: var(--primary-dark); font-weight: 600; font-size: 0.9rem; letter-spacing: 0.3px; border-bottom: 1px solid var(--border-color); } .borrow-table tr { border-bottom: 1px solid var(--border-color); transition: all 0.2s ease; } .borrow-table tbody tr:last-child { border-bottom: none; } .borrow-item { background-color: var(--white); } .borrow-item:hover { background-color: rgba(230, 134, 165, 0.03); } .borrow-item.overdue { background-color: rgba(255, 143, 158, 0.08); } /* 图书封面 */ .book-cover img { width: 65px; height: 90px; object-fit: cover; border-radius: 8px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease; border: 3px solid var(--white); } .book-cover img:hover { transform: scale(1.05); box-shadow: 0 5px 15px rgba(230, 134, 165, 0.3); } /* 书名与作者 */ .book-title { font-weight: 600; font-size: 1rem; } .book-title a { color: var(--primary-dark); text-decoration: none; transition: color 0.3s ease; } .book-title a:hover { color: var(--primary-color); } .book-author { color: var(--text-secondary); font-size: 0.85rem; margin-top: 5px; display: flex; align-items: center; } .book-author:before { content: "🖋"; margin-right: 5px; font-size: 0.9em; } /* 徽章 - 修复状态显示问题 */ .borrow-table .badge, .book-status .badge { padding: 4px 10px; border-radius: 20px; font-weight: 500; font-size: 0.75rem; display: inline-block; margin-bottom: 4px; letter-spacing: 0.3px; white-space: nowrap; text-align: center; min-width: 60px; } .borrow-table .badge { position: static; top: auto; right: auto; } .badge-primary { background-color: var(--primary-color); color: white; } .badge-success { background-color: var(--success-color); color: white; } .badge-danger { background-color: var(--danger-color); color: white; } .badge-info { background-color: var(--accent-color); color: white; } .return-date { color: var(--text-secondary); font-size: 0.85rem; margin-top: 5px; display: flex; align-items: center; } .return-date:before { content: "📅"; margin-right: 5px; } .text-danger { color: var(--danger-color) !important; font-weight: 600; } /* 操作按钮 - 简化样式 */ .actions { display: flex; flex-direction: row; gap: 10px; align-items: center; padding-left: 15px; /* 整体左移5px */ margin-top: 17px; margin-right: 30px; } .actions .btn { min-width: 60px; padding: 8px 15px; font-size: 0.85rem; font-weight: 500; border-radius: 20px; border: none; text-align: center; white-space: nowrap; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; } .btn-success { background-color: var(--success-color); color: white; } .btn-success:hover { background-color: #65b088; transform: translateY(-2px); box-shadow: 0 5px 12px rgba(122, 201, 161, 0.3); } .btn-primary { background-color: var(--primary-color); color: white; } .btn-primary:hover { background-color: var(--primary-dark); transform: translateY(-2px); box-shadow: 0 5px 12px rgba(230, 134, 165, 0.3); } .btn-secondary { background-color: #a0a0a0; color: white; } /* 无记录状态 */ .no-records { text-align: center; padding: 60px 30px; background-color: var(--primary-light); border-radius: 15px; margin: 30px 0; box-shadow: inset 0 0 15px rgba(230, 134, 165, 0.1); } .empty-icon { font-size: 4em; color: var(--primary-color); margin-bottom: 20px; opacity: 0.7; } .empty-text { color: var(--text-primary); margin-bottom: 25px; font-size: 1.1rem; max-width: 450px; margin: 0 auto; line-height: 1.6; } /* 分页 */ .pagination-container { display: flex; justify-content: center; margin-top: 25px; } .pagination { display: flex; list-style: none; padding: 0; gap: 5px; } .page-item { margin: 0 2px; } .page-link { width: 36px; height: 36px; display: flex; justify-content: center; align-items: center; border-radius: 50%; background-color: white; color: var(--text-primary); border: 1px solid var(--border-color); transition: all 0.3s ease; font-size: 0.9rem; } .page-item.active .page-link, .page-link:hover { background-color: var(--primary-color); color: white; border-color: var(--primary-color); box-shadow: 0 3px 8px rgba(230, 134, 165, 0.3); } /* 模态框 */ .modal-dialog { max-width: 95%; width: 500px; margin: 1.75rem auto; } .modal-content { border-radius: 15px; border: none; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); overflow: hidden; } .modal-header { background-color: var(--primary-light); color: var(--primary-dark); border-bottom: 1px solid var(--border-color); padding: 15px 20px; } .modal-body { padding: 25px 20px; font-size: 1.1rem; text-align: center; } .modal-footer { border-top: 1px solid var(--border-color); padding: 15px 20px; display: flex; justify-content: center; gap: 10px; } /* 响应式 */ @media (max-width: 992px) { .container { width: 98% !important; padding: 1rem; margin: 0.5rem auto; } } @media (max-width: 768px) { .tabs { flex-direction: column; background: none; padding: 0; } .tab { border-radius: 15px; margin-bottom: 8px; margin-right: 0; padding: 12px 15px; background-color: var(--primary-light); } .borrow-table { min-width: 700px; /* 确保在小屏幕上可以滚动 */ } .book-cover img { width: 45px; height: 65px; } } ================================================================================ File: ./app/static/css/announcement-list.css ================================================================================ /* Fresh & Vibrant Style for Announcement List */ :root { --mint-green: #A8E6CF; --pale-yellow: #FFD3B6; --coral-pink: #FFAAA5; --sky-blue: #BDE4F4; --clean-white: #FFFFFF; --bright-orange: #FF8C69; /* Emphasis for buttons/key info */ --lemon-yellow: #FFFACD; --text-dark: #424242; --text-medium: #616161; /* Slightly darker medium for better contrast */ --text-light: #888888; /* Adjusted light text */ --font-title: 'Poppins', sans-serif; --font-body: 'Nunito Sans', sans-serif; --card-shadow: 0 5px 18px rgba(0, 0, 0, 0.07); --card-hover-shadow: 0 8px 25px rgba(0, 0, 0, 0.12); --border-radius-main: 16px; /* Slightly larger radius for a softer look */ --border-radius-small: 10px; } /* Apply base font and background to body (likely in base.css) */ body { font-family: var(--font-body); background-color: #fcfdfe; /* Very light off-white, almost white */ color: var(--text-dark); line-height: 1.65; } .announcement-container { padding: 25px 30px; max-width: 960px; margin: 25px auto; background-color: var(--clean-white); border-radius: var(--border-radius-main); /* Optional: Subtle gradient background for the container itself */ /* background-image: linear-gradient(to bottom right, #f0f9ff, #ffffff); */ } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eef2f5; /* Softer border */ } .page-header h1 { font-family: var(--font-title); font-size: 2.2rem; font-weight: 700; color: var(--text-dark); margin: 0; display: flex; align-items: center; } .page-icon { /* Icon for page title */ color: var(--coral-pink); margin-right: 12px; font-size: 1.8rem; } /* Optional: Style for a "Create New" button if you add one */ .btn-fresh-create { background-color: var(--mint-green); color: #3a7c68; /* Darker mint for text */ border: none; padding: 10px 20px; border-radius: 25px; font-family: var(--font-body); font-weight: 600; text-decoration: none; transition: all 0.3s ease; font-size: 0.9rem; box-shadow: 0 2px 8px rgba(168, 230, 207, 0.4); } .btn-fresh-create:hover { background-color: #97e0c6; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(168, 230, 207, 0.5); } .btn-fresh-create i { margin-right: 8px; } .announcement-list { margin-top: 20px; display: grid; gap: 25px; /* Spacing between announcement items */ } .announcement-item { background-color: var(--clean-white); border-radius: var(--border-radius-main); box-shadow: var(--card-shadow); padding: 25px 30px; position: relative; /* For pin-badge */ transition: transform 0.25s ease-out, box-shadow 0.25s ease-out; overflow: hidden; /* If using pseudo-elements for borders */ } .announcement-item:hover { transform: translateY(-5px) scale(1.01); box-shadow: var(--card-hover-shadow); } .announcement-item.pinned { /* Use a top border or a more distinct background */ border-top: 4px solid var(--mint-green); background-color: #f6fffb; /* Light mint */ } .pin-badge { position: absolute; top: 0px; right: 0px; background: linear-gradient(135deg, var(--mint-green), #8fdcc3); color: var(--clean-white); padding: 6px 15px 6px 20px; border-radius: 0 0 0 var(--border-radius-main); /* Creative corner */ font-size: 0.8rem; font-weight: 600; font-family: var(--font-body); box-shadow: -2px 2px 8px rgba(168, 230, 207, 0.3); } .pin-badge i { margin-right: 6px; font-size: 0.75rem; } .announcement-header { display: flex; justify-content: space-between; align-items: flex-start; /* Align date to top if title wraps */ margin-bottom: 10px; } .announcement-header h3 { margin: 0; font-size: 1.4rem; /* Slightly larger title */ font-family: var(--font-title); font-weight: 600; line-height: 1.3; margin-right: 15px; /* Space between title and date */ } .announcement-header h3 a { color: var(--text-dark); text-decoration: none; transition: color 0.2s ease; } .announcement-header h3 a:hover { color: var(--coral-pink); } .date { color: var(--text-light); font-size: 0.85rem; font-weight: 400; white-space: nowrap; /* Prevent date from wrapping */ padding-top: 3px; /* Align better with h3 */ } .announcement-preview { margin: 15px 0; color: var(--text-medium); line-height: 1.7; font-size: 0.95rem; letter-spacing: 0.1px; } .announcement-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; padding-top: 15px; border-top: 1px solid #f0f4f7; /* Lighter separator */ } .publisher { color: var(--text-light); font-size: 0.85rem; display: flex; align-items: center; } .publisher i { margin-right: 6px; color: var(--sky-blue); } .read-more { color: var(--bright-orange); text-decoration: none; font-weight: 600; font-size: 0.9rem; font-family: var(--font-body); display: inline-flex; /* Allows icon alignment and hover effects */ align-items: center; padding: 6px 12px; border-radius: 20px; background-color: transparent; transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease; } .read-more:hover { background-color: var(--bright-orange); color: var(--clean-white); transform: translateX(3px); } .read-more i { margin-left: 6px; transition: transform 0.2s ease-in-out; } /* .read-more:hover i { transform: translateX(4px); } */ /* Handled by transform on .read-more now */ /* Pagination Styles (copied and adapted from previous for consistency) */ .pagination-container { margin-top: 40px; display: flex; justify-content: center; } .pagination { display: flex; list-style: none; padding-left: 0; } .pagination .page-item .page-link { color: var(--coral-pink); background-color: var(--clean-white); border: 1px solid var(--pale-yellow); margin: 0 5px; /* Slightly more spacing */ border-radius: 50%; width: 40px; /* Slightly larger */ height: 40px; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.95rem; font-family: var(--font-body); transition: all 0.25s ease-in-out; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .pagination .page-item .page-link:hover { background-color: var(--pale-yellow); color: var(--coral-pink); border-color: var(--coral-pink); text-decoration: none; transform: translateY(-2px); box-shadow: 0 3px 8px rgba(255, 211, 182, 0.5); } .pagination .page-item.active .page-link { background-color: var(--coral-pink); border-color: var(--coral-pink); color: var(--clean-white); box-shadow: 0 4px 10px rgba(255, 170, 165, 0.6); } .pagination .page-item.disabled .page-link { color: #cccccc; background-color: #f9f9f9; border-color: #eeeeee; pointer-events: none; box-shadow: none; } .no-records { text-align: center; padding: 60px 30px; background-color: #fffaf8; /* Very light coral/yellow tint */ border-radius: var(--border-radius-main); color: var(--text-medium); margin-top: 20px; box-shadow: var(--card-shadow); } .no-records-icon { width: 60px; height: 60px; margin-bottom: 20px; opacity: 0.9; } /* Fallback for FontAwesome if SVG doesn't load or is removed */ .no-records .fas.fa-info-circle { font-size: 3.5rem; margin-bottom: 20px; color: var(--coral-pink); opacity: 0.8; } .no-records p { font-size: 1.15rem; font-family: var(--font-body); font-weight: 600; color: var(--text-dark); line-height: 1.6; } ================================================================================ File: ./app/static/css/notifications.css ================================================================================ /* Fresh & Vibrant Style for Notifications */ :root { --mint-green: #A8E6CF; --pale-yellow: #FFD3B6; --coral-pink: #FFAAA5; --sky-blue: #BDE4F4; --clean-white: #FFFFFF; --bright-orange: #FF8C69; /* Emphasis for buttons/key info */ --lemon-yellow: #FFFACD; /* Can be used for subtle highlights */ --text-dark: #424242; /* Slightly softer than pure black */ --text-medium: #757575; --text-light: #9E9E9E; --font-title: 'Poppins', sans-serif; --font-body: 'Nunito Sans', sans-serif; --card-shadow: 0 4px 15px rgba(0, 0, 0, 0.06); --card-hover-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); --border-radius-main: 12px; --border-radius-small: 8px; } /* Apply base font and background to body (likely in base.css, but good for context) */ body { font-family: var(--font-body); background-color: var(--clean-white); /* Or a very light tint like #FDFCFA */ color: var(--text-dark); line-height: 1.6; font-weight: 400; } .notifications-container { padding: 25px 30px; max-width: 900px; margin: 20px auto; background-color: var(--clean-white); /* Optional: add a subtle pattern or a large soft circular gradient */ /* background-image: linear-gradient(135deg, var(--mint-green) -20%, var(--clean-white) 30%); */ border-radius: var(--border-radius-main); /* box-shadow: 0 10px 30px rgba(168, 230, 207, 0.2); */ /* Subtle shadow for container */ } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #f0f0f0; /* Softer border */ } .page-header h1 { font-family: var(--font-title); font-size: 2rem; /* Slightly larger */ font-weight: 600; color: var(--text-dark); margin: 0; } /* Fresh Action Button Style */ .btn-fresh-action { background-color: var(--bright-orange); color: var(--clean-white); border: none; padding: 10px 20px; border-radius: 25px; /* Pill shape */ font-family: var(--font-body); font-weight: 600; text-decoration: none; transition: all 0.3s ease; font-size: 0.9rem; box-shadow: 0 2px 8px rgba(255, 140, 105, 0.3); } .btn-fresh-action:hover { background-color: #ff7b5a; /* Slightly darker orange */ color: var(--clean-white); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 140, 105, 0.4); } .btn-fresh-action i { margin-right: 8px; } .filter-tabs { display: flex; margin-bottom: 25px; gap: 10px; /* border-bottom: 2px solid var(--pale-yellow); */ /* Optional subtle line */ } .filter-tab { padding: 10px 20px; color: var(--text-medium); text-decoration: none; border-radius: var(--border-radius-small); /* Rounded tabs */ font-weight: 600; font-size: 0.95rem; transition: all 0.3s ease; border-bottom: 3px solid transparent; /* Underline effect for active */ } .filter-tab:hover { color: var(--coral-pink); background-color: rgba(255, 170, 165, 0.1); /* Light coral tint on hover */ } .filter-tab.active { color: var(--coral-pink); border-bottom-color: var(--coral-pink); /* background-color: var(--coral-pink); */ /* color: var(--clean-white); */ } .notifications-list { margin-top: 20px; display: grid; gap: 20px; } .notification-card { background-color: var(--clean-white); border-radius: var(--border-radius-main); box-shadow: var(--card-shadow); padding: 20px 25px; transition: transform 0.25s ease, box-shadow 0.25s ease; display: flex; /* For icon alignment */ align-items: flex-start; /* Align icon to top of content */ gap: 15px; border-left: 5px solid transparent; /* Placeholder for unread state */ } .notification-icon-area { font-size: 1.5rem; color: var(--sky-blue); padding-top: 5px; /* Align with title */ } .notification-card.unread .notification-icon-area { color: var(--mint-green); } .notification-card:hover { transform: translateY(-4px); box-shadow: var(--card-hover-shadow); } .notification-card.unread { border-left-color: var(--mint-green); background-color: #f6fffb; /* Very light mint */ } .notification-content { flex-grow: 1; } .notification-title { display: flex; align-items: center; justify-content: space-between; margin-top: 0; margin-bottom: 8px; font-size: 1.15rem; /* Adjusted size */ font-family: var(--font-title); font-weight: 600; } .notification-title a { color: var(--text-dark); text-decoration: none; transition: color 0.2s ease; } .notification-title a:hover { color: var(--coral-pink); } .unread-badge { background-color: var(--bright-orange); color: white; font-size: 0.7rem; padding: 4px 10px; border-radius: 15px; /* Pill shape */ margin-left: 10px; font-weight: 600; letter-spacing: 0.5px; } .notification-text { color: var(--text-medium); margin-bottom: 15px; line-height: 1.6; font-size: 0.9rem; letter-spacing: 0.2px; } .notification-meta { display: flex; justify-content: space-between; align-items: center; color: var(--text-light); font-size: 0.8rem; } .notification-type { background-color: var(--sky-blue); /* Sky Blue for type */ color: #3E84A8; /* Darker text for contrast on sky blue */ padding: 3px 10px; border-radius: var(--border-radius-small); font-weight: 600; } .notification-time { font-style: italic; } /* Pagination */ .pagination-container { margin-top: 30px; display: flex; justify-content: center; } .pagination { display: flex; list-style: none; padding-left: 0; } .pagination .page-item .page-link { color: var(--coral-pink); background-color: var(--clean-white); border: 1px solid var(--pale-yellow); margin: 0 4px; border-radius: 50%; /* Circular pagination items */ width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.9rem; transition: all 0.2s ease-in-out; } .pagination .page-item .page-link:hover { background-color: var(--pale-yellow); color: var(--coral-pink); border-color: var(--coral-pink); text-decoration: none; } .pagination .page-item.active .page-link { background-color: var(--coral-pink); border-color: var(--coral-pink); color: var(--clean-white); box-shadow: 0 2px 5px rgba(255, 170, 165, 0.5); } .pagination .page-item.disabled .page-link { color: #ccc; background-color: #f8f8f8; border-color: #eee; pointer-events: none; } .no-records { text-align: center; padding: 50px 20px; background-color: #fafffd; /* Very light mint/yellow mix */ border-radius: var(--border-radius-main); color: var(--text-medium); margin-top: 20px; } .no-records-icon { /* Style for the inline SVG */ width: 60px; height: 60px; margin-bottom: 20px; opacity: 0.8; } /* If using Font Awesome for no-records icon: */ .no-records .fas.fa-bell-slash { font-size: 3.5rem; margin-bottom: 20px; color: var(--mint-green); opacity: 0.7; } .no-records p { font-size: 1.1rem; font-family: var(--font-body); font-weight: 600; color: var(--text-dark); } /* Notification Dropdown Styles (assuming this is for a navbar dropdown or similar) */ /* These are kept minimal as the main focus was the page content */ .notification-dropdown { width: 350px; /* Wider for more content */ padding: 0; max-height: 450px; overflow-y: auto; border-radius: var(--border-radius-small); box-shadow: 0 5px 25px rgba(0,0,0,0.1); background-color: var(--clean-white); } .notification-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background-color: var(--pale-yellow); /* Light yellow header */ border-bottom: 1px solid #f0e0d0; } .notification-header h5 { margin:0; font-family: var(--font-title); font-weight: 600; color: var(--text-dark); } .mark-all-read { /* Link in dropdown header */ font-size: 0.8rem; color: var(--coral-pink); font-weight: 600; text-decoration: none; } .mark-all-read:hover { text-decoration: underline; } .notification-items { max-height: 300px; overflow-y: auto; } .notification-item { padding: 12px 15px; border-bottom: 1px solid #f5f5f5; transition: background-color 0.2s ease; } .notification-item:last-child { border-bottom: none; } .notification-item:hover { background-color: var(--mint-green-light, #e6f7f0); /* Very light mint on hover */ } .notification-item.unread { background-color: #fff8f0; /* Very light orange/yellow for unread in dropdown */ border-left: 3px solid var(--bright-orange); padding-left: 12px; } .notification-item .notification-content h6 { /* Assuming title in dropdown is h6 */ margin-bottom: 5px; font-size: 0.9rem; font-family: var(--font-title); font-weight: 600; color: var(--text-dark); } .notification-item .notification-text { /* Text snippet in dropdown */ font-size: 0.8rem; color: var(--text-medium); margin-bottom: 5px; line-height: 1.4; } .notification-item .notification-time { font-size: 0.75rem; color: var(--text-light); } .view-all { /* Footer link in dropdown */ text-align: center; font-weight: 600; padding: 12px 15px; display: block; text-decoration: none; color: var(--bright-orange); background-color: #fffaf5; transition: background-color 0.2s ease; } .view-all:hover { background-color: var(--pale-yellow); } .no-notifications { /* Message in empty dropdown */ padding: 25px; text-align: center; color: var(--text-medium); font-size: 0.9rem; } ================================================================================ File: ./app/static/css/inventory-logs.css ================================================================================ /* 冰雪奇缘主题库存日志页面样式 */ @import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400;700&family=Nunito:wght@300;400;600;700&display=swap'); :root { --primary-blue: #6fa8dc; --light-blue: #cfe2f3; --dark-blue: #1a5190; --accent-pink: #f4b8c4; --accent-purple: #b19cd9; --subtle-gold: #ffd966; --ice-white: #f3f9ff; --snow-white: #ffffff; --text-dark: #2c3e50; --text-light: #ecf0f1; --shadow-color: rgba(0, 53, 102, 0.15); --frost-blue: #a2d5f2; --elsa-blue: #85c1e9; --anna-purple: #c39bd3; --olaf-white: #f9fcff; } /* 全局样式重置 */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Nunito', sans-serif; background: #f5f9ff url('/static/images/disney-bg.jpg') no-repeat center center fixed; background-size: cover; color: var(--text-dark); line-height: 1.6; position: relative; overflow-x: hidden; min-height: 100vh; } /* 容器样式 */ .disney-container { max-width: 95%; margin: 2rem auto; padding: 0 15px; position: relative; z-index: 1; } /* 魔法粒子效果层 */ #magic-particles { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; } /* 主卡片样式 */ .disney-card { background: linear-gradient(135deg, var(--ice-white) 0%, var(--snow-white) 100%); border-radius: 20px; box-shadow: 0 10px 30px var(--shadow-color), 0 0 50px rgba(137, 196, 244, 0.3), inset 0 0 15px rgba(255, 255, 255, 0.8); overflow: hidden; position: relative; margin-bottom: 2rem; border: 1px solid rgba(200, 223, 255, 0.8); animation: card-glow 3s infinite alternate; } /* 卡片发光动画 */ @keyframes card-glow { from { box-shadow: 0 10px 30px var(--shadow-color), 0 0 50px rgba(137, 196, 244, 0.3), inset 0 0 15px rgba(255, 255, 255, 0.8); } to { box-shadow: 0 10px 30px var(--shadow-color), 0 0 70px rgba(137, 196, 244, 0.5), inset 0 0 20px rgba(255, 255, 255, 0.9); } } /* 装饰元素 */ .disney-decoration { position: absolute; background-size: contain; background-repeat: no-repeat; opacity: 0.7; z-index: 1; pointer-events: none; } .book-icon { top: 20px; right: 30px; width: 60px; height: 60px; background-image: url('https://api.iconify.design/ph:books-duotone.svg?color=%236fa8dc'); transform: rotate(10deg); animation: float 6s ease-in-out infinite; } .crown-icon { bottom: 40px; left: 20px; width: 50px; height: 50px; background-image: url('https://api.iconify.design/fa6-solid:crown.svg?color=%23ffd966'); animation: float 5s ease-in-out infinite 1s; } .wand-icon { top: 60px; left: 40px; width: 40px; height: 40px; background-image: url('https://api.iconify.design/fa-solid:magic.svg?color=%23b19cd9'); animation: float 7s ease-in-out infinite 0.5s; } .snowflake-icon { bottom: 70px; right: 50px; width: 45px; height: 45px; background-image: url('https://api.iconify.design/fa-regular:snowflake.svg?color=%23a2d5f2'); animation: float 4s ease-in-out infinite 1.5s, spin 15s linear infinite; } @keyframes float { 0% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-15px) rotate(5deg); } 100% { transform: translateY(0) rotate(0deg); } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* 卡片头部 */ .card-header-disney { background: linear-gradient(45deg, var(--elsa-blue), var(--frost-blue)); color: var(--text-light); padding: 1.5rem; text-align: center; position: relative; border-bottom: 3px solid rgba(255, 255, 255, 0.5); } .card-header-disney h4 { font-size: 1.8rem; font-weight: 700; margin: 0; text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2); position: relative; z-index: 2; font-family: 'Dancing Script', cursive; letter-spacing: 1px; } .card-header-disney i { margin-right: 10px; color: var(--subtle-gold); animation: pulse 2s infinite; } .princess-crown { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); width: 60px; height: 30px; background-image: url('https://api.iconify.design/fa6-solid:crown.svg?color=%23ffd966'); background-size: contain; background-repeat: no-repeat; background-position: center; filter: drop-shadow(0 0 5px rgba(255, 217, 102, 0.7)); } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } /* 卡片内容 */ .card-body-disney { padding: 2rem; position: relative; z-index: 2; } /* 图书信息部分 */ .book-details-container { display: flex; background: linear-gradient(to right, rgba(162, 213, 242, 0.1), rgba(177, 156, 217, 0.1)); border-radius: 15px; padding: 1.5rem; margin-bottom: 2rem; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); border: 1px solid rgba(162, 213, 242, 0.3); position: relative; overflow: hidden; } .book-cover-wrapper { flex: 0 0 150px; margin-right: 2rem; position: relative; } .disney-book-cover { width: 100%; height: auto; border-radius: 8px; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease, box-shadow 0.3s ease; border: 4px solid white; object-fit: cover; z-index: 2; position: relative; } .disney-book-cover:hover { transform: translateY(-5px) scale(1.03); box-shadow: 0 15px 25px rgba(0, 0, 0, 0.15); } .book-cover-glow { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(162, 213, 242, 0.6) 0%, rgba(255, 255, 255, 0) 70%); z-index: 1; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } .book-cover-wrapper:hover .book-cover-glow { opacity: 1; animation: glow-pulse 2s infinite; } @keyframes glow-pulse { 0% { opacity: 0.3; } 50% { opacity: 0.7; } 100% { opacity: 0.3; } } .book-info { flex: 1; } .book-title { font-family: 'Dancing Script', cursive; font-size: 2rem; margin-bottom: 1rem; color: var(--dark-blue); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); position: relative; display: inline-block; } .book-title::after { content: ''; position: absolute; bottom: -5px; left: 0; width: 100%; height: 2px; background: linear-gradient(to right, var(--elsa-blue), var(--anna-purple)); border-radius: 2px; } .info-row { display: flex; align-items: center; margin-bottom: 0.8rem; font-size: 1rem; } .disney-icon { width: 24px; height: 24px; margin-right: 10px; background-size: contain; background-repeat: no-repeat; background-position: center; } .author-icon { background-image: url('https://api.iconify.design/fa-solid:user-edit.svg?color=%236fa8dc'); } .publisher-icon { background-image: url('https://api.iconify.design/fa-solid:building.svg?color=%236fa8dc'); } .isbn-icon { background-image: url('https://api.iconify.design/fa-solid:barcode.svg?color=%236fa8dc'); } .stock-icon { background-image: url('https://api.iconify.design/fa-solid:warehouse.svg?color=%236fa8dc'); } .stock-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-weight: bold; font-size: 0.9rem; color: white; margin-left: 5px; } .high-stock { background-color: #2ecc71; animation: badge-pulse 2s infinite; } .low-stock { background-color: #f39c12; } .out-stock { background-color: #e74c3c; } @keyframes badge-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } /* 日志部分 */ .logs-section { background-color: var(--ice-white); border-radius: 15px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); padding: 1.5rem; position: relative; overflow: hidden; border: 1px solid rgba(162, 213, 242, 0.3); } .logs-section::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-image: url('https://api.iconify.design/ph:snowflake-thin.svg?color=%23a2d5f2'); background-size: 20px; opacity: 0.05; pointer-events: none; animation: snow-bg 60s linear infinite; } @keyframes snow-bg { from { background-position: 0 0; } to { background-position: 100% 100%; } } .logs-title { text-align: center; font-size: 1.5rem; margin-bottom: 1.5rem; color: var(--dark-blue); position: relative; display: flex; align-items: center; justify-content: center; font-family: 'Dancing Script', cursive; } .title-decoration { width: 100px; height: 2px; background: linear-gradient(to right, transparent, var(--elsa-blue), transparent); margin: 0 15px; } .title-decoration.right { transform: scaleX(-1); } .table-container { overflow-x: auto; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); margin-bottom: 1.5rem; } .disney-table { width: 100%; border-collapse: separate; border-spacing: 0; background-color: white; border-radius: 10px; overflow: hidden; } .disney-table thead { background: linear-gradient(45deg, var(--elsa-blue), var(--frost-blue)); color: white; } .disney-table th { padding: 1rem 0.8rem; text-align: left; font-weight: 600; letter-spacing: 0.5px; position: relative; border-bottom: 1px solid rgba(255, 255, 255, 0.3); } .disney-table th:first-child { border-top-left-radius: 10px; } .disney-table th:last-child { border-top-right-radius: 10px; } .disney-table td { padding: 0.8rem; border-bottom: 1px solid rgba(162, 213, 242, 0.2); vertical-align: middle; } .log-row { transition: all 0.3s ease; } .log-row:hover { background-color: rgba(162, 213, 242, 0.1); transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); } .operation-badge { padding: 4px 10px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; text-align: center; display: inline-block; min-width: 80px; } .in-badge { background-color: rgba(46, 204, 113, 0.15); color: #27ae60; border: 1px solid rgba(46, 204, 113, 0.3); } .out-badge { background-color: rgba(231, 76, 60, 0.15); color: #c0392b; border: 1px solid rgba(231, 76, 60, 0.3); } .remark-cell { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .empty-logs { text-align: center; padding: 3rem 0 !important; } .empty-state { display: flex; flex-direction: column; align-items: center; color: #95a5a6; } .empty-icon { width: 80px; height: 80px; background-image: url('https://api.iconify.design/ph:book-open-duotone.svg?color=%2395a5a6'); background-size: contain; background-repeat: no-repeat; background-position: center; margin-bottom: 1rem; opacity: 0.7; } .empty-state p { font-size: 1.1rem; } /* 分页样式 */ .disney-pagination { margin-top: 1.5rem; display: flex; justify-content: center; } .pagination-list { display: flex; list-style: none; gap: 5px; align-items: center; } .page-item { margin: 0 2px; } .page-link { display: flex; align-items: center; justify-content: center; padding: 8px 12px; border-radius: 20px; color: var(--dark-blue); background-color: white; text-decoration: none; font-weight: 600; transition: all 0.3s ease; min-width: 40px; border: 1px solid rgba(111, 168, 220, 0.3); } .page-link:hover:not(.disabled .page-link) { background-color: var(--light-blue); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .page-item.active .page-link { background: linear-gradient(45deg, var(--elsa-blue), var(--frost-blue)); color: white; box-shadow: 0 4px 8px rgba(111, 168, 220, 0.3); } .page-item.dots .page-link { border: none; background: none; pointer-events: none; } .page-item.disabled .page-link { color: #b2bec3; pointer-events: none; background-color: rgba(236, 240, 241, 0.5); border: 1px solid rgba(189, 195, 199, 0.3); } /* 卡片底部 */ .card-footer-disney { padding: 1.5rem; background: linear-gradient(45deg, rgba(162, 213, 242, 0.2), rgba(177, 156, 217, 0.2)); border-top: 1px solid rgba(162, 213, 242, 0.3); } .button-container { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 1rem; } .disney-button { padding: 10px 20px; border-radius: 30px; font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; transition: all 0.3s ease; position: relative; overflow: hidden; z-index: 1; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } .disney-button::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); transition: all 0.6s ease; z-index: -1; } .disney-button:hover::before { left: 100%; } .disney-button:hover { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15); } .button-icon { margin-right: 10px; } .return-btn { background: linear-gradient(45deg, #3498db, #2980b9); color: white; } .adjust-btn { background: linear-gradient(45deg, #9b59b6, #8e44ad); color: white; } /* 响应式设计 */ @media (max-width: 992px) { .book-details-container { flex-direction: column; } .book-cover-wrapper { margin-right: 0; margin-bottom: 1.5rem; text-align: center; width: 180px; margin: 0 auto 1.5rem; } .logs-title { font-size: 1.3rem; } .title-decoration { width: 50px; } } @media (max-width: 768px) { .disney-container { margin: 1rem auto; } .card-header-disney h4 { font-size: 1.5rem; } .card-body-disney { padding: 1.5rem 1rem; } .book-title { font-size: 1.7rem; } .disney-button { padding: 8px 15px; font-size: 0.9rem; } } @media (max-width: 576px) { .button-container { justify-content: center; gap: 1rem; } .book-title { font-size: 1.5rem; } .logs-title { font-size: 1.2rem; } .title-decoration { width: 30px; } } /* 飘落的雪花 */ .snowflake { position: fixed; top: -50px; animation: fall linear infinite; z-index: 0; pointer-events: none; color: rgba(255, 255, 255, 0.8); text-shadow: 0 0 5px rgba(162, 213, 242, 0.5); } @keyframes fall { to { transform: translateY(100vh) rotate(360deg); } } ================================================================================ 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; } } /* 权限选择部分样式 */ .permissions-container { max-height: 350px; overflow-y: auto; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; background-color: #f8f9fa; } .permission-group { margin-bottom: 20px; } .permission-group:last-child { margin-bottom: 0; } .permission-group-title { font-weight: 600; color: #495057; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #dee2e6; } .permission-items { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; } .permission-item { display: flex; align-items: center; } .permission-checkbox { margin-right: 8px; } .permission-name { font-weight: 500; margin-bottom: 5px; display: block; } .permission-description { font-size: 0.85rem; color: #6c757d; display: block; } .loading-permissions { grid-column: 1 / -1; text-align: center; padding: 20px; color: #6c757d; } /* 将模态框调整为大一点 */ .modal-lg { max-width: 800px; } /* 权限项样式 */ .permission-item { position: relative; padding: 8px 12px; border-radius: 4px; transition: background-color 0.2s; } .permission-item:hover { background-color: #e9ecef; } .permission-item label { display: flex; flex-direction: column; cursor: pointer; margin-bottom: 0; padding-left: 25px; } .permission-item input[type="checkbox"] { position: absolute; left: 12px; top: 12px; } ================================================================================ File: ./app/static/css/overdue_analysis.css ================================================================================ /* app/static/css/overdue_analysis.css */ /* 保留您现有的 CSS 样式 */ .stats-cards .stats-card { border-left: 4px solid #007bff; } #current-overdue { border-left-color: #dc3545; } #current-overdue .card-value { color: #dc3545; } #returned-overdue { border-left-color: #ffc107; } #returned-overdue .card-value { color: #ffc107; } #overdue-rate { border-left-color: #28a745; } #overdue-rate .card-value { color: #28a745; } .chart-legend { display: flex; flex-wrap: wrap; margin-top: 15px; gap: 15px; } .legend-item { display: flex; align-items: center; font-size: 14px; } .legend-color { width: 15px; height: 15px; border-radius: 4px; margin-right: 5px; } /* 添加下面的 CSS 修复图表容器问题 */ .chart-container { position: relative; height: 400px; /* 固定高度 */ overflow: hidden; /* 防止内容溢出 */ margin-bottom: 30px; } .chart-container.half { min-height: 350px; max-height: 380px; /* 最大高度限制 */ } .chart-container canvas { max-height: 100%; width: 100% !important; height: 320px !important; /* 确保固定高度 */ } /* 修复图表行的问题 */ .chart-row { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 30px; align-items: stretch; /* 确保两个容器高度一致 */ } .chart-row .half { flex: 1 1 calc(50% - 10px); min-width: 300px; display: flex; flex-direction: column; } /* 添加一个明确的底部间距,防止页面无限延伸 */ .statistics-container { padding-bottom: 50px; } /* 响应式调整 */ @media (max-width: 768px) { .chart-row { flex-direction: column; } .chart-container.half { width: 100%; margin-bottom: 20px; } } ================================================================================ File: ./app/static/css/announcement-detail.css ================================================================================ .announcement-detail-container { padding: 20px; max-width: 900px; margin: 0 auto; } .page-header { margin-bottom: 25px; position: relative; } .back-link { display: inline-block; margin-bottom: 15px; color: #6c757d; text-decoration: none; transition: color 0.2s; } .back-link:hover { color: #007bff; } .page-header h1 { margin-top: 0; margin-bottom: 20px; font-size: 2rem; line-height: 1.3; } .announcement-meta { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .meta-item { display: flex; align-items: center; font-size: 0.95rem; color: #6c757d; } .meta-item i { margin-right: 8px; } .meta-item.pinned { color: #dc3545; font-weight: 500; } .announcement-content { background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); padding: 25px; line-height: 1.7; color: #333; } /* 内容中的富文本样式 */ .announcement-content h1, .announcement-content h2, .announcement-content h3 { margin-top: 1.5em; margin-bottom: 0.8em; } .announcement-content p { margin-bottom: 1em; } .announcement-content img { max-width: 100%; height: auto; border-radius: 4px; margin: 15px 0; } .announcement-content ul, .announcement-content ol { margin-bottom: 1em; padding-left: 2em; } .announcement-content a { color: #007bff; } .announcement-content blockquote { border-left: 4px solid #e3e3e3; padding-left: 15px; color: #6c757d; margin-left: 0; margin-right: 0; } ================================================================================ File: ./app/static/js/book-import.js ================================================================================ // 图书批量导入页面的JavaScript功能 document.addEventListener('DOMContentLoaded', function() { // 显示选择的文件名 const fileInput = document.getElementById('file'); if (fileInput) { fileInput.addEventListener('change', function() { const fileName = this.value.split('\\').pop(); const label = document.querySelector('.custom-file-label'); if (label) { label.textContent = fileName || '点击这里选择文件...'; // 添加有文件的类 if (fileName) { this.parentElement.classList.add('has-file'); // 显示文件类型检查和预览信息 checkFileAndPreview(this.files[0]); } else { this.parentElement.classList.remove('has-file'); } } }); } // 监听表单提交 const form = document.querySelector('form'); if (form) { form.addEventListener('submit', function(e) { const fileInput = document.getElementById('file'); if (!fileInput || !fileInput.files || !fileInput.files.length) { e.preventDefault(); showMessage('请先选择要导入的Excel文件', 'warning'); return; } const importBtn = document.querySelector('.import-btn'); if (importBtn) { importBtn.innerHTML = ' 正在导入...'; importBtn.disabled = true; } // 添加花朵飘落动画效果 addFallingElements(10); // 设置超时处理,如果30秒后还没响应,提示用户 window.importTimeout = setTimeout(function() { showMessage('导入处理时间较长,请耐心等待...', 'info'); }, 30000); }); } // 美化表单提交按钮的点击效果 const importBtn = document.querySelector('.import-btn'); if (importBtn) { importBtn.addEventListener('click', function(e) { // 按钮的点击效果已由表单提交事件处理 // 避免重复处理 if (!form || form.reportValidity() === false) { e.preventDefault(); } }); } // 为浮动元素添加动画 initFloatingElements(); // 检查页面中的flash消息 checkFlashMessages(); }); // 检查页面中的flash消息 function checkFlashMessages() { // Flask的flash消息通常会渲染为带有特定类的元素 const flashMessages = document.querySelectorAll('.alert'); if (flashMessages && flashMessages.length > 0) { // 如果存在flash消息,说明页面是提交后重新加载的 // 恢复按钮状态 const importBtn = document.querySelector('.import-btn'); if (importBtn) { importBtn.innerHTML = ' 开始导入'; importBtn.disabled = false; } // 清除可能的超时 if (window.importTimeout) { clearTimeout(window.importTimeout); } } } // 检查文件类型并尝试预览 function checkFileAndPreview(file) { if (!file) return; // 检查文件类型 const validTypes = ['.xlsx', '.xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel']; let isValid = false; validTypes.forEach(type => { if (file.name.toLowerCase().endsWith(type) || file.type === type) { isValid = true; } }); if (!isValid) { showMessage('请选择有效的Excel文件 (.xlsx 或 .xls)', 'warning'); return; } // 显示文件准备就绪的消息 showMessage(`文件 "${file.name}" 已准备就绪,点击"开始导入"按钮继续。`, 'success'); } // 显示提示消息 function showMessage(message, type = 'info') { // 检查是否已有消息容器 let messageContainer = document.querySelector('.import-message'); if (!messageContainer) { // 创建新的消息容器 messageContainer = document.createElement('div'); messageContainer.className = 'import-message animate__animated animate__fadeIn'; // 插入到按钮之后 const importBtn = document.querySelector('.import-btn'); if (importBtn && importBtn.parentNode) { importBtn.parentNode.insertBefore(messageContainer, importBtn.nextSibling); } } // 设置消息内容和样式 messageContainer.innerHTML = `
${message}
`; // 如果是临时消息,设置自动消失 if (type !== 'danger') { setTimeout(() => { messageContainer.classList.add('animate__fadeOut'); setTimeout(() => { if (messageContainer.parentNode) { messageContainer.parentNode.removeChild(messageContainer); } }, 600); }, 5000); } } // 根据消息类型获取图标 function getIconForMessageType(type) { switch (type) { case 'success': return 'fa-check-circle'; case 'warning': return 'fa-exclamation-triangle'; case 'danger': return 'fa-times-circle'; default: return 'fa-info-circle'; } } // 初始化浮动元素 function initFloatingElements() { const floatingElements = document.querySelectorAll('.snowflake, .flower'); floatingElements.forEach(element => { const randomDuration = 15 + Math.random() * 20; const randomDelay = Math.random() * 10; element.style.animationDuration = `${randomDuration}s`; element.style.animationDelay = `${randomDelay}s`; }); } // 添加花朵飘落效果 function addFallingElements(count) { const container = document.querySelector('.import-container'); if (!container) return; for (let i = 0; i < count; i++) { const element = document.createElement('div'); element.className = 'falling-element animate__animated animate__fadeInDown'; // 随机选择花朵或雪花 const isFlower = Math.random() > 0.5; element.classList.add(isFlower ? 'falling-flower' : 'falling-snowflake'); // 随机位置 const left = Math.random() * 100; element.style.left = `${left}%`; // 随机延迟 const delay = Math.random() * 2; element.style.animationDelay = `${delay}s`; // 随机大小 const size = 10 + Math.random() * 20; element.style.width = `${size}px`; element.style.height = `${size}px`; container.appendChild(element); // 动画结束后移除元素 setTimeout(() => { if (element.parentNode) { element.parentNode.removeChild(element); } }, 5000); } } ================================================================================ File: ./app/static/js/log-list.js ================================================================================ document.addEventListener('DOMContentLoaded', function() { // 日期范围选择器逻辑 const dateRangeSelect = document.getElementById('date_range'); const dateRangeInputs = document.querySelector('.date-range-inputs'); if (dateRangeSelect && dateRangeInputs) { dateRangeSelect.addEventListener('change', function() { if (this.value === 'custom') { dateRangeInputs.style.display = 'flex'; } else { dateRangeInputs.style.display = 'none'; } }); } // 导出日志功能 const btnExport = document.getElementById('btnExport'); const exportModal = new bootstrap.Modal(document.getElementById('exportLogModal')); const confirmExport = document.getElementById('confirmExport'); if (btnExport) { btnExport.addEventListener('click', function() { exportModal.show(); }); } if (confirmExport) { confirmExport.addEventListener('click', function() { // 获取导出格式 const exportFormat = document.getElementById('exportFormat').value; // 获取当前筛选条件 const userId = document.getElementById('user_id').value; const action = document.getElementById('action').value; const targetType = document.getElementById('target_type').value; let startDate = ''; let endDate = ''; const dateRange = document.getElementById('date_range').value; if (dateRange === 'custom') { startDate = document.getElementById('start_date').value; endDate = document.getElementById('end_date').value; } else { // 根据选择的日期范围计算日期 const today = new Date(); endDate = formatDate(today); if (dateRange === '1') { const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); startDate = formatDate(yesterday); } else if (dateRange === '7') { const lastWeek = new Date(today); lastWeek.setDate(lastWeek.getDate() - 7); startDate = formatDate(lastWeek); } else if (dateRange === '30') { const lastMonth = new Date(today); lastMonth.setDate(lastMonth.getDate() - 30); startDate = formatDate(lastMonth); } } // 显示加载提示 showAlert('info', '正在生成导出文件,请稍候...'); // 发送导出请求 fetch('/log/api/export', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: userId || null, action: action || null, target_type: targetType || null, start_date: startDate || null, end_date: endDate || null, format: exportFormat }) }) .then(response => response.json()) .then(data => { exportModal.hide(); if (data.success) { showAlert('success', data.message); // 处理文件下载 if (data.filedata && data.filename) { // 解码Base64数据 const binaryData = atob(data.filedata); // 转换为Blob const blob = new Blob([new Uint8Array([...binaryData].map(char => char.charCodeAt(0)))], { type: data.filetype }); // 创建下载链接 const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = data.filename; // 触发下载 document.body.appendChild(a); a.click(); // 清理 window.URL.revokeObjectURL(url); document.body.removeChild(a); } } else { showAlert('danger', data.message || '导出失败'); } }) .catch(error => { exportModal.hide(); showAlert('danger', '导出失败: ' + error.message); }); }); } // 清除日志功能 const btnClear = document.getElementById('btnClear'); const clearModal = new bootstrap.Modal(document.getElementById('clearLogModal')); const confirmClear = document.getElementById('confirmClear'); if (btnClear) { btnClear.addEventListener('click', function() { clearModal.show(); }); } if (confirmClear) { confirmClear.addEventListener('click', function() { const days = parseInt(document.getElementById('clearDays').value); fetch('/log/api/clear', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ days: days }) }) .then(response => response.json()) .then(data => { clearModal.hide(); if (data.success) { showAlert('success', data.message); // 2秒后刷新页面 setTimeout(() => { window.location.reload(); }, 2000); } else { showAlert('danger', data.message); } }) .catch(error => { clearModal.hide(); showAlert('danger', '操作失败: ' + error.message); }); }); } // 辅助函数 - 格式化日期为 YYYY-MM-DD function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // 辅助函数 - 显示提示框 function showAlert(type, message) { // 移除之前的所有alert const existingAlerts = document.querySelectorAll('.alert-floating'); existingAlerts.forEach(alert => alert.remove()); const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show alert-floating`; alertDiv.innerHTML = ` ${message} `; document.body.appendChild(alertDiv); // 添加CSS,如果还没有添加 if (!document.getElementById('alert-floating-style')) { const style = document.createElement('style'); style.id = 'alert-floating-style'; style.textContent = ` .alert-floating { position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-left: 4px solid; animation: slideIn 0.3s ease-out forwards; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .alert-floating i { margin-right: 8px; } .alert-floating .close { padding: 0.75rem; } `; document.head.appendChild(style); } // 5秒后自动关闭 setTimeout(() => { if (alertDiv.parentNode) { alertDiv.classList.add('fade'); setTimeout(() => alertDiv.remove(), 300); } }, 5000); // 点击关闭按钮关闭 const closeButton = alertDiv.querySelector('.close'); if (closeButton) { closeButton.addEventListener('click', function() { alertDiv.classList.add('fade'); setTimeout(() => alertDiv.remove(), 300); }); } } }); ================================================================================ File: ./app/static/js/my_borrows.js ================================================================================ // my_borrows.js document.addEventListener('DOMContentLoaded', function() { // 归还图书功能 const returnButtons = document.querySelectorAll('.return-btn'); const returnModal = document.getElementById('returnModal'); const returnBookTitle = document.getElementById('returnBookTitle'); const confirmReturnButton = document.getElementById('confirmReturn'); let currentBorrowId = null; returnButtons.forEach(button => { button.addEventListener('click', function() { const borrowId = this.getAttribute('data-id'); const bookTitle = this.getAttribute('data-title'); currentBorrowId = borrowId; returnBookTitle.textContent = bookTitle; // 使用 Bootstrap 的 jQuery 方法显示模态框 $('#returnModal').modal('show'); }); }); confirmReturnButton.addEventListener('click', function() { if (!currentBorrowId) return; // 发送归还请求 fetch(`/borrow/return/${currentBorrowId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({}) }) .then(response => response.json()) .then(data => { // 隐藏模态框 $('#returnModal').modal('hide'); if (data.success) { // 显示成功消息 showAlert('success', data.message); // 重新加载页面以更新借阅状态 setTimeout(() => window.location.reload(), 1500); } else { // 显示错误消息 showAlert('danger', data.message); } }) .catch(error => { $('#returnModal').modal('hide'); showAlert('danger', '操作失败,请稍后重试'); console.error('Error:', error); }); }); // 续借图书功能 const renewButtons = document.querySelectorAll('.renew-btn'); const renewModal = document.getElementById('renewModal'); const renewBookTitle = document.getElementById('renewBookTitle'); const confirmRenewButton = document.getElementById('confirmRenew'); renewButtons.forEach(button => { button.addEventListener('click', function() { const borrowId = this.getAttribute('data-id'); const bookTitle = this.getAttribute('data-title'); currentBorrowId = borrowId; renewBookTitle.textContent = bookTitle; // 使用 Bootstrap 的 jQuery 方法显示模态框 $('#renewModal').modal('show'); }); }); confirmRenewButton.addEventListener('click', function() { if (!currentBorrowId) return; // 发送续借请求 fetch(`/borrow/renew/${currentBorrowId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({}) }) .then(response => response.json()) .then(data => { // 隐藏模态框 $('#renewModal').modal('hide'); if (data.success) { // 显示成功消息 showAlert('success', data.message); // 重新加载页面以更新借阅状态 setTimeout(() => window.location.reload(), 1500); } else { // 显示错误消息 showAlert('danger', data.message); } }) .catch(error => { $('#renewModal').modal('hide'); showAlert('danger', '操作失败,请稍后重试'); console.error('Error:', error); }); }); // 显示提示消息 function showAlert(type, message) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show fixed-top mx-auto mt-3`; alertDiv.style.maxWidth = '500px'; alertDiv.style.zIndex = '9999'; alertDiv.innerHTML = ` ${message} `; document.body.appendChild(alertDiv); // 3秒后自动消失 setTimeout(() => { alertDiv.remove(); }, 3000); } }); ================================================================================ File: ./app/static/js/announcement-form.js ================================================================================ // 公告编辑表单的Javascript document.addEventListener('DOMContentLoaded', function() { // 表单提交前验证 document.getElementById('announcementForm').addEventListener('submit', function(e) { // 由于富文本内容在各页面单独处理,这里仅做一些通用表单验证 const title = document.getElementById('title').value.trim(); if (!title) { e.preventDefault(); alert('请输入公告标题'); return false; } }); // 返回按钮处理 const cancelButton = document.querySelector('button[type="button"]'); if (cancelButton) { cancelButton.addEventListener('click', function() { // 如果有未保存内容,给出提示 if (formHasChanges()) { if (!confirm('表单有未保存的内容,确定要离开吗?')) { return; } } history.back(); }); } // 检测表单是否有变化 function formHasChanges() { // 这里可以添加逻辑来检测表单内容是否有变化 // 简单实现:检查标题是否不为空 const title = document.getElementById('title').value.trim(); return title !== ''; } }); ================================================================================ 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/book_ranking.js ================================================================================ // app/static/js/book_ranking.js document.addEventListener('DOMContentLoaded', function() { const timeRangeSelect = document.getElementById('time-range'); const limitSelect = document.getElementById('limit-count'); let rankingChart = null; // 初始加载 loadRankingData(); // 添加事件监听器 timeRangeSelect.addEventListener('change', loadRankingData); limitSelect.addEventListener('change', loadRankingData); function loadRankingData() { const timeRange = timeRangeSelect.value; const limit = limitSelect.value; // 显示加载状态 document.getElementById('ranking-table-body').innerHTML = `
正在打开书页...
`; // 调用API获取数据 fetch(`/statistics/api/book-ranking?time_range=${timeRange}&limit=${limit}`) .then(response => response.json()) .then(data => { // 更新表格 updateRankingTable(data); // 更新图表 updateRankingChart(data); }) .catch(error => { console.error('加载排行数据失败:', error); document.getElementById('ranking-table-body').innerHTML = ` 加载数据失败,请稍后重试 `; }); } function updateRankingTable(data) { const tableBody = document.getElementById('ranking-table-body'); if (data.length === 0) { tableBody.innerHTML = ` 暂无数据 `; return; } let tableHtml = ''; data.forEach((book, index) => { // 给每个单元格添加适当的类名以匹配CSS tableHtml += ` ${index + 1} ${book.title} ${book.title} ${book.author} ${book.borrow_count} `; }); tableBody.innerHTML = tableHtml; } function updateRankingChart(data) { // 销毁旧图表 if (rankingChart) { rankingChart.destroy(); } if (data.length === 0) { return; } // 准备图表数据 const labels = data.map(book => book.title); const borrowCounts = data.map(book => book.borrow_count); // 创建图表 const ctx = document.getElementById('ranking-chart').getContext('2d'); rankingChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: '借阅次数', data: borrowCounts, backgroundColor: 'rgba(183, 110, 121, 0.6)', // 玫瑰金色调 borderColor: 'rgba(140, 45, 90, 1)', // 浆果红 borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, title: { display: true, text: '借阅次数', font: { family: "'Open Sans', sans-serif", size: 13 }, color: '#5D5053' }, ticks: { color: '#8A797C', font: { family: "'Open Sans', sans-serif" } }, grid: { color: 'rgba(211, 211, 211, 0.3)' } }, x: { title: { display: true, text: '图书', font: { family: "'Open Sans', sans-serif", size: 13 }, color: '#5D5053' }, ticks: { color: '#8A797C', font: { family: "'Open Sans', sans-serif" } }, grid: { display: false } } }, plugins: { legend: { display: false }, title: { display: true, text: '热门图书借阅排行', font: { family: "'Playfair Display', serif", size: 16, weight: 'bold' }, color: '#B76E79' } } } }); } }); ================================================================================ 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(); // 初始化时加载权限列表 loadPermissions(); // 添加角色按钮点击事件 if (addRoleBtn) { addRoleBtn.addEventListener('click', function() { // 重置表单 roleIdInput.value = ''; roleNameInput.value = ''; roleDescriptionInput.value = ''; // 更新模态框标题 document.getElementById('roleModalLabel').textContent = '添加角色'; // 启用所有权限复选框 document.querySelectorAll('.permission-checkbox').forEach(checkbox => { checkbox.checked = false; checkbox.disabled = false; }); // 隐藏系统角色警告 const systemRoleAlert = document.getElementById('systemRoleAlert'); if (systemRoleAlert) { systemRoleAlert.style.display = 'none'; } // 显示模态框 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 = '编辑角色'; // 加载角色权限 loadRolePermissions(roleId); // 显示模态框 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; } // 收集选中的权限ID const permissionIds = []; document.querySelectorAll('.permission-checkbox:checked:not(:disabled)').forEach(checkbox => { permissionIds.push(parseInt(checkbox.value)); }); const roleData = { id: roleIdInput.value || null, role_name: roleNameInput.value.trim(), description: roleDescriptionInput.value.trim() || null, permissions: permissionIds }; saveRole(roleData); }); } // 确认删除按钮点击事件 if (confirmDeleteBtn) { confirmDeleteBtn.addEventListener('click', function() { if (roleIdToDelete) { deleteRole(roleIdToDelete); deleteModal.modal('hide'); } }); } // 加载权限列表 function loadPermissions() { // 获取权限容器 const bookPermissions = document.getElementById('book-permissions'); const userPermissions = document.getElementById('user-permissions'); const borrowPermissions = document.getElementById('borrow-permissions'); const systemPermissions = document.getElementById('system-permissions'); if (!bookPermissions) return; // 如果元素不存在就退出 // 设置加载中状态 bookPermissions.innerHTML = '
加载权限中...
'; userPermissions.innerHTML = ''; borrowPermissions.innerHTML = ''; systemPermissions.innerHTML = ''; // 获取权限数据 fetch('/user/permissions', { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => { if (!response.ok) { throw new Error('网络响应异常'); } return response.json(); }) .then(data => { if (data.success) { // 清除加载中状态 bookPermissions.innerHTML = ''; // 按分组整理权限 const permissionGroups = { book: [], // 图书相关权限 user: [], // 用户相关权限 borrow: [], // 借阅相关权限 system: [] // 系统相关权限 }; // 定义权限分组映射 const permGroupMap = { 'manage_books': 'book', 'manage_categories': 'book', 'import_export_books': 'book', 'manage_users': 'user', 'manage_roles': 'user', 'manage_borrows': 'borrow', 'manage_overdue': 'borrow', // 系统相关权限 'manage_announcements': 'system', 'manage_inventory': 'system', 'view_logs': 'system', 'view_statistics': 'system' }; // 根据映射表分组 data.permissions.forEach(perm => { const group = permGroupMap[perm.code] || 'system'; permissionGroups[group].push(perm); }); // 渲染各组权限 renderPermissionGroup(bookPermissions, permissionGroups.book); renderPermissionGroup(userPermissions, permissionGroups.user); renderPermissionGroup(borrowPermissions, permissionGroups.borrow); renderPermissionGroup(systemPermissions, permissionGroups.system); } else { bookPermissions.innerHTML = '
加载权限失败
'; } }) .catch(error => { console.error('Error:', error); bookPermissions.innerHTML = '
加载权限失败,请刷新页面重试
'; }); } // 渲染权限组 function renderPermissionGroup(container, permissions) { if (permissions.length === 0) { container.innerHTML = '
暂无相关权限
'; return; } let html = ''; permissions.forEach(perm => { html += `
`; }); container.innerHTML = html; } // 加载角色的权限 function loadRolePermissions(roleId) { if (!roleId) return; // 先清空所有已选权限 document.querySelectorAll('.permission-checkbox').forEach(checkbox => { checkbox.checked = false; }); // 如果是系统内置角色,显示警告并禁用权限选择 const isSystemRole = (roleId == 1 || roleId == 2); const systemRoleAlert = document.getElementById('systemRoleAlert'); const permissionCheckboxes = document.querySelectorAll('.permission-checkbox'); if (systemRoleAlert) { systemRoleAlert.style.display = isSystemRole ? 'block' : 'none'; } // 管理员角色自动选中所有权限并禁用 if (roleId == 1) { // 管理员 permissionCheckboxes.forEach(checkbox => { checkbox.checked = true; checkbox.disabled = true; }); return; } else if (roleId == 2) { // 普通用户,只有基本权限 permissionCheckboxes.forEach(checkbox => { checkbox.checked = false; checkbox.disabled = true; // 普通用户权限不可修改 }); // 获取普通用户已分配的权限 fetch(`/user/role/${roleId}/permissions`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(data => { if (data.success) { permissionCheckboxes.forEach(checkbox => { // 如果权限ID在返回的列表中,则选中 checkbox.checked = data.permissions.includes(parseInt(checkbox.value)); }); } }); return; } // 为自定义角色加载并选中权限 fetch(`/user/role/${roleId}/permissions`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .then(data => { if (data.success) { permissionCheckboxes.forEach(checkbox => { // 启用所有复选框 checkbox.disabled = false; // 如果权限ID在返回的列表中,则选中 checkbox.checked = data.permissions.includes(parseInt(checkbox.value)); }); } }); } // 保存角色 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(); }); ================================================================================ File: ./app/static/js/user-add.js ================================================================================ document.addEventListener('DOMContentLoaded', function() { // 密码显示/隐藏切换 const togglePasswordButtons = document.querySelectorAll('.toggle-password'); togglePasswordButtons.forEach(button => { button.addEventListener('click', function() { const passwordField = this.previousElementSibling; const type = passwordField.getAttribute('type') === 'password' ? 'text' : 'password'; passwordField.setAttribute('type', type); // 更改图标 const icon = this.querySelector('i'); if (type === 'text') { icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); } else { icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); } }); }); // 密码一致性检查 const passwordInput = document.getElementById('password'); const confirmPasswordInput = document.getElementById('confirm_password'); const passwordMatchMessage = document.getElementById('password-match-message'); function checkPasswordMatch() { if (confirmPasswordInput.value === '') { passwordMatchMessage.textContent = ''; passwordMatchMessage.className = 'form-text'; return; } if (passwordInput.value === confirmPasswordInput.value) { passwordMatchMessage.textContent = '密码匹配'; passwordMatchMessage.className = 'form-text text-success'; } else { passwordMatchMessage.textContent = '密码不匹配'; passwordMatchMessage.className = 'form-text text-danger'; } } passwordInput.addEventListener('input', checkPasswordMatch); confirmPasswordInput.addEventListener('input', checkPasswordMatch); // 发送邮箱验证码 const sendVerificationCodeButton = document.getElementById('sendVerificationCode'); const emailInput = document.getElementById('email'); sendVerificationCodeButton.addEventListener('click', function() { const email = emailInput.value.trim(); // 验证邮箱格式 if (!email) { alert('请输入邮箱地址'); return; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { alert('请输入有效的邮箱地址'); return; } // 禁用按钮,防止重复点击 this.disabled = true; this.textContent = '发送中...'; // 发送AJAX请求 fetch('/user/send_verification_code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: email }), }) .then(response => response.json()) .then(data => { if (data.success) { // 成功发送,开始倒计时 startCountdown(this); alert(data.message); } else { // 发送失败,恢复按钮状态 this.disabled = false; this.textContent = '发送验证码'; alert(data.message || '发送失败,请稍后重试'); } }) .catch(error => { console.error('Error:', error); this.disabled = false; this.textContent = '发送验证码'; alert('发送失败,请稍后重试'); }); }); // 验证码倒计时(60秒) function startCountdown(button) { let seconds = 60; const originalText = '发送验证码'; const countdownInterval = setInterval(() => { seconds--; button.textContent = `${seconds}秒后重发`; if (seconds <= 0) { clearInterval(countdownInterval); button.textContent = originalText; button.disabled = false; } }, 1000); } // 表单提交前验证 const addUserForm = document.getElementById('addUserForm'); addUserForm.addEventListener('submit', function(event) { // 检查密码是否匹配 if (passwordInput.value !== confirmPasswordInput.value) { event.preventDefault(); alert('两次输入的密码不匹配,请重新输入'); return; } // 如果还有其他前端验证,可以继续添加 }); // 自动填充用户名为昵称的默认值 const usernameInput = document.getElementById('username'); const nicknameInput = document.getElementById('nickname'); usernameInput.addEventListener('change', function() { // 只有当昵称字段为空时才自动填充 if (!nicknameInput.value) { nicknameInput.value = this.value; } }); }); ================================================================================ File: ./app/static/js/browse.js ================================================================================ // 图书浏览页面脚本 $(document).ready(function() { // 分类筛选下拉菜单 $('.category-filter-toggle').click(function() { $(this).toggleClass('active'); $('.category-filter-dropdown').toggleClass('show'); }); // 点击外部关闭下拉菜单 $(document).click(function(e) { if (!$(e.target).closest('.category-filters').length) { $('.category-filter-dropdown').removeClass('show'); $('.category-filter-toggle').removeClass('active'); } }); // 处理借阅图书 let bookIdToBorrow = null; let bookTitleToBorrow = ''; $('.borrow-book').click(function(e) { e.preventDefault(); bookIdToBorrow = $(this).data('id'); // 获取图书标题 const bookCard = $(this).closest('.book-card'); bookTitleToBorrow = bookCard.find('.book-title').text(); $('#borrowBookTitle').text(bookTitleToBorrow); $('#borrowModal').modal('show'); }); $('#confirmBorrow').click(function() { if (!bookIdToBorrow) return; // 禁用按钮防止重复提交 $(this).prop('disabled', true).html(' 处理中...'); $.ajax({ url: `/borrow/add/${bookIdToBorrow}`, type: 'POST', success: function(response) { $('#borrowModal').modal('hide'); if (response.success) { showNotification(response.message, 'success'); // 更新UI显示 const bookCard = $(`.book-card[data-id="${bookIdToBorrow}"]`); // 更改可借状态 bookCard.find('.book-ribbon span').removeClass('available').addClass('unavailable').text('已借出'); // 更改借阅按钮 bookCard.find('.btn-borrow').replaceWith(` `); // 创建借阅成功动画 const successOverlay = $('
借阅成功
'); bookCard.append(successOverlay); setTimeout(() => { successOverlay.fadeOut(500, function() { $(this).remove(); }); }, 2000); } else { showNotification(response.message, 'error'); } // 恢复按钮状态 $('#confirmBorrow').prop('disabled', false).html('确认借阅'); }, error: function() { $('#borrowModal').modal('hide'); showNotification('借阅操作失败,请稍后重试', 'error'); $('#confirmBorrow').prop('disabled', false).html('确认借阅'); } }); }); // 清除模态框数据 $('#borrowModal').on('hidden.bs.modal', function() { bookIdToBorrow = null; bookTitleToBorrow = ''; $('#borrowBookTitle').text(''); }); // 动态添加动画CSS const animationCSS = ` .borrow-success-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(102, 126, 234, 0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; color: white; font-weight: 600; border-radius: 10px; z-index: 10; animation: fadeIn 0.3s; } .borrow-success-overlay i { font-size: 40px; margin-bottom: 10px; animation: scaleIn 0.5s; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scaleIn { from { transform: scale(0); } to { transform: scale(1); } } `; $('
404
噢!页面不见了~

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

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

返回首页
================================================================================ File: ./app/templates/login.html ================================================================================ 用户登录 - 图书管理系统
☀️

© 施琦图书管理系统 - 版权所有

================================================================================ File: ./app/templates/statistics/index.html ================================================================================ {% extends "base.html" %} {% block title %}统计分析 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %} {% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/statistics/overdue_analysis.html ================================================================================ {% extends "base.html" %} {% block title %}逾期分析 - 统计分析{% endblock %} {% block head %} {% endblock %} {% block content %}

逾期分析

0
总借阅数
0
当前逾期数
0
历史逾期数
0%
总逾期率

逾期时长分布

逾期状态分布

{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/statistics/book_ranking.html ================================================================================ {% extends "base.html" %} {% block title %}热门图书排行 - 统计分析{% endblock %} {% block head %} {% endblock %} {% block content %}

✨ 热门图书排行 ✨

时间范围:
显示数量:

📚 热门图书榜单 📖

🏆 排名 🖼️ 封面 📕 书名 ✒️ 作者 📊 借阅次数
正在打开书页...

"一本好书就像一艘船,带领我们从狭隘的地方,驶向生活的无限广阔的海洋。"

—— 海伦·凯勒
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/statistics/user_activity.html ================================================================================ {% extends "base.html" %} {% block title %}用户活跃度分析 - 统计分析{% endblock %} {% block head %} {% endblock %} {% block content %}

用户活跃度分析

最活跃用户排行

活跃用户列表

排名 用户名 昵称 借阅次数
加载中...
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/statistics/borrow_statistics.html ================================================================================ {% extends "base.html" %} {% block title %}借阅趋势分析 - 统计分析{% endblock %} {% block head %} {% endblock %} {% block content %}

借阅趋势分析

探索读者的阅读习惯与喜好,发现图书流通的奥秘

时间范围:

借阅与归还趋势

分类借阅分布

借阅概况

数据加载中...

阅读洞察

根据当前数据分析,发现读者更喜欢在周末借阅图书,女性读者偏爱文学和艺术类书籍,而男性读者则更关注科技和历史类图书。

{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/user/profile.html ================================================================================ {% 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 %} ================================================================================ 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/add.html ================================================================================ {% extends "base.html" %} {% block title %}添加用户 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if error %}
{{ error }}
{% endif %}
用户名将用于登录,不可重复
密码至少包含6个字符
请输入发送到邮箱的6位验证码
默认为普通用户,请根据需要选择合适的角色
{% 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/borrow/my_borrows.html ================================================================================ {% extends 'base.html' %} {% block title %}我的梦幻书架 - 图书管理系统{% endblock %} {% block head %} {{ super() }} {% endblock %} {% block content %}

✨ 我的梦幻书架 ✨

{% if pagination.items %} {% for borrow in pagination.items %} {% endfor %}
图书封面 书名 借阅日期 应还日期 状态 操作
{% if borrow.book.cover_url %} {% if borrow.book.cover_url.startswith('/') %} {{ borrow.book.title }} {% endif %} {% else %} {{ borrow.book.title }} {% endif %} {{ borrow.book.title }}
{{ borrow.book.author }}
{{ borrow.borrow_date.strftime('%Y-%m-%d') }} {{ borrow.due_date.strftime('%Y-%m-%d') }} {% if borrow.status == 1 and borrow.due_date < now %} 已逾期 {% endif %} {% if borrow.status == 1 %} 借阅中 {% if borrow.renew_count > 0 %} 已续借{{ borrow.renew_count }}次 {% endif %} {% else %} 已归还
{{ borrow.return_date.strftime('%Y-%m-%d') }}
{% endif %}
{% if borrow.status == 1 %} {% if borrow.renew_count < 2 and borrow.due_date >= now %} {% endif %} {% endif %}
{% if pagination.pages > 1 %} {% endif %}
{% else %}

{% if status == 1 %} 哎呀~你还没有借阅任何图书呢!快去探索那些等待与你相遇的故事吧~ {% elif status == 0 %} 亲爱的,你还没有归还过任何图书呢~一起开启阅读的奇妙旅程吧! {% else %} 你的书架空空如也~赶快挑选几本心动的书籍,开启你的阅读冒险吧! {% endif %}

探索好书
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/borrow/overdue.html ================================================================================ {% extends 'base.html' %} {% block title %}逾期管理 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
当前共有 {{ overdue_count }} 条逾期未归还的借阅记录,请及时处理。
{% if pagination.items %} {% for borrow in pagination.items %} {% endfor %}
图书封面 书名 借阅用户 借阅日期 应还日期 逾期天数 操作
{{ borrow.book.title }} {{ borrow.book.title }}
{{ borrow.book.author }}
{{ borrow.borrow_date.strftime('%Y-%m-%d') }} {{ borrow.due_date.strftime('%Y-%m-%d') }} {% set days_overdue = ((now - borrow.due_date).days) %} {{ days_overdue }} 天
{% if pagination.pages > 1 %} {% endif %}
{% else %}

目前没有逾期的借阅记录,继续保持!

返回借阅管理
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/borrow/borrow_management.html ================================================================================ {% extends 'base.html' %} {% block title %}借阅管理 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}

借阅管理

{% if status is not none %}{% endif %}
{% if search or user_id or book_id %} {% endif %}
{% if pagination.items %} {% for borrow in pagination.items %} {% endfor %}
图书封面 书名 借阅用户 借阅日期 应还日期 状态 操作
{% if borrow.book.cover_url %} {{ borrow.book.title }} {% else %} {{ borrow.book.title }} {% endif %} {{ borrow.book.title }}
{{ borrow.book.author }}
{{ borrow.borrow_date.strftime('%Y-%m-%d') }} {{ borrow.due_date.strftime('%Y-%m-%d') }} {% if borrow.status == 1 and borrow.due_date < now %} 已逾期 {% endif %} {% if borrow.status == 1 %} 借阅中 {% if borrow.renew_count > 0 %} 已续借{{ borrow.renew_count }}次 {% endif %} {% else %} 已归还
{{ borrow.return_date.strftime('%Y-%m-%d') }}
{% endif %}
{% if borrow.status == 1 %} {% if borrow.renew_count < 2 and borrow.due_date >= now %} {% endif %} {% if borrow.due_date < now %} {% endif %} {% endif %}
{% if pagination.pages > 1 %} {% endif %}
{% else %}

{% if status == 1 %} 没有进行中的借阅记录。 {% elif status == 0 %} 没有已归还的借阅记录。 {% else %} 没有任何借阅记录。 {% endif %}

{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/inventory/adjust.html ================================================================================ {% extends 'base.html' %} {% block title %}调整库存 - {{ book.title }}{% endblock %} {% block head %} {% endblock %} {% block content %}

调整图书库存

{% if book.cover_url %} {{ book.title }} {% else %} 默认封面 {% endif %}

{{ book.title }}

作者: {{ book.author }}

出版社: {{ book.publisher }}

ISBN: {{ book.isbn }}

当前库存: {{ book.stock }}

当前库存: {{ book.stock }}
取消
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/inventory/list.html ================================================================================ {% extends 'base.html' %} {% block title %}图书库存管理{% endblock %} {% block head %} {% endblock %} {% block content %}
{% for book in books %} {% endfor %}
ID 书名 作者 ISBN 当前库存 状态 操作
{{ book.id }} {{ book.title }} {{ book.author }} {{ book.isbn }} {{ book.stock }} {{ '正常' if book.status == 1 else '已下架' }} 调整 日志
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/inventory/book_logs.html ================================================================================ {% extends 'base.html' %} {% block title %}《{{ book.title }}》库存日志{% endblock %} {% block head %} {% endblock %} {% block content %}

《{{ book.title }}》库存变动日志

{% if book.cover_url %} {{ book.title }} {% else %} 默认封面 {% endif %}

{{ book.title }}

作者: {{ book.author }}

出版社: {{ book.publisher }}

ISBN: {{ book.isbn }}

当前库存: {{ book.stock }}

库存变动历史记录
ID
操作类型
变动数量
变动后库存
操作人
备注
操作时间
{% for log in logs %}
{{ log.id }}
{{ '入库' if log.change_type == 'in' else '出库' }}
{{ log.change_amount }}
{{ log.after_stock }}
{{ log.operator.username if log.operator else '系统' }}
{{ log.remark or '-' }}
{{ log.changed_at.strftime('%Y-%m-%d %H:%M:%S') }}
{% endfor %} {% if not logs %}

暂无库存变动记录,要不要堆个雪人?

{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/inventory/logs.html ================================================================================ {% extends 'base.html' %} {% block title %}《{{ book.title }}》库存日志{% endblock %} {% block head %} {% endblock %} {% block content %}

{% if book %} 《{{ book.title }}》库存变动日志 {% else %} 全部库存变动日志 {% endif %}

{% if book %}
{% if book.cover_url %} {{ book.title }} {% else %} 默认封面 {% endif %}

{{ book.title }}

作者: {{ book.author }}
出版社: {{ book.publisher }}
ISBN: {{ book.isbn }}
当前库存: {{ book.stock }}
{% endif %}
库存变动历史记录
{% if not book %}{% endif %} {% for log in logs %} {% if not book %} {% endif %} {% endfor %} {% if not logs %} {% endif %}
ID图书操作类型 变动数量 变动后库存 操作人 备注 操作时间
{{ log.id }} {{ log.book.title if log.book else '未知图书' }} {{ '入库' if log.change_type == 'in' or log.change_amount > 0 else '出库' }} {{ log.change_amount }} {{ log.after_stock }} {{ log.operator.username if log.operator else '系统' }} {{ log.remark or '-' }} {{ log.changed_at.strftime('%Y-%m-%d %H:%M:%S') }}

暂无库存变动记录

{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/browse.html ================================================================================ {% extends 'base.html' %} {% block title %}图书浏览 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% for i in range(15) %}
{% endfor %}
全部 {% for category in categories %} {{ category.name }} {% endfor %}
{{ pagination.total }} 可借图书
{{ categories|length }} 图书分类
{% if search %}
搜索 "{{ search }}" 找到 {{ pagination.total }} 本图书
{% endif %}
{% for book in books %}
{% if book.cover_url %} {{ book.title }} {% else %}
无封面
{% endif %}
{% if book.stock > 0 %} 可借阅 {% else %} 无库存 {% endif %}

{{ book.title }}

{{ book.author }}
{% if book.category %} {{ book.category.name }} {% endif %} {{ book.publish_year or '未知年份' }}
详情 {% if book.stock > 0 %} 借阅 {% else %} {% endif %}
{% else %}
没有找到书籍

没有找到符合条件的图书

尝试调整搜索条件或浏览其他分类

重置搜索
{% endfor %}
{% if pagination.pages > 1 %}
    {% if pagination.has_prev %}
  • {% endif %} {% for p in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %} {% if p %} {% if p == pagination.page %}
  • {{ p }}
  • {% else %}
  • {{ p }}
  • {% endif %} {% else %}
  • ...
  • {% endif %} {% endfor %} {% if pagination.has_next %}
  • {% endif %}
显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }} 到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
{% endif %}
{% endblock %} {% block scripts %} {{ super() }} {% endblock %} ================================================================================ File: ./app/templates/book/list.html ================================================================================ {% extends 'base.html' %} {% block title %}图书列表 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% for book in books %}
{% if book.cover_url %} {{ book.title }} {% else %}
无封面
{% endif %}
{{ book.title }}

{{ book.title }}

{{ book.author }}

{% if book.category %} {{ book.category.name }} {% endif %} {{ '可借阅' if book.stock > 0 else '无库存' }}

ISBN: {{ book.isbn or '无' }}

出版社: {{ book.publisher or '无' }}

库存: {{ book.stock }}

详情 {% if current_user.role_id == 1 %} 编辑 {% endif %} {% if book.stock > 0 %} 借阅 {% endif %}
{% else %}

没有找到符合条件的图书

{% endfor %}
{% if pagination.pages > 1 %}
    {% if pagination.has_prev %}
  • {% endif %} {% for p in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %} {% if p %} {% if p == pagination.page %}
  • {{ p }}
  • {% else %}
  • {{ p }}
  • {% endif %} {% else %}
  • ...
  • {% endif %} {% endfor %} {% if pagination.has_next %}
  • {% endif %}
显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }} 到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
{% endif %}
{% endblock %} {% block scripts %} {{ super() }} {% endblock %} ================================================================================ File: ./app/templates/book/add.html ================================================================================ {% extends 'base.html' %} {% block title %}添加图书 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
基本信息
完整准确的书名有助于读者查找
输入ISBN并点击查询按钮自动填充图书信息
分类与标签
为图书选择合适的分类以便于管理和查找
添加多个标签以提高图书的检索率
图书简介
封面图片
暂无封面

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

推荐尺寸: 500×700px (竖版封面)
支持格式: JPG, PNG, WebP
库存和价格
¥
¥0 ¥250 ¥500
* 的字段为必填项
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/edit.html ================================================================================ {% extends 'base.html' %} {% block title %}编辑图书 - {{ book.title }}{% endblock %} {% block head %} {% endblock %} {% block content %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} {% endfor %} {% endif %} {% endwith %}
基本信息
{% if isbn_error %}
{{ isbn_error }}
{% endif %} ISBN必须是有效的10位或13位格式
图书简介
封面图片
{% if book.cover_url %} {{ book.title }} {% else %}
暂无封面
{% endif %}
库存与价格
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/import.html ================================================================================ {% extends 'base.html' %} {% block title %}图书花园 - 批量导入{% endblock %} {% block head %} {% endblock %} {% block content %}

添加您的图书收藏

支持的文件格式: .xlsx, .xls
导入指南

Excel文件须包含以下字段 (标题行必须与下列完全一致):

  • title - 图书标题 必填
  • author - 作者名称 必填
  • publisher - 出版社
  • category_id - 分类ID
  • tags - 标签 (多个标签用逗号分隔)
  • isbn - ISBN编号
  • publish_year - 出版年份
  • description - 图书简介
  • cover_url - 封面图片URL
  • stock - 库存数量
  • price - 价格

不确定如何开始? 下载我们精心准备的模板:

{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/detail.html ================================================================================ {% extends 'base.html' %} {% block title %}{{ book.title }} - 图书详情{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if book.cover_url %} {{ book.title }} {% else %}
无封面
{% endif %}

{{ book.title }}

作者: {{ book.author }}

出版社: {{ book.publisher or '未知' }}
出版年份: {{ book.publish_year or '未知' }}
ISBN: {{ book.isbn or '未知' }}
分类: {{ book.category.name if book.category else '未分类' }}
{% if book.tags %}
标签: {% for tag in book.tags.split(',') %} {{ tag.strip() }} {% endfor %}
{% endif %}
价格: {{ book.price or '未知' }}
{{ '可借阅' if book.stock > 0 else '无库存' }}
库存: {{ book.stock }}

图书简介

{% if book.description %}

{{ book.description|nl2br }}

{% else %}

暂无图书简介

{% endif %}
{% if current_user.is_authenticated and current_user.role_id == 1 %}

借阅历史

{% if borrow_records %} {% for record in borrow_records %} {% endfor %}
借阅用户 借阅日期 应还日期 实际归还 状态
{{ record.user.username }} {{ record.borrow_date.strftime('%Y-%m-%d') }} {{ record.due_date.strftime('%Y-%m-%d') }} {{ record.return_date.strftime('%Y-%m-%d') if record.return_date else '-' }} {% if record.status == 1 and record.due_date < now %} 已逾期 {% elif record.status == 1 %} 借阅中 {% else %} 已归还 {% endif %}
{% else %}

暂无借阅记录

{% endif %}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/categories.html ================================================================================ {% extends 'base.html' %} {% block title %}图书分类管理 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
添加新分类
分类列表
{% if categories %} {% for category in categories %} {% endfor %}
ID 分类名称 父级分类 排序 操作
{{ category.id }} {{ category.name }} {% if category.parent %} {{ category.parent.name }} {% else %} {% endif %} {{ category.sort }}
{% else %}

暂无分类数据

{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/log/list.html ================================================================================ {% extends 'base.html' %} {% block title %}系统日志管理{% endblock %} {% block head %} {% endblock %} {% block content %}

系统日志管理

日志筛选
重置
{% for log in pagination.items %} {% else %} {% endfor %}
# 用户 操作 目标类型 目标ID IP地址 描述 时间
{{ log.id }} {% if log.user %} {{ log.user.username }} {% else %} 未登录 {% endif %} {{ log.action }} {{ log.target_type }} {{ log.target_id }} {{ log.ip_address }} {{ log.description }} {{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}

没有找到符合条件的日志记录

{% if pagination.has_prev %} 上一页 {% endif %} 第 {{ pagination.page }} 页,共 {{ pagination.pages }} 页,总计 {{ pagination.total }} 条记录 {% if pagination.has_next %} 下一页 {% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/log/detail.html ================================================================================ {% extends 'base.html' %} {% block title %}日志详情{% endblock %} {% block head %} {% endblock %} {% block content %}

日志详情 #{{ log.id }}

操作时间:
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
操作用户:
{% if log.user %} {{ log.user.username }} (ID: {{ log.user_id }}) {% else %} 未登录用户 {% endif %}
操作类型:
{{ log.action }}
目标类型:
{{ log.target_type or '无' }}
目标ID:
{{ log.target_id or '无' }}
IP地址:
{{ log.ip_address or '未记录' }}
详细描述:
{{ log.description or '无描述' }}
{% endblock %} ================================================================================ File: ./app/templates/announcement/manage.html ================================================================================ {% extends 'base.html' %} {% block title %}公告管理 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if pagination.items %} {% for announcement in pagination.items %} {% endfor %}
ID 标题 发布者 发布时间 最后更新 状态 置顶 操作
{{ announcement.id }} {{ announcement.title }} {{ announcement.publisher.username if announcement.publisher else '系统' }} {{ announcement.created_at.strftime('%Y-%m-%d %H:%M') }} {{ announcement.updated_at.strftime('%Y-%m-%d %H:%M') }} {{ '已发布' if announcement.status == 1 else '已撤销' }} {{ '已置顶' if announcement.is_top else '未置顶' }}
编辑
{% if pagination.pages > 1 %} {% endif %}
{% else %}

没有找到符合条件的公告

{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/announcement/list.html ================================================================================ {% extends 'base.html' %} {% block title %}通知公告 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if pagination.items %} {% for announcement in pagination.items %}
{% if announcement.is_top %}
置顶推荐
{% endif %}

{{ announcement.title }}

{{ announcement.created_at.strftime('%Y年%m月%d日') }}
{{ announcement.content|striptags|truncate(130) }}
{% endfor %}
{% if pagination.pages > 1 %} {% endif %}
{% else %}
No announcements icon

暂时还没有新的通知公告哦,敬请期待!

{% endif %}
{% endblock %} {% block scripts %} {# #} {# Assuming announcement-list.js is for interactivity not directly tied to styling #} {% endblock %} ================================================================================ File: ./app/templates/announcement/add.html ================================================================================ {% extends 'base.html' %} {% block title %}添加公告 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if error %}
{{ error }}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/announcement/notifications.html ================================================================================ {% extends 'base.html' %} {% block title %}我的通知 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if pagination.items %} {% for notification in pagination.items %}
{% if notification.status == 0 %} {# Icon for unread #} {% else %} {# Icon for read #} {% endif %}

{{ notification.title }} {% if notification.status == 0 %} 未读 {% endif %}

{{ notification.content|striptags|truncate(120) }}
{{ notification.type }} {{ notification.created_at.strftime('%Y-%m-%d %H:%M') }}
{% endfor %}
{% if pagination.pages > 1 %} {% endif %}
{% else %}
{# Suggestion: Use a more vibrant/friendly icon or illustration #} No notifications icon

{{ '还没有未读通知哦~' if unread_only else '这里空空如也,暂时没有新通知。' }}

{% endif %}
{% endblock %} {% block scripts %} {# #} {# Assuming notifications.js is for interactivity not directly tied to styling #} {% endblock %} ================================================================================ File: ./app/templates/announcement/edit.html ================================================================================ {% extends 'base.html' %} {% block title %}编辑公告 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% if error %}
{{ error }}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/announcement/detail.html ================================================================================ {% extends 'base.html' %} {% block title %}{{ announcement.title }} - 通知公告{% endblock %} {% block head %} {% endblock %} {% block content %}
发布者: {{ announcement.publisher.username if announcement.publisher else '系统' }}
发布时间: {{ announcement.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
{% if announcement.is_top %}
置顶公告
{% endif %}
{{ announcement.content|safe }}
{% endblock %} ================================================================================ File: ./app/controllers/user.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify from werkzeug.security import generate_password_hash, check_password_hash from app.models.user import User, db from app.models.log import Log # 导入日志模型 from app.utils.email import send_verification_email, generate_verification_code 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__) # 使用内存字典代替Redis存储验证码 class VerificationStore: def __init__(self): self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}} def setex(self, email, seconds, code): """设置验证码并指定过期时间""" expiry = datetime.now() + timedelta(seconds=seconds) self.codes[email] = {'code': code, 'expires': expiry} return True def get(self, email): """获取验证码,如果过期则返回None""" if email not in self.codes: return None data = self.codes[email] if datetime.now() > data['expires']: # 验证码已过期,删除它 self.delete(email) return None return data['code'] def delete(self, email): """删除验证码""" if email in self.codes: del self.codes[email] return True # 使用内存存储验证码 verification_codes = VerificationStore() # 添加管理员权限检查装饰器 def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): print( f"DEBUG: admin_required检查,用户认证={current_user.is_authenticated},角色ID={current_user.role_id if current_user.is_authenticated else 'None'}") if not current_user.is_authenticated: print("DEBUG: 用户未登录,重定向到登录页面") return redirect(url_for('user.login', next=request.url)) if current_user.role_id != 1: print(f"DEBUG: 用户{current_user.username}不是管理员,角色ID={current_user.role_id}") flash('您没有管理员权限访问此页面', 'error') return redirect(url_for('index')) print(f"DEBUG: 用户{current_user.username}是管理员,允许访问") return f(*args, **kwargs) return decorated_function @user_bp.route('/login', methods=['GET', 'POST']) def login(): print(f"DEBUG: 登录函数被调用,认证状态={current_user.is_authenticated}") print(f"DEBUG: 请求方法={request.method},next参数={request.args.get('next')}") # 获取next参数 next_page = request.args.get('next') # 如果用户已经登录,处理重定向 if current_user.is_authenticated: if next_page: from urllib.parse import urlparse parsed = urlparse(next_page) path = parsed.path print(f"DEBUG: 提取的路径={path}") # 删除特殊处理,直接重定向到path return redirect(path) return redirect(url_for('index')) if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') remember_me = request.form.get('remember_me') == 'on' if not username or not password: return render_template('login.html', error='用户名和密码不能为空') # 检查用户是否存在 user = User.query.filter((User.username == username) | (User.email == username)).first() if not user or not user.check_password(password): # 记录登录失败日志 Log.add_log( action="登录失败", ip_address=request.remote_addr, description=f"尝试使用用户名/邮箱 {username} 登录失败" ) return render_template('login.html', error='用户名或密码错误') if user.status == 0: # 记录禁用账号登录尝试 Log.add_log( action="登录失败", user_id=user.id, ip_address=request.remote_addr, description=f"禁用账号 {username} 尝试登录" ) return render_template('login.html', error='账号已被禁用,请联系管理员') # 使用 Flask-Login 的 login_user 函数 login_user(user, remember=remember_me) # 记录登录成功日志 Log.add_log( action="用户登录", user_id=user.id, ip_address=request.remote_addr, description=f"用户 {user.username} 登录成功" ) # 这些session信息仍然可以保留,但不再用于认证 session['username'] = user.username session['role_id'] = user.role_id # 获取登录后要跳转的页面 next_page = request.args.get('next') if not next_page or not next_page.startswith('/'): next_page = 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') password = request.form.get('password') confirm_password = request.form.get('confirm_password') verification_code = request.form.get('verification_code') # 验证表单数据 if not username or not email or not password or not confirm_password or not verification_code: return render_template('register.html', error='所有字段都是必填项') if password != confirm_password: return render_template('register.html', error='两次输入的密码不匹配') # 检查用户名和邮箱是否已存在 if User.query.filter_by(username=username).first(): return render_template('register.html', error='用户名已存在') if User.query.filter_by(email=email).first(): return render_template('register.html', error='邮箱已被注册') # 验证验证码 stored_code = verification_codes.get(email) if not stored_code or stored_code != verification_code: return render_template('register.html', error='验证码无效或已过期') # 创建新用户 try: new_user = User( username=username, password=password, # 密码会在模型中自动哈希 email=email, nickname=username # 默认昵称与用户名相同 ) db.session.add(new_user) db.session.commit() # 记录用户注册日志 Log.add_log( action="用户注册", user_id=new_user.id, ip_address=request.remote_addr, description=f"新用户 {username} 注册成功" ) # 清除验证码 verification_codes.delete(email) flash('注册成功,请登录', 'success') return redirect(url_for('user.login')) except Exception as e: db.session.rollback() logging.error(f"User registration failed: {str(e)}") return render_template('register.html', error='注册失败,请稍后重试') return render_template('register.html') @user_bp.route('/logout') @login_required def logout(): username = current_user.username user_id = current_user.id # 先记录日志,再登出 Log.add_log( action="用户登出", user_id=user_id, ip_address=request.remote_addr, description=f"用户 {username} 登出系统" ) logout_user() return redirect(url_for('user.login')) @user_bp.route('/send_verification_code', methods=['POST']) def send_verification_code(): data = request.get_json() email = data.get('email') if not email: return jsonify({'success': False, 'message': '请提供邮箱地址'}) # 检查邮箱格式 import re if not re.match(r"[^@]+@[^@]+\.[^@]+", email): return jsonify({'success': False, 'message': '邮箱格式不正确'}) # 生成验证码 code = generate_verification_code() # 存储验证码(10分钟有效) verification_codes.setex(email, 600, code) # 10分钟过期 # 发送验证码邮件 if send_verification_email(email, code): # 记录发送验证码日志 Log.add_log( action="发送验证码", ip_address=request.remote_addr, description=f"向邮箱 {email} 发送验证码" ) 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 ) # 记录管理员访问用户列表日志 Log.add_log( action="访问用户管理", user_id=current_user.id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 访问用户管理列表" ) 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: # 记录管理员编辑用户信息日志 Log.add_log( action="编辑用户", user_id=current_user.id, target_type="用户", target_id=user_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 编辑用户 {user.username} 的信息" ) flash(message, 'success') return redirect(url_for('user.user_list')) else: flash(message, 'error') # 记录访问用户编辑页面日志 if request.method == 'GET': Log.add_log( action="访问用户编辑", user_id=current_user.id, target_type="用户", target_id=user_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 访问用户 {user.username} 的编辑页面" ) 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': '不能修改自己的状态'}) # 查询用户获取用户名(用于日志) target_user = User.query.get(user_id) if not target_user: return jsonify({'success': False, 'message': '用户不存在'}) success, message = UserService.change_user_status(user_id, status) if success: # 记录修改用户状态日志 status_text = "启用" if status == 1 else "禁用" Log.add_log( action=f"用户{status_text}", user_id=current_user.id, target_type="用户", target_id=user_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} {status_text}用户 {target_user.username}" ) 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': '不能删除自己的账号'}) # 查询用户获取用户名(用于日志) target_user = User.query.get(user_id) if not target_user: return jsonify({'success': False, 'message': '用户不存在'}) target_username = target_user.username # 保存用户名以便记录在日志中 success, message = UserService.delete_user(user_id) if success: # 记录删除用户日志 Log.add_log( action="删除用户", user_id=current_user.id, target_type="用户", ip_address=request.remote_addr, description=f"管理员 {current_user.username} 删除用户 {target_username}" ) 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 password_changed = True else: password_changed = False success, message = UserService.update_user(user.id, data) if success: # 记录用户修改个人信息日志 log_description = f"用户 {user.username} 修改了个人信息" if password_changed: log_description += ",包括密码修改" Log.add_log( action="修改个人信息", user_id=user.id, ip_address=request.remote_addr, description=log_description ) 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() # 记录访问角色管理页面日志 Log.add_log( action="访问角色管理", user_id=current_user.id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 访问角色管理页面" ) return render_template('user/roles.html', roles=roles) # 获取所有系统权限的API @user_bp.route('/permissions', methods=['GET']) @login_required @admin_required def get_permissions(): """获取所有可用的系统权限""" from app.models.permission import Permission try: permissions = Permission.query.order_by(Permission.code).all() # 转换为JSON格式 permissions_data = [{ 'id': p.id, 'code': p.code, 'name': p.name, 'description': p.description } for p in permissions] return jsonify({ 'success': True, 'permissions': permissions_data }) except Exception as e: logging.error(f"获取权限列表失败: {str(e)}") return jsonify({ 'success': False, 'message': f"获取权限列表失败: {str(e)}" }), 500 # 获取特定角色的权限 @user_bp.route('/role//permissions', methods=['GET']) @login_required @admin_required def get_role_permissions(role_id): """获取指定角色的权限ID列表""" from app.models.user import Role try: role = Role.query.get(role_id) if not role: return jsonify({ 'success': False, 'message': '角色不存在' }), 404 # 获取角色的所有权限ID permissions = [p.id for p in role.permissions] return jsonify({ 'success': True, 'permissions': permissions }) except Exception as e: logging.error(f"获取角色权限失败: {str(e)}") return jsonify({ 'success': False, 'message': f"获取角色权限失败: {str(e)}" }), 500 # 修改角色保存路由,支持权限管理 @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') permission_ids = data.get('permissions', []) # 获取权限ID列表 if not role_name: return jsonify({'success': False, 'message': '角色名不能为空'}) # 处理系统内置角色的权限保护 if role_id and int(role_id) in [1, 2]: permission_ids = None # 不修改内置角色的权限 if role_id: # 更新角色 success, message = UserService.update_role(role_id, role_name, description, permission_ids) if success: # 记录编辑角色日志 Log.add_log( action="编辑角色", user_id=current_user.id, target_type="角色", target_id=role_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 编辑角色 {role_name},包含权限设置" ) else: # 创建角色 success, message, new_role_id = UserService.create_role(role_name, description, permission_ids) if success: role_id = new_role_id # 记录创建角色日志 Log.add_log( action="创建角色", user_id=current_user.id, target_type="角色", target_id=role_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 创建新角色 {role_name},设置了 {len(permission_ids)} 个权限" ) return jsonify({'success': success, 'message': message}) # 角色删除API @user_bp.route('/role/delete/', methods=['POST']) @login_required @admin_required def role_delete(role_id): """删除角色""" # 保护系统内置角色 if role_id in [1, 2]: return jsonify({ 'success': False, 'message': '不能删除系统内置角色' }) from app.models.user import Role try: # 获取角色信息用于日志记录 role = Role.query.get(role_id) if not role: return jsonify({ 'success': False, 'message': '角色不存在' }), 404 role_name = role.role_name # 检查是否有用户在使用该角色 user_count = User.query.filter_by(role_id=role_id).count() if user_count > 0: return jsonify({ 'success': False, 'message': f'无法删除:该角色下存在 {user_count} 个用户' }) # 删除角色 db.session.delete(role) db.session.commit() # 记录删除角色日志 Log.add_log( action="删除角色", user_id=current_user.id, target_type="角色", ip_address=request.remote_addr, description=f"管理员 {current_user.username} 删除了角色 {role_name}" ) return jsonify({ 'success': True, 'message': '角色删除成功' }) except Exception as e: db.session.rollback() logging.error(f"删除角色失败: {str(e)}") return jsonify({ 'success': False, 'message': f"删除角色失败: {str(e)}" }), 500 @user_bp.route('/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 @user_bp.route('/add', methods=['GET', 'POST']) @login_required @admin_required def add_user(): roles = UserService.get_all_roles() if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') confirm_password = request.form.get('confirm_password') verification_code = request.form.get('verification_code') nickname = request.form.get('nickname') phone = request.form.get('phone') if phone == '': phone = None nickname = request.form.get('nickname') role_id = request.form.get('role_id', 2, type=int) # 默认为普通用户 status = request.form.get('status', 1, type=int) # 默认为启用状态 # 验证表单数据 if not username or not email or not password or not confirm_password or not verification_code: return render_template('user/add.html', error='所有必填字段不能为空', roles=roles) if password != confirm_password: return render_template('user/add.html', error='两次输入的密码不匹配', roles=roles) # 检查用户名和邮箱是否已存在 if User.query.filter_by(username=username).first(): return render_template('user/add.html', error='用户名已存在', roles=roles) if User.query.filter_by(email=email).first(): return render_template('user/add.html', error='邮箱已被注册', roles=roles) # 验证验证码 stored_code = verification_codes.get(email) if not stored_code or stored_code != verification_code: return render_template('user/add.html', error='验证码无效或已过期', roles=roles) # 创建新用户 try: new_user = User( username=username, password=password, # 密码会在模型中自动哈希 email=email, nickname=nickname or username, # 如果未提供昵称,使用用户名 phone=phone, role_id=role_id, status=status ) db.session.add(new_user) db.session.commit() # 记录管理员添加用户日志 Log.add_log( action="添加用户", user_id=current_user.id, target_type="用户", target_id=new_user.id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 添加新用户 {username}" ) # 清除验证码 verification_codes.delete(email) flash('用户添加成功', 'success') return redirect(url_for('user.user_list')) except Exception as e: db.session.rollback() logging.error(f"用户添加失败: {str(e)}") return render_template('user/add.html', error=f'添加用户失败: {str(e)}', roles=roles) # GET请求,显示添加用户表单 return render_template('user/add.html', roles=roles) ================================================================================ File: ./app/controllers/log.py ================================================================================ from flask import Blueprint, render_template, request, jsonify from flask_login import current_user, login_required from app.models.log import Log from app.models.user import User, db # 导入db from app.utils.auth import permission_required # 更改为导入permission_required装饰器 from datetime import datetime, timedelta # 创建蓝图 log_bp = Blueprint('log', __name__, url_prefix='/log') @log_bp.route('/list') @login_required @permission_required('view_logs') # 替代 @admin_required def log_list(): """日志列表页面""" # 获取筛选参数 page = request.args.get('page', 1, type=int) user_id = request.args.get('user_id', type=int) action = request.args.get('action') target_type = request.args.get('target_type') # 处理日期范围参数 date_range = request.args.get('date_range', '7') # 默认显示7天内的日志 end_date = datetime.now() start_date = None if date_range == '1': start_date = end_date - timedelta(days=1) elif date_range == '7': start_date = end_date - timedelta(days=7) elif date_range == '30': start_date = end_date - timedelta(days=30) elif date_range == 'custom': start_date_str = request.args.get('start_date') end_date_str = request.args.get('end_date') if start_date_str: start_date = datetime.strptime(start_date_str, '%Y-%m-%d') if end_date_str: end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S') # 获取分页数据 pagination = Log.get_logs( page=page, per_page=20, user_id=user_id, action=action, target_type=target_type, start_date=start_date, end_date=end_date ) # 获取用户列表和操作类型列表,用于筛选 users = User.query.all() # 统计各类操作的数量 action_types = db.session.query(Log.action, db.func.count(Log.id)) \ .group_by(Log.action).all() target_types = db.session.query(Log.target_type, db.func.count(Log.id)) \ .filter(Log.target_type != None) \ .group_by(Log.target_type).all() return render_template( 'log/list.html', pagination=pagination, users=users, action_types=action_types, target_types=target_types, filters={ 'user_id': user_id, 'action': action, 'target_type': target_type, 'date_range': date_range, 'start_date': start_date.strftime('%Y-%m-%d') if start_date else '', 'end_date': end_date.strftime('%Y-%m-%d') if end_date != datetime.now() else '' } ) @log_bp.route('/detail/') @login_required @permission_required('view_logs') # 替代 @admin_required def log_detail(log_id): """日志详情页面""" log = Log.query.get_or_404(log_id) return render_template('log/detail.html', log=log) @log_bp.route('/api/export', methods=['POST']) @login_required @permission_required('view_logs') # 替代 @admin_required def export_logs(): """导出日志API""" data = request.get_json() user_id = data.get('user_id') action = data.get('action') target_type = data.get('target_type') start_date_str = data.get('start_date') end_date_str = data.get('end_date') export_format = data.get('format', 'csv') # 获取导出格式参数 # 处理日期范围 start_date = None end_date = datetime.now() if start_date_str: start_date = datetime.strptime(start_date_str, '%Y-%m-%d') if end_date_str: end_date = datetime.strptime(end_date_str + ' 23:59:59', '%Y-%m-%d %H:%M:%S') # 查询日志 query = Log.query.order_by(Log.created_at.desc()) if user_id: query = query.filter(Log.user_id == user_id) if action: query = query.filter(Log.action == action) if target_type: query = query.filter(Log.target_type == target_type) if start_date: query = query.filter(Log.created_at >= start_date) if end_date: query = query.filter(Log.created_at <= end_date) logs = query.all() try: # 根据格式选择导出方法 if export_format == 'xlsx': return export_as_xlsx(logs) else: return export_as_csv(logs) except Exception as e: # 记录错误以便调试 import traceback error_details = traceback.format_exc() current_app.logger.error(f"Export error: {str(e)}\n{error_details}") return jsonify({ 'success': False, 'message': f'导出失败: {str(e)}' }), 500 def export_as_csv(logs): """导出为CSV格式""" import csv from io import StringIO import base64 # 创建CSV文件 output = StringIO() output.write('\ufeff') # 添加BOM标记,解决Excel中文乱码 csv_writer = csv.writer(output) # 写入标题行 csv_writer.writerow(['ID', '用户', '操作类型', '目标类型', '目标ID', 'IP地址', '描述', '创建时间']) # 写入数据行 for log in logs: username = log.user.username if log.user else "未登录" csv_writer.writerow([ log.id, username, log.action, log.target_type or '', log.target_id or '', log.ip_address or '', log.description or '', log.created_at.strftime('%Y-%m-%d %H:%M:%S') ]) # 获取CSV字符串并进行Base64编码 csv_string = output.getvalue() csv_bytes = csv_string.encode('utf-8') b64_encoded = base64.b64encode(csv_bytes).decode('utf-8') # 设置文件名 filename = f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" return jsonify({ 'success': True, 'message': f'已生成 {len(logs)} 条日志记录', 'count': len(logs), 'filename': filename, 'filedata': b64_encoded, 'filetype': 'text/csv' }) def export_as_xlsx(logs): """导出为XLSX格式""" import base64 from io import BytesIO try: # 动态导入openpyxl,如果不存在则抛出异常 import openpyxl except ImportError: raise Exception("未安装openpyxl库,无法导出Excel格式。请安装后重试: pip install openpyxl") # 创建工作簿和工作表 wb = openpyxl.Workbook() ws = wb.active ws.title = "系统日志" # 写入标题行 headers = ['ID', '用户', '操作类型', '目标类型', '目标ID', 'IP地址', '描述', '创建时间'] for col_idx, header in enumerate(headers, 1): ws.cell(row=1, column=col_idx, value=header) # 写入数据行 for row_idx, log in enumerate(logs, 2): username = log.user.username if log.user else "未登录" ws.cell(row=row_idx, column=1, value=log.id) ws.cell(row=row_idx, column=2, value=username) ws.cell(row=row_idx, column=3, value=log.action) ws.cell(row=row_idx, column=4, value=log.target_type or '') ws.cell(row=row_idx, column=5, value=log.target_id or '') ws.cell(row=row_idx, column=6, value=log.ip_address or '') ws.cell(row=row_idx, column=7, value=log.description or '') ws.cell(row=row_idx, column=8, value=log.created_at.strftime('%Y-%m-%d %H:%M:%S')) # 调整列宽 for col_idx, header in enumerate(headers, 1): column_letter = openpyxl.utils.get_column_letter(col_idx) if header == '描述': ws.column_dimensions[column_letter].width = 40 else: ws.column_dimensions[column_letter].width = 15 # 保存到内存 output = BytesIO() wb.save(output) output.seek(0) # 编码为Base64 xlsx_data = output.getvalue() b64_encoded = base64.b64encode(xlsx_data).decode('utf-8') # 设置文件名 filename = f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" return jsonify({ 'success': True, 'message': f'已生成 {len(logs)} 条日志记录', 'count': len(logs), 'filename': filename, 'filedata': b64_encoded, 'filetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) @log_bp.route('/api/clear', methods=['POST']) @login_required @permission_required('view_logs') # 替代 @admin_required def clear_logs(): """清空日志API""" data = request.get_json() days = data.get('days', 0) try: if days > 0: # 清除指定天数前的日志 cutoff_date = datetime.now() - timedelta(days=days) deleted = Log.query.filter(Log.created_at < cutoff_date).delete() else: # 清空全部日志 deleted = Log.query.delete() db.session.commit() return jsonify({ 'success': True, 'message': f'成功清除 {deleted} 条日志记录', 'count': deleted }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': f'清除日志失败: {str(e)}' }), 500 ================================================================================ File: ./app/controllers/__init__.py ================================================================================ ================================================================================ File: ./app/controllers/book.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, send_file from app.models.book import Book, Category from app.models.user import db from app.utils.auth import login_required, permission_required # 修改导入,替换admin_required为permission_required from flask_login import current_user import os from werkzeug.utils import secure_filename import datetime import pandas as pd import uuid from app.models.log import Log from io import BytesIO import xlsxwriter from sqlalchemy import text book_bp = Blueprint('book', __name__) @book_bp.route('/admin/list') @login_required @permission_required('manage_books') # 替换 @admin_required def admin_book_list(): print(f"DEBUG: admin_book_list 函数被调用,用户={current_user.username},认证状态={current_user.is_authenticated}") page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) # 只显示状态为1的图书(未下架的图书) query = Book.query.filter_by(status=1) # 搜索功能 search = request.args.get('search', '') if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) # 分类筛选 category_id = request.args.get('category_id', type=int) if category_id: query = query.filter_by(category_id=category_id) # 排序 sort = request.args.get('sort', 'id') order = request.args.get('order', 'desc') if order == 'desc': query = query.order_by(getattr(Book, sort).desc()) else: query = query.order_by(getattr(Book, sort)) pagination = query.paginate(page=page, per_page=per_page) books = pagination.items # 获取所有分类供筛选使用 categories = Category.query.all() # 记录访问日志 Log.add_log( action='访问管理图书列表', user_id=current_user.id, ip_address=request.remote_addr, description=f"查询条件: 搜索={search}, 分类={category_id}, 排序={sort} {order}" ) return render_template('book/list.html', books=books, pagination=pagination, search=search, categories=categories, category_id=category_id, sort=sort, order=order, current_user=current_user, is_admin_view=True) # 图书列表页面 - 不需要修改,已经只有@login_required @book_bp.route('/list') @login_required def book_list(): print("访问图书列表页面") # 调试输出 page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) # 只显示状态为1的图书(未下架的图书) query = Book.query.filter_by(status=1) # 搜索功能 search = request.args.get('search', '') if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) # 分类筛选 category_id = request.args.get('category_id', type=int) if category_id: query = query.filter_by(category_id=category_id) # 排序 sort = request.args.get('sort', 'id') order = request.args.get('order', 'desc') if order == 'desc': query = query.order_by(getattr(Book, sort).desc()) else: query = query.order_by(getattr(Book, sort)) pagination = query.paginate(page=page, per_page=per_page) books = pagination.items # 获取所有分类供筛选使用 categories = Category.query.all() # 记录访问日志 Log.add_log( action='访问图书列表', user_id=current_user.id, ip_address=request.remote_addr, description=f"查询条件: 搜索={search}, 分类={category_id}, 排序={sort} {order}" ) return render_template('book/list.html', books=books, pagination=pagination, search=search, categories=categories, category_id=category_id, sort=sort, order=order, current_user=current_user) # 图书详情页面 - 不需要修改,已经只有@login_required @book_bp.route('/detail/') @login_required def book_detail(book_id): book = Book.query.get_or_404(book_id) # 添加当前时间用于判断借阅是否逾期 now = datetime.datetime.now() # 如果用户是管理员,预先查询并排序借阅记录 borrow_records = [] # 使用current_user代替g.user if current_user.is_authenticated and current_user.role_id == 1: from app.models.borrow import BorrowRecord borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit( 10).all() # 记录访问日志 Log.add_log( action='查看图书详情', user_id=current_user.id, target_type='book', target_id=book_id, ip_address=request.remote_addr, description=f"查看图书: {book.title}" ) return render_template( 'book/detail.html', book=book, current_user=current_user, borrow_records=borrow_records, now=now ) # 添加图书页面 @book_bp.route('/add', methods=['GET', 'POST']) @login_required @permission_required('manage_books') # 替换 @admin_required def add_book(): if request.method == 'POST': title = request.form.get('title') author = request.form.get('author') publisher = request.form.get('publisher') category_id = request.form.get('category_id') tags = request.form.get('tags') isbn = request.form.get('isbn') publish_year = request.form.get('publish_year') description = request.form.get('description') stock = request.form.get('stock', type=int, default=0) price = request.form.get('price') # 表单验证 errors = [] if not title: errors.append('书名不能为空') if not author: errors.append('作者不能为空') # 检查ISBN是否已存在(如果提供了ISBN) if isbn: existing_book = Book.query.filter_by(isbn=isbn).first() if existing_book: errors.append(f'ISBN "{isbn}" 已存在,请检查ISBN或查找现有图书') if errors: for error in errors: flash(error, 'danger') categories = Category.query.all() # 保留已填写的表单数据 book_data = { 'title': title, 'author': author, 'publisher': publisher, 'category_id': category_id, 'tags': tags, 'isbn': isbn, 'publish_year': publish_year, 'description': description, 'stock': stock, 'price': price } return render_template('book/add.html', categories=categories, current_user=current_user, book=book_data) # 处理封面图片上传 cover_url = None if 'cover' in request.files: cover_file = request.files['cover'] if cover_file and cover_file.filename != '': try: # 更清晰的文件命名 original_filename = secure_filename(cover_file.filename) # 保留原始文件扩展名 _, ext = os.path.splitext(original_filename) if not ext: ext = '.jpg' # 默认扩展名 filename = f"{uuid.uuid4()}{ext}" upload_folder = os.path.join(current_app.static_folder, 'covers') # 确保上传目录存在 if not os.path.exists(upload_folder): os.makedirs(upload_folder) file_path = os.path.join(upload_folder, filename) cover_file.save(file_path) cover_url = f'/static/covers/{filename}' except Exception as e: current_app.logger.error(f"封面上传失败: {str(e)}") flash(f"封面上传失败: {str(e)}", 'warning') try: # 创建新图书 book = Book( title=title, author=author, publisher=publisher, category_id=category_id, tags=tags, isbn=isbn, publish_year=publish_year, description=description, cover_url=cover_url, stock=stock, price=price, status=1, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now() ) db.session.add(book) # 先提交以获取book的id db.session.commit() # 记录库存日志 - 在获取 book.id 后 if stock and int(stock) > 0: from app.models.inventory import InventoryLog inventory_log = InventoryLog( book_id=book.id, change_type='入库', change_amount=stock, after_stock=stock, operator_id=current_user.id, remark='新书入库', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) db.session.commit() # 记录操作日志 Log.add_log( action='添加图书', user_id=current_user.id, target_type='book', target_id=book.id, ip_address=request.remote_addr, description=f"添加图书: {title}, ISBN: {isbn}, 初始库存: {stock}" ) flash(f'《{title}》添加成功', 'success') return redirect(url_for('book.book_list')) except Exception as e: db.session.rollback() error_msg = str(e) # 记录详细错误日志 current_app.logger.error(f"添加图书失败: {error_msg}") # 记录操作失败日志 Log.add_log( action='添加图书失败', user_id=current_user.id, ip_address=request.remote_addr, description=f"添加图书失败: {title}, 错误: {error_msg}" ) flash(f'添加图书失败: {error_msg}', 'danger') categories = Category.query.all() # 保留已填写的表单数据 book_data = { 'title': title, 'author': author, 'publisher': publisher, 'category_id': category_id, 'tags': tags, 'isbn': isbn, 'publish_year': publish_year, 'description': description, 'stock': stock, 'price': price } return render_template('book/add.html', categories=categories, current_user=current_user, book=book_data) categories = Category.query.all() return render_template('book/add.html', categories=categories, current_user=current_user) # 编辑图书 @book_bp.route('/edit/', methods=['GET', 'POST']) @login_required @permission_required('manage_books') # 替换 @admin_required def edit_book(book_id): book = Book.query.get_or_404(book_id) if request.method == 'POST': # 获取表单数据 title = request.form.get('title') author = request.form.get('author') publisher = request.form.get('publisher') category_id = request.form.get('category_id') tags = request.form.get('tags') isbn = request.form.get('isbn') publish_year = request.form.get('publish_year') description = request.form.get('description') price = request.form.get('price') status = request.form.get('status', type=int) # 基本验证 if not title or not author: flash('书名和作者不能为空', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # ISBN验证 if isbn and isbn.strip(): # 确保ISBN不是空字符串 # 移除连字符和空格 clean_isbn = isbn.replace('-', '').replace(' ', '') # 长度检查 if len(clean_isbn) != 10 and len(clean_isbn) != 13: flash('ISBN必须是10位或13位', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # ISBN-10验证 if len(clean_isbn) == 10: # 检查前9位是否为数字 if not clean_isbn[:9].isdigit(): flash('ISBN-10的前9位必须是数字', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # 检查最后一位是否为数字或'X' if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'): flash('ISBN-10的最后一位必须是数字或X', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # 校验和验证 sum = 0 for i in range(9): sum += int(clean_isbn[i]) * (10 - i) check_digit = 10 if clean_isbn[9].upper() == 'X' else int(clean_isbn[9]) sum += check_digit if sum % 11 != 0: flash('ISBN-10校验和无效', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # ISBN-13验证 if len(clean_isbn) == 13: # 检查是否全是数字 if not clean_isbn.isdigit(): flash('ISBN-13必须全是数字', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # 校验和验证 sum = 0 for i in range(12): sum += int(clean_isbn[i]) * (1 if i % 2 == 0 else 3) check_digit = (10 - (sum % 10)) % 10 if check_digit != int(clean_isbn[12]): flash('ISBN-13校验和无效', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # 处理库存变更 new_stock = request.form.get('stock', type=int) or 0 # 默认为0而非None if new_stock != book.stock: from app.models.inventory import InventoryLog change_amount = new_stock - book.stock change_type = '入库' if change_amount > 0 else '出库' inventory_log = InventoryLog( book_id=book.id, change_type=change_type, change_amount=abs(change_amount), after_stock=new_stock, operator_id=current_user.id, remark=f'管理员编辑图书库存 - {book.title}', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) book.stock = new_stock # 处理封面图片上传 if 'cover' in request.files: cover_file = request.files['cover'] if cover_file and cover_file.filename != '': filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}") upload_folder = os.path.join(current_app.static_folder, 'covers') # 确保上传目录存在 if not os.path.exists(upload_folder): os.makedirs(upload_folder) file_path = os.path.join(upload_folder, filename) cover_file.save(file_path) book.cover_url = f'/static/covers/{filename}' # 记录更新前的图书信息 old_info = f"原信息: 书名={book.title}, 作者={book.author}, ISBN={book.isbn}, 库存={book.stock}" # 更新图书信息 book.title = title book.author = author book.publisher = publisher book.category_id = category_id book.tags = tags book.isbn = isbn book.publish_year = publish_year book.description = description book.price = price book.status = status book.updated_at = datetime.datetime.now() try: db.session.commit() # 记录操作日志 Log.add_log( action='编辑图书', user_id=current_user.id, target_type='book', target_id=book.id, ip_address=request.remote_addr, description=f"编辑图书: {title}, ISBN: {isbn}, 新库存: {new_stock}\n{old_info}" ) flash('图书信息更新成功', 'success') return redirect(url_for('book.book_list')) except Exception as e: db.session.rollback() # 记录操作失败日志 Log.add_log( action='编辑图书失败', user_id=current_user.id, target_type='book', target_id=book.id, ip_address=request.remote_addr, description=f"编辑图书失败: {title}, 错误: {str(e)}" ) flash(f'保存失败: {str(e)}', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # GET 请求 categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=current_user) # 删除图书 @book_bp.route('/delete/', methods=['POST']) @login_required @permission_required('manage_books') # 替换 @admin_required def delete_book(book_id): book = Book.query.get_or_404(book_id) # 检查该书是否有借阅记录 from app.models.borrow import BorrowRecord active_borrows = BorrowRecord.query.filter_by(book_id=book_id, status=1).count() if active_borrows > 0: # 记录操作失败日志 Log.add_log( action='删除图书失败', user_id=current_user.id, target_type='book', target_id=book_id, ip_address=request.remote_addr, description=f"删除图书失败: {book.title}, 原因: 该图书有未归还的借阅记录" ) return jsonify({'success': False, 'message': '该图书有未归还的借阅记录,无法删除'}) # 考虑软删除而不是物理删除 book.status = 0 # 0表示已删除/下架 book.updated_at = datetime.datetime.now() db.session.commit() # 记录操作日志 Log.add_log( action='下架图书', user_id=current_user.id, target_type='book', target_id=book_id, ip_address=request.remote_addr, description=f"下架图书: {book.title}, ISBN: {book.isbn}" ) return jsonify({'success': True, 'message': '图书已成功下架'}) # 图书分类管理 @book_bp.route('/categories', methods=['GET']) @login_required @permission_required('manage_categories') # 替换 @admin_required def category_list(): categories = Category.query.all() # 记录访问日志 Log.add_log( action='访问分类管理', user_id=current_user.id, ip_address=request.remote_addr, description="访问图书分类管理页面" ) return render_template('book/categories.html', categories=categories, current_user=current_user) # 添加分类 @book_bp.route('/categories/add', methods=['POST']) @login_required @permission_required('manage_categories') # 替换 @admin_required def add_category(): name = request.form.get('name') parent_id = request.form.get('parent_id') or None sort = request.form.get('sort', 0, type=int) if not name: return jsonify({'success': False, 'message': '分类名称不能为空'}) category = Category(name=name, parent_id=parent_id, sort=sort) db.session.add(category) db.session.commit() # 记录操作日志 Log.add_log( action='添加图书分类', user_id=current_user.id, ip_address=request.remote_addr, description=f"添加图书分类: {name}, 上级分类ID: {parent_id}, 排序: {sort}" ) return jsonify({'success': True, 'message': '分类添加成功', 'id': category.id, 'name': category.name}) # 编辑分类 @book_bp.route('/categories/edit/', methods=['POST']) @login_required @permission_required('manage_categories') # 替换 @admin_required def edit_category(category_id): category = Category.query.get_or_404(category_id) old_name = category.name old_parent_id = category.parent_id old_sort = category.sort name = request.form.get('name') parent_id = request.form.get('parent_id') or None sort = request.form.get('sort', 0, type=int) if not name: return jsonify({'success': False, 'message': '分类名称不能为空'}) category.name = name category.parent_id = parent_id category.sort = sort db.session.commit() # 记录操作日志 Log.add_log( action='编辑图书分类', user_id=current_user.id, target_type='category', target_id=category_id, ip_address=request.remote_addr, description=f"编辑图书分类: 从 [名称={old_name}, 上级={old_parent_id}, 排序={old_sort}] 修改为 [名称={name}, 上级={parent_id}, 排序={sort}]" ) return jsonify({'success': True, 'message': '分类更新成功'}) # 删除分类 @book_bp.route('/categories/delete/', methods=['POST']) @login_required @permission_required('manage_categories') # 替换 @admin_required def delete_category(category_id): category = Category.query.get_or_404(category_id) # 检查是否有书籍使用此分类 books_count = Book.query.filter_by(category_id=category_id).count() if books_count > 0: # 记录操作失败日志 Log.add_log( action='删除图书分类失败', user_id=current_user.id, target_type='category', target_id=category_id, ip_address=request.remote_addr, description=f"删除图书分类失败: {category.name}, 原因: 该分类下有{books_count}本图书" ) return jsonify({'success': False, 'message': f'该分类下有{books_count}本图书,无法删除'}) # 检查是否有子分类 children_count = Category.query.filter_by(parent_id=category_id).count() if children_count > 0: # 记录操作失败日志 Log.add_log( action='删除图书分类失败', user_id=current_user.id, target_type='category', target_id=category_id, ip_address=request.remote_addr, description=f"删除图书分类失败: {category.name}, 原因: 该分类下有{children_count}个子分类" ) return jsonify({'success': False, 'message': f'该分类下有{children_count}个子分类,无法删除'}) category_name = category.name # 保存分类名称以便记录日志 db.session.delete(category) db.session.commit() # 记录操作日志 Log.add_log( action='删除图书分类', user_id=current_user.id, target_type='category', target_id=category_id, ip_address=request.remote_addr, description=f"删除图书分类: {category_name}" ) return jsonify({'success': True, 'message': '分类删除成功'}) # 批量导入图书 @book_bp.route('/import', methods=['GET', 'POST']) @login_required @permission_required('import_export_books') def import_books(): if request.method == 'POST': if 'file' not in request.files: flash('未选择文件', 'danger') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('未选择文件', 'danger') return redirect(request.url) if file and file.filename.endswith(('.xlsx', '.xls')): try: # 添加详细日志 current_app.logger.info(f"开始导入Excel文件: {file.filename}") # 读取Excel文件 df = pd.read_excel(file) current_app.logger.info(f"成功读取Excel文件,共有{len(df)}行数据") # 打印DataFrame的列名和前几行数据,帮助诊断问题 current_app.logger.info(f"Excel文件列名: {df.columns.tolist()}") current_app.logger.info(f"Excel文件前两行数据:\n{df.head(2)}") success_count = 0 update_count = 0 # 新增:更新计数 error_count = 0 errors = [] # 检查必填列是否存在 required_columns = ['title', 'author'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error_msg = f"Excel文件缺少必要的列: {', '.join(missing_columns)}" current_app.logger.error(error_msg) flash(error_msg, 'danger') return redirect(request.url) # 处理每一行数据 for index, row in df.iterrows(): try: current_app.logger.debug(f"处理第{index + 2}行数据") # 检查必填字段 if pd.isna(row.get('title')) or pd.isna(row.get('author')): error_msg = f'第{index + 2}行: 书名或作者为空' current_app.logger.warning(error_msg) errors.append(error_msg) error_count += 1 continue # 处理数据类型转换 try: stock = int(row.get('stock')) if not pd.isna(row.get('stock')) else 0 except ValueError: error_msg = f'第{index + 2}行: 库存数量必须是整数' current_app.logger.warning(error_msg) errors.append(error_msg) error_count += 1 continue try: price = float(row.get('price')) if not pd.isna(row.get('price')) else None except ValueError: error_msg = f'第{index + 2}行: 价格必须是数字' current_app.logger.warning(error_msg) errors.append(error_msg) error_count += 1 continue try: category_id = int(row.get('category_id')) if not pd.isna(row.get('category_id')) else None if category_id and not Category.query.get(category_id): error_msg = f'第{index + 2}行: 分类ID {category_id} 不存在' current_app.logger.warning(error_msg) errors.append(error_msg) error_count += 1 continue except ValueError: error_msg = f'第{index + 2}行: 分类ID必须是整数' current_app.logger.warning(error_msg) errors.append(error_msg) error_count += 1 continue # 检查ISBN是否已存在 isbn = row.get('isbn') existing_book = None if isbn and not pd.isna(isbn): isbn = str(isbn) existing_book = Book.query.filter_by(isbn=isbn).first() # 如果ISBN已存在,检查状态 if existing_book: if existing_book.status == 1: # 活跃的图书,不允许重复导入 error_msg = f'第{index + 2}行: ISBN {isbn} 已存在于活跃图书中' current_app.logger.warning(error_msg) errors.append(error_msg) error_count += 1 continue else: # 已软删除的图书,更新它 current_app.logger.info(f"第{index + 2}行: 发现已删除的ISBN {isbn},将更新该记录") # 更新图书信息 existing_book.title = row.get('title') existing_book.author = row.get('author') existing_book.publisher = row.get('publisher') if not pd.isna( row.get('publisher')) else None existing_book.category_id = category_id existing_book.tags = row.get('tags') if not pd.isna(row.get('tags')) else None existing_book.publish_year = str(row.get('publish_year')) if not pd.isna( row.get('publish_year')) else None existing_book.description = row.get('description') if not pd.isna( row.get('description')) else None existing_book.cover_url = row.get('cover_url') if not pd.isna( row.get('cover_url')) else None existing_book.price = price existing_book.status = 1 # 重新激活图书 existing_book.updated_at = datetime.datetime.now() # 处理库存变更 stock_change = stock - existing_book.stock existing_book.stock = stock # 创建库存日志 if stock_change != 0: from app.models.inventory import InventoryLog change_type = '入库' if stock_change > 0 else '出库' inventory_log = InventoryLog( book_id=existing_book.id, change_type=change_type, change_amount=abs(stock_change), after_stock=stock, operator_id=current_user.id, remark='批量导入更新图书', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) update_count += 1 continue # 继续处理下一行 # 创建新书籍记录 book = Book( title=row.get('title'), author=row.get('author'), publisher=row.get('publisher') if not pd.isna(row.get('publisher')) else None, category_id=category_id, tags=row.get('tags') if not pd.isna(row.get('tags')) else None, isbn=isbn, publish_year=str(row.get('publish_year')) if not pd.isna(row.get('publish_year')) else None, description=row.get('description') if not pd.isna(row.get('description')) else None, cover_url=row.get('cover_url') if not pd.isna(row.get('cover_url')) else None, stock=stock, price=price, status=1, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now() ) db.session.add(book) # 提交以获取book的id db.session.flush() current_app.logger.debug(f"书籍添加成功: {book.title}, ID: {book.id}") # 创建库存日志 if book.stock > 0: from app.models.inventory import InventoryLog inventory_log = InventoryLog( book_id=book.id, change_type='入库', change_amount=book.stock, after_stock=book.stock, operator_id=current_user.id, remark='批量导入图书', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) current_app.logger.debug(f"库存日志添加成功: 书籍ID {book.id}, 数量 {book.stock}") success_count += 1 except Exception as e: error_msg = f'第{index + 2}行: {str(e)}' current_app.logger.error(f"处理第{index + 2}行时出错: {str(e)}") errors.append(error_msg) error_count += 1 db.session.rollback() # 每行错误单独回滚 try: # 最终提交所有成功的记录 if success_count > 0 or update_count > 0: db.session.commit() current_app.logger.info(f"导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}条") # 记录操作日志 Log.add_log( action='批量导入图书', user_id=current_user.id, ip_address=request.remote_addr, description=f"批量导入图书: 新增{success_count}条,更新{update_count}条,失败{error_count}条,文件名:{file.filename}" ) # 输出详细的错误信息 if success_count > 0 or update_count > 0: flash(f'导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}条', 'success') else: flash(f'导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}条', 'warning') if errors: error_display = '
'.join(errors[:10]) if len(errors) > 10: error_display += f'
...等共{len(errors)}个错误' flash(error_display, 'warning') return redirect(url_for('book.book_list')) except Exception as commit_error: db.session.rollback() error_msg = f"提交数据库事务失败: {str(commit_error)}" current_app.logger.error(error_msg) flash(error_msg, 'danger') return redirect(request.url) except Exception as e: db.session.rollback() # 记录详细错误日志 error_msg = f"批量导入图书失败: {str(e)}" current_app.logger.error(error_msg) # 记录操作失败日志 Log.add_log( action='批量导入图书失败', user_id=current_user.id, ip_address=request.remote_addr, description=f"批量导入图书失败: {str(e)}, 文件名:{file.filename}" ) flash(f'导入失败: {str(e)}', 'danger') return redirect(request.url) else: flash('只支持Excel文件(.xlsx, .xls)', 'danger') return redirect(request.url) return render_template('book/import.html', current_user=current_user) # 导出图书 @book_bp.route('/export', methods=['GET']) @login_required @permission_required('import_export_books') def export_books(): try: # 获取所有活跃的图书 books = Book.query.filter_by(status=1).all() # 创建工作簿和工作表 output = BytesIO() workbook = xlsxwriter.Workbook(output) worksheet = workbook.add_worksheet() # 添加表头 headers = ['ID', '书名', '作者', '出版社', '分类', '标签', 'ISBN', '出版年份', '描述', '封面链接', '库存', '价格', '创建时间', '更新时间'] for col_num, header in enumerate(headers): worksheet.write(0, col_num, header) # 不使用status过滤条件,因为Category模型没有status字段 category_dict = {} try: categories = db.session.execute( text("SELECT id, name FROM categories") ).fetchall() for cat in categories: category_dict[cat[0]] = cat[1] except Exception as ex: current_app.logger.warning(f"获取分类失败: {str(ex)}") # 添加数据行 for row_num, book in enumerate(books, 1): # 获取分类名称(如果有) category_name = category_dict.get(book.category_id, "") if book.category_id else "" # 格式化日期 created_at = book.created_at.strftime('%Y-%m-%d %H:%M:%S') if book.created_at else "" updated_at = book.updated_at.strftime('%Y-%m-%d %H:%M:%S') if book.updated_at else "" # 写入图书数据 worksheet.write(row_num, 0, book.id) worksheet.write(row_num, 1, book.title) worksheet.write(row_num, 2, book.author) worksheet.write(row_num, 3, book.publisher or "") worksheet.write(row_num, 4, category_name) worksheet.write(row_num, 5, book.tags or "") worksheet.write(row_num, 6, book.isbn or "") worksheet.write(row_num, 7, book.publish_year or "") worksheet.write(row_num, 8, book.description or "") worksheet.write(row_num, 9, book.cover_url or "") worksheet.write(row_num, 10, book.stock) worksheet.write(row_num, 11, float(book.price) if book.price else 0) worksheet.write(row_num, 12, created_at) worksheet.write(row_num, 13, updated_at) # 关闭工作簿 workbook.close() # 设置响应 output.seek(0) # 记录操作日志 Log.add_log( action='导出图书', user_id=current_user.id, ip_address=request.remote_addr, description=f"导出了{len(books)}本图书" ) # 返回文件 return send_file( output, as_attachment=True, download_name=f"图书导出_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx", mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) except Exception as e: current_app.logger.error(f"导出图书失败: {str(e)}") flash(f'导出失败: {str(e)}', 'danger') return redirect(url_for('book.book_list')) @book_bp.route('/test-permissions') def test_permissions(): """测试当前用户权限""" if not current_user.is_authenticated: return "未登录" return f"""

用户权限信息

用户名: {current_user.username}

角色ID: {current_user.role_id}

是否管理员: {'是' if current_user.role_id == 1 else '否'}

尝试访问管理页面

""" # 图书浏览页面 - 不需要修改,已经只有@login_required @book_bp.route('/browse') @login_required def browse_books(): """图书浏览页面 - 面向普通用户的友好界面""" page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 12, type=int) # 增加每页数量 # 只显示状态为1的图书(未下架的图书) query = Book.query.filter_by(status=1) # 搜索功能 search = request.args.get('search', '') if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) # 分类筛选 category_id = request.args.get('category_id', type=int) if category_id: query = query.filter_by(category_id=category_id) # 排序 sort = request.args.get('sort', 'id') order = request.args.get('order', 'desc') if order == 'desc': query = query.order_by(getattr(Book, sort).desc()) else: query = query.order_by(getattr(Book, sort)) pagination = query.paginate(page=page, per_page=per_page) books = pagination.items # 获取所有分类供筛选使用 categories = Category.query.all() # 记录访问日志 Log.add_log( action='浏览图书', user_id=current_user.id, ip_address=request.remote_addr, description=f"浏览图书: 搜索={search}, 分类={category_id}, 排序={sort} {order}" ) return render_template('book/browse.html', books=books, pagination=pagination, search=search, categories=categories, category_id=category_id, sort=sort, order=order) @book_bp.route('/template/download') @login_required @permission_required('import_export_books') def download_template(): """下载图书导入模板""" import tempfile import os from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter from flask import after_this_request, current_app as app, send_file # 创建工作簿和工作表 wb = Workbook() ws = wb.active ws.title = "图书导入模板" # 定义样式 header_font = Font(name='微软雅黑', size=11, bold=True, color="FFFFFF") header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") required_font = Font(name='微软雅黑', size=11, bold=True, color="FF0000") optional_font = Font(name='微软雅黑', size=11) center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) left_alignment = Alignment(vertical='center', wrap_text=True) # 定义边框样式 thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # 定义列宽 column_widths = { 'A': 30, # title 'B': 20, # author 'C': 20, # publisher 'D': 15, # category_id 'E': 25, # tags 'F': 15, # isbn 'G': 15, # publish_year 'H': 40, # description 'I': 30, # cover_url 'J': 10, # stock 'K': 10, # price } # 设置列宽 for col, width in column_widths.items(): ws.column_dimensions[col].width = width # 定义字段信息 fields = [ {'name': 'title', 'desc': '图书标题', 'required': True, 'example': '哈利·波特与魔法石'}, {'name': 'author', 'desc': '作者名称', 'required': True, 'example': 'J.K.罗琳'}, {'name': 'publisher', 'desc': '出版社', 'required': False, 'example': '人民文学出版社'}, {'name': 'category_id', 'desc': '分类ID', 'required': False, 'example': '1'}, {'name': 'tags', 'desc': '标签', 'required': False, 'example': '魔幻,冒险,青少年文学'}, {'name': 'isbn', 'desc': 'ISBN编号', 'required': False, 'example': '9787020042494'}, {'name': 'publish_year', 'desc': '出版年份', 'required': False, 'example': '2000'}, {'name': 'description', 'desc': '图书简介', 'required': False, 'example': '这是一本关于魔法的书籍,讲述了小男孩哈利...'}, {'name': 'cover_url', 'desc': '封面图片URL', 'required': False, 'example': 'https://example.com/covers/hp.jpg'}, {'name': 'stock', 'desc': '库存数量', 'required': False, 'example': '10'}, {'name': 'price', 'desc': '价格', 'required': False, 'example': '39.5'}, ] # 添加标题行 ws.append([f"{field['name']}" for field in fields]) # 设置标题行样式 for col_idx, field in enumerate(fields, start=1): cell = ws.cell(row=1, column=col_idx) cell.font = header_font cell.fill = header_fill cell.alignment = center_alignment cell.border = thin_border # 添加示例数据行 ws.append([field['example'] for field in fields]) # 设置示例行样式 for col_idx, field in enumerate(fields, start=1): cell = ws.cell(row=2, column=col_idx) cell.alignment = left_alignment cell.border = thin_border if field['required']: cell.font = required_font else: cell.font = optional_font # 冻结首行 ws.freeze_panes = "A2" # 添加说明工作表 instructions_ws = wb.create_sheet(title="填写说明") # 设置说明工作表的列宽 instructions_ws.column_dimensions['A'].width = 15 instructions_ws.column_dimensions['B'].width = 20 instructions_ws.column_dimensions['C'].width = 60 # 添加说明标题 instructions_ws.append(["字段名", "说明", "备注"]) # 设置标题行样式 for col_idx in range(1, 4): cell = instructions_ws.cell(row=1, column=col_idx) cell.font = header_font cell.fill = header_fill cell.alignment = center_alignment cell.border = thin_border # 字段详细说明 notes = { 'title': "图书的完整名称,例如:'哈利·波特与魔法石'。必须填写且不能为空。", 'author': "图书的作者名称,例如:'J.K.罗琳'。必须填写且不能为空。如有多个作者,请用逗号分隔。", 'publisher': "出版社名称,例如:'人民文学出版社'。", 'category_id': "图书分类的系统ID。请在系统的分类管理页面查看ID。如果不确定,可以留空,系统将使用默认分类。", 'tags': "图书的标签,多个标签请用英文逗号分隔,例如:'魔幻,冒险,青少年文学'。", 'isbn': "图书的ISBN号码,例如:'9787020042494'。请输入完整的13位或10位ISBN。", 'publish_year': "图书的出版年份,例如:'2000'。请输入4位数字年份。", 'description': "图书的简介或摘要。可以输入详细的描述信息,系统支持换行和基本格式。", 'cover_url': "图书封面的在线URL地址。如果有图片网址,系统将自动下载并设置为封面。", 'stock': "图书的库存数量,例如:'10'。请输入整数。", 'price': "图书的价格,例如:'39.5'。可以输入小数。" } # 添加字段说明 for idx, field in enumerate(fields, start=2): required_text = "【必填】" if field['required'] else "【选填】" instructions_ws.append([ field['name'], f"{field['desc']} {required_text}", notes.get(field['name'], "") ]) # 设置单元格样式 for col_idx in range(1, 4): cell = instructions_ws.cell(row=idx, column=col_idx) cell.alignment = left_alignment cell.border = thin_border if field['required'] and col_idx == 2: cell.font = required_font else: cell.font = optional_font # 添加示例行 instructions_ws.append(["", "", ""]) instructions_ws.append(["示例", "", "以下是完整的示例数据:"]) example_data = [ {"title": "哈利·波特与魔法石", "author": "J.K.罗琳", "publisher": "人民文学出版社", "category_id": "1", "tags": "魔幻,冒险,青少年文学", "isbn": "9787020042494", "publish_year": "2000", "description": "这是一本关于魔法的书籍,讲述了小男孩哈利...", "cover_url": "https://example.com/covers/hp.jpg", "stock": "10", "price": "39.5"}, {"title": "三体", "author": "刘慈欣", "publisher": "重庆出版社", "category_id": "2", "tags": "科幻,硬科幻,中国科幻", "isbn": "9787536692930", "publish_year": "2008", "description": "文化大革命如火如荼进行的同时...", "cover_url": "https://example.com/covers/threebody.jpg", "stock": "15", "price": "59.8"}, {"title": "平凡的世界", "author": "路遥", "publisher": "北京十月文艺出版社", "category_id": "3", "tags": "文学,现实主义,农村", "isbn": "9787530216781", "publish_year": "2017", "description": "这是一部全景式地表现中国当代城乡社会...", "cover_url": "https://example.com/covers/world.jpg", "stock": "8", "price": "128.0"} ] # 添加3个完整示例到主模板工作表 for example in example_data: row_data = [example.get(field['name'], '') for field in fields] ws.append(row_data) # 设置示例数据样式 for row_idx in range(3, 6): for col_idx in range(1, len(fields) + 1): cell = ws.cell(row=row_idx, column=col_idx) cell.alignment = left_alignment cell.border = thin_border # 使用临时文件保存工作簿 fd, temp_path = tempfile.mkstemp(suffix='.xlsx') os.close(fd) try: wb.save(temp_path) # 注册一个函数在响应完成后删除临时文件 @after_this_request def remove_file(response): try: os.unlink(temp_path) except Exception as error: app.logger.error("删除临时文件出错: %s", error) return response response = send_file( temp_path, as_attachment=True, download_name='图书导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) # 添加防止缓存的头信息 response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response except Exception as e: # 确保在出错时也删除临时文件 try: os.unlink(temp_path) except: pass app.logger.error("生成模板文件出错: %s", str(e)) return {"error": "生成模板文件失败,请稍后再试"}, 500 ================================================================================ File: ./app/controllers/statistics.py ================================================================================ # app/controllers/statistics.py from flask import Blueprint, render_template, jsonify, request from flask_login import login_required, current_user from app.models.book import Book, db from app.models.borrow import BorrowRecord from app.models.user import User from app.utils.auth import permission_required # 修改为导入permission_required from app.models.log import Log # 导入日志模型 from sqlalchemy import func, case, desc, and_ from datetime import datetime, timedelta import calendar statistics_bp = Blueprint('statistics', __name__, url_prefix='/statistics') @statistics_bp.route('/') @login_required @permission_required('view_statistics') # 替代 @admin_required def index(): """统计分析首页""" # 记录访问统计分析首页的日志 Log.add_log( action="访问统计分析", user_id=current_user.id, target_type="statistics", description="访问统计分析首页" ) return render_template('statistics/index.html') @statistics_bp.route('/book-ranking') @login_required @permission_required('view_statistics') # 替代 @admin_required def book_ranking(): """热门图书排行榜页面""" # 记录访问热门图书排行的日志 Log.add_log( action="查看统计数据", user_id=current_user.id, target_type="statistics", description="查看热门图书排行榜" ) return render_template('statistics/book_ranking.html') @statistics_bp.route('/api/book-ranking') @login_required @permission_required('view_statistics') # 替代 @admin_required def api_book_ranking(): """获取热门图书排行数据API""" time_range = request.args.get('time_range', 'month') limit = request.args.get('limit', 10, type=int) # 记录获取热门图书排行数据的日志 Log.add_log( action="获取数据", user_id=current_user.id, target_type="statistics", description=f"获取热门图书排行数据(时间范围:{time_range}, 数量:{limit})" ) # 根据时间范围设置过滤条件 if time_range == 'week': start_date = datetime.now() - timedelta(days=7) elif time_range == 'month': start_date = datetime.now() - timedelta(days=30) elif time_range == 'year': start_date = datetime.now() - timedelta(days=365) else: # all time start_date = datetime(1900, 1, 1) # 查询借阅次数最多的图书 popular_books = db.session.query( Book.id, Book.title, Book.author, Book.cover_url, func.count(BorrowRecord.id).label('borrow_count') ).join( BorrowRecord, Book.id == BorrowRecord.book_id ).filter( BorrowRecord.borrow_date >= start_date ).group_by( Book.id ).order_by( desc('borrow_count') ).limit(limit).all() result = [ { 'id': book.id, 'title': book.title, 'author': book.author, 'cover_url': book.cover_url, 'borrow_count': book.borrow_count } for book in popular_books ] return jsonify(result) @statistics_bp.route('/borrow-statistics') @login_required @permission_required('view_statistics') # 替代 @admin_required def borrow_statistics(): """借阅统计分析页面""" # 记录访问借阅统计分析的日志 Log.add_log( action="查看统计数据", user_id=current_user.id, target_type="statistics", description="查看借阅统计分析" ) return render_template('statistics/borrow_statistics.html') @statistics_bp.route('/api/borrow-trend') @login_required @permission_required('view_statistics') # 替代 @admin_required def api_borrow_trend(): """获取借阅趋势数据API""" time_range = request.args.get('time_range', 'month') now = datetime.now() # 记录获取借阅趋势数据的日志 Log.add_log( action="获取数据", user_id=current_user.id, target_type="statistics", description=f"获取借阅趋势数据(时间范围:{time_range})" ) if time_range == 'week': # 获取过去7天每天的借阅和归还数量 start_date = datetime.now() - timedelta(days=6) results = [] for i in range(7): day = start_date + timedelta(days=i) day_start = day.replace(hour=0, minute=0, second=0, microsecond=0) day_end = day.replace(hour=23, minute=59, second=59, microsecond=999999) # 查询当天借阅量 borrow_count = BorrowRecord.query.filter( BorrowRecord.borrow_date >= day_start, BorrowRecord.borrow_date <= day_end ).count() # 查询当天归还量 return_count = BorrowRecord.query.filter( BorrowRecord.return_date >= day_start, BorrowRecord.return_date <= day_end ).count() # 当天逾期未还的数量 overdue_count = BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), # 未归还 BorrowRecord.due_date < now # 应还日期早于当前时间 ).count() results.append({ 'date': day.strftime('%m-%d'), 'borrow': borrow_count, 'return': return_count, 'overdue': overdue_count }) return jsonify(results) elif time_range == 'month': # 获取过去30天每天的借阅和归还数量 start_date = datetime.now() - timedelta(days=29) results = [] for i in range(30): day = start_date + timedelta(days=i) day_start = day.replace(hour=0, minute=0, second=0, microsecond=0) day_end = day.replace(hour=23, minute=59, second=59, microsecond=999999) # 查询当天借阅量 borrow_count = BorrowRecord.query.filter( BorrowRecord.borrow_date >= day_start, BorrowRecord.borrow_date <= day_end ).count() # 查询当天归还量 return_count = BorrowRecord.query.filter( BorrowRecord.return_date >= day_start, BorrowRecord.return_date <= day_end ).count() # 当天逾期未还的数量 now = datetime.now() overdue_count = BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now ).count() results.append({ 'date': day.strftime('%m-%d'), 'borrow': borrow_count, 'return': return_count, 'overdue': overdue_count }) return jsonify(results) elif time_range == 'year': # 获取过去12个月每月的借阅和归还数量 current_month = datetime.now().month current_year = datetime.now().year results = [] for i in range(12): # 计算月份和年份 month = (current_month - i) % 12 if month == 0: month = 12 year = current_year - ((i - (current_month - 1)) // 12) # 计算该月的开始和结束日期 days_in_month = calendar.monthrange(year, month)[1] month_start = datetime(year, month, 1) month_end = datetime(year, month, days_in_month, 23, 59, 59, 999999) # 查询当月借阅量 borrow_count = BorrowRecord.query.filter( BorrowRecord.borrow_date >= month_start, BorrowRecord.borrow_date <= month_end ).count() # 查询当月归还量 return_count = BorrowRecord.query.filter( BorrowRecord.return_date >= month_start, BorrowRecord.return_date <= month_end ).count() # 当月逾期未还的数量 now = datetime.now() overdue_count = BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now ).count() results.append({ 'date': f'{year}-{month:02d}', 'borrow': borrow_count, 'return': return_count, 'overdue': overdue_count }) # 按时间顺序排序 results.reverse() return jsonify(results) return jsonify([]) @statistics_bp.route('/api/category-distribution') @login_required @permission_required('view_statistics') # 替代 @admin_required def api_category_distribution(): """获取图书分类分布数据API""" # 记录获取图书分类分布数据的日志 Log.add_log( action="获取数据", user_id=current_user.id, target_type="statistics", description="获取图书分类分布数据" ) # 计算每个分类的总借阅次数 category_stats = db.session.query( Book.category_id, func.count(BorrowRecord.id).label('borrow_count') ).join( BorrowRecord, Book.id == BorrowRecord.book_id ).group_by( Book.category_id ).all() # 获取分类名称 from app.models.book import Category categories = {cat.id: cat.name for cat in Category.query.all()} # 准备结果 result = [ { 'category': categories.get(stat.category_id, '未分类'), 'count': stat.borrow_count } for stat in category_stats if stat.category_id is not None ] # 添加未分类数据 uncategorized = next((stat for stat in category_stats if stat.category_id is None), None) if uncategorized: result.append({'category': '未分类', 'count': uncategorized.borrow_count}) return jsonify(result) @statistics_bp.route('/user-activity') @login_required @permission_required('view_statistics') # 替代 @admin_required def user_activity(): """用户活跃度分析页面""" # 记录访问用户活跃度分析的日志 Log.add_log( action="查看统计数据", user_id=current_user.id, target_type="statistics", description="查看用户活跃度分析" ) return render_template('statistics/user_activity.html') @statistics_bp.route('/api/user-activity') @login_required @permission_required('view_statistics') # 替代 @admin_required def api_user_activity(): """获取用户活跃度数据API""" # 记录获取用户活跃度数据的日志 Log.add_log( action="获取数据", user_id=current_user.id, target_type="statistics", description="获取用户活跃度数据" ) # 查询最活跃的用户(借阅量最多) active_users = db.session.query( User.id, User.username, User.nickname, func.count(BorrowRecord.id).label('borrow_count') ).join( BorrowRecord, User.id == BorrowRecord.user_id ).group_by( User.id ).order_by( desc('borrow_count') ).limit(10).all() result = [ { 'id': user.id, 'username': user.username, 'nickname': user.nickname or user.username, 'borrow_count': user.borrow_count } for user in active_users ] return jsonify(result) @statistics_bp.route('/overdue-analysis') @login_required @permission_required('view_statistics') # 替代 @admin_required def overdue_analysis(): """逾期分析页面""" # 记录访问逾期分析的日志 Log.add_log( action="查看统计数据", user_id=current_user.id, target_type="statistics", description="查看借阅逾期分析" ) return render_template('statistics/overdue_analysis.html') @statistics_bp.route('/api/overdue-statistics') @login_required @permission_required('view_statistics') # 替代 @admin_required def api_overdue_statistics(): """获取逾期统计数据API""" # 记录获取逾期统计数据的日志 Log.add_log( action="获取数据", user_id=current_user.id, target_type="statistics", description="获取借阅逾期统计数据" ) now = datetime.now() # 计算总借阅量 total_borrows = BorrowRecord.query.count() # 计算已归还的逾期借阅 returned_overdue = BorrowRecord.query.filter( BorrowRecord.return_date.isnot(None), BorrowRecord.return_date > BorrowRecord.due_date ).count() # 计算未归还的逾期借阅 current_overdue = BorrowRecord.query.filter( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now ).count() # 计算总逾期率 overdue_rate = round((returned_overdue + current_overdue) / total_borrows * 100, 2) if total_borrows > 0 else 0 # 计算各逾期时长区间的数量 overdue_range_data = [] # 1-7天逾期 range1 = BorrowRecord.query.filter( and_( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now, BorrowRecord.due_date >= now - timedelta(days=7) ) ).count() overdue_range_data.append({'range': '1-7天', 'count': range1}) # 8-14天逾期 range2 = BorrowRecord.query.filter( and_( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now - timedelta(days=7), BorrowRecord.due_date >= now - timedelta(days=14) ) ).count() overdue_range_data.append({'range': '8-14天', 'count': range2}) # 15-30天逾期 range3 = BorrowRecord.query.filter( and_( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now - timedelta(days=14), BorrowRecord.due_date >= now - timedelta(days=30) ) ).count() overdue_range_data.append({'range': '15-30天', 'count': range3}) # 30天以上逾期 range4 = BorrowRecord.query.filter( and_( BorrowRecord.return_date.is_(None), BorrowRecord.due_date < now - timedelta(days=30) ) ).count() overdue_range_data.append({'range': '30天以上', 'count': range4}) result = { 'total_borrows': total_borrows, 'returned_overdue': returned_overdue, 'current_overdue': current_overdue, 'overdue_rate': overdue_rate, 'overdue_ranges': overdue_range_data } return jsonify(result) ================================================================================ File: ./app/controllers/borrow.py ================================================================================ from flask import Blueprint, request, redirect, url_for, flash, render_template, jsonify from flask_login import current_user, login_required from app.models.book import Book from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog from app.models.user import db, User from app.models.log import Log # 导入日志模型 import datetime from app.utils.auth import permission_required # 修改导入,使用permission_required而不是admin_required # 创建借阅蓝图 borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow') @borrow_bp.route('/book', methods=['POST']) @login_required def borrow_book(): book_id = request.form.get('book_id', type=int) borrow_days = request.form.get('borrow_days', type=int, default=14) if not book_id: flash('请选择要借阅的图书', 'danger') return redirect(url_for('book.book_list')) # 检查用户当前借阅数量是否达到上限(5本) current_borrows_count = BorrowRecord.query.filter_by( user_id=current_user.id, status=1 # 1表示借阅中 ).count() if current_borrows_count >= 5: flash('您当前已借阅5本图书,达到借阅上限。请先归还后再借阅新书。', 'warning') return redirect(url_for('book.book_detail', book_id=book_id)) book = Book.query.get_or_404(book_id) # 检查库存 if book.stock <= 0: flash(f'《{book.title}》当前无库存,无法借阅', 'danger') return redirect(url_for('book.book_detail', book_id=book_id)) # 检查当前用户是否已借阅此书 existing_borrow = BorrowRecord.query.filter_by( user_id=current_user.id, book_id=book_id, status=1 # 1表示借阅中 ).first() if existing_borrow: flash(f'您已借阅《{book.title}》,请勿重复借阅', 'warning') return redirect(url_for('book.book_detail', book_id=book_id)) try: # 创建借阅记录 now = datetime.datetime.now() due_date = now + datetime.timedelta(days=borrow_days) borrow_record = BorrowRecord( user_id=current_user.id, book_id=book_id, borrow_date=now, due_date=due_date, status=1, # 1表示借阅中 created_at=now, updated_at=now ) # 更新图书库存 book.stock -= 1 book.updated_at = now db.session.add(borrow_record) db.session.commit() # 添加库存变更日志 inventory_log = InventoryLog( book_id=book_id, change_type='借出', change_amount=-1, after_stock=book.stock, operator_id=current_user.id, remark='用户借书', changed_at=now ) db.session.add(inventory_log) # 添加系统操作日志 Log.add_log( action='借阅图书', user_id=current_user.id, target_type='book', target_id=book_id, ip_address=request.remote_addr, description=f'用户借阅图书《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}' ) db.session.commit() flash(f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还', 'success') except Exception as e: db.session.rollback() flash(f'借阅失败: {str(e)}', 'danger') return redirect(url_for('book.book_detail', book_id=book_id)) @borrow_bp.route('/add/', methods=['POST']) @login_required def add_borrow(book_id): # 验证图书存在 book = Book.query.get_or_404(book_id) # 检查用户当前借阅数量是否达到上限(5本) current_borrows_count = BorrowRecord.query.filter_by( user_id=current_user.id, status=1 # 1表示借阅中 ).count() if current_borrows_count >= 5: return jsonify({ 'success': False, 'message': '您当前已借阅5本图书,达到借阅上限。请先归还后再借阅新书。' }) # 默认借阅天数 borrow_days = 14 # 检查库存 if book.stock <= 0: return jsonify({ 'success': False, 'message': f'《{book.title}》当前无库存,无法借阅' }) # 检查是否已借阅 existing_borrow = BorrowRecord.query.filter_by( user_id=current_user.id, book_id=book_id, status=1 # 1表示借阅中 ).first() if existing_borrow: return jsonify({ 'success': False, 'message': f'您已借阅《{book.title}》,请勿重复借阅' }) try: # 创建借阅记录 now = datetime.datetime.now() due_date = now + datetime.timedelta(days=borrow_days) borrow_record = BorrowRecord( user_id=current_user.id, book_id=book_id, borrow_date=now, due_date=due_date, status=1, # 1表示借阅中 created_at=now, updated_at=now ) # 更新图书库存 book.stock -= 1 book.updated_at = now db.session.add(borrow_record) db.session.commit() # 添加库存变更日志 inventory_log = InventoryLog( book_id=book_id, change_type='借出', change_amount=-1, after_stock=book.stock, operator_id=current_user.id, remark='用户借书', changed_at=now ) db.session.add(inventory_log) # 添加系统操作日志 Log.add_log( action='借阅图书', user_id=current_user.id, target_type='book', target_id=book_id, ip_address=request.remote_addr, description=f'用户借阅图书《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}' ) db.session.commit() return jsonify({ 'success': True, 'message': f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还' }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': f'借阅失败: {str(e)}' }) @borrow_bp.route('/return/', methods=['POST']) @login_required def return_book(borrow_id): """还书操作""" # 查找借阅记录 borrow_record = BorrowRecord.query.get_or_404(borrow_id) # 检查是否是自己的借阅记录或者是管理员 if borrow_record.user_id != current_user.id and not current_user.has_permission('manage_borrows'): return jsonify({ 'success': False, 'message': '您无权执行此操作' }) # 检查是否已还 if borrow_record.status != 1: return jsonify({ 'success': False, 'message': '此书已归还,请勿重复操作' }) try: book = Book.query.get(borrow_record.book_id) now = datetime.datetime.now() # 更新借阅记录 borrow_record.status = 0 # 0表示已归还 borrow_record.return_date = now borrow_record.updated_at = now # 更新图书库存 book.stock += 1 book.updated_at = now db.session.commit() # 添加库存变更日志 inventory_log = InventoryLog( book_id=borrow_record.book_id, change_type='归还', change_amount=1, after_stock=book.stock, operator_id=current_user.id, remark='用户还书', changed_at=now ) db.session.add(inventory_log) # 添加系统操作日志 # 判断是否逾期归还 is_overdue = now > borrow_record.due_date overdue_msg = '(逾期归还)' if is_overdue else '' Log.add_log( action='归还图书', user_id=current_user.id, target_type='book', target_id=borrow_record.book_id, ip_address=request.remote_addr, description=f'用户归还图书《{book.title}》{overdue_msg}' ) db.session.commit() return jsonify({ 'success': True, 'message': f'成功归还《{book.title}》' }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': f'归还失败: {str(e)}' }) @borrow_bp.route('/renew/', methods=['POST']) @login_required def renew_book(borrow_id): """续借操作""" # 查找借阅记录 borrow_record = BorrowRecord.query.get_or_404(borrow_id) # 检查是否是自己的借阅记录或者是管理员 if borrow_record.user_id != current_user.id and not current_user.has_permission('manage_borrows'): return jsonify({ 'success': False, 'message': '您无权执行此操作' }) # 检查是否已还 if borrow_record.status != 1: return jsonify({ 'success': False, 'message': '此书已归还,无法续借' }) # 检查续借次数限制(最多续借2次) if borrow_record.renew_count >= 2: return jsonify({ 'success': False, 'message': '此书已达到最大续借次数,无法继续续借' }) try: now = datetime.datetime.now() book = Book.query.get(borrow_record.book_id) # 检查是否已逾期 if now > borrow_record.due_date: return jsonify({ 'success': False, 'message': '此书已逾期,请先归还并处理逾期情况' }) # 续借14天 new_due_date = borrow_record.due_date + datetime.timedelta(days=14) # 更新借阅记录 borrow_record.due_date = new_due_date borrow_record.renew_count += 1 borrow_record.updated_at = now # 添加系统操作日志 Log.add_log( action='续借图书', user_id=current_user.id, target_type='book', target_id=borrow_record.book_id, ip_address=request.remote_addr, description=f'用户续借图书《{book.title}》,新归还日期: {new_due_date.strftime("%Y-%m-%d")}' ) db.session.commit() return jsonify({ 'success': True, 'message': f'续借成功,新的归还日期为 {new_due_date.strftime("%Y-%m-%d")}' }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': f'续借失败: {str(e)}' }) @borrow_bp.route('/my_borrows') @login_required def my_borrows(): """用户查看自己的借阅记录""" page = request.args.get('page', 1, type=int) status = request.args.get('status', default=None, type=int) # 构建查询 query = BorrowRecord.query.filter_by(user_id=current_user.id) # 根据状态筛选 if status is not None: query = query.filter_by(status=status) # 按借阅日期倒序排列 query = query.order_by(BorrowRecord.borrow_date.desc()) # 分页 pagination = query.paginate(page=page, per_page=10, error_out=False) # 获取当前借阅数量和历史借阅数量(用于标签显示) current_borrows_count = BorrowRecord.query.filter_by(user_id=current_user.id, status=1).count() history_borrows_count = BorrowRecord.query.filter_by(user_id=current_user.id, status=0).count() # 记录日志 - 用户查看借阅记录 Log.add_log( action='查看借阅记录', user_id=current_user.id, ip_address=request.remote_addr, description='用户查看个人借阅记录' ) return render_template( 'borrow/my_borrows.html', pagination=pagination, current_borrows_count=current_borrows_count, history_borrows_count=history_borrows_count, status=status, now=datetime.datetime.now() # 添加当前时间变量 ) @borrow_bp.route('/manage') @login_required @permission_required('manage_borrows') # 替代 @admin_required def manage_borrows(): """管理员查看所有借阅记录""" page = request.args.get('page', 1, type=int) status = request.args.get('status', default=None, type=int) user_id = request.args.get('user_id', default=None, type=int) book_id = request.args.get('book_id', default=None, type=int) search = request.args.get('search', default='') # 构建查询 query = BorrowRecord.query # 根据状态筛选 if status is not None: query = query.filter_by(status=status) # 根据用户筛选 if user_id: query = query.filter_by(user_id=user_id) # 根据图书筛选 if book_id: query = query.filter_by(book_id=book_id) # 根据搜索条件筛选(用户名或图书名) if search: query = query.join(User, BorrowRecord.user_id == User.id) \ .join(Book, BorrowRecord.book_id == Book.id) \ .filter((User.username.like(f'%{search}%')) | (Book.title.like(f'%{search}%'))) # 按借阅日期倒序排列 query = query.order_by(BorrowRecord.borrow_date.desc()) # 分页 pagination = query.paginate(page=page, per_page=10, error_out=False) # 获取统计数据 current_borrows_count = BorrowRecord.query.filter_by(status=1).count() history_borrows_count = BorrowRecord.query.filter_by(status=0).count() # 获取所有用户(用于筛选) users = User.query.all() # 记录日志 - 管理员查看借阅记录 Log.add_log( action='管理借阅记录', user_id=current_user.id, ip_address=request.remote_addr, description='管理员查看借阅管理页面' ) return render_template( 'borrow/borrow_management.html', pagination=pagination, current_borrows_count=current_borrows_count, history_borrows_count=history_borrows_count, status=status, user_id=user_id, book_id=book_id, search=search, users=users, now=datetime.datetime.now() # 添加当前时间变量 ) @borrow_bp.route('/admin/add', methods=['POST']) @login_required @permission_required('manage_borrows') # 替代 @admin_required def admin_add_borrow(): """管理员为用户添加借阅记录""" user_id = request.form.get('user_id', type=int) book_id = request.form.get('book_id', type=int) borrow_days = request.form.get('borrow_days', type=int, default=14) if not user_id or not book_id: flash('用户ID和图书ID不能为空', 'danger') return redirect(url_for('borrow.manage_borrows')) # 验证用户和图书是否存在 user = User.query.get_or_404(user_id) book = Book.query.get_or_404(book_id) # 检查库存 if book.stock <= 0: flash(f'《{book.title}》当前无库存,无法借阅', 'danger') return redirect(url_for('borrow.manage_borrows')) # 检查用户是否已借阅此书 existing_borrow = BorrowRecord.query.filter_by( user_id=user_id, book_id=book_id, status=1 # 1表示借阅中 ).first() if existing_borrow: flash(f'用户 {user.username} 已借阅《{book.title}》,请勿重复借阅', 'warning') return redirect(url_for('borrow.manage_borrows')) try: # 创建借阅记录 now = datetime.datetime.now() due_date = now + datetime.timedelta(days=borrow_days) borrow_record = BorrowRecord( user_id=user_id, book_id=book_id, borrow_date=now, due_date=due_date, status=1, # 1表示借阅中 created_at=now, updated_at=now ) # 更新图书库存 book.stock -= 1 book.updated_at = now db.session.add(borrow_record) db.session.commit() # 添加库存变更日志 inventory_log = InventoryLog( book_id=book_id, change_type='借出', change_amount=-1, after_stock=book.stock, operator_id=current_user.id, remark=f'管理员 {current_user.username} 为用户 {user.username} 借书', changed_at=now ) db.session.add(inventory_log) # 添加系统操作日志 Log.add_log( action='管理员借阅操作', user_id=current_user.id, target_type='book', target_id=book_id, ip_address=request.remote_addr, description=f'管理员为用户 {user.username} 借阅图书《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}' ) db.session.commit() flash(f'成功为用户 {user.username} 借阅《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}', 'success') except Exception as e: db.session.rollback() flash(f'借阅失败: {str(e)}', 'danger') return redirect(url_for('borrow.manage_borrows')) @borrow_bp.route('/overdue') @login_required @permission_required('manage_overdue') # 替代 @admin_required def overdue_borrows(): """查看逾期借阅""" page = request.args.get('page', 1, type=int) now = datetime.datetime.now() # 查询所有已逾期且未归还的借阅记录 query = BorrowRecord.query.filter( BorrowRecord.status == 1, # 借阅中 BorrowRecord.due_date < now # 已过期 ).order_by(BorrowRecord.due_date) # 按到期日期排序,最早到期的排在前面 pagination = query.paginate(page=page, per_page=10, error_out=False) # 计算逾期总数 overdue_count = query.count() # 记录日志 - 管理员查看逾期记录 Log.add_log( action='查看逾期记录', user_id=current_user.id, ip_address=request.remote_addr, description='管理员查看逾期借阅记录' ) return render_template( 'borrow/overdue.html', pagination=pagination, overdue_count=overdue_count ) @borrow_bp.route('/overdue/notify/', methods=['POST']) @login_required @permission_required('manage_overdue') # 替代 @admin_required def notify_overdue(borrow_id): """发送逾期通知""" from app.models.notification import Notification borrow_record = BorrowRecord.query.get_or_404(borrow_id) # 检查是否已还 if borrow_record.status != 1: return jsonify({ 'success': False, 'message': '此书已归还,无需发送逾期通知' }) now = datetime.datetime.now() # 检查是否确实逾期 if borrow_record.due_date > now: return jsonify({ 'success': False, 'message': '此借阅记录尚未逾期' }) try: book = Book.query.get(borrow_record.book_id) user = User.query.get(borrow_record.user_id) # 创建通知 notification = Notification( user_id=borrow_record.user_id, title='图书逾期提醒', content=f'您借阅的《{borrow_record.book.title}》已逾期,请尽快归还。应还日期: {borrow_record.due_date.strftime("%Y-%m-%d")}', type='overdue', sender_id=current_user.id, created_at=now ) db.session.add(notification) # 更新借阅记录备注 borrow_record.remark = f'{borrow_record.remark or ""}[{now.strftime("%Y-%m-%d")} 已发送逾期通知]' borrow_record.updated_at = now # 添加系统操作日志 Log.add_log( action='发送逾期通知', user_id=current_user.id, target_type='notification', target_id=borrow_record.user_id, ip_address=request.remote_addr, description=f'管理员向用户 {user.username} 发送图书《{book.title}》逾期通知' ) db.session.commit() return jsonify({ 'success': True, 'message': '已成功发送逾期通知' }) except Exception as e: db.session.rollback() return jsonify({ 'success': False, 'message': f'发送通知失败: {str(e)}' }) ================================================================================ File: ./app/controllers/announcement.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from app.models.announcement import Announcement from app.models.log import Log from app.utils.auth import permission_required # 修改导入 from flask_login import login_required, current_user from datetime import datetime from app.models.notification import Notification from app import db # 为mark_all_as_read函数添加db导入 # 创建蓝图 announcement_bp = Blueprint('announcement', __name__) @announcement_bp.route('/list', methods=['GET']) def announcement_list(): """公告列表页面 - 所有用户可见""" page = request.args.get('page', 1, type=int) per_page = 10 # 查询活跃的公告 query = Announcement.query.filter_by(status=1).order_by( Announcement.is_top.desc(), Announcement.created_at.desc() ) pagination = query.paginate(page=page, per_page=per_page, error_out=False) return render_template('announcement/list.html', pagination=pagination) @announcement_bp.route('/detail/', methods=['GET']) def announcement_detail(announcement_id): """公告详情页面""" announcement = Announcement.get_announcement_by_id(announcement_id) if not announcement or announcement.status == 0: flash('公告不存在或已被删除', 'error') return redirect(url_for('announcement.announcement_list')) return render_template('announcement/detail.html', announcement=announcement) @announcement_bp.route('/manage', methods=['GET']) @login_required @permission_required('manage_announcements') # 替代 @admin_required def manage_announcements(): """管理员公告管理页面""" page = request.args.get('page', 1, type=int) per_page = 10 search = request.args.get('search', '') status = request.args.get('status', type=int) # 构建查询 query = Announcement.query # 搜索过滤 if search: query = query.filter(Announcement.title.like(f'%{search}%')) # 状态过滤 if status is not None: query = query.filter(Announcement.status == status) # 排序 query = query.order_by( Announcement.is_top.desc(), Announcement.created_at.desc() ) pagination = query.paginate(page=page, per_page=per_page, error_out=False) # 记录访问日志 Log.add_log( action="访问公告管理", user_id=current_user.id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 访问公告管理页面" ) return render_template( 'announcement/manage.html', pagination=pagination, search=search, status=status ) @announcement_bp.route('/add', methods=['GET', 'POST']) @login_required @permission_required('manage_announcements') # 替代 @admin_required def add_announcement(): """添加公告""" if request.method == 'POST': title = request.form.get('title') content = request.form.get('content') is_top = request.form.get('is_top') == 'on' if not title or not content: flash('标题和内容不能为空', 'error') return render_template('announcement/add.html') success, result = Announcement.create_announcement( title=title, content=content, publisher_id=current_user.id, is_top=is_top ) if success: # 记录操作日志 Log.add_log( action="添加公告", user_id=current_user.id, target_type="公告", target_id=result.id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 添加了新公告: {title}" ) flash('公告发布成功', 'success') return redirect(url_for('announcement.manage_announcements')) else: flash(f'公告发布失败: {result}', 'error') return render_template('announcement/add.html') return render_template('announcement/add.html') @announcement_bp.route('/edit/', methods=['GET', 'POST']) @login_required @permission_required('manage_announcements') # 替代 @admin_required def edit_announcement(announcement_id): """编辑公告""" announcement = Announcement.get_announcement_by_id(announcement_id) if not announcement: flash('公告不存在', 'error') return redirect(url_for('announcement.manage_announcements')) if request.method == 'POST': title = request.form.get('title') content = request.form.get('content') is_top = request.form.get('is_top') == 'on' if not title or not content: flash('标题和内容不能为空', 'error') return render_template('announcement/edit.html', announcement=announcement) success, result = Announcement.update_announcement( announcement_id=announcement_id, title=title, content=content, is_top=is_top ) if success: # 记录操作日志 Log.add_log( action="编辑公告", user_id=current_user.id, target_type="公告", target_id=announcement_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} 编辑了公告: {title}" ) flash('公告更新成功', 'success') return redirect(url_for('announcement.manage_announcements')) else: flash(f'公告更新失败: {result}', 'error') return render_template('announcement/edit.html', announcement=announcement) return render_template('announcement/edit.html', announcement=announcement) @announcement_bp.route('/status/', methods=['POST']) @login_required @permission_required('manage_announcements') # 替代 @admin_required def change_status(announcement_id): """更改公告状态""" data = request.get_json() status = data.get('status') if status is None or status not in [0, 1]: return jsonify({'success': False, 'message': '无效的状态值'}) # 查询公告获取标题(用于日志) announcement = Announcement.get_announcement_by_id(announcement_id) if not announcement: return jsonify({'success': False, 'message': '公告不存在'}) success, message = Announcement.change_status(announcement_id, status) if success: # 记录状态变更日志 status_text = "发布" if status == 1 else "撤销" Log.add_log( action=f"公告{status_text}", user_id=current_user.id, target_type="公告", target_id=announcement_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} {status_text}公告: {announcement.title}" ) return jsonify({'success': True, 'message': f'公告已{status_text}'}) else: return jsonify({'success': False, 'message': message}) @announcement_bp.route('/top/', methods=['POST']) @login_required @permission_required('manage_announcements') # 替代 @admin_required def change_top_status(announcement_id): """更改公告置顶状态""" data = request.get_json() is_top = data.get('is_top') if is_top is None: return jsonify({'success': False, 'message': '无效的置顶状态'}) # 查询公告获取标题(用于日志) announcement = Announcement.get_announcement_by_id(announcement_id) if not announcement: return jsonify({'success': False, 'message': '公告不存在'}) success, message = Announcement.change_top_status(announcement_id, is_top) if success: # 记录置顶状态变更日志 action_text = "置顶" if is_top else "取消置顶" Log.add_log( action=f"公告{action_text}", user_id=current_user.id, target_type="公告", target_id=announcement_id, ip_address=request.remote_addr, description=f"管理员 {current_user.username} {action_text}公告: {announcement.title}" ) return jsonify({'success': True, 'message': f'公告已{action_text}'}) else: return jsonify({'success': False, 'message': message}) @announcement_bp.route('/latest', methods=['GET']) def get_latest_announcements(): """获取最新公告列表,用于首页和API""" limit = request.args.get('limit', 5, type=int) announcements = Announcement.get_active_announcements(limit=limit) return jsonify({ 'success': True, 'announcements': [announcement.to_dict() for announcement in announcements] }) @announcement_bp.route('/notifications') @login_required def user_notifications(): """用户个人通知列表页面""" page = request.args.get('page', 1, type=int) per_page = 10 unread_only = request.args.get('unread_only') == '1' pagination = Notification.get_user_notifications( user_id=current_user.id, page=page, per_page=per_page, unread_only=unread_only ) return render_template( 'announcement/notifications.html', pagination=pagination, unread_only=unread_only ) @announcement_bp.route('/notification/') @login_required def view_notification(notification_id): """查看单条通知""" notification = Notification.query.get_or_404(notification_id) # 检查权限 - 只能查看自己的通知 if notification.user_id != current_user.id: flash('您无权查看此通知', 'error') return redirect(url_for('announcement.user_notifications')) # 标记为已读 if notification.status == 0: Notification.mark_as_read(notification_id, current_user.id) # 如果是借阅类型的通知,可能需要跳转到相关页面 if notification.type == 'borrow' and 'borrow_id' in notification.content: # 这里可以解析content获取borrow_id然后重定向 pass return render_template('announcement/notification_detail.html', notification=notification) @announcement_bp.route('/notifications/mark-all-read') @login_required def mark_all_as_read(): """标记所有通知为已读""" try: # 获取所有未读通知 unread_notifications = Notification.query.filter_by( user_id=current_user.id, status=0 ).all() # 标记为已读 for notification in unread_notifications: notification.status = 1 notification.read_at = datetime.now() db.session.commit() flash('所有通知已标记为已读', 'success') except Exception as e: db.session.rollback() flash(f'操作失败: {str(e)}', 'error') return redirect(url_for('announcement.user_notifications')) ================================================================================ File: ./app/controllers/inventory.py ================================================================================ # app/controllers/inventory.py from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for from flask_login import login_required, current_user from app.models.book import Book from app.models.inventory import InventoryLog from app.models.log import Log # 导入日志模型 from app.models.user import db from app.utils.auth import permission_required # 修改导入,使用permission_required替代admin_required from datetime import datetime inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory') @inventory_bp.route('/') @login_required @permission_required('manage_inventory') # 替代 @admin_required def inventory_list(): """库存管理页面 - 只有拥有库存管理权限的用户有权限进入""" page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) # 搜索功能 search = request.args.get('search', '') query = Book.query if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) # 排序 sort = request.args.get('sort', 'id') order = request.args.get('order', 'asc') if order == 'desc': query = query.order_by(getattr(Book, sort).desc()) else: query = query.order_by(getattr(Book, sort)) pagination = query.paginate(page=page, per_page=per_page) books = pagination.items # 记录系统日志 - 访问库存管理页面 Log.add_log( action="访问库存管理", user_id=current_user.id, target_type="inventory", ip_address=request.remote_addr, description=f"用户访问库存管理页面,搜索条件:{search if search else '无'}" ) return render_template('inventory/list.html', books=books, pagination=pagination, search=search, sort=sort, order=order) @inventory_bp.route('/adjust/', methods=['GET', 'POST']) @login_required @permission_required('manage_inventory') def adjust_inventory(book_id): """调整图书库存""" book = Book.query.get_or_404(book_id) # GET请求记录日志 if request.method == 'GET': Log.add_log( action="查看库存调整", user_id=current_user.id, target_type="book", target_id=book.id, ip_address=request.remote_addr, description=f"用户查看图书《{book.title}》的库存调整页面" ) if request.method == 'POST': change_type = request.form.get('change_type') change_amount = int(request.form.get('change_amount', 0)) remark = request.form.get('remark', '') if change_amount <= 0: flash('调整数量必须大于0', 'danger') return redirect(url_for('inventory.adjust_inventory', book_id=book_id)) # 计算库存变化 original_stock = book.stock if change_type == 'in': book.stock += change_amount after_stock = book.stock operation_desc = "入库" elif change_type == 'out': if book.stock < change_amount: flash('出库数量不能大于当前库存', 'danger') return redirect(url_for('inventory.adjust_inventory', book_id=book_id)) book.stock -= change_amount after_stock = book.stock operation_desc = "出库" else: flash('无效的操作类型', 'danger') return redirect(url_for('inventory.adjust_inventory', book_id=book_id)) # 创建库存日志 log = InventoryLog( book_id=book.id, change_type=change_type, change_amount=change_amount, after_stock=after_stock, operator_id=current_user.id, remark=remark, changed_at=datetime.now() ) try: db.session.add(log) # 记录系统日志 - 库存调整 Log.add_log( action=f"库存{operation_desc}", user_id=current_user.id, target_type="book", target_id=book.id, ip_address=request.remote_addr, description=f"用户对图书《{book.title}》进行{operation_desc}操作,数量:{change_amount}," f"原库存:{original_stock},现库存:{after_stock},备注:{remark}" ) db.session.commit() flash(f'图书《{book.title}》库存调整成功!原库存:{original_stock},现库存:{after_stock}', 'success') return redirect(url_for('inventory.inventory_list')) except Exception as e: db.session.rollback() flash(f'操作失败:{str(e)}', 'danger') return redirect(url_for('inventory.adjust_inventory', book_id=book_id)) return render_template('inventory/adjust.html', book=book) @inventory_bp.route('/logs') @login_required @permission_required('manage_inventory') # 替代 @admin_required def inventory_logs(): """查看库存变动日志""" page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) # 搜索和筛选 book_id = request.args.get('book_id', type=int) change_type = request.args.get('change_type', '') date_from = request.args.get('date_from', '') date_to = request.args.get('date_to', '') query = InventoryLog.query if book_id: query = query.filter_by(book_id=book_id) if change_type: query = query.filter_by(change_type=change_type) if date_from: query = query.filter(InventoryLog.changed_at >= datetime.strptime(date_from, '%Y-%m-%d')) if date_to: query = query.filter(InventoryLog.changed_at <= datetime.strptime(date_to + ' 23:59:59', '%Y-%m-%d %H:%M:%S')) # 默认按时间倒序 query = query.order_by(InventoryLog.changed_at.desc()) pagination = query.paginate(page=page, per_page=per_page) logs = pagination.items # 获取所有图书用于筛选 books = Book.query.all() # 如果特定 book_id 被指定,也获取该书的详细信息 book = Book.query.get(book_id) if book_id else None # 记录系统日志 - 查看库存日志 filter_desc = [] if book_id: book_title = book.title if book else f"ID:{book_id}" filter_desc.append(f"图书:{book_title}") if change_type: change_type_text = "入库" if change_type == "in" else "出库" filter_desc.append(f"操作类型:{change_type_text}") if date_from or date_to: date_range = f"{date_from or '无限制'} 至 {date_to or '无限制'}" filter_desc.append(f"日期范围:{date_range}") Log.add_log( action="查看库存日志", user_id=current_user.id, target_type="inventory_log", ip_address=request.remote_addr, description=f"用户查看库存变动日志,筛选条件:{', '.join(filter_desc) if filter_desc else '无'}" ) return render_template('inventory/logs.html', logs=logs, pagination=pagination, books=books, book=book, book_id=book_id, change_type=change_type, date_from=date_from, date_to=date_to) @inventory_bp.route('/book//logs') @login_required @permission_required('manage_inventory') # 替代 @admin_required def book_inventory_logs(book_id): """查看特定图书的库存变动日志""" book = Book.query.get_or_404(book_id) page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) logs = InventoryLog.query.filter_by(book_id=book_id) \ .order_by(InventoryLog.changed_at.desc()) \ .paginate(page=page, per_page=per_page) # 记录系统日志 - 查看特定图书的库存日志 Log.add_log( action="查看图书库存日志", user_id=current_user.id, target_type="book", target_id=book.id, ip_address=request.remote_addr, description=f"用户查看图书《{book.title}》的库存变动日志" ) return render_template('inventory/book_logs.html', book=book, logs=logs.items, pagination=logs) ================================================================================ File: ./app/services/borrow_service.py ================================================================================ ================================================================================ File: ./app/services/inventory_service.py ================================================================================ ================================================================================ File: ./app/services/__init__.py ================================================================================ ================================================================================ File: ./app/services/book_service.py ================================================================================ ================================================================================ File: ./app/services/user_service.py ================================================================================ # app/services/user_service.py from app.models.user import User, Role, db from sqlalchemy import or_ from datetime import datetime from sqlalchemy import text 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): """删除用户 (物理删除)""" user = User.query.get(user_id) if not user: return False, "用户不存在" try: # 检查是否有未归还的图书 - 使用SQLAlchemy的text函数 active_borrows_count = db.session.execute( text("SELECT COUNT(*) FROM borrow_records WHERE user_id = :user_id AND return_date IS NULL"), {"user_id": user_id} ).scalar() if active_borrows_count > 0: return False, f"无法删除:该用户还有 {active_borrows_count} 本未归还的图书" # 删除用户相关的通知记录 - 使用SQLAlchemy的text函数 db.session.execute( text("DELETE FROM notifications WHERE user_id = :user_id OR sender_id = :user_id"), {"user_id": user_id} ) # 物理删除用户 db.session.delete(user) 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, permission_ids=None): """创建新角色,包括权限分配""" from app.models.user import Role from app.models.permission import Permission # 检查角色名是否已存在 if Role.query.filter_by(role_name=role_name).first(): return False, "角色名称已存在", None role = Role(role_name=role_name, description=description) # 添加权限 if permission_ids: permissions = Permission.query.filter(Permission.id.in_(permission_ids)).all() role.permissions = permissions try: db.session.add(role) db.session.commit() return True, "角色创建成功", role.id except Exception as e: db.session.rollback() return False, f"创建失败: {str(e)}", None @staticmethod def update_role(role_id, role_name, description, permission_ids=None): """更新角色信息,包括权限""" from app.models.user import Role from app.models.permission import Permission role = Role.query.get(role_id) if not role: return False, "角色不存在" # 系统内置角色不允许修改名称 if role_id in [1, 2] and role.role_name != role_name: return False, "系统内置角色不允许修改名称" # 检查角色名是否已存在 existing_role = Role.query.filter(Role.role_name == role_name, Role.id != role_id).first() if existing_role: return False, "角色名称已存在" role.role_name = role_name role.description = description # 更新权限(如果提供了权限列表且不是内置角色) if permission_ids is not None and role_id not in [1, 2]: permissions = Permission.query.filter(Permission.id.in_(permission_ids)).all() role.permissions = permissions try: db.session.commit() return True, "角色更新成功" except Exception as e: db.session.rollback() return False, f"更新失败: {str(e)}" @staticmethod def create_user(data): """创建新用户""" try: new_user = User( username=data['username'], password=data['password'], email=data['email'], nickname=data.get('nickname') or data['username'], phone=data.get('phone'), role_id=data.get('role_id', 2), # 默认为普通用户 status=data.get('status', 1) # 默认为启用状态 ) db.session.add(new_user) db.session.commit() return True, '用户创建成功' except Exception as e: db.session.rollback() logging.error(f"创建用户失败: {str(e)}") return False, f'创建用户失败: {str(e)}'