
================================================================================
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', '127.0.0.1')
DB_PORT = os.environ.get('DB_PORT', '3306')
DB_USER = os.environ.get('DB_USER', 'root')
DB_PASSWORD = os.environ.get('DB_PASSWORD', '12345678')
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
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))

    # 注册蓝图
    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.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', '<br>'))
        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
        )

    return app

    @app.context_processor
    def inject_now():
        return {'now': datetime.datetime.now()}



================================================================================
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/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"""
        <html>
        <body>
            <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e1e1e1; border-radius: 5px;">
                <h2 style="color: #4a89dc;">图书管理系统 - 邮箱验证</h2>
                <p>您好，</p>
                <p>感谢您注册图书管理系统，您的验证码是:</p>
                <div style="background-color: #f5f5f5; padding: 10px; border-radius: 5px; text-align: center; font-size: 24px; letter-spacing: 5px; font-weight: bold; margin: 20px 0;">
                    {verification_code}
                </div>
                <p>该验证码将在10分钟内有效，请勿将验证码分享给他人。</p>
                <p>如果您没有请求此验证码，请忽略此邮件。</p>
                <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e1e1e1; font-size: 12px; color: #888;">
                    <p>此邮件为系统自动发送，请勿回复。</p>
                    <p>&copy; 2025 图书管理系统</p>
                </div>
            </div>
        </body>
        </html>
        """

        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'<Category {self.name}>'


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'<Book {self.title}>'

================================================================================
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'<BorrowRecord {self.id}>'

================================================================================
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'<InventoryLog {self.id}>'

================================================================================
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 样式，避免与 badge 冲突 */
.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;
}

.form-control {
    border: 1px solid #d9c7b8;
    border-radius: 6px;
    color: #5d3511;
    background-color: #fff9f5;
    transition: all 0.3s ease;
    font-family: 'Georgia', serif;
}

.form-control:focus {
    border-color: #704214;
    box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.15);
    background-color: #fff;
}

.btn-outline-secondary {
    color: #704214;
    border-color: #d9c7b8;
    background-color: transparent;
}

.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;
}

/* 修改借阅用户显示方式 */
.user-info {
    text-align: center;
    display: table-cell;
    vertical-align: middle;
    height: 100%;
}

.user-info a {
    color: #5d3511;
    text-decoration: none;
    font-weight: 600;
    transition: color 0.3s ease;
    display: block;
    margin-bottom: 8px;
}

.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 - 修复显示问题 */
.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;
}

.badge-primary {
    background-color: #704214;
    color: white;
}

.badge-success {
    background-color: #5b8a72;
    color: white;
}

.badge-danger {
    background-color: #a15950;
    color: white;
}

.badge-info {
    background-color: #6a8da9;
    color: white;
}

.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;
}

/* Buttons */
.btn {
    border-radius: 20px;
    padding: 8px 16px;
    transition: all 0.3s ease;
    font-family: 'Georgia', serif;
    letter-spacing: 0.5px;
}

.btn-primary {
    background-color: #704214;
    border-color: #704214;
}

.btn-primary:hover, .btn-primary:focus {
    background-color: #5d3511;
    border-color: #5d3511;
    box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.25);
}

.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;
}

.actions .btn {
    margin-right: 5px;
    margin-bottom: 6px;
}

.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;
    }
}

================================================================================
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;
    cursor: pointer;
}

.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;
}

.user-info {
    display: flex;
    align-items: center;
    cursor: pointer;
    position: relative;
}

.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;
}

.dropdown-menu {
    position: absolute;
    top: 100%;
    right: 0;
    background-color: white;
    box-shadow: 0 3px 10px rgba(0,0,0,0.1);
    border-radius: 5px;
    width: 200px;
    padding: 10px 0;
    display: none;
    z-index: 1000;
}

.user-info.active .dropdown-menu {
    display: block;
}

.dropdown-menu a {
    display: block;
    padding: 8px 15px;
    color: #333;
    text-decoration: none;
    transition: background-color 0.3s;
}

.dropdown-menu a:hover {
    background-color: #f8f9fa;
}

.dropdown-menu a i {
    width: 20px;
    margin-right: 10px;
    text-align: center;
}

/* 内容区域 */
.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;
    }
}

================================================================================
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,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M95,50A45,45,0,1,1,50,5,45,45,0,0,1,95,50Z" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="10"/></svg>') 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 importBtn = document.querySelector('.import-btn');
    if (importBtn) {
        importBtn.addEventListener('click', function(e) {
            if (!fileInput || !fileInput.files || !fileInput.files.length) {
                e.preventDefault();
                showMessage('请先选择要导入的Excel文件', 'warning');
                return;
            }

            this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在导入...';
            this.disabled = true;

            // 添加花朵飘落动画效果
            addFallingElements(10);
        });
    }

    // 为浮动元素添加动画
    initFloatingElements();
});

// 检查文件类型并尝试预览
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 = `
        <div class="alert alert-${type} mt-3">
            <i class="fas ${getIconForMessageType(type)}"></i> ${message}
        </div>
    `;

    // 如果是临时消息，设置自动消失
    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 = `
            <i class="fas fa-${type === 'success' ? 'check-circle' : 
                              type === 'danger' ? 'exclamation-circle' : 
                              type === 'info' ? 'info-circle' : 'bell'}"></i>
            ${message}
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        `;

        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}
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        `;

        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 = `
            <tr class="loading-row">
                <td colspan="5"><div class="loading-animation"><span>正在打开书页</span><span class="dot-animation">...</span></div></td>
            </tr>
        `;

        // 调用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 = `
                    <tr class="error-row">
                        <td colspan="5">加载数据失败，请稍后重试</td>
                    </tr>
                `;
            });
    }

    function updateRankingTable(data) {
        const tableBody = document.getElementById('ranking-table-body');

        if (data.length === 0) {
            tableBody.innerHTML = `
                <tr class="no-data-row">
                    <td colspan="5">暂无数据</td>
                </tr>
            `;
            return;
        }

        let tableHtml = '';

        data.forEach((book, index) => {
            // 给每个单元格添加适当的类名以匹配CSS
            tableHtml += `
                <tr>
                    <td class="rank">${index + 1}</td>
                    <td class="book-cover">
                        <img src="${book.cover_url || '/static/images/book-placeholder.jpg'}" alt="${book.title}">
                    </td>
                    <td class="book-title-cell"><span class="book-title">${book.title}</span></td>
                    <td class="author">${book.author}</td>
                    <td><span class="borrow-count">${book.borrow_count}</span></td>
                </tr>
            `;
        });

        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 = '<div class="loading-permissions"><i class="fas fa-spinner fa-spin"></i> 加载权限中...</div>';
        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 = '<div class="text-danger">加载权限失败</div>';
            }
        })
        .catch(error => {
            console.error('Error:', error);
            bookPermissions.innerHTML = '<div class="text-danger">加载权限失败，请刷新页面重试</div>';
        });
    }

    // 渲染权限组
    function renderPermissionGroup(container, permissions) {
        if (permissions.length === 0) {
            container.innerHTML = '<div class="text-muted">暂无相关权限</div>';
            return;
        }

        let html = '';
        permissions.forEach(perm => {
            html += `
                <div class="permission-item">
                    <input type="checkbox" id="perm_${perm.id}" class="permission-checkbox" value="${perm.id}">
                    <label for="perm_${perm.id}">
                        <span class="permission-name">${perm.name}</span>
                        <span class="permission-description">${perm.description || '无描述'}</span>
                    </label>
                </div>
            `;
        });

        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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
        saveRoleBtn.disabled = true;

        fetch('/user/role/save', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify(roleData)
        })
        .then(response => {
            if (!response.ok) {
                throw new Error('网络响应异常');
            }
            return response.json();
        })
        .then(data => {
            // 恢复按钮状态
            saveRoleBtn.innerHTML = '保存';
            saveRoleBtn.disabled = false;

            if (data.success) {
                // 关闭模态框
                roleModal.modal('hide');
                showAlert(data.message, 'success');
                setTimeout(() => {
                    window.location.reload();
                }, 1500);
            } else {
                showAlert(data.message, 'error');
            }
        })
        .catch(error => {
            console.error('Error:', error);
            // 恢复按钮状态
            saveRoleBtn.innerHTML = '保存';
            saveRoleBtn.disabled = false;
            showAlert('保存失败，请稍后重试', 'error');
        });
    }

    // 删除角色
    function deleteRole(roleId) {
        // 显示加载状态
        confirmDeleteBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 删除中...';
        confirmDeleteBtn.disabled = true;

        fetch(`/user/role/delete/${roleId}`, {
            method: 'POST',
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Content-Type': 'application/json'
            }
        })
        .then(response => {
            if (!response.ok) {
                throw new Error('网络响应异常');
            }
            return response.json();
        })
        .then(data => {
            // 恢复按钮状态
            confirmDeleteBtn.innerHTML = '确认删除';
            confirmDeleteBtn.disabled = false;

            if (data.success) {
                showAlert(data.message, 'success');
                setTimeout(() => {
                    window.location.reload();
                }, 1500);
            } else {
                showAlert(data.message, 'error');
            }
        })
        .catch(error => {
            console.error('Error:', error);
            // 恢复按钮状态
            confirmDeleteBtn.innerHTML = '确认删除';
            confirmDeleteBtn.disabled = false;
            showAlert('删除失败，请稍后重试', 'error');
        });
    }

    // 获取角色用户数量
    function fetchRoleUserCounts() {
        const roleCards = document.querySelectorAll('.role-card');

        roleCards.forEach(card => {
            const roleId = card.getAttribute('data-id');
            const countElement = document.getElementById(`userCount-${roleId}`);

            if (countElement) {
                // 设置"加载中"状态
                countElement.innerHTML = '<small>加载中...</small>';

                // 定义默认的角色用户数量 (用于API不可用时)
                const defaultCounts = {
                    '1': 1, // 管理员
                    '2': 5, // 普通用户
                };

                // 尝试获取用户数量
                fetch(`/user/role/${roleId}/count`)
                    .then(response => {
                        if (!response.ok) {
                            throw new Error('API不可用');
                        }
                        return response.json();
                    })
                    .then(data => {
                        // 检查返回数据的success属性
                        if (data.success) {
                            countElement.textContent = data.count;
                        } else {
                            throw new Error(data.message || 'API返回错误');
                        }
                    })
                    .catch(error => {
                        console.warn(`获取角色ID=${roleId}的用户数量失败:`, error);

                        // 使用默认值
                        const defaultCounts = {
                            '1': 1, // 固定值而非随机值
                            '2': 5,
                            '3': 3
                        };
                        countElement.textContent = defaultCounts[roleId] || 0;

                        // 静默失败 - 不向用户显示错误，只在控制台记录
                    });
            }
        });
    }

    // 显示通知
    function showAlert(message, type) {
        // 检查是否已有通知元素
        let alertBox = document.querySelector('.alert-box');
        if (!alertBox) {
            alertBox = document.createElement('div');
            alertBox.className = 'alert-box';
            document.body.appendChild(alertBox);
        }

        // 创建新的通知
        const alert = document.createElement('div');
        alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
        alert.innerHTML = message;

        // 添加到通知框中
        alertBox.appendChild(alert);

        // 自动关闭
        setTimeout(() => {
            alert.classList.add('fade-out');
            setTimeout(() => {
                alertBox.removeChild(alert);
            }, 500);
        }, 3000);
    }

    // 添加CSS样式以支持通知动画
    function addAlertStyles() {
        if (!document.getElementById('alert-styles')) {
            const style = document.createElement('style');
            style.id = 'alert-styles';
            style.textContent = `
                .alert-box {
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    z-index: 9999;
                    max-width: 350px;
                }
                .alert {
                    margin-bottom: 10px;
                    padding: 15px;
                    border-radius: 4px;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                    opacity: 0;
                    transition: opacity 0.3s ease;
                }
                .fade-in {
                    opacity: 1;
                }
                .fade-out {
                    opacity: 0;
                }
            `;
            document.head.appendChild(style);
        }
    }

    // 添加通知样式
    addAlertStyles();
});

================================================================================
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('<i class="fas fa-spinner fa-spin"></i> 处理中...');

        $.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(`
                        <button class="btn-borrow disabled" disabled>
                            <i class="fas fa-check-circle"></i> 已借出
                        </button>
                    `);

                    // 创建借阅成功动画
                    const successOverlay = $('<div class="borrow-success-overlay"><i class="fas fa-check-circle"></i><span>借阅成功</span></div>');
                    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); }
        }
    `;

    $('<style>').text(animationCSS).appendTo('head');

    // 显示通知
    function showNotification(message, type) {
        // 移除可能存在的旧通知
        $('.notification-alert').remove();

        const alertClass = type === 'success' ? 'notification-success' : 'notification-error';
        const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';

        const notification = `
            <div class="notification-alert ${alertClass}">
                <div class="notification-icon">
                    <i class="fas ${iconClass}"></i>
                </div>
                <div class="notification-message">${message}</div>
                <button class="notification-close">
                    <i class="fas fa-times"></i>
                </button>
            </div>
        `;

        $('body').append(notification);

        // 显示通知
        setTimeout(() => {
            $('.notification-alert').addClass('show');
        }, 10);

        // 通知自动关闭
        setTimeout(() => {
            $('.notification-alert').removeClass('show');
            setTimeout(() => {
                $('.notification-alert').remove();
            }, 300);
        }, 4000);

        // 点击关闭按钮
        $('.notification-close').click(function() {
            $(this).closest('.notification-alert').removeClass('show');
            setTimeout(() => {
                $(this).closest('.notification-alert').remove();
            }, 300);
        });
    }

    // 添加通知样式
    const notificationCSS = `
        .notification-alert {
            position: fixed;
            top: 20px;
            right: 20px;
            min-width: 300px;
            max-width: 400px;
            background-color: white;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
            display: flex;
            align-items: center;
            padding: 15px;
            transform: translateX(calc(100% + 20px));
            transition: transform 0.3s ease;
            z-index: 9999;
        }
        
        .notification-alert.show {
            transform: translateX(0);
        }
        
        .notification-success {
            border-left: 4px solid #4caf50;
        }
        
        .notification-error {
            border-left: 4px solid #f44336;
        }
        
        .notification-icon {
            margin-right: 15px;
            font-size: 24px;
        }
        
        .notification-success .notification-icon {
            color: #4caf50;
        }
        
        .notification-error .notification-icon {
            color: #f44336;
        }
        
        .notification-message {
            flex: 1;
            font-size: 0.95rem;
            color: #3c4858;
        }
        
        .notification-close {
            background: none;
            border: none;
            color: #a0aec0;
            cursor: pointer;
            padding: 5px;
            margin-left: 10px;
            font-size: 0.8rem;
        }
        
        .notification-close:hover {
            color: #4a5568;
        }
        
        @media (max-width: 576px) {
            .notification-alert {
                top: auto;
                bottom: 20px;
                left: 20px;
                right: 20px;
                min-width: auto;
                max-width: none;
                transform: translateY(calc(100% + 20px));
            }
            
            .notification-alert.show {
                transform: translateY(0);
            }
        }
    `;

    $('<style>').text(notificationCSS).appendTo('head');

    // 卡片淡入效果
    $('.book-card').each(function(index) {
        $(this).css('animation-delay', `${0.05 * index}s`);
    });

    // 鼠标悬停时添加卡片提示信息
    $('.book-card').each(function() {
        const title = $(this).find('.book-title').text();
        const author = $(this).find('.book-author').text();
        $(this).attr('title', `${title} - ${author}`);
    });

    // 懒加载图片处理（可选）
    if ('IntersectionObserver' in window) {
        const imgObserver = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    img.src = img.dataset.src;
                    img.classList.remove('lazy');
                    observer.unobserve(img);
                }
            });
        });

        document.querySelectorAll('img.lazy').forEach(img => {
            imgObserver.observe(img);
        });
    } else {
        // 回退机制，直接加载所有图片
        document.querySelectorAll('img.lazy').forEach(img => {
            img.src = img.dataset.src;
        });
    }
});

================================================================================
File: ./app/static/js/inventory-list.js
================================================================================

// 库存管理页面的JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
    // 库存显示颜色标记
    const stockElements = document.querySelectorAll('.book-stock');
    stockElements.forEach(element => {
        const stockValue = parseInt(element.textContent.trim());
        if (stockValue <= 0) {
            element.classList.add('book-stock-critical');
        } else if (stockValue <= 5) {
            element.classList.add('book-stock-warning');
        } else {
            element.classList.add('book-stock-normal');
        }
    });

    // 表格排序功能
    const tableHeaders = document.querySelectorAll('th[data-sort]');
    tableHeaders.forEach(header => {
        header.addEventListener('click', function() {
            const sort = this.dataset.sort;
            const currentOrder = new URLSearchParams(window.location.search).get('order') || 'asc';
            const newOrder = currentOrder === 'asc' ? 'desc' : 'asc';

            const url = new URL(window.location);
            url.searchParams.set('sort', sort);
            url.searchParams.set('order', newOrder);
            window.location.href = url.toString();
        });
    });
});

================================================================================
File: ./app/static/js/user-list.js
================================================================================

// 用户列表页面交互
document.addEventListener('DOMContentLoaded', function() {
    // 处理状态切换按钮
    const toggleStatusButtons = document.querySelectorAll('.toggle-status');
    toggleStatusButtons.forEach(button => {
        button.addEventListener('click', function() {
            const userId = this.getAttribute('data-id');
            const newStatus = parseInt(this.getAttribute('data-status'));
            const statusText = newStatus === 1 ? '启用' : '禁用';

            if (confirm(`确定要${statusText}该用户吗？`)) {
                toggleUserStatus(userId, newStatus);
            }
        });
    });

    // 处理删除按钮
    const deleteButtons = document.querySelectorAll('.delete-user');
    const deleteModal = $('#deleteModal');
    let userIdToDelete = null;

    deleteButtons.forEach(button => {
        button.addEventListener('click', function() {
            userIdToDelete = this.getAttribute('data-id');
            deleteModal.modal('show');
        });
    });

    // 确认删除按钮
    document.getElementById('confirmDelete').addEventListener('click', function() {
        if (userIdToDelete) {
            deleteUser(userIdToDelete);
            deleteModal.modal('hide');
        }
    });

    // 自动提交表单的下拉菜单
    const autoSubmitSelects = document.querySelectorAll('select[name="status"], select[name="role_id"]');
    autoSubmitSelects.forEach(select => {
        select.addEventListener('change', function() {
            this.closest('form').submit();
        });
    });
});

// 切换用户状态
function toggleUserStatus(userId, status) {
    fetch(`/user/status/${userId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ status: status })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            showAlert(data.message, 'success');
            setTimeout(() => {
                window.location.reload();
            }, 1500);
        } else {
            showAlert(data.message, 'error');
        }
    })
    .catch(error => {
        console.error('Error:', error);
        showAlert('操作失败，请稍后重试', 'error');
    });
}

// 删除用户
function deleteUser(userId) {
    fetch(`/user/delete/${userId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            showAlert(data.message, 'success');
            setTimeout(() => {
                window.location.reload();
            }, 1500);
        } else {
            showAlert(data.message, 'error');
        }
    })
    .catch(error => {
        console.error('Error:', error);
        showAlert('操作失败，请稍后重试', 'error');
    });
}

// 显示通知
function showAlert(message, type) {
    // 检查是否已有通知元素
    let alertBox = document.querySelector('.alert-box');
    if (!alertBox) {
        alertBox = document.createElement('div');
        alertBox.className = 'alert-box';
        document.body.appendChild(alertBox);
    }

    // 创建新的通知
    const alert = document.createElement('div');
    alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
    alert.innerHTML = message;

    // 添加到通知框中
    alertBox.appendChild(alert);

    // 自动关闭
    setTimeout(() => {
        alert.classList.add('fade-out');
        setTimeout(() => {
            alertBox.removeChild(alert);
        }, 500);
    }, 3000);
}

================================================================================
File: ./app/static/js/announcement-manage.js
================================================================================

// 公告管理页面的Javascript
document.addEventListener('DOMContentLoaded', function() {
    // 提示框自动关闭
    setTimeout(function() {
        const alerts = document.querySelectorAll('.alert-success, .alert-info');
        alerts.forEach(alert => {
            const bsAlert = new bootstrap.Alert(alert);
            bsAlert.close();
        });
    }, 5000);
});

// 更改公告状态（发布/撤销）
function changeStatus(announcementId, status) {
    if (!confirm('确定要' + (status === 1 ? '发布' : '撤销') + '这条公告吗？')) {
        return;
    }

    fetch(`/announcement/status/${announcementId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ status: status })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            alert(data.message);
            location.reload();
        } else {
            alert('操作失败: ' + data.message);
        }
    })
    .catch(error => {
        console.error('Error:', error);
        alert('发生错误，请重试');
    });
}

// 更改公告置顶状态
function changeTopStatus(announcementId, isTop) {
    if (!confirm('确定要' + (isTop ? '置顶' : '取消置顶') + '这条公告吗？')) {
        return;
    }

    fetch(`/announcement/top/${announcementId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ is_top: isTop })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            alert(data.message);
            location.reload();
        } else {
            alert('操作失败: ' + data.message);
        }
    })
    .catch(error => {
        console.error('Error:', error);
        alert('发生错误，请重试');
    });
}

================================================================================
File: ./app/static/js/borrow_management.js
================================================================================

// borrow_management.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);
        });
    });

    // 逾期通知功能
    const notifyButtons = document.querySelectorAll('.notify-btn');
    const notifyModal = document.getElementById('notifyModal');
    const notifyBookTitle = document.getElementById('notifyBookTitle');
    const confirmNotifyButton = document.getElementById('confirmNotify');

    notifyButtons.forEach(button => {
        button.addEventListener('click', function() {
            const borrowId = this.getAttribute('data-id');
            const bookTitle = this.getAttribute('data-title');

            currentBorrowId = borrowId;
            notifyBookTitle.textContent = bookTitle;

            // 使用 Bootstrap 的 jQuery 方法显示模态框
            $('#notifyModal').modal('show');
        });
    });

    confirmNotifyButton.addEventListener('click', function() {
        if (!currentBorrowId) return;

        // 发送通知请求
        fetch(`/borrow/overdue/notify/${currentBorrowId}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify({})
        })
        .then(response => response.json())
        .then(data => {
            // 隐藏模态框
            $('#notifyModal').modal('hide');

            if (data.success) {
                // 显示成功消息
                showAlert('success', data.message);
                // 禁用已点击的通知按钮
                document.querySelector(`.notify-btn[data-id="${currentBorrowId}"]`).disabled = true;
            } else {
                // 显示错误消息
                showAlert('danger', data.message);
            }
        })
        .catch(error => {
            $('#notifyModal').modal('hide');
            showAlert('danger', '操作失败，请稍后重试');
            console.error('Error:', error);
        });
    });

    // 图书搜索功能
    const bookSearchInput = document.getElementById('bookSearch');
    const searchBookBtn = document.getElementById('searchBookBtn');
    const bookSelect = document.getElementById('bookSelect');

    function searchBooks() {
        const searchTerm = bookSearchInput.value.trim();
        if (searchTerm.length < 2) {
            showAlert('warning', '请输入至少2个字符进行搜索');
            return;
        }

        // 清空当前选项
        bookSelect.innerHTML = '<option value="">正在搜索...</option>';

        // 发送搜索请求
        fetch(`/book/api/search?q=${encodeURIComponent(searchTerm)}`)
            .then(response => response.json())
            .then(data => {
                bookSelect.innerHTML = '';

                if (data.success && data.books.length > 0) {
                    // 添加找到的图书
                    data.books.forEach(book => {
                        const option = document.createElement('option');
                        option.value = book.id;
                        option.textContent = `${book.title} - ${book.author} (库存: ${book.stock})`;
                        // 如果库存为0，禁用该选项
                        if (book.stock <= 0) {
                            option.disabled = true;
                            option.textContent += ' [无库存]';
                        }
                        bookSelect.appendChild(option);
                    });
                } else {
                    // 未找到图书
                    const option = document.createElement('option');
                    option.value = '';
                    option.textContent = '未找到相关图书';
                    bookSelect.appendChild(option);
                }
            })
            .catch(error => {
                console.error('搜索图书时出错:', error);
                bookSelect.innerHTML = '<option value="">搜索失败，请重试</option>';
            });
    }

    // 绑定搜索按钮点击事件
    searchBookBtn.addEventListener('click', searchBooks);

    // 绑定回车键搜索
    bookSearchInput.addEventListener('keypress', function(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            searchBooks();
        }
    });

    // 显示提示消息
    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}
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        `;

        document.body.appendChild(alertDiv);

        // 3秒后自动消失
        setTimeout(() => {
            alertDiv.remove();
        }, 3000);
    }
});

================================================================================
File: ./app/static/js/user_activity.js
================================================================================

// app/static/js/user_activity.js
document.addEventListener('DOMContentLoaded', function() {
    let activityChart = null;

    // 初始加载
    loadUserActivity();

    function loadUserActivity() {
        // 显示加载状态
        document.getElementById('user-table-body').innerHTML = `
            <tr class="loading-row">
                <td colspan="4">加载中...</td>
            </tr>
        `;

        // 调用API获取数据
        fetch('/statistics/api/user-activity')
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                updateUserTable(data);
                updateUserChart(data);
            })
            .catch(error => {
                console.error('加载用户活跃度数据失败:', error);
                document.getElementById('user-table-body').innerHTML = `
                    <tr class="error-row">
                        <td colspan="4">加载数据失败，请稍后重试</td>
                    </tr>
                `;
                // 也可以考虑清除或提示图表加载失败
                if (activityChart) {
                    activityChart.destroy();
                    activityChart = null;
                }
                const canvas = document.getElementById('user-activity-chart');
                const ctx = canvas.getContext('2d');
                ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除画布
                // 可以在画布上显示错误信息，但这比较复杂，通常表格的错误提示已足够
            });
    }

    function updateUserTable(data) {
        const tableBody = document.getElementById('user-table-body');

        if (!data || data.length === 0) { // 增加了对 data 本身的检查
            tableBody.innerHTML = `
                <tr class="no-data-row">
                    <td colspan="4">暂无数据</td>
                </tr>
            `;
            return;
        }

        let tableHtml = '';

        data.forEach((user, index) => {
            tableHtml += `
                <tr>
                    <td class="rank">${index + 1}</td>
                    <td>${user.username || 'N/A'}</td>
                    <td>${user.nickname || 'N/A'}</td>
                    <td class="borrow-count">${user.borrow_count}</td>
                </tr>
            `;
        });

        tableBody.innerHTML = tableHtml;
    }

    function updateUserChart(data) {
        // 销毁旧图表
        if (activityChart) {
            activityChart.destroy();
            activityChart = null; // 确保旧实例被完全清除
        }

        const canvas = document.getElementById('user-activity-chart');
        const ctx = canvas.getContext('2d');

        if (!data || data.length === 0) {
            // 如果没有数据，清除画布，避免显示旧图表或空白图表框架
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // 也可以在这里显示 "暂无数据" 的文本到 canvas 上，如果需要
            // 例如:
            // ctx.textAlign = 'center';
            // ctx.fillText('暂无图表数据', canvas.width / 2, canvas.height / 2);
            return;
        }

        // 准备图表数据
        const labels = data.map(user => user.nickname || user.username || '未知用户');
        const borrowCounts = data.map(user => user.borrow_count);

        // 创建图表
        activityChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: labels,
                datasets: [{
                    label: '借阅次数',
                    data: borrowCounts,
                    backgroundColor: 'rgba(153, 102, 255, 0.6)',
                    borderColor: 'rgba(153, 102, 255, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        title: {
                            display: true,
                            text: '借阅次数'
                        }
                    },
                    x: {
                        title: {
                            display: true,
                            text: '用户' // 这个标题现在应该有空间显示了
                        }
                    }
                },
                plugins: {
                    legend: {
                        display: false // 保持不显示图例
                    },
                    title: {
                        display: true, // 如果HTML中已有h3标题，这里可以设为 false
                        text: '最活跃用户排行' // 这个是图表内部的标题
                    }
                },
                layout: { // <--- 这是添加的部分
                    padding: {
                        bottom: 30 // 为X轴标题和标签留出足够的底部空间，可以根据实际显示效果调整此数值
                    }
                }
            }
        });
    }
});

================================================================================
File: ./app/static/js/main.js
================================================================================

// 主JS文件 - 包含登录和注册功能
document.addEventListener('DOMContentLoaded', function() {
    // 主题切换
    const themeToggle = document.getElementById('theme-toggle');
    if (themeToggle) {
        const body = document.body;

        themeToggle.addEventListener('click', function() {
            body.classList.toggle('dark-mode');
            const isDarkMode = body.classList.contains('dark-mode');
            localStorage.setItem('dark-mode', isDarkMode);
            themeToggle.innerHTML = isDarkMode ? '🌙' : '☀️';
        });

        // 从本地存储中加载主题首选项
        const savedDarkMode = localStorage.getItem('dark-mode') === 'true';
        if (savedDarkMode) {
            body.classList.add('dark-mode');
            themeToggle.innerHTML = '🌙';
        }
    }

    // 密码可见性切换
    const passwordToggle = document.getElementById('password-toggle');
    if (passwordToggle) {
        const passwordInput = document.getElementById('password');
        passwordToggle.addEventListener('click', function() {
            const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
            passwordInput.setAttribute('type', type);
            passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️‍🗨️';
        });
    }

    // 登录表单验证
    const loginForm = document.getElementById('login-form');
    if (loginForm) {
        const usernameInput = document.getElementById('username');
        const passwordInput = document.getElementById('password');
        const usernameError = document.getElementById('username-error');
        const passwordError = document.getElementById('password-error');
        const loginButton = document.getElementById('login-button');

        if (usernameInput && usernameError) {
            usernameInput.addEventListener('input', function() {
                if (usernameInput.value.trim() === '') {
                    usernameError.textContent = '用户名不能为空';
                    usernameError.classList.add('show');
                } else {
                    usernameError.classList.remove('show');
                }
            });
        }

        if (passwordInput && passwordError) {
            passwordInput.addEventListener('input', function() {
                if (passwordInput.value.trim() === '') {
                    passwordError.textContent = '密码不能为空';
                    passwordError.classList.add('show');
                } else if (passwordInput.value.length < 6) {
                    passwordError.textContent = '密码长度至少6位';
                    passwordError.classList.add('show');
                } else {
                    passwordError.classList.remove('show');
                }
            });
        }

        loginForm.addEventListener('submit', function(e) {
            let isValid = true;

            // 验证用户名
            if (usernameInput.value.trim() === '') {
                usernameError.textContent = '用户名不能为空';
                usernameError.classList.add('show');
                isValid = false;
            }

            // 验证密码
            if (passwordInput.value.trim() === '') {
                passwordError.textContent = '密码不能为空';
                passwordError.classList.add('show');
                isValid = false;
            } else if (passwordInput.value.length < 6) {
                passwordError.textContent = '密码长度至少6位';
                passwordError.classList.add('show');
                isValid = false;
            }

            if (!isValid) {
                e.preventDefault();
            } else if (loginButton) {
                loginButton.classList.add('loading-state');
            }
        });
    }
    // 注册表单验证
    const registerForm = document.getElementById('register-form');
    if (registerForm) {
        const usernameInput = document.getElementById('username');
        const emailInput = document.getElementById('email');
        const passwordInput = document.getElementById('password');
        const confirmPasswordInput = document.getElementById('confirm_password');
        const verificationCodeInput = document.getElementById('verification_code');

        const usernameError = document.getElementById('username-error');
        const emailError = document.getElementById('email-error');
        const passwordError = document.getElementById('password-error');
        const confirmPasswordError = document.getElementById('confirm-password-error');
        const verificationCodeError = document.getElementById('verification-code-error');

        const registerButton = document.getElementById('register-button');
        const sendCodeBtn = document.getElementById('send-code-btn');

        // 用户名验证
        if (usernameInput && usernameError) {
            usernameInput.addEventListener('input', function() {
                if (usernameInput.value.trim() === '') {
                    usernameError.textContent = '用户名不能为空';
                    usernameError.classList.add('show');
                } else if (usernameInput.value.length < 3) {
                    usernameError.textContent = '用户名至少3个字符';
                    usernameError.classList.add('show');
                } else {
                    usernameError.classList.remove('show');
                }
            });
        }

        // 邮箱验证
        if (emailInput && emailError) {
            emailInput.addEventListener('input', function() {
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (emailInput.value.trim() === '') {
                    emailError.textContent = '邮箱不能为空';
                    emailError.classList.add('show');
                } else if (!emailRegex.test(emailInput.value)) {
                    emailError.textContent = '请输入有效的邮箱地址';
                    emailError.classList.add('show');
                } else {
                    emailError.classList.remove('show');
                }
            });
        }

        // 密码验证
        if (passwordInput && passwordError) {
            passwordInput.addEventListener('input', function() {
                if (passwordInput.value.trim() === '') {
                    passwordError.textContent = '密码不能为空';
                    passwordError.classList.add('show');
                } else if (passwordInput.value.length < 6) {
                    passwordError.textContent = '密码长度至少6位';
                    passwordError.classList.add('show');
                } else {
                    passwordError.classList.remove('show');
                }

                // 检查确认密码是否匹配
                if (confirmPasswordInput && confirmPasswordInput.value) {
                    if (confirmPasswordInput.value !== passwordInput.value) {
                        confirmPasswordError.textContent = '两次输入的密码不匹配';
                        confirmPasswordError.classList.add('show');
                    } else {
                        confirmPasswordError.classList.remove('show');
                    }
                }
            });
        }

        // 确认密码验证
        if (confirmPasswordInput && confirmPasswordError) {
            confirmPasswordInput.addEventListener('input', function() {
                if (confirmPasswordInput.value.trim() === '') {
                    confirmPasswordError.textContent = '请确认密码';
                    confirmPasswordError.classList.add('show');
                } else if (confirmPasswordInput.value !== passwordInput.value) {
                    confirmPasswordError.textContent = '两次输入的密码不匹配';
                    confirmPasswordError.classList.add('show');
                } else {
                    confirmPasswordError.classList.remove('show');
                }
            });
        }

        // 发送验证码按钮
        if (sendCodeBtn) {
            sendCodeBtn.addEventListener('click', function() {
                const email = emailInput.value.trim();
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

                if (!email) {
                    emailError.textContent = '请输入邮箱地址';
                    emailError.classList.add('show');
                    return;
                } else if (!emailRegex.test(email)) {
                    emailError.textContent = '请输入有效的邮箱地址';
                    emailError.classList.add('show');
                    return;
                }

                // 禁用按钮并显示倒计时
                let countdown = 60;
                sendCodeBtn.disabled = true;
                const originalText = sendCodeBtn.textContent;
                sendCodeBtn.textContent = `${countdown}秒后重试`;

                const timer = setInterval(() => {
                    countdown--;
                    sendCodeBtn.textContent = `${countdown}秒后重试`;

                    if (countdown <= 0) {
                        clearInterval(timer);
                        sendCodeBtn.disabled = false;
                        sendCodeBtn.textContent = originalText;
                    }
                }, 1000);

                // 发送请求获取验证码
                fetch('/user/send_verification_code', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ email: email }),
                })
                .then(response => response.json())
                .then(data => {
                    console.log("验证码发送响应:", data);  // 添加调试日志
                    if (data.success) {
                        showMessage('验证码已发送', '请检查您的邮箱', 'success');
                    } else {
                        showMessage('发送失败', data.message || '请稍后重试', 'error');
                        clearInterval(timer);
                        sendCodeBtn.disabled = false;
                        sendCodeBtn.textContent = originalText;
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                    showMessage('发送失败', '网络错误，请稍后重试', 'error');
                    clearInterval(timer);
                    sendCodeBtn.disabled = false;
                    sendCodeBtn.textContent = originalText;
                });
            });
        }

        // 表单提交验证
        registerForm.addEventListener('submit', function(e) {
            let isValid = true;

            // 验证用户名
            if (usernameInput.value.trim() === '') {
                usernameError.textContent = '用户名不能为空';
                usernameError.classList.add('show');
                isValid = false;
            } else if (usernameInput.value.length < 3) {
                usernameError.textContent = '用户名至少3个字符';
                usernameError.classList.add('show');
                isValid = false;
            }

            // 验证邮箱
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            if (emailInput.value.trim() === '') {
                emailError.textContent = '邮箱不能为空';
                emailError.classList.add('show');
                isValid = false;
            } else if (!emailRegex.test(emailInput.value)) {
                emailError.textContent = '请输入有效的邮箱地址';
                emailError.classList.add('show');
                isValid = false;
            }

            // 验证密码
            if (passwordInput.value.trim() === '') {
                passwordError.textContent = '密码不能为空';
                passwordError.classList.add('show');
                isValid = false;
            } else if (passwordInput.value.length < 6) {
                passwordError.textContent = '密码长度至少6位';
                passwordError.classList.add('show');
                isValid = false;
            }

            // 验证确认密码
            if (confirmPasswordInput.value.trim() === '') {
                confirmPasswordError.textContent = '请确认密码';
                confirmPasswordError.classList.add('show');
                isValid = false;
            } else if (confirmPasswordInput.value !== passwordInput.value) {
                confirmPasswordError.textContent = '两次输入的密码不匹配';
                confirmPasswordError.classList.add('show');
                isValid = false;
            }

            // 验证验证码
            if (verificationCodeInput.value.trim() === '') {
                verificationCodeError.textContent = '请输入验证码';
                verificationCodeError.classList.add('show');
                isValid = false;
            }

            if (!isValid) {
                e.preventDefault();
            } else if (registerButton) {
                registerButton.classList.add('loading-state');
            }
        });
    }

    // 通知消息显示函数
    function showMessage(title, message, type) {
        const notification = document.createElement('div');
        notification.className = `notification ${type}`;

        const icon = type === 'success' ? '✓' : '✗';

        notification.innerHTML = `
            <div class="notification-icon">${icon}</div>
            <div class="notification-content">
                <h3>${title}</h3>
                <p>${message}</p>
            </div>
        `;

        document.body.appendChild(notification);

        setTimeout(() => {
            notification.classList.add('show');
        }, 10);

        setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => {
                document.body.removeChild(notification);
            }, 300);
        }, 3000);
    }
});
================================================================================
File: ./app/static/js/book-add.js
================================================================================

/**
 * 图书添加页面脚本
 * 处理图书表单的交互、验证和预览功能
 */
let isSubmitting = false;
$(document).ready(function() {
    // 全局变量
    let cropper;
    let coverBlob;
    let tags = [];
    const coverPreview = $('#coverPreview');
    const coverInput = $('#cover');
    const tagInput = $('#tagInput');
    const tagsContainer = $('#tagsContainer');
    const tagsHiddenInput = $('#tags');

    // 初始化函数
    function initialize() {
        initSelect2();
        initFormProgress();
        initTagsFromInput();
        initCoverHandlers();
        initNumberControls();
        initPriceSlider();
        initCharCounter();
        initFormValidation();
        attachEventListeners();
    }

    // ========== 组件初始化 ==========

    // 初始化Select2
    function initSelect2() {
        $('.select2').select2({
            placeholder: "选择分类...",
            allowClear: true,
            theme: "classic",
            width: '100%'
        });
    }

    // 初始化表单进度条
    function initFormProgress() {
        updateFormProgress();
        $('input, textarea, select').on('change keyup', function() {
            updateFormProgress();
        });
    }

    // 初始化标签（从隐藏输入字段）
    function initTagsFromInput() {
        const tagsValue = $('#tags').val();
        if (tagsValue) {
            tags = tagsValue.split(',');
            renderTags();
        }
    }

    // 初始化封面处理
    function initCoverHandlers() {
        // 拖放上传功能
        coverPreview.on('dragover', function(e) {
            e.preventDefault();
            $(this).addClass('dragover');
        }).on('dragleave drop', function(e) {
            e.preventDefault();
            $(this).removeClass('dragover');
        }).on('drop', function(e) {
            e.preventDefault();
            const file = e.originalEvent.dataTransfer.files[0];
            if (file && file.type.match('image.*')) {
                coverInput[0].files = e.originalEvent.dataTransfer.files;
                coverInput.trigger('change');
            }
        }).on('click', function() {
            if (!$(this).find('img').length) {
                coverInput.click();
            }
        });

        // 重置页面加载完后的字符计数
        if ($('#description').val()) {
            $('#charCount').text($('#description').val().length);
        }
    }

    // 初始化数字控制
    function initNumberControls() {
        $('#stockDecrement').on('click', function() {
            const input = $('#stock');
            const value = parseInt(input.val());
            if (value > parseInt(input.attr('min'))) {
                input.val(value - 1).trigger('change');
            }
        });

        $('#stockIncrement').on('click', function() {
            const input = $('#stock');
            const value = parseInt(input.val());
            input.val(value + 1).trigger('change');
        });
    }

    // 初始化价格滑块
    function initPriceSlider() {
        $('#priceRange').on('input', function() {
            $('#price').val($(this).val());
        });

        $('#price').on('input', function() {
            const value = parseFloat($(this).val()) || 0;
            $('#priceRange').val(Math.min(value, 500));
        });
    }

    // 初始化字符计数器
    function initCharCounter() {
        $('#description').on('input', function() {
            const count = $(this).val().length;
            $('#charCount').text(count);
            if (count > 2000) {
                $('#charCount').addClass('text-danger');
            } else {
                $('#charCount').removeClass('text-danger');
            }
        });
    }

    // 初始化表单验证
    function initFormValidation() {
        $('#bookForm').on('submit', function(e) {
            // 如果表单正在提交中，阻止重复提交
            if (isSubmitting) {
                e.preventDefault();
                showNotification('表单正在提交中，请勿重复点击', 'warning');
                return false;
            }
            let isValid = true;
            $('[required]').each(function() {
                if (!$(this).val().trim()) {
                    isValid = false;
                    $(this).addClass('is-invalid');
                    // 添加错误提示
                    if (!$(this).next('.invalid-feedback').length) {
                        $(this).after(`<div class="invalid-feedback">此字段不能为空</div>`);
                    }
                } else {
                    $(this).removeClass('is-invalid').next('.invalid-feedback').remove();
                }
            });
            // 验证ISBN格式（如果已填写）
            const isbn = $('#isbn').val().trim();
            if (isbn) {
                // 移除所有非数字、X和x字符后检查
                const cleanIsbn = isbn.replace(/[^0-9Xx]/g, '');
                const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/;
                if (!isbnRegex.test(cleanIsbn)) {
                    isValid = false;
                    $('#isbn').addClass('is-invalid');
                    if (!$('#isbn').next('.invalid-feedback').length) {
                        $('#isbn').after(`<div class="invalid-feedback">ISBN格式不正确，应为10位或13位</div>`);
                    }
                }
            }
            if (!isValid) {
                e.preventDefault();
                // 滚动到第一个错误字段
                $('html, body').animate({
                    scrollTop: $('.is-invalid:first').offset().top - 100
                }, 500);
                showNotification('请正确填写所有标记的字段', 'error');
            } else {
                // 设置表单锁定状态
                isSubmitting = true;

                // 修改提交按钮样式
                const submitBtn = $(this).find('button[type="submit"]');
                const originalHtml = submitBtn.html();
                submitBtn.prop('disabled', true)
                        .html('<i class="fas fa-spinner fa-spin"></i> 保存中...');

                // 显示提交中通知
                showNotification('表单提交中...', 'info');

                // 如果表单提交时间过长，30秒后自动解锁
                setTimeout(function() {
                    if (isSubmitting) {
                        isSubmitting = false;
                        submitBtn.prop('disabled', false).html(originalHtml);
                        showNotification('提交超时，请重试', 'warning');
                    }
                }, 30000);
            }
        });
        // 输入时移除错误样式
        $('input, textarea, select').on('input change', function() {
            $(this).removeClass('is-invalid').next('.invalid-feedback').remove();
        });
    }
    // 还需要在服务端处理成功后重置状态
    // 在页面加载完成时，添加监听服务器重定向事件
    $(window).on('pageshow', function(event) {
        if (event.originalEvent.persisted ||
            (window.performance && window.performance.navigation.type === 2)) {
            // 如果页面是从缓存加载的或通过后退按钮回到的
            isSubmitting = false;
            $('button[type="submit"]').prop('disabled', false)
                                     .html('<i class="fas fa-save"></i> 保存图书');
        }
    });

    // 绑定事件监听器
    function attachEventListeners() {
        // 文件选择处理
        coverInput.on('change', handleCoverSelect);

        // 裁剪控制
        $('#rotateLeft').on('click', function() { cropper && cropper.rotate(-90); });
        $('#rotateRight').on('click', function() { cropper && cropper.rotate(90); });
        $('#zoomIn').on('click', function() { cropper && cropper.zoom(0.1); });
        $('#zoomOut').on('click', function() { cropper && cropper.zoom(-0.1); });
        $('#cropImage').on('click', applyCrop);
        $('#removeCover').on('click', removeCover);

        // 标签处理
        tagInput.on('keydown', handleTagKeydown);
        $('#addTagBtn').on('click', addTag);
        $(document).on('click', '.tag-remove', removeTag);

        // ISBN查询
        $('#isbnLookup').on('click', lookupISBN);

        // 预览按钮
        $('#previewBtn').on('click', showPreview);

        // 表单重置
        $('#resetBtn').on('click', confirmReset);
    }

    // ========== 功能函数 ==========

    // 更新表单进度条
    function updateFormProgress() {
        const requiredFields = $('[required]');
        const filledFields = requiredFields.filter(function() {
            return $(this).val() !== '';
        });

        const otherFields = $('input:not([required]), textarea:not([required]), select:not([required])').not('[type="file"]');
        const filledOtherFields = otherFields.filter(function() {
            return $(this).val() !== '';
        });

        let requiredWeight = 70; // 必填字段权重70%
        let otherWeight = 30;    // 非必填字段权重30%

        let requiredProgress = requiredFields.length ? (filledFields.length / requiredFields.length) * requiredWeight : requiredWeight;
        let otherProgress = otherFields.length ? (filledOtherFields.length / otherFields.length) * otherWeight : 0;

        let totalProgress = Math.floor(requiredProgress + otherProgress);

        $('#formProgress').css('width', totalProgress + '%').attr('aria-valuenow', totalProgress);
        $('#progressText').text('完成 ' + totalProgress + '%');

        if (totalProgress >= 100) {
            $('.btn-primary').addClass('pulse');
        } else {
            $('.btn-primary').removeClass('pulse');
        }
    }

    // 处理封面选择
    function handleCoverSelect(e) {
        const file = e.target.files[0];
        if (!file) return;
        // 验证文件类型
        if (!file.type.match('image.*')) {
            showNotification('请选择图片文件', 'warning');
            return;
        }
        // 验证文件大小（最大5MB）
        if (file.size > 5 * 1024 * 1024) {
            showNotification('图片大小不能超过5MB', 'warning');
            return;
        }
        const reader = new FileReader();
        reader.onload = function(e) {
            // 先显示在预览框中，确保用户能立即看到上传的图片
            coverPreview.html(`<img src="${e.target.result}" class="cover-image" alt="图书封面预览">`);

            // 准备裁剪图片
            $('#cropperImage').attr('src', e.target.result);

            // 确保图片加载完成后再显示模态框
            $('#cropperImage').on('load', function() {
                // 打开模态框
                $('#cropperModal').modal('show');

                // 在模态框完全显示后初始化裁剪器
                $('#cropperModal').on('shown.bs.modal', function() {
                    if (cropper) {
                        cropper.destroy();
                    }

                    try {
                        cropper = new Cropper(document.getElementById('cropperImage'), {
                            aspectRatio: 5 / 7,
                            viewMode: 2,
                            responsive: true,
                            guides: true,
                            background: true,
                            ready: function() {
                                console.log('Cropper初始化成功');
                            }
                        });
                    } catch (err) {
                        console.error('Cropper初始化失败:', err);
                        showNotification('图片处理工具初始化失败，请重试', 'error');
                    }
                });
            });
        };

        // 处理读取错误
        reader.onerror = function() {
            showNotification('读取图片失败，请重试', 'error');
        };

        reader.readAsDataURL(file);
    }
    // 应用裁剪
    function applyCrop() {
        if (!cropper) {
            showNotification('图片处理工具未就绪，请重新上传', 'error');
            $('#cropperModal').modal('hide');
            return;
        }

        try {
            const canvas = cropper.getCroppedCanvas({
                width: 500,
                height: 700,
                fillColor: '#fff',
                imageSmoothingEnabled: true,
                imageSmoothingQuality: 'high',
            });
            if (!canvas) {
                throw new Error('无法生成裁剪后的图片');
            }
            canvas.toBlob(function(blob) {
                if (!blob) {
                    showNotification('图片处理失败，请重试', 'error');
                    return;
                }

                const url = URL.createObjectURL(blob);
                coverPreview.html(`<img src="${url}" class="cover-image" alt="图书封面">`);
                coverBlob = blob;
                // 模拟File对象
                const fileList = new DataTransfer();
                const file = new File([blob], "cover.jpg", {type: "image/jpeg"});
                fileList.items.add(file);
                document.getElementById('cover').files = fileList.files;
                $('#cropperModal').modal('hide');
                showNotification('封面图片已更新', 'success');
            }, 'image/jpeg', 0.95);
        } catch (err) {
            console.error('裁剪失败:', err);
            showNotification('图片裁剪失败，请重试', 'error');
            $('#cropperModal').modal('hide');
        }
    }

    // 移除封面
    function removeCover() {
        coverPreview.html(`
            <div class="no-cover-placeholder">
                <i class="fas fa-book-open"></i>
                <span>暂无封面</span>
                <p class="placeholder-tip">点击上传或拖放图片至此处</p>
            </div>
        `);
        coverInput.val('');
        coverBlob = null;
    }

    // 渲染标签
    function renderTags() {
        tagsContainer.empty();
        tags.forEach(tag => {
            tagsContainer.append(`
                <div class="tag">
                    <span class="tag-text">${tag}</span>
                    <button type="button" class="tag-remove" data-tag="${tag}">
                        <i class="fas fa-times"></i>
                    </button>
                </div>
            `);
        });
        tagsHiddenInput.val(tags.join(','));
    }

    // 添加标签
    function addTag() {
        const tag = tagInput.val().trim();
        if (tag && !tags.includes(tag)) {
            tags.push(tag);
            renderTags();
            tagInput.val('').focus();
        }
    }

    // 处理标签输入键盘事件
    function handleTagKeydown(e) {
        if (e.key === 'Enter' || e.key === ',') {
            e.preventDefault();
            addTag();
        }
    }

    // 移除标签
    function removeTag() {
        const tagToRemove = $(this).data('tag');
        tags = tags.filter(t => t !== tagToRemove);
        renderTags();
    }

    // ISBN查询
    function lookupISBN() {
        const isbn = $('#isbn').val().trim();
        if (!isbn) {
            showNotification('请先输入ISBN', 'warning');
            return;
        }

        // 验证ISBN格式
        const cleanIsbn = isbn.replace(/[^0-9Xx]/g, '');
        const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/;
        if (!isbnRegex.test(cleanIsbn)) {
            showNotification('ISBN格式不正确，应为10位或13位', 'warning');
            return;
        }

        $(this).html('<i class="fas fa-spinner fa-spin"></i>');

        // 先检查ISBN是否已存在
        $.get('/book/api/check-isbn', {isbn: isbn}, function(data) {
            if (data.exists) {
                $('#isbnLookup').html('<i class="fas fa-search"></i>');
                showNotification(`ISBN "${isbn}" 已存在: 《${data.book_title}》`, 'warning');
                $('#isbn').addClass('is-invalid');
                if (!$('#isbn').next('.invalid-feedback').length) {
                    $('#isbn').after(`<div class="invalid-feedback">此ISBN已被图书《${data.book_title}》使用</div>`);
                }
            } else {
                // 继续查询外部API（模拟）
                simulateISBNLookup(isbn);
            }
        }).fail(function() {
            $('#isbnLookup').html('<i class="fas fa-search"></i>');
            showNotification('服务器查询失败，请稍后再试', 'error');
        });
    }

    // 模拟ISBN查询
    function simulateISBNLookup(isbn) {
        // 模拟API查询延迟
        setTimeout(() => {
            // 模拟查到的数据
            if (isbn === '9787020002207') {
                $('#title').val('红楼梦').trigger('blur');
                $('#author').val('曹雪芹').trigger('blur');
                $('#publisher').val('人民文学出版社').trigger('blur');
                $('#publish_year').val('1996').trigger('blur');
                $('#category_id').val('1').trigger('change');
                tags = ['中国文学', '古典', '名著'];
                renderTags();
                $('#description').val('《红楼梦》是中国古代章回体长篇小说，中国古典四大名著之一，通行本共120回，一般认为前80回是清代作家曹雪芹所著，后40回作者有争议。小说以贾、史、王、薛四大家族的兴衰为背景，以贾府的家庭琐事、闺阁闲情为脉络，以贾宝玉、林黛玉、薛宝钗的爱情婚姻悲剧为主线，刻画了以贾宝玉和金陵十二钗为中心的正邪两赋有情人的人性美和悲剧美。').trigger('input');
                $('#price').val('59.70').trigger('input');
                $('#priceRange').val('59.70');

                showNotification('ISBN查询成功', 'success');
            } else if (isbn === '9787544270878') {
                $('#title').val('挪威的森林').trigger('blur');
                $('#author').val('村上春树').trigger('blur');
                $('#publisher').val('南海出版社').trigger('blur');
                $('#publish_year').val('2017').trigger('blur');
                $('#category_id').val('2').trigger('change');
                tags = ['外国文学', '日本', '小说'];
                renderTags();
                $('#description').val('《挪威的森林》是日本作家村上春树创作的长篇小说，首次出版于1987年。小说讲述了一个悲伤的爱情故事，背景设定在20世纪60年代末的日本。主人公渡边纠缠在与平静的直子和开朗的绿子两人的感情中，最终选择了生活。').trigger('input');
                $('#price').val('39.50').trigger('input');
                $('#priceRange').val('39.50');

                showNotification('ISBN查询成功', 'success');
            } else {
                showNotification('未找到相关图书信息', 'warning');
            }

            $('#isbnLookup').html('<i class="fas fa-search"></i>');
            updateFormProgress();
        }, 1500);
    }

    // 显示预览
    function showPreview() {
        // 检查必填字段
        if (!$('#title').val().trim() || !$('#author').val().trim()) {
            showNotification('请至少填写书名和作者后再预览', 'warning');
            return;
        }
        try {
            // 确保所有值都有默认值，防止undefined错误
            const title = $('#title').val() || '未填写标题';
            const author = $('#author').val() || '未填写作者';
            const publisher = $('#publisher').val() || '-';
            const isbn = $('#isbn').val() || '-';
            const publishYear = $('#publish_year').val() || '-';
            const description = $('#description').val() || '';
            const stock = $('#stock').val() || '0';
            let price = parseFloat($('#price').val()) || 0;

            // 填充预览内容
            $('#previewTitle').text(title);
            $('#previewAuthor').text(author ? '作者: ' + author : '未填写作者');
            $('#previewPublisher').text(publisher);
            $('#previewISBN').text(isbn);
            $('#previewYear').text(publishYear);
            // 获取分类文本
            const categoryId = $('#category_id').val();
            const categoryText = categoryId ? $('#category_id option:selected').text() : '-';
            $('#previewCategory').text(categoryText);
            // 价格和库存
            $('#previewPrice').text(price ? '¥' + price.toFixed(2) : '¥0.00');
            $('#previewStock').text('库存: ' + stock);
            // 标签
            const previewTags = $('#previewTags');
            previewTags.empty();
            if (tags && tags.length > 0) {
                tags.forEach(tag => {
                    previewTags.append(`<span class="preview-tag">${tag}</span>`);
                });
            } else {
                previewTags.append('<span class="no-tags">暂无标签</span>');
            }
            // 描述
            if (description) {
                $('#previewDescription').html(`<p>${description.replace(/\n/g, '<br>')}</p>`);
            } else {
                $('#previewDescription').html(`<p class="placeholder-text">暂无简介内容</p>`);
            }
            // 封面
            const previewCover = $('#previewCover');
            previewCover.empty(); // 清空现有内容

            if ($('#coverPreview img').length) {
                const coverSrc = $('#coverPreview img').attr('src');
                previewCover.html(`<img src="${coverSrc}" class="preview-cover-img" alt="封面预览">`);
            } else {
                previewCover.html(`
                    <div class="no-cover-placeholder">
                        <i class="fas fa-book-open"></i>
                        <span>暂无封面</span>
                    </div>
                `);
            }
            // 显示预览模态框
            $('#previewModal').modal('show');

            console.log('预览模态框已显示');
        } catch (err) {
            console.error('生成预览时发生错误:', err);
            showNotification('生成预览时出错，请重试', 'error');
        }
    }

    // 确认重置表单
    function confirmReset() {
        if (confirm('确定要重置表单吗？所有已填写的内容将被清空。')) {
            $('#bookForm')[0].reset();
            removeCover();
            tags = [];
            renderTags();
            updateFormProgress();
            $('.select2').val(null).trigger('change');
            $('#charCount').text('0');

            showNotification('表单已重置', 'info');
        }
    }

    // 通知提示函数
    function showNotification(message, type) {
        // 创建通知元素
        const notification = $(`
            <div class="notification ${type}-notification animate__animated animate__fadeInRight">
                <div class="notification-icon">
                    <i class="fas ${getIconForType(type)}"></i>
                </div>
                <div class="notification-content">
                    <p>${message}</p>
                </div>
                <button class="notification-close">
                    <i class="fas fa-times"></i>
                </button>
            </div>
        `);

        // 添加到页面
        if ($('.notification-container').length === 0) {
            $('body').append('<div class="notification-container"></div>');
        }
        $('.notification-container').append(notification);

        // 自动关闭
        setTimeout(() => {
            notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
            setTimeout(() => {
                notification.remove();
            }, 500);
        }, 5000);

        // 点击关闭
        notification.find('.notification-close').on('click', function() {
            notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
            setTimeout(() => {
                notification.remove();
            }, 500);
        });
    }

    function getIconForType(type) {
        switch(type) {
            case 'success': return 'fa-check-circle';
            case 'warning': return 'fa-exclamation-triangle';
            case 'error': return 'fa-times-circle';
            case 'info':
            default: return 'fa-info-circle';
        }
    }

    // 初始化页面
    initialize();
});

================================================================================
File: ./app/static/js/overdue_analysis.js
================================================================================

// app/static/js/overdue_analysis.js
document.addEventListener('DOMContentLoaded', function() {
    let overdueRangeChart = null;
    let overdueStatusChart = null;

    // Initial load
    loadOverdueStatistics();

    function loadOverdueStatistics() {
        // Call API to get data
        fetch('/statistics/api/overdue-statistics')
            .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response.json();
            })
            .then(data => {
                if (!data) {
                    console.error('加载逾期统计数据失败: API返回空数据');
                    // Optionally update UI to show error for cards
                    return;
                }
                updateOverdueCards(data);
                updateOverdueRangeChart(data.overdue_ranges);
                updateOverdueStatusChart(data);
            })
            .catch(error => {
                console.error('加载逾期统计数据失败:', error);
                // Optionally update UI to show error for cards and charts
                // For charts, you might want to clear them or show an error message
                clearChart('overdue-range-chart');
                clearChart('overdue-status-chart');
            });
    }

    function updateOverdueCards(data) {
        document.getElementById('total-borrows').querySelector('.card-value').textContent = data.total_borrows || 0;
        document.getElementById('current-overdue').querySelector('.card-value').textContent = data.current_overdue || 0;
        document.getElementById('returned-overdue').querySelector('.card-value').textContent = data.returned_overdue || 0;
        const overdueRate = data.overdue_rate !== undefined ? data.overdue_rate : 0;
        document.getElementById('overdue-rate').querySelector('.card-value').textContent = overdueRate + '%';
    }

    function clearChart(canvasId) {
        const canvas = document.getElementById(canvasId);
        if (canvas) {
            const ctx = canvas.getContext('2d');
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // Optionally display a message like "数据加载失败" or "暂无数据"
            // ctx.textAlign = 'center';
            // ctx.fillText('数据加载失败', canvas.width / 2, canvas.height / 2);
        }
    }

    function updateOverdueRangeChart(rangeData) {
        // Destroy old chart
        if (overdueRangeChart) {
            overdueRangeChart.destroy();
            overdueRangeChart = null;
        }

        const canvas = document.getElementById('overdue-range-chart');
        if (!canvas) return;
        const ctx = canvas.getContext('2d');

        if (!rangeData || rangeData.length === 0) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // Optionally display "暂无数据"
            // ctx.textAlign = 'center';
            // ctx.fillText('暂无逾期时长数据', canvas.width / 2, canvas.height / 2);
            return;
        }

        // Prepare chart data
        const labels = rangeData.map(item => item.range);
        const counts = rangeData.map(item => item.count);

        // Create chart
        overdueRangeChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: labels,
                datasets: [{
                    label: '逾期数量', // This will appear in the legend
                    data: counts,
                    backgroundColor: [
                        'rgba(255, 206, 86, 0.7)', // 1-7天
                        'rgba(255, 159, 64, 0.7)', // 8-14天
                        'rgba(255, 99, 132, 0.7)', // 15-30天
                        'rgba(230, 0, 0, 0.7)'    // 30天以上 (made it darker red)
                    ],
                    borderColor: [ // Optional: Add border colors if you want them distinct
                        'rgba(255, 206, 86, 1)',
                        'rgba(255, 159, 64, 1)',
                        'rgba(255, 99, 132, 1)',
                        'rgba(230, 0, 0, 1)'
                    ],
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        title: {
                            display: true,
                            text: '数量'
                        }
                    },
                    x: {
                        // No title for X-axis needed here based on current config
                    }
                },
                plugins: {
                    title: { // Chart.js internal title
                        display: false, // Set to false to use HTML <h3> title
                        // text: '逾期时长分布' // This would be the Chart.js title if display: true
                    },
                    legend: {
                        display: true, // Show legend for '逾期数量'
                        position: 'top',
                    }
                },
                layout: {
                    padding: {
                        left: 25,    // Increased left padding for Y-axis title "数量"
                        bottom: 10,  // Padding for X-axis labels
                        top: 10,     // Padding for legend/top elements
                        right: 10
                    }
                }
            }
        });
    }

    function updateOverdueStatusChart(data) {
        // Destroy old chart
        if (overdueStatusChart) {
            overdueStatusChart.destroy();
            overdueStatusChart = null;
        }

        const canvas = document.getElementById('overdue-status-chart');
        if (!canvas) return;
        const ctx = canvas.getContext('2d');

        const totalBorrows = data.total_borrows || 0;
        const currentOverdue = data.current_overdue || 0;
        const returnedOverdue = data.returned_overdue || 0;

        if (totalBorrows === 0 && currentOverdue === 0 && returnedOverdue === 0) {
             ctx.clearRect(0, 0, canvas.width, canvas.height);
            // Optionally display "暂无数据"
            // ctx.textAlign = 'center';
            // ctx.fillText('暂无借阅状态数据', canvas.width / 2, canvas.height / 2);
            return;
        }


        // Prepare chart data
        const statusLabels = ['当前逾期', '历史逾期 (已归还)', '正常在借/已还']; // Clarified labels
        const normalCount = totalBorrows - currentOverdue - returnedOverdue;
        const statusData = [
            currentOverdue,
            returnedOverdue,
            normalCount < 0 ? 0 : normalCount // Ensure not negative
        ];

        // Create chart
        overdueStatusChart = new Chart(ctx, {
            type: 'pie',
            data: {
                labels: statusLabels,
                datasets: [{
                    data: statusData,
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.7)',  // 当前逾期
                        'rgba(255, 206, 86, 0.7)',  // 历史逾期
                        'rgba(75, 192, 192, 0.7)'   // 正常
                    ],
                    borderColor: [ // Optional: Add border colors
                        'rgba(255, 99, 132, 1)',
                        'rgba(255, 206, 86, 1)',
                        'rgba(75, 192, 192, 1)'
                    ],
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    title: { // Chart.js internal title
                        display: false, // Set to false to use HTML <h3> title
                        // text: '借阅状态分布' // This would be the Chart.js title if display: true
                    },
                    legend: {
                        display: true,
                        position: 'top', // Pie chart legends are often at top or side
                    }
                },
                layout: {
                    padding: 20 // General padding for pie chart (can be object: {top: val, ...})
                                // e.g., { top: 20, bottom: 20, left: 10, right: 10 }
                }
            }
        });
    }
});

================================================================================
File: ./app/static/js/announcement-list.js
================================================================================

// 通知公告列表页面的Javascript
document.addEventListener('DOMContentLoaded', function() {
    // 平滑滚动到页面锚点
    const scrollToAnchor = (anchorId) => {
        const element = document.getElementById(anchorId);
        if (element) {
            element.scrollIntoView({
                behavior: 'smooth',
                block: 'start'
            });
        }
    };

    // 如果URL中有锚点，执行滚动
    if (window.location.hash) {
        const anchorId = window.location.hash.substring(1);
        setTimeout(() => scrollToAnchor(anchorId), 300);
    }
});

================================================================================
File: ./app/static/js/statistics.js
================================================================================

// app/static/js/statistics.js
document.addEventListener('DOMContentLoaded', function() {
    // 统计模块主页初始化
    console.log('统计分析模块加载完成');
});

================================================================================
File: ./app/static/js/user-profile.js
================================================================================

// 用户个人中心页面交互
document.addEventListener('DOMContentLoaded', function() {
    // 获取表单对象
    const profileForm = document.getElementById('profileForm');
    const passwordForm = document.getElementById('passwordForm');

    // 表单验证逻辑
    if (profileForm) {
        profileForm.addEventListener('submit', function(event) {
            let valid = true;

            // 清除之前的错误提示
            clearValidationErrors();

            // 验证邮箱
            const emailField = document.getElementById('email');
            if (emailField.value.trim() !== '') {
                const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (!emailPattern.test(emailField.value.trim())) {
                    showError(emailField, '请输入有效的邮箱地址');
                    valid = false;
                }
            }

            // 验证手机号
            const phoneField = document.getElementById('phone');
            if (phoneField.value.trim() !== '') {
                const phonePattern = /^1[3456789]\d{9}$/;
                if (!phonePattern.test(phoneField.value.trim())) {
                    showError(phoneField, '请输入有效的手机号码');
                    valid = false;
                }
            }

            if (!valid) {
                event.preventDefault();
            }
        });
    }

    // 密码修改表单验证
    if (passwordForm) {
        passwordForm.addEventListener('submit', function(event) {
            let valid = true;

            // 清除之前的错误提示
            clearValidationErrors();

            // 验证当前密码
            const currentPasswordField = document.getElementById('current_password');
            if (currentPasswordField.value.trim() === '') {
                showError(currentPasswordField, '请输入当前密码');
                valid = false;
            }

            // 验证新密码
            const newPasswordField = document.getElementById('new_password');
            if (newPasswordField.value.trim() === '') {
                showError(newPasswordField, '请输入新密码');
                valid = false;
            } else if (newPasswordField.value.length < 6) {
                showError(newPasswordField, '密码长度至少为6个字符');
                valid = false;
            }

            // 验证确认密码
            const confirmPasswordField = document.getElementById('confirm_password');
            if (confirmPasswordField.value.trim() === '') {
                showError(confirmPasswordField, '请确认新密码');
                valid = false;
            } else if (confirmPasswordField.value !== newPasswordField.value) {
                showError(confirmPasswordField, '两次输入的密码不一致');
                valid = false;
            }

            if (!valid) {
                event.preventDefault();
            }
        });
    }

    // 获取用户统计数据
    fetchUserStats();

    // 获取用户活动记录
    const activityFilter = document.getElementById('activityFilter');
    if (activityFilter) {
        // 初始加载
        fetchUserActivities('all');

        // 监听过滤器变化
        activityFilter.addEventListener('change', function() {
            fetchUserActivities(this.value);
        });
    }

    // 处理URL中的tab参数
    const urlParams = new URLSearchParams(window.location.search);
    const tabParam = urlParams.get('tab');
    if (tabParam) {
        const tabElement = document.getElementById(`${tabParam}-tab`);
        if (tabElement) {
            $('#profileTabs a[href="#' + tabParam + '"]').tab('show');
        }
    }

    // 清除表单验证错误
    function clearValidationErrors() {
        const invalidFields = document.querySelectorAll('.is-invalid');
        const feedbackElements = document.querySelectorAll('.invalid-feedback');

        invalidFields.forEach(field => {
            field.classList.remove('is-invalid');
        });

        feedbackElements.forEach(element => {
            element.parentNode.removeChild(element);
        });
    }

    // 显示错误消息
    function showError(field, message) {
        field.classList.add('is-invalid');

        const feedback = document.createElement('div');
        feedback.className = 'invalid-feedback';
        feedback.innerText = message;

        field.parentNode.appendChild(feedback);
    }

    // 获取用户统计数据
    function fetchUserStats() {
        // 这里使用虚拟数据，实际应用中应当从后端获取
        // fetch('/api/user/stats')
        // .then(response => response.json())
        // .then(data => {
        //     updateUserStats(data);
        // });

        // 模拟数据
        setTimeout(() => {
            const mockData = {
                borrow: 2,
                returned: 15,
                overdue: 0
            };
            updateUserStats(mockData);
        }, 500);
    }

    // 更新用户统计显示
    function updateUserStats(data) {
        const borrowCount = document.getElementById('borrowCount');
        const returnedCount = document.getElementById('returnedCount');
        const overdueCount = document.getElementById('overdueCount');

        if (borrowCount) borrowCount.textContent = data.borrow;
        if (returnedCount) returnedCount.textContent = data.returned;
        if (overdueCount) overdueCount.textContent = data.overdue;
    }

    // 获取用户活动记录
    function fetchUserActivities(type) {
        const timelineContainer = document.getElementById('activityTimeline');
        if (!timelineContainer) return;

        // 显示加载中
        timelineContainer.innerHTML = `
            <div class="timeline-loading">
                <div class="spinner-border text-primary" role="status">
                    <span class="sr-only">Loading...</span>
                </div>
                <p>加载中...</p>
            </div>
        `;

        // 实际应用中应当从后端获取
        // fetch(`/api/user/activities?type=${type}`)
        // .then(response => response.json())
        // .then(data => {
        //     renderActivityTimeline(data, timelineContainer);
        // });

        // 模拟数据
        setTimeout(() => {
            const mockActivities = [
                {
                    id: 1,
                    type: 'login',
                    title: '系统登录',
                    details: '成功登录系统',
                    time: '2023-04-28 15:30:22',
                    ip: '192.168.1.1'
                },
                {
                    id: 2,
                    type: 'borrow',
                    title: '借阅图书',
                    details: '借阅《JavaScript高级编程》',
                    time: '2023-04-27 11:45:10',
                    book_id: 101
                },
                {
                    id: 3,
                    type: 'return',
                    title: '归还图书',
                    details: '归还《Python数据分析》',
                    time: '2023-04-26 09:15:33',
                    book_id: 95
                },
                {
                    id: 4,
                    type: 'login',
                    title: '系统登录',
                    details: '成功登录系统',
                    time: '2023-04-25 08:22:15',
                    ip: '192.168.1.1'
                }
            ];

            // 根据筛选条件过滤活动
            let filteredActivities = mockActivities;
            if (type !== 'all') {
                filteredActivities = mockActivities.filter(activity => activity.type === type);
            }

            renderActivityTimeline(filteredActivities, timelineContainer);
        }, 800);
    }

    // 渲染活动时间线
    function renderActivityTimeline(activities, container) {
        if (!activities || activities.length === 0) {
            container.innerHTML = '<div class="text-center p-4">暂无活动记录</div>';
            return;
        }

        let timelineHTML = '';

        activities.forEach((activity, index) => {
            let iconClass = 'fas fa-info';

            if (activity.type === 'login') {
                iconClass = 'fas fa-sign-in-alt';
            } else if (activity.type === 'borrow') {
                iconClass = 'fas fa-book';
            } else if (activity.type === 'return') {
                iconClass = 'fas fa-undo';
            }

            const isLast = index === activities.length - 1;

            timelineHTML += `
                <div class="timeline-item ${isLast ? 'last' : ''} timeline-type-${activity.type}">
                    <div class="timeline-icon">
                        <i class="${iconClass}"></i>
                    </div>
                    <div class="timeline-content">
                        <div class="timeline-header">
                            <h5 class="timeline-title">${activity.title}</h5>
                            <div class="timeline-time">${activity.time}</div>
                        </div>
                        <div class="timeline-details">
                            ${activity.details}
                            ${activity.ip ? `<div class="text-muted small">IP: ${activity.ip}</div>` : ''}
                        </div>
                    </div>
                </div>
            `;
        });

        container.innerHTML = timelineHTML;
    }
});

================================================================================
File: ./app/static/js/book-list.js
================================================================================

// 图书列表页面脚本
$(document).ready(function() {
  // 处理分类筛选
  function setFilter(button, categoryId) {
    // 移除所有按钮的活跃状态
    $('.filter-btn').removeClass('active');
    // 为当前点击的按钮添加活跃状态
    $(button).addClass('active');
    // 设置隐藏的分类ID输入值
    $('#category_id').val(categoryId);
    // 提交表单
    $(button).closest('form').submit();
  }

  // 处理排序方向切换
  function toggleSortDirection(button) {
    const $button = $(button);
    const isAsc = $button.hasClass('asc');

    // 切换方向类
    $button.toggleClass('asc desc');

    // 更新图标
    if (isAsc) {
      $button.find('i').removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down');
      $('#sort_order').val('desc');
    } else {
      $button.find('i').removeClass('fa-sort-amount-down').addClass('fa-sort-amount-up');
      $('#sort_order').val('asc');
    }

    // 提交表单
    $button.closest('form').submit();
  }

  // 将函数暴露到全局作用域
  window.setFilter = setFilter;
  window.toggleSortDirection = toggleSortDirection;

  // 处理删除图书
  let bookIdToDelete = null;

  $('.delete-book').click(function(e) {
    e.preventDefault();
    bookIdToDelete = $(this).data('id');
    const bookTitle = $(this).data('title');
    $('#deleteBookTitle').text(bookTitle);
    $('#deleteModal').modal('show');
  });

  $('#confirmDelete').click(function() {
    if (!bookIdToDelete) return;

    $.ajax({
      url: `/book/delete/${bookIdToDelete}`,
      type: 'POST',
      success: function(response) {
        if (response.success) {
          $('#deleteModal').modal('hide');
          // 显示成功消息
          showNotification(response.message, 'success');
          // 移除图书卡片
          $(`.book-card[data-id="${bookIdToDelete}"]`).fadeOut(300, function() {
            $(this).remove();
          });
          setTimeout(() => {
            if ($('.book-card').length === 0) {
              location.reload(); // 如果没有图书了，刷新页面显示"无图书"提示
            }
          }, 500);
        } else {
          $('#deleteModal').modal('hide');
          showNotification(response.message, 'error');
        }
      },
      error: function() {
        $('#deleteModal').modal('hide');
        showNotification('删除操作失败，请稍后重试', 'error');
      }
    });
  });

  // 处理借阅图书
  $('.borrow-book').click(function(e) {
    e.preventDefault();
    const bookId = $(this).data('id');

    $.ajax({
      url: `/borrow/add/${bookId}`,
      type: 'POST',
      success: function(response) {
        if (response.success) {
          showNotification(response.message, 'success');
          // 可以更新UI显示，比如更新库存或禁用借阅按钮
          setTimeout(() => {
            location.reload();
          }, 800);
        } else {
          showNotification(response.message, 'error');
        }
      },
      error: function() {
        showNotification('借阅操作失败，请稍后重试', 'error');
      }
    });
  });

  // 显示通知
  function showNotification(message, type) {
    // 移除可能存在的旧通知
    $('.notification-alert').remove();

    const alertClass = type === 'success' ? 'notification-success' : 'notification-error';
    const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';

    const notification = `
      <div class="notification-alert ${alertClass}">
        <div class="notification-icon">
          <i class="fas ${iconClass}"></i>
        </div>
        <div class="notification-message">${message}</div>
        <button class="notification-close">
          <i class="fas fa-times"></i>
        </button>
      </div>
    `;

    $('body').append(notification);

    // 显示通知
    setTimeout(() => {
      $('.notification-alert').addClass('show');
    }, 10);

    // 通知自动关闭
    setTimeout(() => {
      $('.notification-alert').removeClass('show');
      setTimeout(() => {
        $('.notification-alert').remove();
      }, 300);
    }, 4000);

    // 点击关闭按钮
    $('.notification-close').click(function() {
      $(this).closest('.notification-alert').removeClass('show');
      setTimeout(() => {
        $(this).closest('.notification-alert').remove();
      }, 300);
    });
  }

  // 添加通知样式
  const notificationCSS = `
    .notification-alert {
      position: fixed;
      top: 20px;
      right: 20px;
      min-width: 280px;
      max-width: 350px;
      background-color: white;
      border-radius: 8px;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
      display: flex;
      align-items: center;
      padding: 15px;
      transform: translateX(calc(100% + 20px));
      transition: transform 0.3s ease;
      z-index: 9999;
    }
    
    .notification-alert.show {
      transform: translateX(0);
    }
    
    .notification-success {
      border-left: 4px solid var(--success-color);
    }
    
    .notification-error {
      border-left: 4px solid var(--danger-color);
    }
    
    .notification-icon {
      margin-right: 15px;
      font-size: 24px;
    }
    
    .notification-success .notification-icon {
      color: var(--success-color);
    }
    
    .notification-error .notification-icon {
      color: var(--danger-color);
    }
    
    .notification-message {
      flex: 1;
      font-size: 0.95rem;
      color: var(--text-color);
    }
    
    .notification-close {
      background: none;
      border: none;
      color: var(--text-lighter);
      cursor: pointer;
      padding: 5px;
      margin-left: 10px;
      font-size: 0.8rem;
    }
    
    .notification-close:hover {
      color: var(--text-color);
    }
    
    @media (max-width: 576px) {
      .notification-alert {
        top: auto;
        bottom: 20px;
        left: 20px;
        right: 20px;
        min-width: auto;
        max-width: none;
        transform: translateY(calc(100% + 20px));
      }
      
      .notification-alert.show {
        transform: translateY(0);
      }
    }
  `;

  // 将通知样式添加到头部
  $('<style>').text(notificationCSS).appendTo('head');

  // 修复图书卡片布局的高度问题
  function adjustCardHeights() {
    // 重置所有卡片高度
    $('.book-card').css('height', 'auto');

    // 在大屏幕上应用等高布局
    if (window.innerWidth >= 768) {
      // 分组按行
      const rows = {};
      $('.book-card').each(function() {
        const offsetTop = $(this).offset().top;
        if (!rows[offsetTop]) {
          rows[offsetTop] = [];
        }
        rows[offsetTop].push($(this));
      });

      // 为每行设置相同高度
      Object.keys(rows).forEach(offsetTop => {
        const cards = rows[offsetTop];
        let maxHeight = 0;

        // 找出最大高度
        cards.forEach(card => {
          const height = card.outerHeight();
          if (height > maxHeight) {
            maxHeight = height;
          }
        });

        // 应用最大高度
        cards.forEach(card => {
          card.css('height', maxHeight + 'px');
        });
      });
    }
  }

  // 初始调整高度
  $(window).on('load', adjustCardHeights);

  // 窗口大小变化时调整高度
  $(window).on('resize', adjustCardHeights);

  // 为封面图片添加加载错误处理
  $('.book-cover').on('error', function() {
    const $this = $(this);
    const title = $this.attr('alt') || '图书';

    // 替换为默认封面
    $this.replaceWith(`
      <div class="default-cover">
        <i class="fas fa-book"></i>
        <span class="default-cover-text">${title.charAt(0)}</span>
      </div>
    `);
  });

  // 添加初始动画效果
  $('.book-card').each(function(index) {
    $(this).css({
      'opacity': '0',
      'transform': 'translateY(20px)'
    });

    setTimeout(() => {
      $(this).css({
        'opacity': '1',
        'transform': 'translateY(0)',
        'transition': 'opacity 0.5s ease, transform 0.5s ease'
      });
    }, 50 * index);
  });
});

================================================================================
File: ./app/static/js/inventory-book-logs.js
================================================================================

// 冰雪奇缘风格的图书库存日志页面JavaScript
document.addEventListener('DOMContentLoaded', function() {

    // 雪花动画效果
    createSnowflakes();

    // 表格行动画效果
    animateTableRows();

    // 为表格行添加互动效果
    addTableRowInteractions();

    // 添加书籍封面魔法效果
    addBookCoverMagic();

    // 添加按钮魔法效果
    addButtonMagic();

    // 响应式表格标签
    makeTableResponsive();
});

// 创建额外的雪花
function createSnowflakes() {
    const snowflakesContainer = document.querySelector('.snowflakes');
    if (!snowflakesContainer) return;

    // 添加更多雪花，共30个
    for (let i = 0; i < 20; i++) {
        const snowflake = document.createElement('div');
        snowflake.className = 'snowflake';
        snowflake.textContent = ['❄', '❅', '❆'][Math.floor(Math.random() * 3)];

        // 随机位置和动画
        const left = Math.random() * 100;
        const animDuration = 8 + Math.random() * 10;
        const animDelay = Math.random() * 5;
        const fontSize = 0.8 + Math.random() * 1.2;

        snowflake.style.left = `${left}%`;
        snowflake.style.animationDuration = `${animDuration}s`;
        snowflake.style.animationDelay = `${animDelay}s`;
        snowflake.style.fontSize = `${fontSize}em`;

        snowflakesContainer.appendChild(snowflake);
    }
}

// 表格行动画效果
function animateTableRows() {
    const tableRows = document.querySelectorAll('.table-row');

    tableRows.forEach((row, index) => {
        // 设置动画延迟，创建瀑布效果
        row.style.animationDelay = `${index * 0.08}s`;
        row.classList.add('fade-in');

        // 为入库和出库行添加不同的动画效果
        if (row.dataset.type === 'in') {
            row.classList.add('in-animation');
        } else if (row.dataset.type === 'out') {
            row.classList.add('out-animation');
        }
    });
}

// 为表格行添加互动效果
function addTableRowInteractions() {
    const tableRows = document.querySelectorAll('.table-row');

    tableRows.forEach(row => {
        // 点击高亮效果
        row.addEventListener('click', function() {
            // 移除其他行的选中状态
            document.querySelectorAll('.table-row').forEach(r => {
                r.classList.remove('selected-row');
            });

            // 添加当前行的选中状态
            this.classList.add('selected-row');

            // 添加闪光效果
            const sparkle = document.createElement('div');
            sparkle.className = 'row-sparkle';
            this.appendChild(sparkle);

            // 移除闪光效果
            setTimeout(() => {
                sparkle.remove();
            }, 800);
        });

        // 鼠标悬停效果
        row.addEventListener('mouseenter', function() {
            // 添加冰晶效果
            const iceEffect = document.createElement('div');
            iceEffect.className = 'ice-effect';
            this.appendChild(iceEffect);

            // 移除冰晶效果
            this.addEventListener('mouseleave', function() {
                iceEffect.remove();
            }, { once: true });
        });
    });
}

// 添加书籍封面魔法效果
function addBookCoverMagic() {
    const bookCover = document.querySelector('.book-cover');
    const bookFrame = document.querySelector('.book-frame');

    if (bookCover && bookFrame) {
        // 鼠标移动时添加3D效果
        bookFrame.addEventListener('mousemove', function(e) {
            const rect = this.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;

            const centerX = rect.width / 2;
            const centerY = rect.height / 2;

            const deltaX = (x - centerX) / 20;
            const deltaY = (y - centerY) / 20;

            bookCover.style.transform = `rotate(0deg) perspective(800px) rotateX(${-deltaY}deg) rotateY(${deltaX}deg)`;
        });

        // 鼠标离开时恢复
        bookFrame.addEventListener('mouseleave', function() {
            bookCover.style.transform = 'rotate(3deg)';
        });

        // 点击时添加闪光效果
        bookFrame.addEventListener('click', function() {
            const glow = document.querySelector('.book-glow');
            glow.style.opacity = '1';
            glow.style.background = 'radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.8), rgba(173, 216, 230, 0) 70%)';

            setTimeout(() => {
                glow.style.opacity = '0';
            }, 500);
        });
    }
}

// 添加按钮魔法效果
function addButtonMagic() {
    const buttons = document.querySelectorAll('.frozen-btn');

    buttons.forEach(button => {
        button.addEventListener('mouseenter', function() {
            // 创建冰晶效果
            const sparkles = document.createElement('div');
            sparkles.className = 'btn-sparkles';
            this.appendChild(sparkles);

            // 冰晶动画
            for (let i = 0; i < 5; i++) {
                const sparkle = document.createElement('div');
                sparkle.className = 'btn-sparkle';
                sparkle.style.left = `${Math.random() * 100}%`;
                sparkle.style.top = `${Math.random() * 100}%`;
                sparkle.style.animationDelay = `${Math.random() * 0.5}s`;
                sparkles.appendChild(sparkle);
            }
        });

        button.addEventListener('mouseleave', function() {
            const sparkles = this.querySelector('.btn-sparkles');
            if (sparkles) {
                sparkles.remove();
            }
        });
    });
}

// 响应式表格
function makeTableResponsive() {
    if (window.innerWidth <= 768) {
        const headerCells = document.querySelectorAll('.th-frozen');
        const headers = Array.from(headerCells).map(cell => cell.textContent);

        const dataCells = document.querySelectorAll('.td-frozen');

        dataCells.forEach((cell, index) => {
            const headerIndex = index % headers.length;
            cell.setAttribute('data-label', headers[headerIndex]);
        });
    }
}

// 创建额外CSS样式
function addExtraStyles() {
    const styleSheet = document.createElement('style');
    styleSheet.type = 'text/css';
    styleSheet.innerHTML = `
        .row-sparkle {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(to right, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
            pointer-events: none;
            animation: rowSparkle 0.8s ease forwards;
        }
        
        @keyframes rowSparkle {
            0% { transform: translateX(-100%); }
            100% { transform: translateX(100%); }
        }
        
        .ice-effect {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(135deg, rgba(255, 255, 255, 0), rgba(173, 216, 230, 0.1), rgba(255, 255, 255, 0));
            pointer-events: none;
            backdrop-filter: brightness(1.03);
        }
        
        .btn-sparkles {
            position: absolute;
            inset: 0;
            overflow: hidden;
            pointer-events: none;
        }
        
        .btn-sparkle {
            position: absolute;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background-color: rgba(255, 255, 255, 0.8);
            pointer-events: none;
            animation: btnSparkle 1s ease infinite;
        }
        
        @keyframes btnSparkle {
            0%, 100% { transform: scale(0); opacity: 0; }
            50% { transform: scale(1); opacity: 1; }
        }
        
        .in-animation {
            border-left: 3px solid rgba(3, 169, 244, 0.5);
        }
        
        .out-animation {
            border-left: 3px solid rgba(255, 152, 0, 0.5);
        }
    `;

    document.head.appendChild(styleSheet);
}

// 初始化额外样式
addExtraStyles();

// 窗口大小变化时重新调整
window.addEventListener('resize', makeTableResponsive);

================================================================================
File: ./app/static/js/overdue.js
================================================================================

// overdue.js

document.addEventListener('DOMContentLoaded', function() {
    // 归还图书功能
    const returnButtons = document.querySelectorAll('.return-btn');
    const returnModal = document.getElementById('returnModal');
    const returnBookTitle = document.getElementById('returnBookTitle');
    const overdueRemark = document.getElementById('overdueRemark');
    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;
            overdueRemark.value = '';

            // 使用 Bootstrap 的 jQuery 方法显示模态框
            $('#returnModal').modal('show');
        });
    });

    confirmReturnButton.addEventListener('click', function() {
        if (!currentBorrowId) return;

        const remark = overdueRemark.value.trim();

        // 发送归还请求
        fetch(`/borrow/return/${currentBorrowId}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify({
                remark: remark
            })
        })
        .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 notifyButtons = document.querySelectorAll('.notify-btn');
    const notifyModal = document.getElementById('notifyModal');
    const notifyBookTitle = document.getElementById('notifyBookTitle');
    const confirmNotifyButton = document.getElementById('confirmNotify');

    notifyButtons.forEach(button => {
        button.addEventListener('click', function() {
            const borrowId = this.getAttribute('data-id');
            const bookTitle = this.getAttribute('data-title');

            currentBorrowId = borrowId;
            notifyBookTitle.textContent = bookTitle;

            // 使用 Bootstrap 的 jQuery 方法显示模态框
            $('#notifyModal').modal('show');
        });
    });

    confirmNotifyButton.addEventListener('click', function() {
        if (!currentBorrowId) return;

        // 发送通知请求
        fetch(`/borrow/overdue/notify/${currentBorrowId}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify({})
        })
        .then(response => response.json())
        .then(data => {
            // 隐藏模态框
            $('#notifyModal').modal('hide');

            if (data.success) {
                // 显示成功消息
                showAlert('success', data.message);
                // 禁用已点击的通知按钮
                document.querySelector(`.notify-btn[data-id="${currentBorrowId}"]`).disabled = true;
            } else {
                // 显示错误消息
                showAlert('danger', data.message);
            }
        })
        .catch(error => {
            $('#notifyModal').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}
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        `;

        document.body.appendChild(alertDiv);

        // 3秒后自动消失
        setTimeout(() => {
            alertDiv.remove();
        }, 3000);
    }
});

================================================================================
File: ./app/static/js/inventory-logs.js
================================================================================

// 库存日志页面的JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
    // 日期选择器联动
    const dateFrom = document.getElementById('date_from');
    const dateTo = document.getElementById('date_to');

    // 确保结束日期不早于开始日期
    if (dateFrom && dateTo) {
        dateFrom.addEventListener('change', function() {
            if (dateTo.value && dateFrom.value > dateTo.value) {
                dateTo.value = dateFrom.value;
            }
        });

        dateTo.addEventListener('change', function() {
            if (dateFrom.value && dateFrom.value > dateTo.value) {
                dateFrom.value = dateTo.value;
            }
        });
    }

    // 重置筛选按钮
    const resetButton = document.getElementById('reset-filters');
    if (resetButton) {
        resetButton.addEventListener('click', function() {
            document.getElementById('book_id').value = '';
            document.getElementById('change_type').value = '';
            document.getElementById('date_from').value = '';
            document.getElementById('date_to').value = '';

            // 提交表单以应用重置的筛选条件
            document.querySelector('form').submit();
        });
    }

    // 备注信息悬停显示完整内容
    const remarkCells = document.querySelectorAll('.remark-cell');
    remarkCells.forEach(cell => {
        if (cell.offsetWidth < cell.scrollWidth) {
            cell.title = cell.textContent;
        }
    });

    // 创建雪花效果
    createSnowflakes();

    // 初始化particles.js特效
    initParticles();

    // 给表格行添加动画延迟
    animateTableRows();
});

// 创建雪花效果
function createSnowflakes() {
    const snowflakesCount = 50; // 雪花数量
    const container = document.body;
    const snowflakeChars = ['❄', '❅', '❆', '✱', '*'];

    for (let i = 0; i < snowflakesCount; i++) {
        const snowflake = document.createElement('div');
        snowflake.className = 'snowflake';
        snowflake.textContent = snowflakeChars[Math.floor(Math.random() * snowflakeChars.length)];

        // 随机样式
        snowflake.style.left = `${Math.random() * 100}%`;
        snowflake.style.opacity = Math.random();
        snowflake.style.fontSize = `${Math.random() * 15 + 10}px`;

        // 动画
        const duration = Math.random() * 30 + 20;
        snowflake.style.animationDuration = `${duration}s`;
        snowflake.style.animationDelay = `${Math.random() * 5}s`;

        container.appendChild(snowflake);
    }
}

// 初始化particles.js
function initParticles() {
    if (typeof particlesJS !== 'undefined') {
        particlesJS('magic-particles', {
            "particles": {
                "number": {
                    "value": 80,
                    "density": {
                        "enable": true,
                        "value_area": 800
                    }
                },
                "color": {
                    "value": ["#a2d5f2", "#6fa8dc", "#cfe2f3", "#b19cd9"]
                },
                "shape": {
                    "type": ["circle", "star"],
                    "stroke": {
                        "width": 0,
                        "color": "#000000"
                    },
                    "polygon": {
                        "nb_sides": 5
                    }
                },
                "opacity": {
                    "value": 0.3,
                    "random": true,
                    "anim": {
                        "enable": true,
                        "speed": 1,
                        "opacity_min": 0.1,
                        "sync": false
                    }
                },
                "size": {
                    "value": 5,
                    "random": true,
                    "anim": {
                        "enable": false,
                        "speed": 40,
                        "size_min": 0.1,
                        "sync": false
                    }
                },
                "line_linked": {
                    "enable": true,
                    "distance": 150,
                    "color": "#a2d5f2",
                    "opacity": 0.2,
                    "width": 1
                },
                "move": {
                    "enable": true,
                    "speed": 2,
                    "direction": "none",
                    "random": true,
                    "straight": false,
                    "out_mode": "out",
                    "bounce": false,
                    "attract": {
                        "enable": true,
                        "rotateX": 600,
                        "rotateY": 1200
                    }
                }
            },
            "interactivity": {
                "detect_on": "canvas",
                "events": {
                    "onhover": {
                        "enable": true,
                        "mode": "grab"
                    },
                    "onclick": {
                        "enable": true,
                        "mode": "push"
                    },
                    "resize": true
                },
                "modes": {
                    "grab": {
                        "distance": 140,
                        "line_linked": {
                            "opacity": 0.5
                        }
                    },
                    "bubble": {
                        "distance": 400,
                        "size": 4,
                        "duration": 2,
                        "opacity": 1,
                        "speed": 3
                    },
                    "repulse": {
                        "distance": 200,
                        "duration": 0.4
                    },
                    "push": {
                        "particles_nb": 4
                    },
                    "remove": {
                        "particles_nb": 2
                    }
                }
            },
            "retina_detect": true
        });
    }
}

// 给表格行添加动画延迟
function animateTableRows() {
    const rows = document.querySelectorAll('.log-row');
    rows.forEach((row, index) => {
        row.style.animationDelay = `${index * 0.05}s`;
        row.style.animationDuration = '0.5s';
        row.style.animationName = 'fadeInUp';
        row.style.animationFillMode = 'both';
    });
}

// 添加淡入向上动画
const fadeInUpKeyframes = `
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translate3d(0, 20px, 0);
    }
    to {
        opacity: 1;
        transform: translate3d(0, 0, 0);
    }
}`;

// 添加动画样式到文档
document.addEventListener('DOMContentLoaded', function() {
    const style = document.createElement('style');
    style.textContent = fadeInUpKeyframes;
    document.head.appendChild(style);
});

================================================================================
File: ./app/static/js/notifications.js
================================================================================

document.addEventListener('DOMContentLoaded', function() {
    // 标记通知为已读
    const markAsRead = (notificationId) => {
        fetch(`/announcement/notification/${notificationId}/mark-read`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest'
            }
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                // 更新UI，如移除未读标记
                const notificationCard = document.querySelector(`.notification-card[data-id="${notificationId}"]`);
                if (notificationCard) {
                    notificationCard.classList.remove('unread');
                    const unreadBadge = notificationCard.querySelector('.unread-badge');
                    if (unreadBadge) {
                        unreadBadge.remove();
                    }
                }

                // 更新通知计数
                updateNotificationCount();
            }
        })
        .catch(error => console.error('Error:', error));
    };

    // 自动标记为已读
    const notificationCards = document.querySelectorAll('.notification-card.unread');
    notificationCards.forEach(card => {
        const notificationId = card.dataset.id;
        if (notificationId) {
            // 当用户查看通知列表时自动标记为已读
            // 这可以是可选的功能，也可能需要用户点击后才标记
            // markAsRead(notificationId);
        }
    });

    // 更新通知计数
    function updateNotificationCount() {
        // 获取当前未读通知数
        fetch('/announcement/notifications/count')
            .then(response => response.json())
            .then(data => {
                const badge = document.querySelector('.notifications .badge');
                if (data.count > 0) {
                    if (badge) {
                        badge.textContent = data.count;
                    } else {
                        const newBadge = document.createElement('span');
                        newBadge.className = 'badge';
                        newBadge.textContent = data.count;
                        document.querySelector('.notifications').appendChild(newBadge);
                    }
                } else {
                    if (badge) {
                        badge.remove();
                    }
                }
            })
            .catch(error => console.error('Error:', error));
    }
});

================================================================================
File: ./app/static/js/borrow_statistics.js
================================================================================

// app/static/js/borrow_statistics.js
document.addEventListener('DOMContentLoaded', function() {
    const timeRangeSelect = document.getElementById('time-range');
    let trendChart = null;
    let categoryChart = null;

    // 初始加载
    loadBorrowTrend();
    loadCategoryDistribution();

    // 添加事件监听器
    timeRangeSelect.addEventListener('change', loadBorrowTrend);

    function loadBorrowTrend() {
        const timeRange = timeRangeSelect.value;

        // 调用API获取数据
        fetch(`/statistics/api/borrow-trend?time_range=${timeRange}`)
            .then(response => response.json())
            .then(data => {
                updateTrendChart(data);
                updateBorrowSummary(data);
            })
            .catch(error => {
                console.error('加载借阅趋势数据失败:', error);
            });
    }

    function loadCategoryDistribution() {
        // 调用API获取数据
        fetch('/statistics/api/category-distribution')
            .then(response => response.json())
            .then(data => {
                updateCategoryChart(data);
            })
            .catch(error => {
                console.error('加载分类分布数据失败:', error);
            });
    }

    function updateTrendChart(data) {
        // 销毁旧图表
        if (trendChart) {
            trendChart.destroy();
        }

        if (!data || data.length === 0) {
            return;
        }

        // 准备图表数据
        const labels = data.map(item => item.date);
        const borrowData = data.map(item => item.borrow);
        const returnData = data.map(item => item.return);
        const overdueData = data.map(item => item.overdue);

        // 创建图表
        const ctx = document.getElementById('trend-chart').getContext('2d');
        trendChart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: labels,
                datasets: [
                    {
                        label: '借阅数',
                        data: borrowData,
                        borderColor: 'rgba(54, 162, 235, 1)',
                        backgroundColor: 'rgba(54, 162, 235, 0.1)',
                        tension: 0.1,
                        fill: true
                    },
                    {
                        label: '归还数',
                        data: returnData,
                        borderColor: 'rgba(75, 192, 192, 1)',
                        backgroundColor: 'rgba(75, 192, 192, 0.1)',
                        tension: 0.1,
                        fill: true
                    },
                    {
                        label: '逾期数',
                        data: overdueData,
                        borderColor: 'rgba(255, 99, 132, 1)',
                        backgroundColor: 'rgba(255, 99, 132, 0.1)',
                        tension: 0.1,
                        fill: true
                    }
                ]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        title: {
                            display: true,
                            text: '数量'
                        }
                    },
                    x: {
                        title: {
                            display: true,
                            text: '日期'
                        }
                    }
                },
                plugins: {
                    title: {
                        display: true,
                        text: '借阅与归还趋势'
                    }
                }
            }
        });
    }

    function updateCategoryChart(data) {
        // 销毁旧图表
        if (categoryChart) {
            categoryChart.destroy();
        }

        if (!data || data.length === 0) {
            return;
        }

        // 准备图表数据
        const labels = data.map(item => item.category);
        const counts = data.map(item => item.count);

        // 创建图表
        const ctx = document.getElementById('category-chart').getContext('2d');
        categoryChart = new Chart(ctx, {
            type: 'pie',
            data: {
                labels: labels,
                datasets: [{
                    data: counts,
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.7)',
                        'rgba(54, 162, 235, 0.7)',
                        'rgba(255, 206, 86, 0.7)',
                        'rgba(75, 192, 192, 0.7)',
                        'rgba(153, 102, 255, 0.7)',
                        'rgba(255, 159, 64, 0.7)',
                        'rgba(199, 199, 199, 0.7)',
                        'rgba(83, 102, 255, 0.7)',
                        'rgba(40, 159, 64, 0.7)',
                        'rgba(210, 199, 199, 0.7)'
                    ],
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    title: {
                        display: true,
                        text: '分类借阅分布'
                    },
                    legend: {
                        position: 'right'
                    }
                }
            }
        });
    }

    function updateBorrowSummary(data) {
        if (!data || data.length === 0) {
            return;
        }

        // 计算总数和平均数
        let totalBorrows = 0;
        let totalReturns = 0;
        let totalOverdue = data[data.length - 1].overdue || 0;

        data.forEach(item => {
            totalBorrows += item.borrow;
            totalReturns += item.return;
        });

        const summary = document.getElementById('borrow-summary');
        summary.innerHTML = `
            <div class="stats-item">
                <div class="stats-value">${totalBorrows}</div>
                <div class="stats-title">总借阅</div>
            </div>
            <div class="stats-item">
                <div class="stats-value">${totalReturns}</div>
                <div class="stats-title">总归还</div>
            </div>
            <div class="stats-item">
                <div class="stats-value">${totalOverdue}</div>
                <div class="stats-title">当前逾期</div>
            </div>
            <div class="stats-item">
                <div class="stats-value">${Math.round((totalBorrows - totalReturns) - totalOverdue)}</div>
                <div class="stats-title">正常借出</div>
            </div>
        `;
    }
});

================================================================================
File: ./app/static/js/inventory-adjust.js
================================================================================

// 库存调整页面的JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
    const changeTypeSelect = document.getElementById('change_type');
    const changeAmountInput = document.getElementById('change_amount');
    const stockHint = document.getElementById('stock-hint');

    // 当前库存数量通过HTML中的全局变量CURRENT_STOCK获取
    const currentStock = CURRENT_STOCK;

    // 当改变调整类型时，更新提示信息
    changeTypeSelect.addEventListener('change', updateStockHint);
    changeAmountInput.addEventListener('input', updateStockHint);

    function updateStockHint() {
        const changeType = changeTypeSelect.value;
        const changeAmount = parseInt(changeAmountInput.value) || 0;

        if (changeType === 'out') {
            // 出库检查
            if (changeAmount > currentStock) {
                stockHint.textContent = `警告: 出库数量(${changeAmount})超过当前库存(${currentStock})!`;
                stockHint.className = 'form-text stock-hint danger';
                changeAmountInput.setCustomValidity('出库数量不能超过当前库存');
            } else {
                const newStock = currentStock - changeAmount;
                stockHint.textContent = `出库后库存将变为: ${newStock}`;
                stockHint.className = 'form-text stock-hint';
                changeAmountInput.setCustomValidity('');

                if (newStock <= 5 && newStock > 0) {
                    stockHint.classList.add('warning');
                } else if (newStock <= 0) {
                    stockHint.classList.add('danger');
                }
            }
        } else {
            // 入库提示
            const newStock = currentStock + changeAmount;
            stockHint.textContent = `入库后库存将变为: ${newStock}`;
            stockHint.className = 'form-text stock-hint';
            changeAmountInput.setCustomValidity('');
        }

        // 添加一些迪士尼风格的交互效果
        addDisneyInteractions();
    }

    // 添加迪士尼风格的交互效果
    function addDisneyInteractions() {
        // 闪光效果
        const sparkleEffect = document.querySelector('.disney-sparkles');
        if (sparkleEffect) {
            sparkleEffect.style.opacity = '0.7';
            setTimeout(() => { sparkleEffect.style.opacity = '0'; }, 800);
        }

        // 按钮动画效果
        const confirmBtn = document.querySelector('.disney-confirm-btn');
        if (confirmBtn) {
            confirmBtn.classList.add('active');
            setTimeout(() => { confirmBtn.classList.remove('active'); }, 300);
        }
    }

    // 初始化提示
    updateStockHint();

    // 表单提交前验证
    document.querySelector('form').addEventListener('submit', function(event) {
        const changeType = changeTypeSelect.value;
        const changeAmount = parseInt(changeAmountInput.value) || 0;

        if (changeType === 'out' && changeAmount > currentStock) {
            event.preventDefault();
            alert('出库数量不能超过当前库存!');
            return false;
        }

        if (changeAmount <= 0) {
            event.preventDefault();
            alert('调整数量必须大于0!');
            return false;
        }

        // 添加提交成功的动画效果
        const card = document.querySelector('.disney-inventory-card');
        if (card) {
            card.classList.add('submitting');
        }
    });

    // 为表单元素添加迪士尼风格的交互效果
    const formElements = document.querySelectorAll('.disney-select, .disney-input, .disney-textarea');
    formElements.forEach(element => {
        element.addEventListener('focus', function() {
            this.parentNode.classList.add('focused');
        });

        element.addEventListener('blur', function() {
            this.parentNode.classList.remove('focused');
        });
    });
});

================================================================================
File: ./app/static/js/book-edit.js
================================================================================

// 用于调试
console.log("ISBN验证脚本已加载 v2.0");

$(document).ready(function() {
    console.log("DOM已加载，开始设置ISBN验证");

    // 获取ISBN输入框
    const isbnInput = $("#isbn");

    if (isbnInput.length === 0) {
        console.error("找不到ISBN输入字段!");
        return;
    }

    console.log("找到ISBN输入框:", isbnInput.val());

    // 添加ISBN帮助文本和错误提示
    isbnInput.after('<div class="invalid-feedback" id="isbn-error">请输入有效的10位或13位ISBN号码</div>');
    isbnInput.after('<small class="form-text text-muted">请输入有效的ISBN-10或ISBN-13格式</small>');

    // 验证ISBN函数
    function validateISBN(isbn) {
        // 空值视为有效
        if (!isbn || isbn.trim() === '') return true;

        // 移除所有连字符和空格
        isbn = isbn.replace(/[-\s]/g, '');

        console.log("验证ISBN:", isbn, "长度:", isbn.length);

        // 长度检查
        if (isbn.length !== 10 && isbn.length !== 13) {
            console.log("ISBN长度无效");
            return false;
        }

        // ISBN-10验证
        if (isbn.length === 10) {
            // 检查前9位是否为数字，最后一位可以是X
            if (!/^\d{9}[\dXx]$/.test(isbn)) {
                console.log("ISBN-10格式错误");
                return false;
            }

            let sum = 0;
            for (let i = 0; i < 9; i++) {
                sum += parseInt(isbn[i]) * (10 - i);
            }

            // 处理校验位
            let checkDigit = isbn[9].toUpperCase() === 'X' ? 10 : parseInt(isbn[9]);
            sum += checkDigit;

            let valid = (sum % 11 === 0);
            console.log("ISBN-10校验结果:", valid);
            return valid;
        }

        // ISBN-13验证
        if (isbn.length === 13) {
            // 检查是否全是数字
            if (!/^\d{13}$/.test(isbn)) {
                console.log("ISBN-13必须全是数字");
                return false;
            }

            let sum = 0;
            for (let i = 0; i < 12; i++) {
                sum += parseInt(isbn[i]) * (i % 2 === 0 ? 1 : 3);
            }

            let checkDigit = (10 - (sum % 10)) % 10;
            let valid = (checkDigit === parseInt(isbn[12]));

            console.log("ISBN-13校验结果:", valid);
            return valid;
        }

        return false;
    }

    // 添加输入事件处理
    isbnInput.on('input', function() {
        const value = $(this).val().trim();

        if (value === '') {
            // 不验证空值
            $(this).removeClass('is-invalid is-valid');
        } else if (validateISBN(value)) {
            $(this).removeClass('is-invalid').addClass('is-valid');
        } else {
            $(this).removeClass('is-valid').addClass('is-invalid');
        }
    });

    // 表单提交验证
    const form = $(".book-form");
    form.on('submit', function(event) {
        const isbnValue = isbnInput.val().trim();

        console.log("表单提交，ISBN值:", isbnValue);

        // 仅当有ISBN且无效时才阻止提交
        if (isbnValue !== '' && !validateISBN(isbnValue)) {
            event.preventDefault();
            event.stopPropagation();

            isbnInput.addClass('is-invalid');
            isbnInput.focus();

            alert('请输入有效的ISBN号码，或将此字段留空');
            console.log("表单提交被阻止 - ISBN无效");

            return false;
        }

        console.log("表单提交继续 - ISBN有效或为空");
    });

    // 处理封面图片预览
    $('#cover').change(function() {
        const file = this.files[0];
        if (file) {
            const reader = new FileReader();
            reader.onload = function(e) {
                $('#coverPreview').html(`<img src="${e.target.result}" class="cover-image" alt="新封面预览">`);
            };
            reader.readAsDataURL(file);
        } else {
            // 默认回退
            if (typeof bookCoverUrl !== 'undefined' && bookCoverUrl) {
                $('#coverPreview').html(`<img src="${bookCoverUrl}" class="cover-image" alt="${bookTitle || '图书封面'}">`);
            } else {
                $('#coverPreview').html(`
                    <div class="no-cover-placeholder">
                        <i class="fas fa-book"></i>
                        <span>暂无封面</span>
                    </div>
                `);
            }
        }
    });

    // 在页面加载时初始验证现有ISBN
    if (isbnInput.val()) {
        isbnInput.trigger('input');
    }

    // 添加帮助样式
    $("<style>")
        .prop("type", "text/css")
        .html(`
            .is-invalid {
                border-color: #dc3545 !important;
                background-color: #fff8f8 !important;
            }

            .is-valid {
                border-color: #28a745 !important;
                background-color: #f8fff8 !important;
            }

            .invalid-feedback {
                display: none;
                color: #dc3545;
                font-size: 80%;
                margin-top: 0.25rem;
            }

            .is-invalid + .invalid-feedback,
            .is-invalid ~ .invalid-feedback {
                display: block !important;
            }
        `)
        .appendTo("head");
});

================================================================================
File: ./app/templates/index.html
================================================================================

{% extends 'base.html' %}

{% block title %}首页 - 图书管理系统{% endblock %}

{% block head %}
<!-- 只引用index页面的专用样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
{% endblock %}

{% block content %}
<!-- 欢迎区域 -->
<div class="welcome-section">
    <h1>欢迎回来，{{ current_user.username }}！</h1>
    <p>今天是 <span id="current-date"></span>，祝您使用愉快。</p>
</div>

<!-- 快速统计 -->
<div class="stats-container">
    <div class="stat-card">
        <i class="fas fa-book stat-icon"></i>
        <div class="stat-info">
            <h3>馆藏总量</h3>
            <p class="stat-number">{{ stats.total_books }}</p>
        </div>
    </div>
    <div class="stat-card">
        <i class="fas fa-users stat-icon"></i>
        <div class="stat-info">
            <h3>注册用户</h3>
            <p class="stat-number">{{ stats.total_users }}</p>
        </div>
    </div>
    <div class="stat-card">
        <i class="fas fa-exchange-alt stat-icon"></i>
        <div class="stat-info">
            <h3>当前借阅</h3>
            <p class="stat-number">{{ stats.active_borrows }}</p>
        </div>
    </div>
    <div class="stat-card">
        <i class="fas fa-clock stat-icon"></i>
        <div class="stat-info">
            <h3>待还图书</h3>
            <p class="stat-number">{{ stats.user_borrows if current_user.is_authenticated else 0 }}</p>
        </div>
    </div>
</div>

<!-- 主要内容区 -->
<div class="main-sections">
    <!-- 最新图书 -->
    <div class="content-section book-section">
        <div class="section-header">
            <h2>最新图书</h2>
            <a href="{{ url_for('book.browse_books', sort='newest') }}" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
        </div>
        <div class="book-grid">
            {% if latest_books %}
                {% for book in latest_books %}
                <div class="book-card">
                    <div class="book-cover">
                        {% if book.cover_url %}
                        <img src="{{ book.cover_url }}" alt="{{ book.title }} 封面">
                        {% else %}
                        <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="无封面">
                        {% endif %}
                    </div>
                    <div class="book-info">
                        <h3 class="book-title">{{ book.title }}</h3>
                        <p class="book-author">{{ book.author }}</p>
                        <div class="book-meta">
                            <span class="book-category">{{ book.category.name if book.category else '未分类' }}</span>
                            <span class="book-status {{ 'available' if book.stock > 0 else 'unavailable' }}">
                                {{ '可借阅' if book.stock > 0 else '已借完' }}
                            </span>
                        </div>
                        <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="borrow-btn">
                            {{ '借阅' if book.stock > 0 else '详情' }}
                        </a>
                    </div>
                </div>
                {% endfor %}
            {% else %}
                {% for i in range(4) %}
                <div class="book-card">
                    <div class="book-cover">
                        <img src="https://via.placeholder.com/150x210?text=No+Cover" alt="Book Cover">
                    </div>
                    <div class="book-info">
                        <h3 class="book-title">示例图书标题</h3>
                        <p class="book-author">作者名</p>
                        <div class="book-meta">
                            <span class="book-category">计算机</span>
                            <span class="book-status available">可借阅</span>
                        </div>
                        <button class="borrow-btn">借阅</button>
                    </div>
                </div>
                {% endfor %}
            {% endif %}
        </div>
    </div>

    <!-- 通知公告 -->
    <div class="content-section notice-section">
        <div class="section-header">
            <h2>通知公告</h2>
            <a href="{{ url_for('announcement.announcement_list') }}" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
        </div>
        <div class="notice-list">
            {% if announcements %}
                {% for announcement in announcements %}
                <div class="notice-item {% if announcement.is_top %}pinned{% endif %}">
                    <div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
                    <div class="notice-content">
                        <h3><a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}">{{ announcement.title }}</a></h3>
                        <p>{{ announcement.content|striptags|truncate(100) }}</p>
                        <div class="notice-meta">
                            <span class="notice-time">{{ announcement.created_at.strftime('%Y-%m-%d') }}</span>
                        </div>
                    </div>
                </div>
                {% endfor %}
            {% else %}
                <div class="notice-item">
                    <div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
                    <div class="notice-content">
                        <h3>暂无公告</h3>
                        <p>目前没有任何通知公告</p>
                        <div class="notice-meta">
                            <span class="notice-time">{{ now.strftime('%Y-%m-%d') }}</span>
                        </div>
                    </div>
                </div>
            {% endif %}

            {% if current_user.is_authenticated and user_notifications %}
                <div class="notice-item">
                    <div class="notice-icon"><i class="fas fa-bell"></i></div>
                    <div class="notice-content">
                        <h3>您有{{ user_notifications|length }}条未读通知</h3>
                        <p>
                            {% for notification in user_notifications %}
                                {% if loop.index <= 2 %}
                                {{ notification.title }}{% if not loop.last and loop.index < 2 %}、{% endif %}
                                {% endif %}
                            {% endfor %}
                            {% if user_notifications|length > 2 %}等{% endif %}
                        </p>
                        <div class="notice-meta">
                            <span class="notice-time">{{ now.strftime('%Y-%m-%d') }}</span>
                            <a href="{{ url_for('announcement.user_notifications') }}" class="renew-btn">查看详情</a>
                        </div>
                    </div>
                </div>
            {% endif %}
        </div>
    </div>
</div>

<!-- 热门图书区域 -->
<div class="content-section popular-section">
    <div class="section-header">
        <h2>热门图书</h2>
        <a href="{{ url_for('book.browse_books', sort='popular') }}" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
    </div>
    <div class="popular-books">
        {% if popular_books %}
            {% for book in popular_books %}
            <div class="popular-book-item">
                <div class="rank-badge">{{ loop.index }}</div>
                <div class="book-cover small">
                    {% if book.cover_url %}
                    <img src="{{ book.cover_url }}" alt="{{ book.title }} 封面">
                    {% else %}
                    <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="无封面">
                    {% endif %}
                </div>
                <div class="book-details">
                    <h3 class="book-title">{{ book.title }}</h3>
                    <p class="book-author">{{ book.author }}</p>
                    <div class="book-stats">
                        <span><i class="fas fa-eye"></i> {{ book.view_count if book.view_count else 0 }} 次浏览</span>
                        <span><i class="fas fa-bookmark"></i> {{ book.borrow_count if book.borrow_count else 0 }} 次借阅</span>
                    </div>
                </div>
            </div>
            {% endfor %}
        {% else %}
            {% for i in range(5) %}
            <div class="popular-book-item">
                <div class="rank-badge">{{ i+1 }}</div>
                <div class="book-cover small">
                    <img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
                </div>
                <div class="book-details">
                    <h3 class="book-title">热门图书标题示例</h3>
                    <p class="book-author">知名作者</p>
                    <div class="book-stats">
                        <span><i class="fas fa-eye"></i> 1024 次浏览</span>
                        <span><i class="fas fa-bookmark"></i> 89 次借阅</span>
                    </div>
                </div>
            </div>
            {% endfor %}
        {% endif %}
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // 设置当前日期
        const now = new Date();
        const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
        document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', options);
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/base.html
================================================================================

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}图书管理系统{% endblock %}</title>
    <!-- 通用CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
    <!-- 页面特定CSS -->
    {% block head %}{% endblock %}
</head>
<body>
    <div class="app-container">
        <!-- 侧边导航栏 -->
        <nav class="sidebar">
            <div class="logo-container">
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo">
                <h2>图书管理系统</h2>
            </div>
            <ul class="nav-links">
                <li class="{% if request.path == '/' %}active{% endif %}">
                    <a href="{{ url_for('index') }}"><i class="fas fa-home"></i> 首页</a>
                </li>
                <li class="{% if '/book/list' in request.path %}active{% endif %}">
                    <a href="{{ url_for('book.browse_books') }}"><i class="fas fa-book"></i> 图书浏览</a>
                </li>
                <li class="{% if '/borrow' in request.path %}active{% endif %}">
                    <a href="{{ url_for('borrow.my_borrows') }}"><i class="fas fa-bookmark"></i> 我的借阅</a>
                </li>
                <li class="{% if '/announcement' in request.path %}active{% endif %}">
                    <a href="{{ url_for('announcement.announcement_list') }}"><i class="fas fa-bell"></i> 通知公告</a>
                </li>
                {% if current_user.is_authenticated and current_user.role_id == 1 %}
                <li class="nav-category">管理功能</li>
                <li class="{% if '/user/manage' in request.path %}active{% endif %}">
                    <a href="{{ url_for('user.user_list') }}"><i class="fas fa-users"></i> 用户管理</a>
                </li>
                <li class="{% if '/user/roles' in request.path %}active{% endif %}">
                    <a href="{{ url_for('user.role_list') }}"><i class="fas fa-user-tag"></i> 角色管理</a>
                </li>
                <li class="{% if '/book/admin/list' in request.path %}active{% endif %}">
                    <a href="{{ url_for('book.admin_book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
                </li>
                <li class="{% if '/borrow/manage' in request.path %}active{% endif %}">
                    {% if current_user.role_id == 1 %}
                    <a href="{{ url_for('borrow.manage_borrows') }}"><i class="fas fa-exchange-alt"></i> 借阅管理</a>
                    {% endif %}
                </li>
                <li class="{% if '/inventory' in request.path %}active{% endif %}">
                    <a href="{{ url_for('inventory.inventory_list') }}"><i class="fas fa-warehouse"></i> 库存管理</a>
                </li>
                <li class="{% if '/statistics' in request.path %}active{% endif %}">
                    <a href="{{ url_for('statistics.index') }}"><i class="fas fa-chart-bar"></i> 统计分析</a>
                </li>
                <li class="{% if '/log' in request.path %}active{% endif %}">
                    <a href="{{ url_for('log.log_list') }}"><i class="fas fa-history"></i> 日志管理</a>
                </li>
                <li class="{% if '/announcement/manage' in request.path %}active{% endif %}">
                    <a href="{{ url_for('announcement.manage_announcements') }}"><i class="fas fa-bullhorn"></i> 公告管理</a>
                </li>
                {% endif %}
            </ul>
        </nav>

        <!-- 主内容区 -->
        <main class="main-content">
            <!-- 顶部导航 -->
            <header class="top-bar">
                <div class="search-container">
                    <i class="fas fa-search search-icon"></i>
                    <input type="text" placeholder="搜索图书..." class="search-input">
                </div>
                <div class="user-menu">
                    <div class="notifications dropdown">
                        <a href="#" class="notification-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            <i class="fas fa-bell"></i>
                            {% if current_user.is_authenticated %}
                                {% set unread_count = get_unread_notifications_count(current_user.id) %}
                                {% if unread_count > 0 %}
                                    <span class="badge">{{ unread_count }}</span>
                                {% endif %}
                            {% endif %}
                        </a>

                        {% if current_user.is_authenticated %}
                        <div class="dropdown-menu notification-dropdown">
                            <div class="notification-header">
                                <h6 class="dropdown-header">通知中心</h6>
                                {% if unread_count > 0 %}
                                    <a href="{{ url_for('announcement.mark_all_as_read') }}" class="mark-all-read">全部标为已读</a>
                                {% endif %}
                            </div>

                            <div class="notification-items">
                                {% set recent_notifications = get_recent_notifications(current_user.id, 5) %}
                                {% if recent_notifications %}
                                    {% for notification in recent_notifications %}
                                    <a class="dropdown-item notification-item {% if notification.status == 0 %}unread{% endif %}"
                                       href="{{ url_for('announcement.view_notification', notification_id=notification.id) }}">
                                        <div class="notification-content">
                                            <h6 class="notification-title">{{ notification.title }}</h6>
                                            <p class="notification-text">{{ notification.content|striptags|truncate(50) }}</p>
                                            <span class="notification-time">{{ notification.created_at }}</span>
                                        </div>
                                    </a>
                                    {% endfor %}
                                {% else %}
                                    <div class="no-notifications">
                                        <p>暂无通知</p>
                                    </div>
                                {% endif %}
                            </div>

                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item view-all" href="{{ url_for('announcement.user_notifications') }}">
                                查看所有通知
                            </a>
                        </div>
                        {% endif %}
                    </div>
                    {% if current_user.is_authenticated %}
                    <div class="user-info">
                        <div class="user-avatar">
                            {{ current_user.username[0] }}
                        </div>
                        <div class="user-details">
                            <span class="user-name">{{ current_user.username }}</span>
                            <span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
                        </div>
                        <div class="dropdown-menu">
                            <a href="{{ url_for('user.user_profile') }}"><i class="fas fa-user-circle"></i> 个人中心</a>
                            <a href="#"><i class="fas fa-cog"></i> 设置</a>
                            <a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
                        </div>
                    </div>
                    {% else %}
                    <div class="user-info">
                        <a href="{{ url_for('user.login') }}" class="login-link">登录</a>
                        <a href="{{ url_for('user.register') }}" class="register-link">注册</a>
                    </div>
                    {% endif %}
                </div>
            </header>

            <!-- 内容区 - 这里是核心变化 -->
            <div class="content-wrapper">
                {% block content %}
                <!-- 子模板将在这里添加内容 -->
                {% endblock %}
            </div>
        </main>
    </div>

    <!-- 通用JavaScript -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 用户菜单下拉
            const userInfo = document.querySelector('.user-info');
            if (userInfo) {
                userInfo.addEventListener('click', function(e) {
                    if (!e.target.classList.contains('login-link') &&
                        !e.target.classList.contains('register-link')) {
                        userInfo.classList.toggle('active');
                    }
                });

                // 点击其他区域关闭下拉菜单
                document.addEventListener('click', function(e) {
                    if (!userInfo.contains(e.target)) {
                        userInfo.classList.remove('active');
                    }
                });
            }
        });
    </script>

    <!-- 页面特定JavaScript -->
    {% block scripts %}{% endblock %}
</body>
</html>

================================================================================
File: ./app/templates/register.html
================================================================================

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户注册 - 图书管理系统</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/register.css') }}">
</head>
<body>
    <div class="overlay"></div>

    <div class="theme-toggle" id="theme-toggle">☀️</div>

    <div class="main-container">
        <div class="login-container register-container">
            <div class="logo">
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
            </div>
            <h1>图书管理系统</h1>
            <p class="subtitle">创建您的新账户</p>

            <div id="register-form-container">
                <form id="register-form" action="{{ url_for('user.register') }}" method="post">
                    <div class="form-group">
                        <label for="username">用户名</label>
                        <div class="input-with-icon">
                            <span class="input-icon">👤</span>
                            <input type="text" id="username" name="username" class="form-control" placeholder="请输入用户名" required>
                        </div>
                        <div class="validation-message" id="username-error"></div>
                    </div>

                    <div class="form-group">
                        <label for="email">邮箱</label>
                        <div class="input-with-icon">
                            <span class="input-icon">📧</span>
                            <input type="email" id="email" name="email" class="form-control" placeholder="请输入邮箱" required>
                        </div>
                        <div class="validation-message" id="email-error"></div>
                    </div>

                    <div class="form-group">
                        <label for="verification_code">邮箱验证码</label>
                        <div class="verification-code-container">
                            <input type="text" id="verification_code" name="verification_code" class="verification-input" placeholder="请输入验证码" required>
                            <button type="button" id="send-code-btn" class="send-code-btn">发送验证码</button>
                        </div>
                        <div class="validation-message" id="verification-code-error"></div>
                    </div>

                    <div class="form-group">
                        <label for="password">密码</label>
                        <div class="input-with-icon">
                            <span class="input-icon">🔒</span>
                            <input type="password" id="password" name="password" class="form-control" placeholder="请设置密码" required>
                            <span class="password-toggle" id="password-toggle">👁️</span>
                        </div>
                        <div class="validation-message" id="password-error"></div>
                    </div>

                    <div class="form-group">
                        <label for="confirm_password">确认密码</label>
                        <div class="input-with-icon">
                            <span class="input-icon">🔒</span>
                            <input type="password" id="confirm_password" name="confirm_password" class="form-control" placeholder="请再次输入密码" required>
                        </div>
                        <div class="validation-message" id="confirm-password-error"></div>
                    </div>

                    {% if error %}
                    <div class="alert alert-danger">{{ error }}</div>
                    {% endif %}

                    <button type="submit" class="btn-login" id="register-button">
                        <span>注册</span>
                        <span class="loading">⟳</span>
                    </button>
                </form>

                <div class="signup">
                    已有账号？ <a href="{{ url_for('user.login') }}">返回登录</a>
                </div>
            </div>
        </div>
    </div>

    <footer>
        <p>© 施琦图书管理系统 - 版权所有</p>
    </footer>

    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
================================================================================
File: ./app/templates/404.html
================================================================================

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>页面未找到 - 图书管理系统</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Poppins', sans-serif;
            background-color: #fff5f7;
            margin: 0;
            padding: 0;
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .error-container {
            text-align: center;
            max-width: 650px;
            padding: 40px;
            border-radius: 20px;
            background: #ffffff;
            box-shadow: 0 10px 30px rgba(252, 162, 193, 0.2);
            position: relative;
            overflow: hidden;
        }

        .error-container::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 6px;
            background: linear-gradient(to right, #ff8ab3, #f17ab3, #f56eb8);
        }

        .error-code {
            font-size: 120px;
            font-weight: bold;
            background: linear-gradient(to right, #ff8ab3, #f17ab3, #f56eb8);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            margin: 0 0 20px 0;
            line-height: 1;
        }

        .error-title {
            font-size: 28px;
            color: #ff8ab3;
            margin-bottom: 15px;
            font-weight: 600;
        }

        .error-message {
            font-size: 18px;
            color: #7a7a7a;
            margin-bottom: 30px;
            line-height: 1.6;
        }

        .back-button {
            display: inline-block;
            padding: 12px 30px;
            background: linear-gradient(to right, #ff8ab3, #f17ab3);
            color: white;
            text-decoration: none;
            border-radius: 50px;
            font-weight: 500;
            letter-spacing: 1px;
            box-shadow: 0 5px 15px rgba(241, 122, 179, 0.4);
            transition: all 0.3s ease;
        }

        .back-button:hover {
            transform: translateY(-3px);
            box-shadow: 0 8px 20px rgba(241, 122, 179, 0.6);
        }

        .decoration {
            position: absolute;
            width: 80px;
            height: 80px;
            border-radius: 50%;
            background: rgba(255, 138, 179, 0.1);
        }

        .decoration-1 {
            top: -20px;
            left: -20px;
            width: 120px;
            height: 120px;
        }

        .decoration-2 {
            bottom: -30px;
            right: -30px;
            width: 150px;
            height: 150px;
        }

        .decoration-3 {
            top: 60%;
            left: -40px;
            width: 100px;
            height: 100px;
        }

        .book-icon {
            margin-bottom: 20px;
            width: 80px;
            height: 80px;
            display: inline-block;
            position: relative;
        }

        .book-icon svg {
            fill: #ff8ab3;
        }

        @media (max-width: 768px) {
            .error-code {
                font-size: 100px;
            }
            .error-title {
                font-size: 24px;
            }
            .error-container {
                margin: 0 20px;
                padding: 30px;
            }
        }
    </style>
</head>
<body>
    <div class="error-container">
        <div class="decoration decoration-1"></div>
        <div class="decoration decoration-2"></div>
        <div class="decoration decoration-3"></div>

        <div class="book-icon">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                <path d="M19 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h13c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H6V4h13v16z"/>
                <path d="M9 5h7v2H9zM9 8h7v2H9zM9 11h7v2H9zM9 14h7v2H9z"/>
            </svg>
        </div>

        <div class="error-code">404</div>
        <div class="error-title">噢！页面不见了～</div>
        <div class="error-message">
            <p>抱歉，您要找的页面似乎藏起来了，或者从未存在过。</p>
            <p>请检查您输入的网址是否正确，或者回到首页继续浏览吧！</p>
        </div>
        <a href="{{ url_for('index') }}" class="back-button">返回首页</a>
    </div>
</body>
</html>

================================================================================
File: ./app/templates/login.html
================================================================================

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户登录 - 图书管理系统</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
</head>
<body>
    <div class="overlay"></div>

    <div class="theme-toggle" id="theme-toggle">☀️</div>

    <div class="main-container">
        <div class="login-container">
            <div class="logo">
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
            </div>
            <h1>图书管理系统</h1>
            <p class="subtitle">欢迎回来，请登录您的账户</p>

            <div id="account-login">
                <form id="login-form" action="{{ url_for('user.login') }}" method="post">
                    <div class="form-group">
                        <label for="username">用户名/邮箱</label>
                        <div class="input-with-icon">
                            <span class="input-icon">👤</span>
                            <input type="text" id="username" name="username" class="form-control" placeholder="请输入账号">
                        </div>
                        <div class="validation-message" id="username-error"></div>
                    </div>

                    <div class="form-group">
                        <label for="password">密码</label>
                        <div class="input-with-icon">
                            <span class="input-icon">🔒</span>
                            <input type="password" id="password" name="password" class="form-control" placeholder="请输入密码">
                            <span class="password-toggle" id="password-toggle">👁️</span>
                        </div>
                        <div class="validation-message" id="password-error"></div>
                    </div>

                    <div class="remember-forgot">
                        <label class="custom-checkbox">
                            <input type="checkbox" name="remember_me">
                            <span class="checkmark"></span>
                            记住我 (7天内免登录)
                        </label>
                        <div class="forgot-password">
                            <a href="#">忘记密码？</a>
                        </div>
                    </div>

                    {% if error %}
                    <div class="alert alert-danger">{{ error }}</div>
                    {% endif %}

                    <button type="submit" class="btn-login" id="login-button">
                        <span>登录</span>
                        <span class="loading">⟳</span>
                    </button>
                </form>

                <div class="signup">
                    还没有账号？ <a href="{{ url_for('user.register') }}">立即注册</a>
                </div>

                <div class="features">
                    <div class="feature-item">
                        <span class="feature-icon">🔒</span>
                        <span>安全登录</span>
                    </div>
                    <div class="feature-item">
                        <span class="feature-icon">🔐</span>
                        <span>数据加密</span>
                    </div>
                    <div class="feature-item">
                        <span class="feature-icon">📚</span>
                        <span>图书管理</span>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <footer>
        <p>©  施琦图书管理系统 - 版权所有</p>
    </footer>

    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

================================================================================
File: ./app/templates/statistics/index.html
================================================================================

<!-- app/templates/statistics/index.html -->
{% extends "base.html" %}

{% block title %}统计分析 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<!-- Updated Google Fonts Link -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Lora:wght@400;600&family=Open+Sans:wght@300;400&family=Sacramento&family=EB+Garamond:ital@0;1&display=swap" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="statistics-container animate__animated animate__fadeIn">
    <h1 class="page-title">统计分析</h1>

    <div class="quote-banner">
        <p>"统计数字是文学世界的星辰，照亮知识的海洋"</p>
    </div>

    <div class="stats-grid">
        <a href="{{ url_for('statistics.book_ranking') }}" class="stats-card animate__animated animate__fadeInUp">
            <div class="card-inner">
                <div class="card-icon"><i class="fas fa-chart-line"></i></div>
                <div class="card-title">热门图书排行</div>
                <div class="card-description">查看最受欢迎的图书，按借阅次数排名</div>
                <div class="card-decoration book-decoration"></div>
            </div>
        </a>

        <a href="{{ url_for('statistics.borrow_statistics') }}" class="stats-card animate__animated animate__fadeInUp" style="animation-delay: 0.1s;">
            <div class="card-inner">
                <div class="card-icon"><i class="fas fa-exchange-alt"></i></div>
                <div class="card-title">借阅趋势分析</div>
                <div class="card-description">查看借阅和归还的历史趋势和分布</div>
                <div class="card-decoration trend-decoration"></div>
            </div>
        </a>

        <a href="{{ url_for('statistics.user_activity') }}" class="stats-card animate__animated animate__fadeInUp" style="animation-delay: 0.2s;">
            <div class="card-inner">
                <div class="card-icon"><i class="fas fa-users"></i></div>
                <div class="card-title">用户活跃度分析</div>
                <div class="card-description">查看最活跃的用户和用户行为分析</div>
                <div class="card-decoration user-decoration"></div>
            </div>
        </a>

        <a href="{{ url_for('statistics.overdue_analysis') }}" class="stats-card animate__animated animate__fadeInUp" style="animation-delay: 0.3s;">
            <div class="card-inner">
                <div class="card-icon"><i class="fas fa-exclamation-circle"></i></div>
                <div class="card-title">逾期分析</div>
                <div class="card-description">分析图书逾期情况和趋势</div>
                <div class="card-decoration overdue-decoration"></div>
            </div>
        </a>
    </div>

    <div class="page-decoration left"></div>
    <div class="page-decoration right"></div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/statistics.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 添加卡片悬停动画效果
    const cards = document.querySelectorAll('.stats-card');

    cards.forEach(card => {
        card.addEventListener('mouseenter', function() {
            const decoration = this.querySelector('.card-decoration');
            if (decoration) { // Check if decoration exists
                decoration.classList.add('active');
            }
            this.classList.add('hovered'); // Add class for general hover style
        });

        card.addEventListener('mouseleave', function() {
            const decoration = this.querySelector('.card-decoration');
            if (decoration) { // Check if decoration exists
                decoration.classList.remove('active');
            }
            this.classList.remove('hovered'); // Remove class
        });
    });
});
</script>
{% endblock %}

================================================================================
File: ./app/templates/statistics/overdue_analysis.html
================================================================================

<!-- app/templates/statistics/overdue_analysis.html -->
{% extends "base.html" %}

{% block title %}逾期分析 - 统计分析{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/overdue_analysis.css') }}">
{% endblock %}

{% block content %}
<div class="statistics-container">
    <div class="breadcrumb">
        <a href="{{ url_for('statistics.index') }}">统计分析</a> / 逾期分析
    </div>

    <h1 class="page-title">逾期分析</h1>

    <div class="stats-cards">
        <div class="stats-card" id="total-borrows">
            <div class="card-value">0</div>
            <div class="card-title">总借阅数</div>
        </div>

        <div class="stats-card" id="current-overdue">
            <div class="card-value">0</div>
            <div class="card-title">当前逾期数</div>
        </div>

        <div class="stats-card" id="returned-overdue">
            <div class="card-value">0</div>
            <div class="card-title">历史逾期数</div>
        </div>

        <div class="stats-card" id="overdue-rate">
            <div class="card-value">0%</div>
            <div class="card-title">总逾期率</div>
        </div>
    </div>

    <div class="chart-row">
        <div class="chart-container half">
            <h3>逾期时长分布</h3>
            <canvas id="overdue-range-chart"></canvas>
        </div>

        <div class="chart-container half">
            <h3>逾期状态分布</h3>
            <canvas id="overdue-status-chart"></canvas>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/overdue_analysis.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/statistics/book_ranking.html
================================================================================

<!-- app/templates/statistics/book_ranking.html -->
{% extends "base.html" %}

{% block title %}热门图书排行 - 统计分析{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/book_ranking.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap">
{% endblock %}

{% block content %}
<div class="statistics-container">
    <div class="breadcrumb">
        <a href="{{ url_for('statistics.index') }}">统计分析</a> / <span class="current-page">热门图书排行</span>
    </div>

    <h1 class="page-title animate__animated animate__fadeIn">✨ 热门图书排行 ✨</h1>

    <div class="filter-section">
        <div class="filter-label">时间范围：</div>
        <select id="time-range" class="filter-select">
            <option value="week">最近7天</option>
            <option value="month" selected>最近30天</option>
            <option value="year">最近一年</option>
            <option value="all">全部时间</option>
        </select>

        <div class="filter-label ml-20">显示数量：</div>
        <select id="limit-count" class="filter-select">
            <option value="5">5本</option>
            <option value="10" selected>10本</option>
            <option value="20">20本</option>
            <option value="50">50本</option>
        </select>
    </div>

    <div class="chart-container animate__animated animate__fadeInUp">
        <div class="chart-decoration left"></div>
        <div class="chart-decoration right"></div>
        <canvas id="ranking-chart"></canvas>
    </div>

    <div class="table-container animate__animated animate__fadeInUp">
        <h3 class="book-list-title"><span class="book-icon">📚</span> 热门图书榜单 <span class="book-icon">📖</span></h3>
        <table class="data-table">
            <thead>
                <tr>
                    <th><span class="column-icon">🏆</span> 排名</th>
                    <th><span class="column-icon">🖼️</span> 封面</th>
                    <th><span class="column-icon">📕</span> 书名</th>
                    <th><span class="column-icon">✒️</span> 作者</th>
                    <th><span class="column-icon">📊</span> 借阅次数</th>
                </tr>
            </thead>
            <tbody id="ranking-table-body">
                <!-- 数据将通过JS动态填充 -->
                <tr class="loading-row">
                    <td colspan="5"><div class="loading-animation"><span>正在打开书页</span><span class="dot-animation">...</span></div></td>
                </tr>
            </tbody>
        </table>
    </div>

    <div class="quote-container animate__animated animate__fadeIn">
        <p>"一本好书就像一艘船，带领我们从狭隘的地方，驶向生活的无限广阔的海洋。"</p>
        <span class="quote-author">—— 海伦·凯勒</span>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/book_ranking.js') }}"></script>
<script>
    // 原有的 Chart.js 初始化代码可能会在 book_ranking.js 中
    // 这里我们添加额外的动画效果
    document.addEventListener('DOMContentLoaded', function() {
        // 添加表格行的动画效果
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    let delay = 0;
                    entry.target.querySelectorAll('tr').forEach(row => {
                        setTimeout(() => {
                            row.classList.add('fade-in');
                        }, delay);
                        delay += 100;
                    });
                    observer.unobserve(entry.target);
                }
            });
        });

        const tableBody = document.getElementById('ranking-table-body');
        if (tableBody) observer.observe(tableBody);

        // 装饰元素动画
        const decorations = document.querySelectorAll('.chart-decoration');
        decorations.forEach(decor => {
            decor.classList.add('floating');
        });
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/statistics/user_activity.html
================================================================================

<!-- app/templates/statistics/user_activity.html -->
{% extends "base.html" %}

{% block title %}用户活跃度分析 - 统计分析{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/user_activity.css') }}">
{% endblock %}

{% block content %}
<div class="statistics-container">
    <div class="breadcrumb">
        <a href="{{ url_for('statistics.index') }}">统计分析</a> / 用户活跃度分析
    </div>

    <h1 class="page-title">用户活跃度分析</h1>

    <div class="chart-container">
        <h3>最活跃用户排行</h3>
        <canvas id="user-activity-chart"></canvas>
    </div>

    <div class="table-container">
        <h3>活跃用户列表</h3>
        <table class="data-table">
            <thead>
                <tr>
                    <th>排名</th>
                    <th>用户名</th>
                    <th>昵称</th>
                    <th>借阅次数</th>
                </tr>
            </thead>
            <tbody id="user-table-body">
                <!-- 数据将通过JS动态填充 -->
                <tr class="loading-row">
                    <td colspan="4">加载中...</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/user_activity.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/statistics/borrow_statistics.html
================================================================================

<!-- app/templates/statistics/borrow_statistics.html -->
{% extends "base.html" %}

{% block title %}借阅趋势分析 - 统计分析{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/borrow_statistics.css') }}">
{% endblock %}

{% block content %}
<div class="statistics-container">
    <!-- 页面装饰元素 -->
    <div class="page-decoration left"></div>
    <div class="page-decoration right"></div>

    <div class="breadcrumb">
        <a href="{{ url_for('statistics.index') }}">统计分析</a> / <span class="current-page">借阅趋势分析</span>
    </div>

    <h1 class="page-title">借阅趋势分析</h1>

    <div class="intro-text">
        <p>探索读者的阅读习惯与喜好，发现图书流通的奥秘</p>
    </div>

    <div class="filter-section">
        <div class="filter-label">时间范围：</div>
        <select id="time-range" class="filter-select">
            <option value="week">最近7天</option>
            <option value="month" selected>最近30天</option>
            <option value="year">最近一年</option>
        </select>
    </div>

    <div class="chart-container trend-chart">
        <h3>借阅与归还趋势</h3>
        <div class="chart-decoration left floating"></div>
        <div class="chart-decoration right floating"></div>
        <div class="chart-wrapper">
            <canvas id="trend-chart"></canvas>
        </div>
    </div>

    <div class="chart-row">
        <div class="chart-container half">
            <h3>分类借阅分布</h3>
            <div class="chart-wrapper">
                <canvas id="category-chart"></canvas>
            </div>
        </div>

        <div class="stats-summary half">
            <h3>借阅概况</h3>
            <div class="stats-grid" id="borrow-summary">
                <!-- 数据将通过JS动态填充 -->
                <div class="loading">
                    <div class="loader"></div>
                    <span>数据加载中<span class="dot-animation">...</span></span>
                </div>
            </div>
        </div>
    </div>

    <div class="insights-container">
        <h3>阅读洞察</h3>
        <div class="insights-content">
            <p>根据当前数据分析，发现读者更喜欢在周末借阅图书，女性读者偏爱文学和艺术类书籍，而男性读者则更关注科技和历史类图书。</p>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/borrow_statistics.js') }}"></script>
<script>
    // 添加动画效果
    document.addEventListener('DOMContentLoaded', function() {
        // 为统计项添加hover效果
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    entry.target.classList.add('animate-fadeInUp');
                    observer.unobserve(entry.target);
                }
            });
        }, {
            threshold: 0.1
        });

        // 监听所有主要元素
        document.querySelectorAll('.chart-container, .stats-summary, .insights-container').forEach(el => {
            observer.observe(el);
        });

        // 统计卡片的交互效果
        document.addEventListener('mouseover', function(e) {
            if (e.target.closest('.stats-item')) {
                const item = e.target.closest('.stats-item');
                const items = document.querySelectorAll('.stats-item');

                items.forEach(i => {
                    if (i !== item) {
                        i.style.opacity = '0.7';
                    }
                });
            }
        });

        document.addEventListener('mouseout', function(e) {
            if (e.target.closest('.stats-item')) {
                document.querySelectorAll('.stats-item').forEach(i => {
                    i.style.opacity = '1';
                });
            }
        });
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/user/profile.html
================================================================================

{% extends "base.html" %}

{% block title %}个人中心 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-profile.css') }}">
{% endblock %}

{% block content %}
<div class="profile-container">
    <div class="page-header">
        <h1>个人中心</h1>
    </div>

    <div class="profile-content">
        <!-- 左侧用户信息展示 -->
        <div class="profile-sidebar">
            <div class="user-avatar-container">
                <div class="user-avatar large">
                    {{ user.username[0] }}
                </div>
                <h3 class="user-name">{{ user.nickname or user.username }}</h3>
                <p class="user-role">{{ '管理员' if user.role_id == 1 else '普通用户' }}</p>
            </div>

            <div class="user-stats">
                <div class="stat-item">
                    <div class="stat-value" id="borrowCount">--</div>
                    <div class="stat-label">借阅中</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value" id="returnedCount">--</div>
                    <div class="stat-label">已归还</div>
                </div>
                <div class="stat-item">
                    <div class="stat-value" id="overdueCount">--</div>
                    <div class="stat-label">已逾期</div>
                </div>
            </div>

            <div class="account-info">
                <div class="info-row">
                    <span class="info-label">用户名</span>
                    <span class="info-value">{{ user.username }}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">用户ID</span>
                    <span class="info-value">{{ user.id }}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">注册时间</span>
                    <span class="info-value">{{ user.created_at }}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">最后更新</span>
                    <span class="info-value">{{ user.updated_at }}</span>
                </div>
            </div>
        </div>

        <!-- 右侧内容区域：包含编辑选项卡 -->
        <div class="profile-main">
            <!-- 提示消息 -->
            {% with messages = get_flashed_messages(with_categories=true) %}
              {% if messages %}
                {% for category, message in messages %}
                  <div class="alert alert-{{ category }}">{{ message }}</div>
                {% endfor %}
              {% endif %}
            {% endwith %}

            <!-- 选项卡导航 -->
            <ul class="nav nav-tabs" id="profileTabs" role="tablist">
                <li class="nav-item">
                    <a class="nav-link active" id="basic-tab" data-toggle="tab" href="#basic" role="tab" aria-controls="basic" aria-selected="true">
                        <i class="fas fa-user"></i> 基本信息
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" id="security-tab" data-toggle="tab" href="#security" role="tab" aria-controls="security" aria-selected="false">
                        <i class="fas fa-lock"></i> 安全设置
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" id="activity-tab" data-toggle="tab" href="#activity" role="tab" aria-controls="activity" aria-selected="false">
                        <i class="fas fa-history"></i> 最近活动
                    </a>
                </li>
            </ul>

            <!-- 选项卡内容 -->
            <div class="tab-content" id="profileTabsContent">
                <!-- 基本信息选项卡 -->
                <div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
                    <form method="POST" action="{{ url_for('user.user_profile') }}" id="profileForm">
                        <div class="form-section">
                            <h4>个人信息</h4>

                            <div class="form-group">
                                <label for="nickname">昵称</label>
                                <input type="text" class="form-control" id="nickname" name="nickname" value="{{ user.nickname or '' }}" placeholder="请输入您的昵称">
                            </div>

                            <div class="form-group">
                                <label for="email">邮箱地址</label>
                                <input type="email" class="form-control" id="email" name="email" value="{{ user.email or '' }}" placeholder="请输入您的邮箱">
                                <small class="form-text text-muted">用于接收系统通知和找回密码</small>
                            </div>

                            <div class="form-group">
                                <label for="phone">手机号码</label>
                                <input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone or '' }}" placeholder="请输入您的手机号">
                                <small class="form-text text-muted">用于接收借阅提醒和系统通知</small>
                            </div>
                        </div>

                        <div class="form-actions">
                            <button type="submit" class="btn btn-primary" name="form_type" value="profile">
                                <i class="fas fa-save"></i> 保存修改
                            </button>
                        </div>
                    </form>
                </div>

                <!-- 安全设置选项卡 -->
                <div class="tab-pane fade" id="security" role="tabpanel" aria-labelledby="security-tab">
                    <form method="POST" action="{{ url_for('user.user_profile') }}" id="passwordForm">
                        <div class="form-section">
                            <h4>修改密码</h4>

                            <div class="form-group">
                                <label for="current_password">当前密码</label>
                                <input type="password" class="form-control" id="current_password" name="current_password" placeholder="请输入当前密码">
                            </div>

                            <div class="form-group">
                                <label for="new_password">新密码</label>
                                <input type="password" class="form-control" id="new_password" name="new_password" placeholder="请输入新密码">
                                <small class="form-text text-muted">密码长度至少为6个字符</small>
                            </div>

                            <div class="form-group">
                                <label for="confirm_password">确认新密码</label>
                                <input type="password" class="form-control" id="confirm_password" name="confirm_password" placeholder="请再次输入新密码">
                            </div>
                        </div>

                        <div class="form-actions">
                            <button type="submit" class="btn btn-primary" name="form_type" value="password">
                                <i class="fas fa-key"></i> 更新密码
                            </button>
                        </div>
                    </form>
                </div>

                <!-- 最近活动选项卡 -->
                <div class="tab-pane fade" id="activity" role="tabpanel" aria-labelledby="activity-tab">
                    <div class="activity-header">
                        <h4>最近活动</h4>
                        <div class="activity-filter">
                            <label for="activityFilter">显示:</label>
                            <select id="activityFilter" class="form-control form-control-sm">
                                <option value="all">所有活动</option>
                                <option value="login">登录记录</option>
                                <option value="borrow">借阅活动</option>
                                <option value="return">归还活动</option>
                            </select>
                        </div>
                    </div>

                    <div class="activity-timeline" id="activityTimeline">
                        <div class="timeline-loading">
                            <div class="spinner-border text-primary" role="status">
                                <span class="sr-only">Loading...</span>
                            </div>
                            <p>加载中...</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/user-profile.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/user/list.html
================================================================================

{% extends "base.html" %}

{% block title %}用户管理 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-list.css') }}">
{% endblock %}

{% block content %}
<div class="user-list-container">
    <!-- 页面标题 -->
    <div class="page-header">
        <h1>用户管理</h1>
        <div class="actions">
            <a href="{{ url_for('user.add_user') }}" class="btn btn-primary">
                <i class="fas fa-user-plus"></i> 添加用户
            </a>
        </div>
    </div>

    <!-- 搜索和过滤区域 -->
    <div class="search-filter-container">
        <form method="GET" action="{{ url_for('user.user_list') }}" class="search-filter-form">
            <div class="form-row">
                <div class="search-box">
                    <input type="text" name="search" value="{{ search }}" placeholder="搜索用户名/邮箱/昵称/手机" class="form-control">
                    <button type="submit" class="btn btn-search">
                        <i class="fas fa-search"></i>
                    </button>
                </div>

                <div class="filter-box">
                    <select name="status" class="form-control">
                        <option value="">所有状态</option>
                        <option value="1" {% if status == 1 %}selected{% endif %}>正常</option>
                        <option value="0" {% if status == 0 %}selected{% endif %}>禁用</option>
                    </select>

                    <select name="role_id" class="form-control">
                        <option value="">所有角色</option>
                        {% for role in roles %}
                        <option value="{{ role.id }}" {% if role_id == role.id %}selected{% endif %}>{{ role.role_name }}</option>
                        {% endfor %}
                    </select>

                    <button type="submit" class="btn btn-filter">筛选</button>
                    <a href="{{ url_for('user.user_list') }}" class="btn btn-reset">重置</a>
                </div>
            </div>
        </form>
    </div>

    <!-- 用户列表表格 -->
    <div class="table-responsive">
        <table class="table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>用户名</th>
                    <th>昵称</th>
                    <th>邮箱</th>
                    <th>手机号</th>
                    <th>角色</th>
                    <th>状态</th>
                    <th>注册时间</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                {% for user in pagination.items %}
                <tr>
                    <td>{{ user.id }}</td>
                    <td>{{ user.username }}</td>
                    <td>{{ user.nickname or '-' }}</td>
                    <td>{{ user.email or '-' }}</td>
                    <td>{{ user.phone or '-' }}</td>
                    <td>
                        {% for role in roles %}
                            {% if role.id == user.role_id %}
                                {{ role.role_name }}
                            {% endif %}
                        {% endfor %}
                    </td>
                    <td>
                        <span class="status-badge {% if user.status == 1 %}active{% else %}inactive{% endif %}">
                            {{ '正常' if user.status == 1 else '禁用' }}
                        </span>
                    </td>
                    <td>{{ user.created_at }}</td>
                    <td class="actions">
                        <a href="{{ url_for('user.user_edit', user_id=user.id) }}" class="btn btn-sm btn-info" title="编辑">
                            <i class="fas fa-edit"></i>
                        </a>

                        {% if user.id != session.get('user_id') %}
                            {% if user.status == 1 %}
                            <button class="btn btn-sm btn-warning toggle-status" data-id="{{ user.id }}" data-status="0" title="禁用">
                                <i class="fas fa-ban"></i>
                            </button>
                            {% else %}
                            <button class="btn btn-sm btn-success toggle-status" data-id="{{ user.id }}" data-status="1" title="启用">
                                <i class="fas fa-check"></i>
                            </button>
                            {% endif %}

                            <button class="btn btn-sm btn-danger delete-user" data-id="{{ user.id }}" title="删除">
                                <i class="fas fa-trash"></i>
                            </button>
                        {% else %}
                            <span class="text-muted">(当前用户)</span>
                        {% endif %}
                    </td>
                </tr>
                {% else %}
                <tr>
                    <td colspan="9" class="text-center">暂无用户数据</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>

    <!-- 分页控件 -->
    {% if pagination.pages > 1 %}
    <div class="pagination-container">
        <ul class="pagination">
            {% if pagination.has_prev %}
            <li class="page-item">
                <a class="page-link" href="{{ url_for('user.user_list', page=pagination.prev_num, search=search, status=status, role_id=role_id) }}">
                    <i class="fas fa-chevron-left"></i>
                </a>
            </li>
            {% endif %}

            {% for page in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
                {% if page %}
                    {% if page == pagination.page %}
                    <li class="page-item active">
                        <span class="page-link">{{ page }}</span>
                    </li>
                    {% else %}
                    <li class="page-item">
                        <a class="page-link" href="{{ url_for('user.user_list', page=page, search=search, status=status, role_id=role_id) }}">{{ page }}</a>
                    </li>
                    {% endif %}
                {% else %}
                    <li class="page-item disabled">
                        <span class="page-link">...</span>
                    </li>
                {% endif %}
            {% endfor %}

            {% if pagination.has_next %}
            <li class="page-item">
                <a class="page-link" href="{{ url_for('user.user_list', page=pagination.next_num, search=search, status=status, role_id=role_id) }}">
                    <i class="fas fa-chevron-right"></i>
                </a>
            </li>
            {% endif %}
        </ul>
    </div>
    {% endif %}
</div>

<!-- 确认删除模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                您确定要删除这个用户吗？此操作不可逆。
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/user-list.js') }}"></script>
{% endblock %}


================================================================================
File: ./app/templates/user/add.html
================================================================================

{% extends "base.html" %}

{% block title %}添加用户 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-form.css') }}">
{% endblock %}

{% block content %}
<div class="user-form-container">
    <div class="page-header">
        <h1>添加用户</h1>
        <div class="actions">
            <a href="{{ url_for('user.user_list') }}" class="btn btn-secondary">
                <i class="fas fa-arrow-left"></i> 返回用户列表
            </a>
        </div>
    </div>

    <div class="form-card">
        <form id="addUserForm" method="POST" action="{{ url_for('user.add_user') }}">
            {% if error %}
            <div class="alert alert-danger">{{ error }}</div>
            {% endif %}

            <div class="form-group required">
                <label for="username">用户名</label>
                <input type="text" id="username" name="username" class="form-control" required
                       placeholder="请输入用户名（3-20个字符）" minlength="3" maxlength="20">
                <small class="form-text text-muted">用户名将用于登录，不可重复</small>
            </div>

            <div class="form-group required">
                <label for="password">密码</label>
                <div class="password-field">
                    <input type="password" id="password" name="password" class="form-control" required
                           placeholder="请输入密码（至少6位）" minlength="6">
                    <button type="button" class="toggle-password">
                        <i class="fas fa-eye"></i>
                    </button>
                </div>
                <small class="form-text text-muted">密码至少包含6个字符</small>
            </div>

            <div class="form-group required">
                <label for="confirm_password">确认密码</label>
                <div class="password-field">
                    <input type="password" id="confirm_password" name="confirm_password" class="form-control" required
                           placeholder="请再次输入密码" minlength="6">
                    <button type="button" class="toggle-password">
                        <i class="fas fa-eye"></i>
                    </button>
                </div>
                <small id="password-match-message" class="form-text"></small>
            </div>

            <div class="form-group required">
                <label for="email">电子邮箱</label>
                <div class="input-with-button">
                    <input type="email" id="email" name="email" class="form-control" required
                           placeholder="请输入有效的电子邮箱">
                    <button type="button" id="sendVerificationCode" class="btn btn-outline-primary">
                        发送验证码
                    </button>
                </div>
            </div>

            <div class="form-group required">
                <label for="verification_code">验证码</label>
                <input type="text" id="verification_code" name="verification_code" class="form-control" required
                       placeholder="请输入邮箱验证码" maxlength="6">
                <small class="form-text text-muted">请输入发送到邮箱的6位验证码</small>
            </div>

            <div class="form-group">
                <label for="nickname">昵称</label>
                <input type="text" id="nickname" name="nickname" class="form-control"
                       placeholder="请输入昵称（选填）" maxlength="64">
            </div>

            <div class="form-group">
                <label for="phone">手机号码</label>
                <input type="tel" id="phone" name="phone" class="form-control"
                       placeholder="请输入手机号码（选填）" maxlength="20">
            </div>

            <div class="form-group required">
                <label for="role_id">用户角色</label>
                <select id="role_id" name="role_id" class="form-control" required>
                    {% for role in roles %}
                    <option value="{{ role.id }}">{{ role.role_name }}</option>
                    {% endfor %}
                </select>
                <small class="form-text text-muted">默认为普通用户，请根据需要选择合适的角色</small>
            </div>

            <div class="form-group required">
                <label for="status">账号状态</label>
                <select id="status" name="status" class="form-control" required>
                    <option value="1" selected>正常</option>
                    <option value="0">禁用</option>
                </select>
            </div>

            <div class="form-actions">
                <button type="submit" class="btn btn-primary">
                    <i class="fas fa-save"></i> 保存用户
                </button>
                <button type="reset" class="btn btn-secondary">
                    <i class="fas fa-undo"></i> 重置表单
                </button>
            </div>
        </form>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/user-add.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/user/edit.html
================================================================================

{% extends "base.html" %}

{% block title %}编辑用户 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-edit.css') }}">
{% endblock %}

{% block content %}
<div class="user-edit-container">
    <div class="page-header">
        <h1>编辑用户</h1>
        <div class="actions">
            <a href="{{ url_for('user.user_list') }}" class="btn btn-outline-secondary">
                <i class="fas fa-arrow-left"></i> 返回用户列表
            </a>
        </div>
    </div>

    <div class="card">
        <div class="card-body">
            {% with messages = get_flashed_messages(with_categories=true) %}
              {% if messages %}
                {% for category, message in messages %}
                  <div class="alert alert-{{ category }}">{{ message }}</div>
                {% endfor %}
              {% endif %}
            {% endwith %}

            <form method="POST" action="{{ url_for('user.user_edit', user_id=user.id) }}" id="userEditForm">
                <div class="form-row">
                    <!-- 用户基本信息 -->
                    <div class="col-md-6">
                        <div class="form-group">
                            <label for="username">用户名</label>
                            <input type="text" class="form-control" id="username" value="{{ user.username }}" readonly>
                            <small class="form-text text-muted">用户名不可修改</small>
                        </div>

                        <div class="form-group">
                            <label for="email">邮箱地址</label>
                            <input type="email" class="form-control" id="email" name="email" value="{{ user.email or '' }}">
                        </div>

                        <div class="form-group">
                            <label for="phone">手机号码</label>
                            <input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone or '' }}">
                        </div>

                        <div class="form-group">
                            <label for="nickname">昵称</label>
                            <input type="text" class="form-control" id="nickname" name="nickname" value="{{ user.nickname or '' }}">
                        </div>
                    </div>

                    <!-- 用户权限和密码 -->
                    <div class="col-md-6">
                        <div class="form-group">
                            <label for="role_id">用户角色</label>
                            <select class="form-control" id="role_id" name="role_id">
                                {% for role in roles %}
                                <option value="{{ role.id }}" {% if role.id == user.role_id %}selected{% endif %}>
                                    {{ role.role_name }}
                                </option>
                                {% endfor %}
                            </select>
                        </div>

                        <div class="form-group">
                            <label for="status">用户状态</label>
                            <select class="form-control" id="status" name="status">
                                <option value="1" {% if user.status == 1 %}selected{% endif %}>正常</option>
                                <option value="0" {% if user.status == 0 %}selected{% endif %}>禁用</option>
                            </select>
                        </div>

                        <div class="form-group">
                            <label for="password">重置密码</label>
                            <input type="password" class="form-control" id="password" name="password">
                            <small class="form-text text-muted">留空表示不修改密码</small>
                        </div>

                        <div class="form-group" id="confirmPasswordGroup" style="display: none;">
                            <label for="confirm_password">确认密码</label>
                            <input type="password" class="form-control" id="confirm_password" name="confirm_password">
                        </div>
                    </div>
                </div>

                <!-- 附加信息 -->
                <div class="form-row">
                    <div class="col-md-12">
                        <div class="user-info-box">
                            <div class="info-item">
                                <span class="info-label">用户ID:</span>
                                <span class="info-value">{{ user.id }}</span>
                            </div>
                            <div class="info-item">
                                <span class="info-label">注册时间:</span>
                                <span class="info-value">{{ user.created_at }}</span>
                            </div>
                            <div class="info-item">
                                <span class="info-label">最后更新:</span>
                                <span class="info-value">{{ user.updated_at }}</span>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- 提交按钮区域 -->
                <div class="form-actions">
                    <button type="submit" class="btn btn-primary">
                        <i class="fas fa-save"></i> 保存修改
                    </button>
                    <a href="{{ url_for('user.user_list') }}" class="btn btn-secondary">
                        取消
                    </a>
                </div>
            </form>
        </div>
    </div>
</div>

<!-- 操作成功提示模态框 -->
<div class="modal fade" id="successModal" tabindex="-1" role="dialog" aria-labelledby="successModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="successModalLabel">操作成功</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                用户信息已成功更新。
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
                <a href="{{ url_for('user.user_list') }}" class="btn btn-primary">返回用户列表</a>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/user-edit.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/user/roles.html
================================================================================

{% extends "base.html" %}

{% block title %}角色管理 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-roles.css') }}">
{% endblock %}

{% block content %}
<div class="roles-container">
    <!-- 页面标题 -->
    <div class="page-header">
        <h1>角色管理</h1>
        <div class="actions">
            <button class="btn btn-primary" id="addRoleBtn">
                <i class="fas fa-plus"></i> 添加角色
            </button>
        </div>
    </div>

    <!-- 角色列表卡片 -->
    <div class="role-list">
        {% for role in roles %}
        <div class="role-card" data-id="{{ role.id }}">
            <div class="role-header">
                <h3 class="role-name">{{ role.role_name }}</h3>
                <div class="role-actions">
                    <button class="btn btn-sm btn-edit-role" title="编辑角色">
                        <i class="fas fa-edit"></i>
                    </button>
                    {% if role.id not in [1, 2] %}
                    <button class="btn btn-sm btn-delete-role" title="删除角色">
                        <i class="fas fa-trash"></i>
                    </button>
                    {% endif %}
                </div>
            </div>
            <div class="role-description">
                {% if role.description %}
                {{ role.description }}
                {% else %}
                <span class="text-muted">暂无描述</span>
                {% endif %}
            </div>
            <div class="role-stats">
                <div class="stat-item">
                    <i class="fas fa-users"></i> <span id="userCount-{{ role.id }}">--</span> 用户
                </div>
                {% if role.id == 1 %}
                <div class="role-badge admin">管理员</div>
                {% elif role.id == 2 %}
                <div class="role-badge user">普通用户</div>
                {% else %}
                <div class="role-badge custom">自定义</div>
                {% endif %}
            </div>
        </div>
        {% else %}
        <div class="no-data-message">
            <i class="fas fa-users-slash"></i>
            <p>暂无角色数据</p>
        </div>
        {% endfor %}
    </div>

    <!-- 权限描述 -->
    <div class="permissions-info">
        <h3>角色权限说明</h3>
        <div class="card">
            <div class="card-body">
                <table class="table permission-table">
                    <thead>
                        <tr>
                            <th>功能模块</th>
                            <th>管理员</th>
                            <th>普通用户</th>
                            <th>自定义角色</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>图书浏览</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-check text-success"></i></td>
                        </tr>
                        <tr>
                            <td>借阅图书</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-check text-success"></i></td>
                        </tr>
                        <tr>
                            <td>图书管理</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-times text-danger"></i></td>
                            <td>可配置</td>
                        </tr>
                        <tr>
                            <td>用户管理</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-times text-danger"></i></td>
                            <td>可配置</td>
                        </tr>
                        <tr>
                            <td>借阅管理</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-times text-danger"></i></td>
                            <td>可配置</td>
                        </tr>
                        <tr>
                            <td>库存管理</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-times text-danger"></i></td>
                            <td>可配置</td>
                        </tr>
                        <tr>
                            <td>统计分析</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-times text-danger"></i></td>
                            <td>可配置</td>
                        </tr>
                        <tr>
                            <td>系统设置</td>
                            <td><i class="fas fa-check text-success"></i></td>
                            <td><i class="fas fa-times text-danger"></i></td>
                            <td>可配置</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<!-- 角色编辑模态框 -->
<div class="modal fade" id="roleModal" tabindex="-1" role="dialog" aria-labelledby="roleModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="roleModalLabel">添加角色</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <form id="roleForm">
                    <input type="hidden" id="roleId" value="">

                    <div class="form-group">
                        <label for="roleName">角色名称</label>
                        <input type="text" class="form-control" id="roleName" required>
                    </div>

                    <div class="form-group">
                        <label for="roleDescription">角色描述</label>
                        <textarea class="form-control" id="roleDescription" rows="3"></textarea>
                    </div>

                    <!-- 权限选择部分 -->
                    <div class="form-group" id="permissionsSection">
                        <label>角色权限</label>
                        <div class="alert alert-info" id="systemRoleAlert" style="display: none;">
                            <i class="fas fa-info-circle"></i> 系统内置角色的权限配置受到限制。
                        </div>

                        <div class="permissions-container">
                            <!-- 权限分组：图书管理 -->
                            <div class="permission-group">
                                <h6 class="permission-group-title">图书管理</h6>
                                <div class="permission-items" id="book-permissions">
                                    <!-- 权限项将通过JS动态加载 -->
                                    <div class="loading-permissions">
                                        <i class="fas fa-spinner fa-spin"></i> 加载权限中...
                                    </div>
                                </div>
                            </div>

                            <!-- 权限分组：用户管理 -->
                            <div class="permission-group">
                                <h6 class="permission-group-title">用户管理</h6>
                                <div class="permission-items" id="user-permissions">
                                    <!-- 权限项将通过JS动态加载 -->
                                </div>
                            </div>

                            <!-- 权限分组：借阅管理 -->
                            <div class="permission-group">
                                <h6 class="permission-group-title">借阅管理</h6>
                                <div class="permission-items" id="borrow-permissions">
                                    <!-- 权限项将通过JS动态加载 -->
                                </div>
                            </div>

                            <!-- 权限分组：系统管理 -->
                            <div class="permission-group">
                                <h6 class="permission-group-title">系统管理</h6>
                                <div class="permission-items" id="system-permissions">
                                    <!-- 权限项将通过JS动态加载 -->
                                </div>
                            </div>
                        </div>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary" id="saveRoleBtn">保存</button>
            </div>
        </div>
    </div>
</div>

<!-- 确认删除模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                您确定要删除这个角色吗？此操作不可逆。
                <p class="text-danger mt-3">注意：删除角色将会影响所有使用此角色的用户。</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-danger" id="confirmDeleteBtn">确认删除</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/user-roles.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/borrow/my_borrows.html
================================================================================

{% extends 'base.html' %}

{% block title %}我的梦幻书架 - 图书管理系统{% endblock %}

{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/my_borrows.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="container">
    <h1 class="page-title">✨ 我的梦幻书架 ✨</h1>

    <div class="tabs">
        <a href="{{ url_for('borrow.my_borrows', status=1) }}" class="tab {% if status == 1 %}active{% endif %}">
            <i class="fas fa-book-open"></i> 当前借阅 <span class="count">{{ current_borrows_count }}</span>
        </a>
        <a href="{{ url_for('borrow.my_borrows', status=0) }}" class="tab {% if status == 0 %}active{% endif %}">
            <i class="fas fa-history"></i> 历史借阅 <span class="count">{{ history_borrows_count }}</span>
        </a>
        <a href="{{ url_for('borrow.my_borrows') }}" class="tab {% if status is none %}active{% endif %}">
            <i class="fas fa-heart"></i> 全部借阅 <span class="count">{{ current_borrows_count + history_borrows_count }}</span>
        </a>
    </div>

    <div class="borrow-list">
        {% if pagination.items %}
            <table class="borrow-table">
                <thead>
                    <tr>
                        <th>图书封面</th>
                        <th>书名</th>
                        <th>借阅日期</th>
                        <th>应还日期</th>
                        <th>状态</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    {% for borrow in pagination.items %}
                    <tr class="borrow-item {% if borrow.status == 1 and borrow.due_date < now %}overdue{% endif %}">
                        <td class="book-cover">
                            {% if borrow.book.cover_url %}
                                {% if borrow.book.cover_url.startswith('/') %}
                                    <img src="{{ borrow.book.cover_url }}" alt="{{ borrow.book.title }}">
                                {% endif %}
                            {% else %}
                                <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="{{ borrow.book.title }}">
                            {% endif %}
                        </td>
                        <td class="book-title">
                            <a href="{{ url_for('book.book_detail', book_id=borrow.book_id) }}">{{ borrow.book.title }}</a>
                            <div class="book-author">{{ borrow.book.author }}</div>
                        </td>
                        <td>{{ borrow.borrow_date.strftime('%Y-%m-%d') }}</td>
                        <td class="due-date {% if borrow.status == 1 and borrow.due_date < now %}text-danger{% endif %}">
                            {{ borrow.due_date.strftime('%Y-%m-%d') }}
                            {% if borrow.status == 1 and borrow.due_date < now %}
                                <span class="badge badge-danger">已逾期</span>
                            {% endif %}
                        </td>
                        <td>
                            {% if borrow.status == 1 %}
                                <span class="badge badge-primary">借阅中</span>
                                {% if borrow.renew_count > 0 %}
                                    <span class="badge badge-info">已续借{{ borrow.renew_count }}次</span>
                                {% endif %}
                            {% else %}
                                <span class="badge badge-success">已归还</span>
                                <div class="return-date">{{ borrow.return_date.strftime('%Y-%m-%d') }}</div>
                            {% endif %}
                        </td>
                        <td class="actions">
                            {% if borrow.status == 1 %}
                                <button class="btn btn-sm btn-success return-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">归还</button>
                                {% if borrow.renew_count < 2 and borrow.due_date >= now %}
                                    <button class="btn btn-sm btn-primary renew-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">续借</button>
                                {% endif %}
                            {% endif %}
                        </td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>

            <!-- 分页 -->
            <div class="pagination-container">
                {% if pagination.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        {% if pagination.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('borrow.my_borrows', page=pagination.prev_num, status=status) }}" aria-label="Previous">
                                    <span aria-hidden="true"><i class="fas fa-chevron-left"></i></span>
                                </a>
                            </li>
                        {% endif %}

                        {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
                            {% if page_num %}
                                <li class="page-item {% if page_num == pagination.page %}active{% endif %}">
                                    <a class="page-link" href="{{ url_for('borrow.my_borrows', page=page_num, status=status) }}">{{ page_num }}</a>
                                </li>
                            {% else %}
                                <li class="page-item disabled"><span class="page-link">...</span></li>
                            {% endif %}
                        {% endfor %}

                        {% if pagination.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('borrow.my_borrows', page=pagination.next_num, status=status) }}" aria-label="Next">
                                    <span aria-hidden="true"><i class="fas fa-chevron-right"></i></span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            </div>
        {% else %}
            <div class="no-records">
                <i class="fas fa-book-reader empty-icon"></i>
                <p class="empty-text">
                    {% if status == 1 %}
                        哎呀～你还没有借阅任何图书呢！快去探索那些等待与你相遇的故事吧～
                    {% elif status == 0 %}
                        亲爱的，你还没有归还过任何图书呢～一起开启阅读的奇妙旅程吧！
                    {% else %}
                        你的书架空空如也～赶快挑选几本心动的书籍，开启你的阅读冒险吧！
                    {% endif %}
                </p>
                <a href="{{ url_for('book.book_list') }}" class="btn btn-primary">
                    <i class="fas fa-heart"></i> 探索好书
                </a>
            </div>
        {% endif %}
    </div>
</div>

<!-- 归还确认模态框 -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="returnModalLabel"><i class="fas fa-heart"></i> 归还确认</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                亲爱的读者，你确定要归还《<span id="returnBookTitle"></span>》这本书吗？希望它带给你美好的阅读体验～
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">再想想</button>
                <button type="button" class="btn btn-success" id="confirmReturn"><i class="fas fa-check"></i> 确认归还</button>
            </div>
        </div>
    </div>
</div>

<!-- 续借确认模态框 -->
<div class="modal fade" id="renewModal" tabindex="-1" role="dialog" aria-labelledby="renewModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="renewModalLabel"><i class="fas fa-magic"></i> 续借确认</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                亲爱的读者，想要与《<span id="renewBookTitle"></span>》多相处一段时间吗？续借后将延长14天的阅读时光哦～
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">再想想</button>
                <button type="button" class="btn btn-primary" id="confirmRenew"><i class="fas fa-check"></i> 确认续借</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/my_borrows.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/borrow/overdue.html
================================================================================

{% extends 'base.html' %}

{% block title %}逾期管理 - 图书管理系统{% endblock %}

{% block head %}
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/overdue.css') }}">
{% endblock %}

{% block content %}
<div class="container">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1 class="page-title">逾期管理</h1>
        <a href="{{ url_for('borrow.manage_borrows') }}" class="btn btn-outline-secondary">
            <i class="fas fa-arrow-left"></i> 返回借阅管理
        </a>
    </div>

    <div class="alert alert-warning">
        <i class="fas fa-exclamation-triangle"></i>
        当前共有 <strong>{{ overdue_count }}</strong> 条逾期未归还的借阅记录，请及时处理。
    </div>

    <div class="overdue-list">
        {% if pagination.items %}
            <table class="overdue-table">
                <thead>
                    <tr>
                        <th width="10%">图书封面</th>
                        <th width="20%">书名</th>
                        <th width="15%">借阅用户</th>
                        <th width="12%">借阅日期</th>
                        <th width="12%">应还日期</th>
                        <th width="15%">逾期天数</th>
                        <th width="16%">操作</th>
                    </tr>
                </thead>
                <tbody>
                    {% for borrow in pagination.items %}
                    <tr class="overdue-item">
                        <td class="book-cover">
                            <img src="{{ url_for('static', filename='covers/' + borrow.book.cover_url) if borrow.book.cover_url else url_for('static', filename='images/book-placeholder.jpg') }}" alt="{{ borrow.book.title }}">
                        </td>
                        <td class="book-title">
                            <a href="{{ url_for('book.book_detail', book_id=borrow.book_id) }}">{{ borrow.book.title }}</a>
                            <div class="book-author">{{ borrow.book.author }}</div>
                        </td>
                        <td class="user-info">
                            <a href="{{ url_for('user.user_edit', user_id=borrow.user_id) }}">{{ borrow.user.username }}</a>
                            {% if borrow.user.nickname %}
                                <div class="user-nickname">{{ borrow.user.nickname }}</div>
                            {% endif %}
                            <div class="user-contact">
                                {% if borrow.user.email %}
                                    <a href="mailto:{{ borrow.user.email }}" class="email-link">
                                        <i class="fas fa-envelope"></i>
                                    </a>
                                {% endif %}
                                {% if borrow.user.phone %}
                                    <a href="tel:{{ borrow.user.phone }}" class="phone-link">
                                        <i class="fas fa-phone"></i>
                                    </a>
                                {% endif %}
                            </div>
                        </td>
                        <td>{{ borrow.borrow_date.strftime('%Y-%m-%d') }}</td>
                        <td class="due-date text-danger">
                            {{ borrow.due_date.strftime('%Y-%m-%d') }}
                        </td>
                        <td class="overdue-days">
                            {% set days_overdue = ((now - borrow.due_date).days) %}
                            <span class="badge {% if days_overdue > 30 %}badge-danger{% elif days_overdue > 14 %}badge-warning{% else %}badge-info{% endif %}">
                                {{ days_overdue }} 天
                            </span>
                        </td>
                        <td class="actions">
                            <button class="btn btn-sm btn-success return-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">归还处理</button>
                            <button class="btn btn-sm btn-warning notify-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">发送通知</button>
                        </td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>

            <!-- 分页 -->
            <div class="pagination-container">
                {% if pagination.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        {% if pagination.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('borrow.overdue_borrows', page=pagination.prev_num) }}" aria-label="Previous">
                                    <span aria-hidden="true">&laquo;</span>
                                </a>
                            </li>
                        {% endif %}

                        {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
                            {% if page_num %}
                                <li class="page-item {% if page_num == pagination.page %}active{% endif %}">
                                    <a class="page-link" href="{{ url_for('borrow.overdue_borrows', page=page_num) }}">{{ page_num }}</a>
                                </li>
                            {% else %}
                                <li class="page-item disabled"><span class="page-link">...</span></li>
                            {% endif %}
                        {% endfor %}

                        {% if pagination.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('borrow.overdue_borrows', page=pagination.next_num) }}" aria-label="Next">
                                    <span aria-hidden="true">&raquo;</span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            </div>
        {% else %}
            <div class="no-records">
                <i class="fas fa-check-circle empty-icon"></i>
                <p class="empty-text">目前没有逾期的借阅记录，继续保持！</p>
                <a href="{{ url_for('borrow.manage_borrows') }}" class="btn btn-primary">返回借阅管理</a>
            </div>
        {% endif %}
    </div>
</div>

<!-- 归还确认模态框 -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="returnModalLabel">逾期归还处理</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p>您正在处理《<span id="returnBookTitle"></span>》的逾期归还：</p>
                <div class="form-group">
                    <label for="overdueRemark">备注信息（可选）</label>
                    <textarea class="form-control" id="overdueRemark" rows="3" placeholder="可以输入处理结果、是否收取逾期费用等信息..."></textarea>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-success" id="confirmReturn">确认归还</button>
            </div>
        </div>
    </div>
</div>

<!-- 通知确认模态框 -->
<div class="modal fade" id="notifyModal" tabindex="-1" role="dialog" aria-labelledby="notifyModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="notifyModalLabel">发送逾期通知</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                您确定要发送《<span id="notifyBookTitle"></span>》的逾期通知吗？
                此操作将向借阅用户发送逾期提醒消息。
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-warning" id="confirmNotify">发送通知</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/overdue.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/borrow/borrow_management.html
================================================================================

{% extends 'base.html' %}

{% block title %}借阅管理 - 图书管理系统{% endblock %}

{% block head %}
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/borrow_management.css') }}">
{% endblock %}

{% block content %}
<div class="container">
    <div class="decorative-corner"></div>
    <h1 class="page-title">借阅管理</h1>

    <div class="row">
        <div class="col-md-8">
            <div class="tabs">
                <a href="{{ url_for('borrow.manage_borrows', status=1, search=search, user_id=user_id, book_id=book_id) }}" class="tab {% if status == 1 %}active{% endif %}">
                    当前借阅 <span class="count">{{ current_borrows_count }}</span>
                </a>
                <a href="{{ url_for('borrow.manage_borrows', status=0, search=search, user_id=user_id, book_id=book_id) }}" class="tab {% if status == 0 %}active{% endif %}">
                    已归还 <span class="count">{{ history_borrows_count }}</span>
                </a>
                <a href="{{ url_for('borrow.manage_borrows', search=search, user_id=user_id, book_id=book_id) }}" class="tab {% if status is none %}active{% endif %}">
                    全部借阅 <span class="count">{{ current_borrows_count + history_borrows_count }}</span>
                </a>
                <a href="{{ url_for('borrow.overdue_borrows') }}" class="tab overdue-tab">
                    逾期管理 <span class="count overdue-count">{{ overdue_count }}</span>
                </a>
            </div>
        </div>
        <div class="col-md-4">
            <div class="float-right">
                <button class="btn btn-primary" data-toggle="modal" data-target="#addBorrowModal">
                    <i class="fas fa-plus"></i> 添加借阅
                </button>
            </div>
        </div>
    </div>

    <div class="card search-card">
        <div class="card-body">
            <form action="{{ url_for('borrow.manage_borrows') }}" method="GET" class="search-form">
                {% if status is not none %}<input type="hidden" name="status" value="{{ status }}">{% endif %}

                <div class="row">
                    <div class="col-md-6">
                        <div class="input-group">
                            <input type="text" class="form-control" name="search" value="{{ search }}" placeholder="搜索用户名或图书标题...">
                            <div class="input-group-append">
                                <button class="btn btn-outline-secondary" type="submit">
                                    <i class="fas fa-search"></i>
                                </button>
                            </div>
                        </div>
                    </div>
                    <div class="col-md-3">
                        <select class="form-control" name="user_id" onchange="this.form.submit()">
                            <option value="">选择用户</option>
                            {% for user in users %}
                                <option value="{{ user.id }}" {% if user_id == user.id %}selected{% endif %}>
                                    {{ user.username }}
                                </option>
                            {% endfor %}
                        </select>
                    </div>
                    {% if search or user_id or book_id %}
                    <div class="col-md-3">
                        <a href="{{ url_for('borrow.manage_borrows', status=status) }}" class="btn btn-outline-secondary clear-filters">
                            <i class="fas fa-times"></i> 清除筛选
                        </a>
                    </div>
                    {% endif %}
                </div>
            </form>
        </div>
    </div>

    <div class="borrow-list">
        {% if pagination.items %}
            <table class="borrow-table">
                <thead>
                    <tr>
                        <th width="10%">图书封面</th>
                        <th width="20%">书名</th>
                        <th width="15%">借阅用户</th>
                        <th width="12%">借阅日期</th>
                        <th width="12%">应还日期</th>
                        <th width="15%">状态</th>
                        <th width="16%">操作</th>
                    </tr>
                </thead>
                <tbody>
                    {% for borrow in pagination.items %}
                    <tr class="borrow-item {% if borrow.status == 1 and borrow.due_date < now %}overdue{% endif %}">
                        <td class="book-cover">
                            {% if borrow.book.cover_url %}
                                <img src="{{ borrow.book.cover_url }}" alt="{{ borrow.book.title }}">
                            {% else %}
                                <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="{{ borrow.book.title }}">
                            {% endif %}
                        </td>
                        <td class="book-title">
                            <a href="{{ url_for('book.book_detail', book_id=borrow.book_id) }}">{{ borrow.book.title }}</a>
                            <div class="book-author">{{ borrow.book.author }}</div>
                        </td>
                        <td class="user-info">
                            <a href="{{ url_for('user.user_edit', user_id=borrow.user_id) }}">{{ borrow.user.username }}</a>
                            {% if borrow.user.nickname %}
                                <div class="user-nickname">{{ borrow.user.nickname }}</div>
                            {% endif %}
                        </td>
                        <td>{{ borrow.borrow_date.strftime('%Y-%m-%d') }}</td>
                        <td class="due-date {% if borrow.status == 1 and borrow.due_date < now %}text-danger{% endif %}">
                            {{ borrow.due_date.strftime('%Y-%m-%d') }}
                            {% if borrow.status == 1 and borrow.due_date < now %}
                                <span class="badge badge-danger">已逾期</span>
                            {% endif %}
                        </td>
                        <td>
                            {% if borrow.status == 1 %}
                                <span class="badge badge-primary">借阅中</span>
                                {% if borrow.renew_count > 0 %}
                                    <span class="badge badge-info">已续借{{ borrow.renew_count }}次</span>
                                {% endif %}
                            {% else %}
                                <span class="badge badge-success">已归还</span>
                                <div class="return-date">{{ borrow.return_date.strftime('%Y-%m-%d') }}</div>
                            {% endif %}
                        </td>
                        <td class="actions">
                            {% if borrow.status == 1 %}
                                <button class="btn btn-sm btn-success return-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">归还</button>
                                {% if borrow.renew_count < 2 and borrow.due_date >= now %}
                                    <button class="btn btn-sm btn-primary renew-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">续借</button>
                                {% endif %}
                                {% if borrow.due_date < now %}
                                    <button class="btn btn-sm btn-warning notify-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">逾期通知</button>
                                {% endif %}
                            {% endif %}
                        </td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>

            <!-- 分页 -->
            <div class="pagination-container">
                {% if pagination.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        {% if pagination.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('borrow.manage_borrows', page=pagination.prev_num, status=status, search=search, user_id=user_id, book_id=book_id) }}" aria-label="Previous">
                                    <span aria-hidden="true">&laquo;</span>
                                </a>
                            </li>
                        {% endif %}

                        {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
                            {% if page_num %}
                                <li class="page-item {% if page_num == pagination.page %}active{% endif %}">
                                    <a class="page-link" href="{{ url_for('borrow.manage_borrows', page=page_num, status=status, search=search, user_id=user_id, book_id=book_id) }}">{{ page_num }}</a>
                                </li>
                            {% else %}
                                <li class="page-item disabled"><span class="page-link">...</span></li>
                            {% endif %}
                        {% endfor %}

                        {% if pagination.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('borrow.manage_borrows', page=pagination.next_num, status=status, search=search, user_id=user_id, book_id=book_id) }}" aria-label="Next">
                                    <span aria-hidden="true">&raquo;</span>
                                </a>
                            </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            </div>
        {% else %}
            <div class="no-records">
                <i class="fas fa-book-reader empty-icon"></i>
                <p class="empty-text">
                    {% if status == 1 %}
                        没有进行中的借阅记录。
                    {% elif status == 0 %}
                        没有已归还的借阅记录。
                    {% else %}
                        没有任何借阅记录。
                    {% endif %}
                </p>
            </div>
        {% endif %}
    </div>
</div>

<!-- 添加借阅模态框 -->
<div class="modal fade" id="addBorrowModal" tabindex="-1" role="dialog" aria-labelledby="addBorrowModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="addBorrowModalLabel">添加借阅记录</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <form action="{{ url_for('borrow.admin_add_borrow') }}" method="POST">
                <div class="modal-body">
                    <div class="form-group">
                        <label for="userSelect">借阅用户</label>
                        <select class="form-control" id="userSelect" name="user_id" required>
                            <option value="">请选择用户</option>
                            {% for user in users %}
                                <option value="{{ user.id }}">{{ user.username }}{% if user.nickname %} ({{ user.nickname }}){% endif %}</option>
                            {% endfor %}
                        </select>
                    </div>

                    <div class="form-group">
                        <label for="bookSelect">借阅图书</label>
                        <div class="input-group">
                            <input type="text" class="form-control" id="bookSearch" placeholder="搜索图书...">
                            <div class="input-group-append">
                                <button class="btn btn-outline-secondary" type="button" id="searchBookBtn">
                                    <i class="fas fa-search"></i>
                                </button>
                            </div>
                        </div>
                        <select class="form-control mt-2" id="bookSelect" name="book_id" required>
                            <option value="">请先搜索图书</option>
                        </select>
                    </div>

                    <div class="form-group">
                        <label for="borrowDays">借阅天数</label>
                        <input type="number" class="form-control" id="borrowDays" name="borrow_days" value="14" min="1" max="60">
                        <small class="form-text text-muted">默认借阅时间为14天</small>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    <button type="submit" class="btn btn-primary">添加借阅</button>
                </div>
            </form>
        </div>
    </div>
</div>

<!-- 归还确认模态框 -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="returnModalLabel">归还确认</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                您确定要归还《<span id="returnBookTitle"></span>》吗？
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-success" id="confirmReturn">确认归还</button>
            </div>
        </div>
    </div>
</div>

<!-- 续借确认模态框 -->
<div class="modal fade" id="renewModal" tabindex="-1" role="dialog" aria-labelledby="renewModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="renewModalLabel">续借确认</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                您确定要续借《<span id="renewBookTitle"></span>》吗？续借后将延长14天的借阅期限。
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary" id="confirmRenew">确认续借</button>
            </div>
        </div>
    </div>
</div>

<!-- 通知确认模态框 -->
<div class="modal fade" id="notifyModal" tabindex="-1" role="dialog" aria-labelledby="notifyModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="notifyModalLabel">发送逾期通知</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                您确定要发送《<span id="notifyBookTitle"></span>》的逾期通知吗？
                此操作将向借阅用户发送逾期提醒消息。
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-warning" id="confirmNotify">发送通知</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/borrow_management.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/inventory/adjust.html
================================================================================

{% extends 'base.html' %}

{% block title %}调整库存 - {{ book.title }}{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-adjust.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% endblock %}

{% block content %}
<div class="container mt-5">
    <div class="disney-inventory-card">
        <div class="disney-decoration top-left"></div>
        <div class="disney-decoration top-right"></div>
        <div class="disney-decoration bottom-left"></div>
        <div class="disney-decoration bottom-right"></div>

        <div class="card-header-disney">
            <div class="mickey-ears"></div>
            <h4>调整图书库存</h4>
        </div>

        <div class="card-body-disney">
            <div class="row mb-4">
                <div class="col-md-4 book-cover-container">
                    {% if book.cover_url %}
                    <img src="{{ book.cover_url }}" alt="{{ book.title }}" class="img-fluid book-cover">
                    {% else %}
                    <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="默认封面" class="img-fluid book-cover">
                    {% endif %}
                    <div class="disney-sparkles"></div>
                </div>
                <div class="col-md-8 book-details">
                    <h3 class="book-title">{{ book.title }}</h3>
                    <div class="book-info">
                        <p><span class="disney-icon author-icon"></span> <strong>作者:</strong> {{ book.author }}</p>
                        <p><span class="disney-icon publisher-icon"></span> <strong>出版社:</strong> {{ book.publisher }}</p>
                        <p><span class="disney-icon isbn-icon"></span> <strong>ISBN:</strong> {{ book.isbn }}</p>
                        <p><span class="disney-icon inventory-icon"></span> <strong>当前库存:</strong>
                            <span class="stock-badge {{ 'high-stock' if book.stock > 5 else 'low-stock' if book.stock > 0 else 'out-stock' }}">
                                {{ book.stock }}
                            </span>
                        </p>
                    </div>
                </div>
            </div>

            <div class="form-container">
                <form method="POST" action="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}">
                    <div class="mb-4 form-group">
                        <label for="change_type" class="form-label disney-label">
                            <span class="disney-icon type-icon"></span> 调整类型
                        </label>
                        <select class="form-select disney-select" id="change_type" name="change_type" required>
                            <option value="in">入库（增加库存）</option>
                            <option value="out">出库（减少库存）</option>
                        </select>
                    </div>

                    <div class="mb-4 form-group">
                        <label for="change_amount" class="form-label disney-label">
                            <span class="disney-icon amount-icon"></span> 调整数量
                        </label>
                        <input type="number" class="form-control disney-input" id="change_amount" name="change_amount" min="1" value="1" required>
                        <div class="form-text stock-hint" id="stock-hint">当前库存: {{ book.stock }}</div>
                    </div>

                    <div class="mb-4 form-group">
                        <label for="remark" class="form-label disney-label">
                            <span class="disney-icon remark-icon"></span> 备注
                        </label>
                        <textarea class="form-control disney-textarea" id="remark" name="remark" rows="3" placeholder="填写库存调整原因，如:新书入库、丢失、损坏等"></textarea>
                    </div>

                    <div class="button-group">
                        <a href="{{ url_for('inventory.inventory_list') }}" class="btn disney-cancel-btn">
                            取消
                        </a>
                        <button type="submit" class="btn disney-confirm-btn">
                            确认调整
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    // 将当前库存数量传递给JavaScript
    const CURRENT_STOCK = {{ book.stock }};
</script>
<script src="{{ url_for('static', filename='js/inventory-adjust.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/inventory/list.html
================================================================================

{% extends 'base.html' %}

{% block title %}图书库存管理{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-list.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% endblock %}

{% block content %}
<div class="inventory-container">
    <div class="page-header">
        <div class="header-content">
            <h1><i class="fas fa-book-open header-icon"></i>图书库存管理</h1>
            <p class="subtitle">优雅管理您的书籍资源</p>
        </div>
    </div>

    <div class="search-card">
        <form method="GET" action="{{ url_for('inventory.inventory_list') }}" class="search-form">
            <div class="search-input-group">
                <div class="search-input-container">
                    <i class="fas fa-search search-icon"></i>
                    <input type="text" class="search-input" name="search" placeholder="搜索书名、作者或ISBN" value="{{ search }}">
                </div>
                <button class="search-button" type="submit">搜索</button>
            </div>
            <a href="{{ url_for('inventory.inventory_logs') }}" class="log-button">
                <i class="fas fa-history"></i> 查看库存日志
            </a>
        </form>
    </div>

    <div class="table-container">
        <table class="inventory-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>书名</th>
                    <th>作者</th>
                    <th>ISBN</th>
                    <th>当前库存</th>
                    <th>状态</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                {% for book in books %}
                <tr>
                    <td>{{ book.id }}</td>
                    <td class="book-title">{{ book.title }}</td>
                    <td class="book-author">{{ book.author }}</td>
                    <td>{{ book.isbn }}</td>
                    <td>
                        <span class="stock-badge {{ 'stock-high' if book.stock > 5 else 'stock-medium' if book.stock > 0 else 'stock-low' }}">
                            {{ book.stock }}
                        </span>
                    </td>
                    <td>
                        <span class="status-badge {{ 'status-active' if book.status == 1 else 'status-inactive' }}">
                            {{ '正常' if book.status == 1 else '已下架' }}
                        </span>
                    </td>
                    <td class="action-buttons">
                        <a href="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}" class="btn-adjust">
                            <i class="fas fa-edit"></i> 调整
                        </a>
                        <a href="{{ url_for('inventory.book_inventory_logs', book_id=book.id) }}" class="btn-view">
                            <i class="fas fa-list-alt"></i> 日志
                        </a>
                    </td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>

    <!-- 分页 -->
    <div class="pagination-wrapper">
        <nav aria-label="Page navigation">
            <ul class="pagination">
                {% if pagination.has_prev %}
                <li class="page-item">
                    <a class="page-link" href="{{ url_for('inventory.inventory_list', page=pagination.prev_num, search=search, sort=sort, order=order) }}">
                        <i class="fas fa-chevron-left"></i> 上一页
                    </a>
                </li>
                {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="#">
                        <i class="fas fa-chevron-left"></i> 上一页
                    </a>
                </li>
                {% endif %}

                {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
                    {% if page_num %}
                        {% if page_num == pagination.page %}
                        <li class="page-item active">
                            <a class="page-link" href="#">{{ page_num }}</a>
                        </li>
                        {% else %}
                        <li class="page-item">
                            <a class="page-link" href="{{ url_for('inventory.inventory_list', page=page_num, search=search, sort=sort, order=order) }}">{{ page_num }}</a>
                        </li>
                        {% endif %}
                    {% else %}
                        <li class="page-item disabled">
                            <a class="page-link" href="#">...</a>
                        </li>
                    {% endif %}
                {% endfor %}

                {% if pagination.has_next %}
                <li class="page-item">
                    <a class="page-link" href="{{ url_for('inventory.inventory_list', page=pagination.next_num, search=search, sort=sort, order=order) }}">
                        下一页 <i class="fas fa-chevron-right"></i>
                    </a>
                </li>
                {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="#">
                        下一页 <i class="fas fa-chevron-right"></i>
                    </a>
                </li>
                {% endif %}
            </ul>
        </nav>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/inventory-list.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/inventory/book_logs.html
================================================================================

{% extends 'base.html' %}

{% block title %}《{{ book.title }}》库存日志{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-book-logs.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% endblock %}

{% block content %}
<div class="frozen-background">
    <div class="snowflakes" aria-hidden="true">
        <div class="snowflake">❅</div>
        <div class="snowflake">❆</div>
        <div class="snowflake">❅</div>
        <div class="snowflake">❆</div>
        <div class="snowflake">❅</div>
        <div class="snowflake">❆</div>
        <div class="snowflake">❅</div>
        <div class="snowflake">❆</div>
        <div class="snowflake">❅</div>
        <div class="snowflake">❆</div>
    </div>

    <div class="container mt-5">
        <div class="frozen-card">
            <div class="castle-decoration"></div>

            <div class="card-header-frozen">
                <div class="ice-crystal left"></div>
                <h4><i class="fas fa-book-open"></i> 《{{ book.title }}》库存变动日志</h4>
                <div class="ice-crystal right"></div>
            </div>

            <div class="card-body-frozen">
                <div class="row mb-4 book-info-row">
                    <div class="col-md-3 book-cover-container">
                        <div class="book-frame">
                            {% if book.cover_url %}
                            <img src="{{ book.cover_url }}" alt="{{ book.title }}" class="img-fluid book-cover">
                            {% else %}
                            <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="默认封面" class="img-fluid book-cover">
                            {% endif %}
                            <div class="book-glow"></div>
                        </div>
                    </div>
                    <div class="col-md-9 book-details">
                        <h3 class="book-title">{{ book.title }}</h3>
                        <div class="book-info">
                            <p class="info-item"><i class="fas fa-feather-alt"></i> <strong>作者:</strong> {{ book.author }}</p>
                            <p class="info-item"><i class="fas fa-building"></i> <strong>出版社:</strong> {{ book.publisher }}</p>
                            <p class="info-item"><i class="fas fa-barcode"></i> <strong>ISBN:</strong> {{ book.isbn }}</p>
                            <p class="info-item">
                                <i class="fas fa-cubes"></i> <strong>当前库存:</strong>
                                <span class="frozen-badge {{ 'high-stock' if book.stock > 5 else 'low-stock' if book.stock > 0 else 'out-stock' }}">
                                    {{ book.stock }}
                                </span>
                            </p>
                        </div>
                    </div>
                </div>

                <div class="history-section">
                    <h5 class="section-title">
                        <i class="fas fa-history"></i> 库存变动历史记录
                        <div class="magic-underline"></div>
                    </h5>

                    <div class="table-container">
                        <div class="table-frozen">
                            <div class="table-header-row">
                                <div class="th-frozen">ID</div>
                                <div class="th-frozen">操作类型</div>
                                <div class="th-frozen">变动数量</div>
                                <div class="th-frozen">变动后库存</div>
                                <div class="th-frozen">操作人</div>
                                <div class="th-frozen">备注</div>
                                <div class="th-frozen">操作时间</div>
                            </div>

                            <div class="table-body">
                                {% for log in logs %}
                                <div class="table-row log-entry" data-type="{{ log.change_type }}">
                                    <div class="td-frozen">{{ log.id }}</div>
                                    <div class="td-frozen">
                                        <span class="operation-badge {{ 'in-badge' if log.change_type == 'in' else 'out-badge' }}">
                                            {{ '入库' if log.change_type == 'in' else '出库' }}
                                            <i class="fas {{ 'fa-arrow-circle-down' if log.change_type == 'in' else 'fa-arrow-circle-up' }}"></i>
                                        </span>
                                    </div>
                                    <div class="td-frozen">{{ log.change_amount }}</div>
                                    <div class="td-frozen">{{ log.after_stock }}</div>
                                    <div class="td-frozen">{{ log.operator.username if log.operator else '系统' }}</div>
                                    <div class="td-frozen remark-cell">{{ log.remark or '-' }}</div>
                                    <div class="td-frozen">{{ log.changed_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
                                </div>
                                {% endfor %}

                                {% if not logs %}
                                <div class="table-row empty-log">
                                    <div class="td-frozen empty-message" colspan="7">
                                        <div class="olaf-empty">
                                            <div class="olaf-image"></div>
                                            <p>暂无库存变动记录，要不要堆个雪人？</p>
                                        </div>
                                    </div>
                                </div>
                                {% endif %}
                            </div>
                        </div>
                    </div>

                    <!-- 分页 -->
                    <div class="pagination-container">
                        <nav aria-label="Page navigation">
                            <ul class="frozen-pagination">
                                {% if pagination.has_prev %}
                                <li class="page-item">
                                    <a class="page-link" href="{{ url_for('inventory.book_inventory_logs', book_id=book.id, page=pagination.prev_num) }}">
                                        <i class="fas fa-chevron-left"></i> 上一页
                                    </a>
                                </li>
                                {% else %}
                                <li class="page-item disabled">
                                    <a class="page-link" href="#">
                                        <i class="fas fa-chevron-left"></i> 上一页
                                    </a>
                                </li>
                                {% endif %}

                                {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
                                    {% if page_num %}
                                        {% if page_num == pagination.page %}
                                        <li class="page-item active">
                                            <a class="page-link" href="#">{{ page_num }}</a>
                                        </li>
                                        {% else %}
                                        <li class="page-item">
                                            <a class="page-link" href="{{ url_for('inventory.book_inventory_logs', book_id=book.id, page=page_num) }}">{{ page_num }}</a>
                                        </li>
                                        {% endif %}
                                    {% else %}
                                        <li class="page-item disabled">
                                            <a class="page-link" href="#">...</a>
                                        </li>
                                    {% endif %}
                                {% endfor %}

                                {% if pagination.has_next %}
                                <li class="page-item">
                                    <a class="page-link" href="{{ url_for('inventory.book_inventory_logs', book_id=book.id, page=pagination.next_num) }}">
                                        下一页 <i class="fas fa-chevron-right"></i>
                                    </a>
                                </li>
                                {% else %}
                                <li class="page-item disabled">
                                    <a class="page-link" href="#">
                                        下一页 <i class="fas fa-chevron-right"></i>
                                    </a>
                                </li>
                                {% endif %}
                            </ul>
                        </nav>
                    </div>
                </div>
            </div>

            <div class="card-footer-frozen">
                <div class="footer-actions">
                    <a href="{{ url_for('inventory.inventory_list') }}" class="btn frozen-btn return-btn">
                        <i class="fas fa-arrow-left"></i> 返回库存管理
                    </a>
                    <a href="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}" class="btn frozen-btn adjust-btn">
                        <i class="fas fa-sliders-h"></i> 调整库存
                    </a>
                </div>
                <div class="footer-decoration"></div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/inventory-book-logs.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/inventory/logs.html
================================================================================

{% extends 'base.html' %}

{% block title %}《{{ book.title }}》库存日志{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-logs.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
{% endblock %}

{% block content %}
<div class="disney-container"
    <!-- 雪花/魔法效果层 -->
    <div id="magic-particles"></div>

    <!-- 主内容区 -->
    <div class="disney-card">
        <!-- 装饰元素 -->
        <div class="disney-decoration book-icon"></div>
        <div class="disney-decoration crown-icon"></div>
        <div class="disney-decoration wand-icon"></div>
        <div class="disney-decoration snowflake-icon"></div>

        <!-- 卡片头部 -->
        <div class="card-header-disney">
            <div class="princess-crown"></div>
            <h4><i class="fas fa-book-open"></i>
              {% if book %}
                《{{ book.title }}》库存变动日志
              {% else %}
                全部库存变动日志
              {% endif %}
            </h4>
        </div>

        <!-- 卡片内容 -->
        <div class="card-body-disney">
            <!-- 图书信息部分 -->
            {% if book %}
            <div class="book-details-container">
                <div class="book-cover-wrapper">
                    {% if book.cover_url %}
                    <img src="{{ book.cover_url }}" alt="{{ book.title }}" class="disney-book-cover">
                    {% else %}
                    <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="默认封面" class="disney-book-cover">
                    {% endif %}
                    <div class="book-cover-glow"></div>
                </div>

                <div class="book-info">
                    <h3 class="book-title">{{ book.title }}</h3>
                    <div class="info-row">
                        <div class="disney-icon author-icon"></div>
                        <div><strong>作者:</strong> {{ book.author }}</div>
                    </div>
                    <div class="info-row">
                        <div class="disney-icon publisher-icon"></div>
                        <div><strong>出版社:</strong> {{ book.publisher }}</div>
                    </div>
                    <div class="info-row">
                        <div class="disney-icon isbn-icon"></div>
                        <div><strong>ISBN:</strong> {{ book.isbn }}</div>
                    </div>
                    <div class="info-row">
                        <div class="disney-icon stock-icon"></div>
                        <div>
                            <strong>当前库存:</strong>
                            <span class="stock-badge {{ 'high-stock' if book.stock > 5 else 'low-stock' if book.stock > 0 else 'out-stock' }}">
                                {{ book.stock }}
                            </span>
                        </div>
                    </div>
                </div>
            </div>
            {% endif %}

            <!-- 日志表格部分 -->
            <div class="logs-section">
                <h5 class="logs-title">
                    <div class="title-decoration left"></div>
                    库存变动历史记录
                    <div class="title-decoration right"></div>
                </h5>

                <div class="table-container">
                    <table class="disney-table">
                        <thead>
                            <tr>
                                <th>ID</th>
                                {% if not book %}<th>图书</th>{% endif %}
                                <th>操作类型</th>
                                <th>变动数量</th>
                                <th>变动后库存</th>
                                <th>操作人</th>
                                <th>备注</th>
                                <th>操作时间</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for log in logs %}
                            <tr class="log-row">
                                <td>{{ log.id }}</td>
                                {% if not book %}
                                <td>
                                    <a href="{{ url_for('inventory.book_inventory_logs', book_id=log.book_id) }}">
                                        {{ log.book.title if log.book else '未知图书' }}
                                    </a>
                                </td>
                                {% endif %}
                                <td>
                                    <span class="operation-badge {{ 'in-badge' if log.change_type == 'in' or log.change_amount > 0 else 'out-badge' }}">
                                        {{ '入库' if log.change_type == 'in' or log.change_amount > 0 else '出库' }}
                                    </span>
                                </td>
                                <td>{{ log.change_amount }}</td>
                                <td>{{ log.after_stock }}</td>
                                <td>{{ log.operator.username if log.operator else '系统' }}</td>
                                <td class="remark-cell" title="{{ log.remark or '-' }}">{{ log.remark or '-' }}</td>
                                <td>{{ log.changed_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
                            </tr>
                            {% endfor %}

                            {% if not logs %}
                            <tr>
                                <td colspan="{{ 8 if not book else 7 }}" class="empty-logs">
                                    <div class="empty-state">
                                        <div class="empty-icon"></div>
                                        <p>暂无库存变动记录</p>
                                    </div>
                                </td>
                            </tr>
                            {% endif %}
                        </tbody>
                    </table>
                </div>

                <!-- 分页 -->
                <div class="disney-pagination">
                    <nav aria-label="Page navigation">
                        <ul class="pagination-list">
                            {% if pagination.has_prev %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('inventory.inventory_logs' if not book else 'inventory.book_inventory_logs', book_id=book.id if book else None, page=pagination.prev_num) }}">
                                    <i class="fas fa-chevron-left"></i> 上一页
                                </a>
                            </li>
                            {% else %}
                            <li class="page-item disabled">
                                <span class="page-link"><i class="fas fa-chevron-left"></i> 上一页</span>
                            </li>
                            {% endif %}

                            {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
                                {% if page_num %}
                                    {% if page_num == pagination.page %}
                                    <li class="page-item active">
                                        <span class="page-link">{{ page_num }}</span>
                                    </li>
                                    {% else %}
                                    <li class="page-item">
                                        <a class="page-link" href="{{ url_for('inventory.inventory_logs' if not book else 'inventory.book_inventory_logs', book_id=book.id if book else None, page=page_num) }}">{{ page_num }}</a>
                                    </li>
                                    {% endif %}
                                {% else %}
                                    <li class="page-item dots">
                                        <span class="page-link">...</span>
                                    </li>
                                {% endif %}
                            {% endfor %}

                            {% if pagination.has_next %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('inventory.inventory_logs' if not book else 'inventory.book_inventory_logs', book_id=book.id if book else None, page=pagination.next_num) }}">
                                    下一页 <i class="fas fa-chevron-right"></i>
                                </a>
                            </li>
                            {% else %}
                            <li class="page-item disabled">
                                <span class="page-link">下一页 <i class="fas fa-chevron-right"></i></span>
                            </li>
                            {% endif %}
                        </ul>
                    </nav>
                </div>
            </div>
        </div>

        <!-- 卡片底部 -->
        <div class="card-footer-disney">
            <div class="button-container">
                <a href="{{ url_for('inventory.inventory_list') }}" class="disney-button return-btn">
                    <span class="button-icon"><i class="fas fa-arrow-left"></i></span>
                    <span class="button-text">返回库存管理</span>
                </a>
                {% if book %}
                <a href="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}" class="disney-button adjust-btn">
                    <span class="button-icon"><i class="fas fa-edit"></i></span>
                    <span class="button-text">调整库存</span>
                </a>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js"></script>
<script src="{{ url_for('static', filename='js/inventory-book-logs.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/book/browse.html
================================================================================

{% extends 'base.html' %}

{% block title %}图书浏览 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/browse.css') }}">
{% endblock %}

{% block content %}
<div class="browse-container">
    <!-- 装饰气泡 -->
    {% for i in range(15) %}
    <div class="bubble"></div>
    {% endfor %}

    <div class="page-header">
        <h1>图书浏览</h1>
        <p class="welcome-text">欢迎 <strong>{{ current_user.nickname or current_user.username }}</strong>，今天想读点什么？</p>
    </div>

    <div class="filter-section">
        <form method="GET" action="{{ url_for('book.browse_books') }}" class="search-form">
            <div class="search-row">
                <div class="form-group search-group">
                    <input type="text" name="search" class="form-control" placeholder="搜索书名/作者/ISBN" value="{{ search }}">
                    <button type="submit" class="btn btn-primary">
                        <i class="fas fa-search"></i>
                    </button>
                </div>
            </div>
            <div class="filter-row">
                <div class="category-filters">
                    <button type="button" class="category-filter-toggle">
                        <i class="fas fa-tags"></i> 分类筛选
                        <i class="fas fa-chevron-down"></i>
                    </button>
                    <div class="category-filter-dropdown">
                        <a href="{{ url_for('book.browse_books', search=search, sort=sort, order=order) }}"
                           class="category-item {% if not category_id %}active{% endif %}">
                            <i class="fas fa-globe"></i> 全部
                        </a>
                        {% for category in categories %}
                        <a href="{{ url_for('book.browse_books', search=search, category_id=category.id, sort=sort, order=order) }}"
                           class="category-item {% if category_id == category.id %}active{% endif %}">
                            <i class="fas fa-tag"></i> {{ category.name }}
                        </a>
                        {% endfor %}
                    </div>
                </div>
                <div class="form-group filter-group">
                    <select name="sort" class="form-control" onchange="this.form.submit()">
                        <option value="id" {% if sort == 'id' %}selected{% endif %}>默认排序</option>
                        <option value="created_at" {% if sort == 'created_at' %}selected{% endif %}>入库时间</option>
                        <option value="title" {% if sort == 'title' %}selected{% endif %}>书名</option>
                        <option value="stock" {% if sort == 'stock' %}selected{% endif %}>库存</option>
                    </select>
                </div>
                <div class="form-group filter-group">
                    <select name="order" class="form-control" onchange="this.form.submit()">
                        <option value="desc" {% if order == 'desc' %}selected{% endif %}>降序</option>
                        <option value="asc" {% if order == 'asc' %}selected{% endif %}>升序</option>
                    </select>
                </div>
            </div>
        </form>
    </div>

    <!-- 图书数据统计 -->
    <div class="browse-stats">
        <div class="stat-item">
            <i class="fas fa-book"></i>
            <div class="stat-content">
                <span class="stat-value">{{ pagination.total }}</span>
                <span class="stat-label">可借图书</span>
            </div>
        </div>

        <div class="stat-item">
            <i class="fas fa-tag"></i>
            <div class="stat-content">
                <span class="stat-value">{{ categories|length }}</span>
                <span class="stat-label">图书分类</span>
            </div>
        </div>

        {% if search %}
        <div class="search-results">
            搜索 "{{ search }}" 找到 {{ pagination.total }} 本图书
        </div>
        {% endif %}
    </div>

    <div class="books-grid">
        {% for book in books %}
        <div class="book-card" data-id="{{ book.id }}">
            <div class="book-cover">
                {% if book.cover_url %}
                <img src="{{ book.cover_url }}" alt="{{ book.title }}">
                {% else %}
                <div class="no-cover">
                    <i class="fas fa-book"></i>
                    <span>无封面</span>
                </div>
                {% endif %}
                <div class="cover-overlay"></div>
                <div class="book-ribbon">
                    {% if book.stock > 0 %}
                    <span class="available">可借阅</span>
                    {% else %}
                    <span class="unavailable">无库存</span>
                    {% endif %}
                </div>
            </div>
            <div class="book-info">
                <h3 class="book-title" title="{{ book.title }}">{{ book.title }}</h3>
                <div class="book-author">{{ book.author }}</div>
                <div class="book-meta">
                    {% if book.category %}
                    <span class="book-category">{{ book.category.name }}</span>
                    {% endif %}
                    <span class="book-year">{{ book.publish_year or '未知年份' }}</span>
                </div>

                <div class="book-actions">
                    <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn-detail">
                        <i class="fas fa-info-circle"></i> 详情
                    </a>
                    {% if book.stock > 0 %}
                    <a href="#" class="btn-borrow borrow-book" data-id="{{ book.id }}">
                        <i class="fas fa-hand-holding"></i> 借阅
                    </a>
                    {% else %}
                    <button class="btn-borrow disabled" disabled>
                        <i class="fas fa-hand-holding"></i> 暂无库存
                    </button>
                    {% endif %}
                </div>
            </div>
        </div>
        {% else %}
        <div class="no-books">
            <img src="{{ url_for('static', filename='images/no-books.svg') }}" alt="没有找到书籍" class="no-books-img">
            <h3>没有找到符合条件的图书</h3>
            <p>尝试调整搜索条件或浏览其他分类</p>
            <a href="{{ url_for('book.browse_books') }}" class="btn-reset-search">
                <i class="fas fa-sync"></i> 重置搜索
            </a>
        </div>
        {% endfor %}
    </div>

    <!-- 分页 -->
    {% if pagination.pages > 1 %}
    <div class="pagination-container">
        <ul class="pagination">
            {% if pagination.has_prev %}
            <li class="page-item">
                <a class="page-link" href="{{ url_for('book.browse_books', page=pagination.prev_num, search=search, category_id=category_id, sort=sort, order=order) }}">
                    <i class="fas fa-chevron-left"></i>
                </a>
            </li>
            {% 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 %}
                    <li class="page-item active">
                        <span class="page-link">{{ p }}</span>
                    </li>
                    {% else %}
                    <li class="page-item">
                        <a class="page-link" href="{{ url_for('book.browse_books', page=p, search=search, category_id=category_id, sort=sort, order=order) }}">{{ p }}</a>
                    </li>
                    {% endif %}
                {% else %}
                <li class="page-item disabled">
                    <span class="page-link">...</span>
                </li>
                {% endif %}
            {% endfor %}

            {% if pagination.has_next %}
            <li class="page-item">
                <a class="page-link" href="{{ url_for('book.browse_books', page=pagination.next_num, search=search, category_id=category_id, sort=sort, order=order) }}">
                    <i class="fas fa-chevron-right"></i>
                </a>
            </li>
            {% endif %}
        </ul>
        <div class="pagination-info">
            显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }}
            到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
        </div>
    </div>
    {% endif %}
</div>

<!-- 借阅确认模态框 -->
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="borrowModalLabel">确认借阅</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <p>确定要借阅《<span id="borrowBookTitle"></span>》吗？</p>
        <p class="modal-info">借阅期限为30天，请在到期前归还。</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
        <button type="button" class="btn btn-primary" id="confirmBorrow">确认借阅</button>
      </div>
    </div>
  </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/browse.js') }}"></script>
{{ super() }}
{% endblock %}

================================================================================
File: ./app/templates/book/list.html
================================================================================

{% extends 'base.html' %}

{% block title %}图书列表 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book.css') }}">
{% endblock %}

{% block content %}
<div class="book-list-container">

    <div class="page-header">
        <h1>图书管理</h1>

        {% if current_user.role_id == 1 %}
        <div class="action-buttons">
            <a href="{{ url_for('book.add_book') }}" class="btn btn-primary">
                <i class="fas fa-plus"></i> 添加图书
            </a>
            <a href="{{ url_for('book.import_books') }}" class="btn btn-success">
                <i class="fas fa-file-upload"></i> 批量导入
            </a>
            <a href="{{ url_for('book.export_books') }}" class="btn btn-info">
                <i class="fas fa-file-download"></i> 导出图书
            </a>
            <a href="{{ url_for('book.category_list') }}" class="btn btn-secondary">
                <i class="fas fa-tags"></i> 分类管理
            </a>
        </div>
        {% endif %}
    </div>

    <div class="filter-section">
        <form method="GET" action="{{ url_for('book.book_list') }}" class="search-form">
            <div class="search-row">
                <div class="form-group search-group">
                    <input type="text" name="search" class="form-control" placeholder="搜索书名/作者/ISBN" value="{{ search }}">
                    <button type="submit" class="btn btn-primary">
                        <i class="fas fa-search"></i>
                    </button>
                </div>
            </div>
            <div class="filter-row">
                <div class="form-group filter-group">
                    <select name="category_id" class="form-control" onchange="this.form.submit()">
                        <option value="">全部分类</option>
                        {% for category in categories %}
                        <option value="{{ category.id }}" {% if category_id == category.id %}selected{% endif %}>
                            {{ category.name }}
                        </option>
                        {% endfor %}
                    </select>
                </div>
                <div class="form-group filter-group">
                    <select name="sort" class="form-control" onchange="this.form.submit()">
                        <option value="id" {% if sort == 'id' %}selected{% endif %}>默认排序</option>
                        <option value="created_at" {% if sort == 'created_at' %}selected{% endif %}>入库时间</option>
                        <option value="title" {% if sort == 'title' %}selected{% endif %}>书名</option>
                        <option value="stock" {% if sort == 'stock' %}selected{% endif %}>库存</option>
                    </select>
                </div>
                <div class="form-group filter-group">
                    <select name="order" class="form-control" onchange="this.form.submit()">
                        <option value="desc" {% if order == 'desc' %}selected{% endif %}>降序</option>
                        <option value="asc" {% if order == 'asc' %}selected{% endif %}>升序</option>
                    </select>
                </div>
            </div>
        </form>
    </div>

    <div class="books-grid">
        {% for book in books %}
        <!-- 为每个book-card添加data-id属性 -->
        <div class="book-card" data-id="{{ book.id }}">
            <div class="book-cover">
                {% if book.cover_url %}
                <img src="{{ book.cover_url }}" alt="{{ book.title }}">
                {% else %}
                <div class="no-cover">
                    <i class="fas fa-book"></i>
                    <span>无封面</span>
                </div>
                {% endif %}
                <!-- 添加书名覆盖层 -->
                <div class="cover-title-bar">{{ book.title }}</div>
            </div>
            <div class="book-info">
                <h3 class="book-title">{{ book.title }}</h3>
                <p class="book-author">{{ book.author }}</p>

                <div class="book-meta">
                    {% if book.category %}
                    <span class="book-category">{{ book.category.name }}</span>
                    {% endif %}
                    <span class="book-status {{ 'available' if book.stock > 0 else 'unavailable' }}">
                        {{ '可借阅' if book.stock > 0 else '无库存' }}
                    </span>
                </div>

                <div class="book-details">
                    <p><strong>ISBN:</strong> <span>{{ book.isbn or '无' }}</span></p>
                    <p><strong>出版社:</strong> <span>{{ book.publisher or '无' }}</span></p>
                    <p><strong>库存:</strong> <span>{{ book.stock }}</span></p>
                </div>

                <div class="book-actions">
                    <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info btn-sm">
                        <i class="fas fa-info-circle"></i> 详情
                    </a>
                    {% if current_user.role_id == 1 %}
                    <a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary btn-sm">
                        <i class="fas fa-edit"></i> 编辑
                    </a>
                    <button class="btn btn-danger btn-sm delete-book" data-id="{{ book.id }}" data-title="{{ book.title }}">
                        <i class="fas fa-trash"></i> 删除
                    </button>
                    {% endif %}
                    {% if book.stock > 0 %}
                    <a href="#" class="btn btn-success btn-sm borrow-book" data-id="{{ book.id }}">
                        <i class="fas fa-hand-holding"></i> 借阅
                    </a>
                    {% endif %}
                </div>
            </div>
        </div>
        {% else %}
        <div class="no-books">
            <i class="fas fa-exclamation-circle"></i>
            <p>没有找到符合条件的图书</p>
        </div>
        {% endfor %}
    </div>

    <!-- 分页 -->
    {% if pagination.pages > 1 %}
    <div class="pagination-container">
        <ul class="pagination">
            {% if pagination.has_prev %}
            <li class="page-item">
                <a class="page-link" href="{{ url_for('book.book_list', page=pagination.prev_num, search=search, category_id=category_id, sort=sort, order=order) }}">
                    <i class="fas fa-chevron-left"></i>
                </a>
            </li>
            {% 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 %}
                    <li class="page-item active">
                        <span class="page-link">{{ p }}</span>
                    </li>
                    {% else %}
                    <li class="page-item">
                        <a class="page-link" href="{{ url_for('book.book_list', page=p, search=search, category_id=category_id, sort=sort, order=order) }}">{{ p }}</a>
                    </li>
                    {% endif %}
                {% else %}
                <li class="page-item disabled">
                    <span class="page-link">...</span>
                </li>
                {% endif %}
            {% endfor %}

            {% if pagination.has_next %}
            <li class="page-item">
                <a class="page-link" href="{{ url_for('book.book_list', page=pagination.next_num, search=search, category_id=category_id, sort=sort, order=order) }}">
                    <i class="fas fa-chevron-right"></i>
                </a>
            </li>
            {% endif %}
        </ul>
        <div class="pagination-info">
            显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }}
            到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
        </div>
    </div>
    {% endif %}
</div>

<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        确定要删除《<span id="deleteBookTitle"></span>》吗？此操作不可恢复。
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
        <button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
      </div>
    </div>
  </div>
</div>

{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/book-list.js') }}"></script>
{{ super() }}
{% endblock %}


================================================================================
File: ./app/templates/book/add.html
================================================================================

{% extends 'base.html' %}

{% block title %}添加图书 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-form.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css">
{% endblock %}

{% block content %}
<div class="book-form-container animate__animated animate__fadeIn">
    <!-- 顶部导航和标题区域 -->
    <div class="page-header-wrapper">
        <div class="page-header">
            <div class="header-title-section">
                <h1 class="page-title">添加新图书</h1>
                <p class="subtitle">创建新书籍记录并添加到系统库存</p>
            </div>
            <div class="header-actions">
                <a href="{{ url_for('book.book_list') }}" class="btn-back">
                    <i class="fas fa-arrow-left"></i>
                    <span>返回列表</span>
                </a>
                <div class="form-progress">
                    <div class="progress-bar-container">
                        <div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" id="formProgress"></div>
                    </div>
                    <span class="progress-text" id="progressText">完成 0%</span>
                </div>
            </div>
        </div>
    </div>

    <!-- 主表单区域 -->
    <form method="POST" enctype="multipart/form-data" class="book-form" id="bookForm">
        <div class="form-grid">
            <!-- 左侧表单区域 -->
            <div class="form-main-content">
                <!-- 基本信息卡片 -->
                <div class="form-card">
                    <div class="card-header">
                        <span class="card-title">基本信息</span>
                    </div>
                    <div class="card-body">
                        <div class="form-section">
                            <div class="form-group">
                                <label for="title" class="form-label">书名 <span class="required">*</span></label>
                                <input type="text" class="form-control" id="title" name="title" required
                                       placeholder="请输入完整图书名称" value="{{ book.title if book else '' }}">
                                <div class="form-help">完整准确的书名有助于读者查找</div>
                            </div>

                            <div class="form-row">
                                <div class="form-group">
                                    <label for="author" class="form-label">作者 <span class="required">*</span></label>
                                    <input type="text" class="form-control" id="author" name="author" required
                                           placeholder="请输入作者姓名" value="{{ book.author if book else '' }}">
                                </div>
                                <div class="form-group">
                                    <label for="publisher" class="form-label">出版社</label>
                                    <input type="text" class="form-control" id="publisher" name="publisher"
                                           placeholder="请输入出版社名称" value="{{ book.publisher if book else '' }}">
                                </div>
                            </div>

                            <div class="form-row">
                                <div class="form-group">
                                    <label for="isbn" class="form-label">ISBN</label>
                                    <div class="input-with-button">
                                        <input type="text" class="form-control" id="isbn" name="isbn"
                                               placeholder="例如: 978-7-XXXXX-XXX-X" value="{{ book.isbn if book else '' }}">
                                        <button type="button" class="btn-append" id="isbnLookup">
                                            <i class="fas fa-search"></i>
                                        </button>
                                    </div>
                                    <div class="form-help">输入ISBN并点击查询按钮自动填充图书信息</div>
                                </div>
                                <div class="form-group">
                                    <label for="publish_year" class="form-label">出版年份</label>
                                    <input type="text" class="form-control" id="publish_year" name="publish_year"
                                           placeholder="例如: 2023" value="{{ book.publish_year if book else '' }}">
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- 分类和标签卡片 -->
                <div class="form-card">
                    <div class="card-header">
                        <span class="card-title">分类与标签</span>
                    </div>
                    <div class="card-body">
                        <div class="form-section">
                            <div class="form-row">
                                <div class="form-group">
                                    <label for="category_id" class="form-label">图书分类</label>
                                    <select class="form-control select2" id="category_id" name="category_id">
                                        <option value="">选择分类...</option>
                                        {% for category in categories %}
                                        <option value="{{ category.id }}" {% if book and book.category_id|string == category.id|string %}selected{% endif %}>
                                            {{ category.name }}
                                        </option>
                                        {% endfor %}
                                    </select>
                                    <div class="form-help">为图书选择合适的分类以便于管理和查找</div>
                                </div>
                                <div class="form-group">
                                    <label for="tagInput" class="form-label">标签</label>
                                    <div class="tag-input-wrapper">
                                        <input type="text" class="form-control" id="tagInput" placeholder="输入标签后按回车添加">
                                        <input type="hidden" id="tags" name="tags" value="{{ book.tags if book else '' }}">
                                        <button type="button" class="btn-tag-add" id="addTagBtn">
                                            <i class="fas fa-plus"></i>
                                        </button>
                                    </div>
                                    <div class="tags-container" id="tagsContainer"></div>
                                    <div class="form-help">添加多个标签以提高图书的检索率</div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- 图书简介卡片 -->
                <div class="form-card">
                    <div class="card-header">
                        <span class="card-title">图书简介</span>
                    </div>
                    <div class="card-body">
                        <div class="form-section">
                            <div class="form-group">
                                <label for="description" class="form-label">内容简介</label>
                                <textarea class="form-control" id="description" name="description" rows="8"
                                          placeholder="请输入图书的简要介绍...">{{ book.description if book else '' }}</textarea>
                                <div class="form-footer">
                                    <div class="form-help">简要描述图书的内容、特点和主要观点</div>
                                    <div class="char-counter"><span id="charCount">0</span>/2000</div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- 右侧表单区域 -->
            <div class="form-sidebar">
                <!-- 封面图片卡片 -->
                <div class="form-card">
                    <div class="card-header">
                        <span class="card-title">封面图片</span>
                    </div>
                    <div class="card-body">
                        <div class="cover-preview-container">
                            <div class="cover-preview" id="coverPreview">
                                <div class="no-cover-placeholder">
                                    <i class="fas fa-book-open"></i>
                                    <span>暂无封面</span>
                                    <p class="placeholder-tip">点击上传或拖放图片至此处</p>
                                </div>
                            </div>
                            <div class="upload-options">
                                <div class="upload-btn-group">
                                    <label for="cover" class="btn-upload">
                                        <i class="fas fa-upload"></i> 上传图片
                                    </label>
                                    <button type="button" class="btn-remove" id="removeCover">
                                        <i class="fas fa-trash-alt"></i>
                                    </button>
                                </div>
                                <input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
                                <div class="upload-tips">
                                    <div>推荐尺寸: 500×700px (竖版封面)</div>
                                    <div>支持格式: JPG, PNG, WebP</div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- 库存和价格卡片 -->
                <div class="form-card">
                    <div class="card-header">
                        <span class="card-title">库存和价格</span>
                    </div>
                    <div class="card-body">
                        <div class="form-section">
                            <div class="form-group">
                                <label for="stock" class="form-label">库存数量</label>
                                <div class="number-control">
                                    <button type="button" class="number-btn decrement" id="stockDecrement">－</button>
                                    <input type="number" class="form-control text-center" id="stock" name="stock" min="0"
                                           value="{{ book.stock if book else 0 }}">
                                    <button type="button" class="number-btn increment" id="stockIncrement">＋</button>
                                </div>
                            </div>
                            <div class="form-group">
                                <label for="price" class="form-label">价格</label>
                                <div class="price-input">
                                    <span class="currency-symbol">¥</span>
                                    <input type="number" class="form-control" id="price" name="price" step="0.01" min="0"
                                           placeholder="0.00" value="{{ book.price if book else '' }}">
                                </div>
                                <div class="price-slider">
                                    <input type="range" class="range-slider" id="priceRange" min="0" max="500" step="0.5"
                                           value="{{ book.price if book and book.price else 0 }}">
                                    <div class="slider-marks">
                                        <span>¥0</span>
                                        <span>¥250</span>
                                        <span>¥500</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- 提交按钮区域 -->
                <div class="form-actions">
                    <button type="submit" class="btn-primary">
                        <i class="fas fa-save"></i> 保存图书
                    </button>
                    <div class="secondary-actions">
                        <button type="button" class="btn-secondary" id="previewBtn">
                            <i class="fas fa-eye"></i> 预览
                        </button>
                        <button type="reset" class="btn-secondary" id="resetBtn">
                            <i class="fas fa-undo"></i> 重置
                        </button>
                    </div>
                    <div class="form-tip">
                        <i class="fas fa-info-circle"></i>
                        <span>带 <span class="required">*</span> 的字段为必填项</span>
                    </div>
                </div>
            </div>
        </div>
    </form>
</div>

<!-- 图片裁剪模态框 -->
<div class="modal fade" id="cropperModal" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">调整封面图片</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div class="img-container">
                    <img id="cropperImage" src="" alt="图片预览">
                </div>
                <div class="cropper-controls">
                    <div class="control-group">
                        <button type="button" class="control-btn" id="rotateLeft" title="向左旋转">
                            <i class="fas fa-undo"></i>
                        </button>
                        <button type="button" class="control-btn" id="rotateRight" title="向右旋转">
                            <i class="fas fa-redo"></i>
                        </button>
                    </div>
                    <div class="control-group">
                        <button type="button" class="control-btn" id="zoomOut" title="缩小">
                            <i class="fas fa-search-minus"></i>
                        </button>
                        <button type="button" class="control-btn" id="zoomIn" title="放大">
                            <i class="fas fa-search-plus"></i>
                        </button>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn-secondary modal-btn" data-dismiss="modal">取消</button>
                <button type="button" class="btn-primary modal-btn" id="cropImage">应用裁剪</button>
            </div>
        </div>
    </div>
</div>

<!-- 图书预览模态框 -->
<div class="modal fade" id="previewModal" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header preview-header">
                <h5 class="modal-title">图书预览</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body preview-body">
                <div class="book-preview">
                    <div class="preview-cover-section">
                        <div class="book-preview-cover" id="previewCover">
                            <div class="no-cover-placeholder">
                                <i class="fas fa-book-open"></i>
                                <span>暂无封面</span>
                            </div>
                        </div>
                        <div class="book-meta">
                            <div class="book-price" id="previewPrice">¥0.00</div>
                            <div class="book-stock" id="previewStock">库存: 0</div>
                        </div>
                    </div>
                    <div class="preview-details-section">
                        <h2 class="book-title" id="previewTitle">书名加载中...</h2>
                        <div class="book-author" id="previewAuthor">作者加载中...</div>

                        <div class="book-info-grid">
                            <div class="info-item">
                                <div class="info-label">出版社</div>
                                <div class="info-value" id="previewPublisher">-</div>
                            </div>
                            <div class="info-item">
                                <div class="info-label">ISBN</div>
                                <div class="info-value" id="previewISBN">-</div>
                            </div>
                            <div class="info-item">
                                <div class="info-label">出版年份</div>
                                <div class="info-value" id="previewYear">-</div>
                            </div>
                            <div class="info-item">
                                <div class="info-label">分类</div>
                                <div class="info-value" id="previewCategory">-</div>
                            </div>
                        </div>

                        <div class="book-tags-preview" id="previewTags"></div>

                        <div class="book-description-preview">
                            <h3 class="section-title">图书简介</h3>
                            <div class="description-content" id="previewDescription">
                                <p class="placeholder-text">暂无简介内容</p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="modal-footer preview-footer">
                <button type="button" class="btn-secondary modal-btn" data-dismiss="modal">关闭</button>
                <button type="button" class="btn-primary modal-btn" data-dismiss="modal" id="continueEditBtn">继续编辑</button>
            </div>
        </div>
    </div>
</div>

<!-- 通知容器 -->
<div class="notification-container"></div>
{% endblock %}

{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
<script src="{{ url_for('static', filename='js/book-add.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/book/edit.html
================================================================================

{% extends 'base.html' %}

{% block title %}编辑图书 - {{ book.title }}{% endblock %}

{% block head %}
  <!-- 使用我们为成熟御姐风新建的 CSS -->
  <link rel="stylesheet" href="{{ url_for('static', filename='css/book-edit.css') }}">
  <!-- 字体 -->
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="book-form-container">
  <!-- 显示Flash消息 -->
  {% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
      {% for category, message in messages %}
        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
          {{ message }}
          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
      {% endfor %}
    {% endif %}
  {% endwith %}

  <div class="page-header">
    <h1>编辑图书</h1>
    <div class="actions">
      <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-secondary">
        <i class="fas fa-eye"></i> 查看详情
      </a>
      <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
        <i class="fas fa-arrow-left"></i> 返回列表
      </a>
    </div>
  </div>

  <form method="POST" enctype="multipart/form-data" class="book-form">
    <div class="form-row">
      <div class="col-md-8">
        <div class="card">
          <div class="card-header">基本信息</div>
          <div class="card-body">

            <div class="form-group">
              <label for="title">书名 <span class="required">*</span></label>
              <input type="text" class="form-control" id="title" name="title" value="{{ book.title }}" required>
            </div>

            <div class="form-row">
              <div class="form-group col-md-6">
                <label for="author">作者 <span class="required">*</span></label>
                <input type="text" class="form-control" id="author" name="author" value="{{ book.author }}" required>
              </div>
              <div class="form-group col-md-6">
                <label for="publisher">出版社</label>
                <input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher or '' }}">
              </div>
            </div>

            <div class="form-row">
              <div class="form-group col-md-6">
                <label for="isbn">ISBN</label>
                <input type="text" class="form-control {% if isbn_error %}is-invalid{% endif %}"
                       id="isbn" name="isbn" value="{{ book.isbn or '' }}">
                {% if isbn_error %}
                <div class="invalid-feedback">
                  {{ isbn_error }}
                </div>
                {% endif %}
                <small class="form-text text-muted">ISBN必须是有效的10位或13位格式</small>
              </div>
              <div class="form-group col-md-6">
                <label for="publish_year">出版年份</label>
                <input type="text" class="form-control" id="publish_year" name="publish_year" value="{{ book.publish_year or '' }}">
              </div>
            </div>

            <div class="form-row">
              <div class="form-group col-md-6">
                <label for="category_id">分类</label>
                <select class="form-control" id="category_id" name="category_id">
                  <option value="">请选择分类</option>
                  {% for category in categories %}
                    <option value="{{ category.id }}" {% if book.category_id == category.id %}selected{% endif %}>
                      {{ category.name }}
                    </option>
                  {% endfor %}
                </select>
              </div>
              <div class="form-group col-md-6">
                <label for="tags">标签</label>
                <input type="text" class="form-control" id="tags" name="tags" value="{{ book.tags or '' }}" placeholder="多个标签用逗号分隔">
              </div>
            </div>

          </div>
        </div>

        <div class="card">
          <div class="card-header">图书简介</div>
          <div class="card-body">
            <div class="form-group">
              <textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介...">{{ book.description or '' }}</textarea>
            </div>
          </div>
        </div>
      </div>

      <div class="col-md-4">
        <div class="card">
          <div class="card-header">封面图片</div>
          <div class="card-body">
            <div class="cover-preview" id="coverPreview">
              {% if book.cover_url %}
                <img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
              {% else %}
                <div class="no-cover-placeholder">
                  <i class="fas fa-book"></i>
                  <span>暂无封面</span>
                </div>
              {% endif %}
            </div>
            <div class="upload-container" style="margin-top: 1rem;">
              <label for="cover" class="btn btn-secondary btn-block">
                更换封面
              </label>
              <input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-header">库存与价格</div>
          <div class="card-body">
            <div class="form-group">
              <label for="stock">库存数量</label>
              <input type="number" class="form-control" id="stock" name="stock" min="0" value="{{ book.stock }}">
            </div>
            <div class="form-group">
              <label for="price">价格（¥）</label>
              <input type="number" class="form-control" id="price" name="price" step="0.01" min="0" value="{{ book.price or '' }}">
            </div>
            <div class="form-group">
              <label for="status">状态</label>
              <select class="form-control" id="status" name="status">
                <option value="1" {% if book.status == 1 %}selected{% endif %}>上架</option>
                <option value="0" {% if book.status == 0 %}selected{% endif %}>下架</option>
              </select>
            </div>
          </div>
        </div>

        <div class="form-submit-container">
          <button type="submit" class="btn btn-primary btn-lg btn-block">
            保存修改
          </button>
        </div>
      </div>
    </div>
  </form>
</div>
{% endblock %}

{% block scripts %}
<script>
  // 安全地传递变量给JS
  const bookCoverUrl = {{ book.cover_url|default('', true)|tojson|safe }};
  const bookTitle = {{ book.title|default('', true)|tojson|safe }};
  const bookId = {{ book.id|default(0)|tojson|safe }};
</script>
<!-- 确保jQuery已加载 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- 加载验证脚本 -->
<script src="{{ url_for('static', filename='js/book-edit.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/book/import.html
================================================================================

{% extends 'base.html' %}

{% block title %}图书花园 - 批量导入{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-import.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link href="https://fonts.googleapis.com/css2?family=Dancing+Script&family=Playfair+Display&display=swap" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="import-container">
    <div class="page-header animate__animated animate__fadeIn">
        <h1 class="fancy-title">图书花园 <span class="subtitle">批量导入</span></h1>
        <a href="{{ url_for('book.book_list') }}" class="btn btn-return">
            <i class="fas fa-arrow-left"></i> 返回图书列表
        </a>
    </div>

    <div class="row">
        <div class="col-md-8 offset-md-2">
            <div class="card animate__animated animate__fadeInUp">
                <div class="card-header">
                    <h4><i class="fas fa-magic sparkle"></i> 添加您的图书收藏</h4>
                </div>
                <div class="card-body">
                    <form method="POST" enctype="multipart/form-data">
                        <div class="form-group file-upload-wrapper">
                            <label for="file" class="elegant-label">选择您的Excel文件</label>
                            <div class="custom-file">
                                <input type="file" class="custom-file-input" id="file" name="file" accept=".xlsx, .xls" required>
                                <label class="custom-file-label" for="file">点击这里选择文件...</label>
                            </div>
                            <small class="form-text text-muted">支持的文件格式: .xlsx, .xls</small>
                        </div>

                        <button type="submit" class="btn btn-primary btn-lg btn-block import-btn">
                            <i class="fas fa-upload"></i> 开始导入
                        </button>
                    </form>

                    <div class="divider">
                        <span class="divider-content"><i class="fas fa-book-open"></i></span>
                    </div>

                    <div class="import-instructions animate__animated animate__fadeIn">
                        <h5 class="instruction-title"><i class="fas fa-leaf"></i> 导入指南</h5>
                        <div class="instruction-content">
                            <p>Excel文件须包含以下字段 (标题行必须与下列完全一致):</p>
                            <ul class="elegant-list">
                                <li class="required-field"><span class="field-name">title</span> - 图书标题 <span class="required-badge">必填</span></li>
                                <li class="required-field"><span class="field-name">author</span> - 作者名称 <span class="required-badge">必填</span></li>
                                <li><span class="field-name">publisher</span> - 出版社</li>
                                <li><span class="field-name">category_id</span> - 分类ID</li>
                                <li><span class="field-name">tags</span> - 标签 (多个标签用逗号分隔)</li>
                                <li><span class="field-name">isbn</span> - ISBN编号</li>
                                <li><span class="field-name">publish_year</span> - 出版年份</li>
                                <li><span class="field-name">description</span> - 图书简介</li>
                                <li><span class="field-name">cover_url</span> - 封面图片URL</li>
                                <li><span class="field-name">stock</span> - 库存数量</li>
                                <li><span class="field-name">price</span> - 价格</li>
                            </ul>
                        </div>

                        <div class="template-download animate__animated animate__pulse animate__infinite animate__slower">
                            <p>不确定如何开始? 下载我们精心准备的模板:</p>
                            <a href="{{ url_for('book.download_template') }}" class="btn download-btn">
                                <i class="fas fa-download"></i> 下载Excel模板
                            </a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="floating-elements">
        <div class="snowflake snowflake-1"></div>
        <div class="snowflake snowflake-2"></div>
        <div class="snowflake snowflake-3"></div>
        <div class="snowflake snowflake-4"></div>
        <div class="flower flower-1"></div>
        <div class="flower flower-2"></div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/book-import.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/book/detail.html
================================================================================

{% extends 'base.html' %}

{% block title %}{{ book.title }} - 图书详情{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-detail.css') }}">
{% endblock %}

{% block content %}
<div class="book-detail-container">
    <div class="page-header">
        <h1>图书详情</h1>
        <div class="actions">
            <!-- 根据来源返回不同页面 -->
            {% if request.referrer and 'browse' in request.referrer %}
            <a href="{{ url_for('book.browse_books') }}" class="btn btn-secondary">
                <i class="fas fa-arrow-left"></i> 返回浏览
            </a>
            {% else %}
            <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
                <i class="fas fa-arrow-left"></i> 返回列表
            </a>
            {% endif %}

            <!-- 编辑按钮只对管理员显示 -->
            {% if current_user.is_authenticated and current_user.role_id == 1 %}
            <a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary">
                <i class="fas fa-edit"></i> 编辑图书
            </a>
            {% endif %}

            <!-- 借阅按钮对所有用户显示，但需要有库存 -->
            {% if book.stock > 0 %}
            <a href="#" class="btn btn-success" id="borrowBtn">
                <i class="fas fa-hand-holding"></i> 借阅此书
            </a>
            {% else %}
            <button class="btn btn-secondary" disabled>
                <i class="fas fa-ban"></i> 暂无库存
            </button>
            {% endif %}
        </div>
    </div>

    <div class="book-content">
        <div class="book-header">
            <div class="book-cover-large">
                {% if book.cover_url %}
                <img src="{{ book.cover_url }}" alt="{{ book.title }}">
                {% else %}
                <div class="no-cover-large">
                    <i class="fas fa-book"></i>
                    <span>无封面</span>
                </div>
                {% endif %}
            </div>

            <div class="book-main-info">
                <h2 class="book-title">{{ book.title }}</h2>
                <p class="book-author"><i class="fas fa-user-edit"></i> 作者: {{ book.author }}</p>

                <div class="book-meta-info">
                    <div class="meta-item">
                        <i class="fas fa-building"></i>
                        <span>出版社: </span>
                        <span class="meta-value">{{ book.publisher or '未知' }}</span>
                    </div>
                    <div class="meta-item">
                        <i class="fas fa-calendar-alt"></i>
                        <span>出版年份: </span>
                        <span class="meta-value">{{ book.publish_year or '未知' }}</span>
                    </div>
                    <div class="meta-item">
                        <i class="fas fa-barcode"></i>
                        <span>ISBN: </span>
                        <span class="meta-value">{{ book.isbn or '未知' }}</span>
                    </div>
                    <div class="meta-item">
                        <i class="fas fa-layer-group"></i>
                        <span>分类: </span>
                        <span class="meta-value">{{ book.category.name if book.category else '未分类' }}</span>
                    </div>
                    {% if book.tags %}
                    <div class="meta-item">
                        <i class="fas fa-tags"></i>
                        <span>标签: </span>
                        <span class="meta-value">
                            {% for tag in book.tags.split(',') %}
                            <span class="tag">{{ tag.strip() }}</span>
                            {% endfor %}
                        </span>
                    </div>
                    {% endif %}
                    <div class="meta-item">
                        <i class="fas fa-yuan-sign"></i>
                        <span>价格: </span>
                        <span class="meta-value">{{ book.price or '未知' }}</span>
                    </div>
                </div>

                <div class="book-status-info">
                    <div class="status-badge {{ 'available' if book.stock > 0 else 'unavailable' }}">
                        {{ '可借阅' if book.stock > 0 else '无库存' }}
                    </div>
                    <div class="stock-info">
                        <i class="fas fa-cubes"></i> 库存: {{ book.stock }}
                    </div>
                </div>
            </div>
        </div>

        <div class="book-details-section">
            <h3>图书简介</h3>
            <div class="book-description">
                {% if book.description %}
                <p>{{ book.description|nl2br }}</p>
                {% else %}
                <p class="no-description">暂无图书简介</p>
                {% endif %}
            </div>
        </div>

        <!-- 借阅历史 (仅管理员可见) -->
        {% if current_user.is_authenticated and current_user.role_id == 1 %}
        <div class="book-borrow-history">
            <h3>借阅历史</h3>
            {% if borrow_records %}
            <table class="table borrow-table">
                <thead>
                    <tr>
                        <th>借阅用户</th>
                        <th>借阅日期</th>
                        <th>应还日期</th>
                        <th>实际归还</th>
                        <th>状态</th>
                    </tr>
                </thead>
                <tbody>
                    {% for record in borrow_records %}
                    <tr>
                        <td>{{ record.user.username }}</td>
                        <td>{{ record.borrow_date.strftime('%Y-%m-%d') }}</td>
                        <td>{{ record.due_date.strftime('%Y-%m-%d') }}</td>
                        <td>{{ record.return_date.strftime('%Y-%m-%d') if record.return_date else '-' }}</td>
                        <td>
                            {% if record.status == 1 and record.due_date < now %}
                            <span class="badge badge-danger">已逾期</span>
                            {% elif record.status == 1 %}
                            <span class="badge badge-warning">借阅中</span>
                            {% else %}
                            <span class="badge badge-success">已归还</span>
                            {% endif %}
                        </td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
            {% else %}
            <p class="no-records">暂无借阅记录</p>
            {% endif %}
        </div>
        {% endif %}
    </div>
</div>

<!-- 借阅确认模态框 -->
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true" style="display: none;">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="borrowModalLabel">借阅确认</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <form id="borrowForm" action="{{ url_for('borrow.borrow_book') }}" method="POST">
                <div class="modal-body">
                    <p>您确定要借阅《{{ book.title }}》吗？</p>
                    <input type="hidden" name="book_id" value="{{ book.id }}">

                    <div class="form-group">
                        <label for="borrow_days">借阅天数</label>
                        <select class="form-control" id="borrow_days" name="borrow_days">
                            <option value="7">7天</option>
                            <option value="14" selected>14天</option>
                            <option value="30">30天</option>
                        </select>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    <button type="submit" class="btn btn-success">确认借阅</button>
                </div>
            </form>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    $(document).ready(function() {
        // 借阅按钮点击事件
        $('#borrowBtn').click(function(e) {
            e.preventDefault();
            $('#borrowModal').modal('show');
        });
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/book/categories.html
================================================================================

{% extends 'base.html' %}

{% block title %}图书分类管理 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/categories.css') }}">
{% endblock %}

{% block content %}
<div class="categories-container">
    <div class="page-header">
        <h1>图书分类管理</h1>
        <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
            <i class="fas fa-arrow-left"></i> 返回图书列表
        </a>
    </div>

    <div class="row">
        <div class="col-md-4">
            <div class="card">
                <div class="card-header">
                    添加新分类
                </div>
                <div class="card-body">
                    <form id="categoryForm">
                        <div class="form-group">
                            <label for="categoryName">分类名称</label>
                            <input type="text" class="form-control" id="categoryName" name="name" required>
                        </div>
                        <div class="form-group">
                            <label for="parentCategory">父级分类</label>
                            <select class="form-control" id="parentCategory" name="parent_id">
                                <option value="">无 (顶级分类)</option>
                                {% for category in categories %}
                                <option value="{{ category.id }}">{{ category.name }}</option>
                                {% endfor %}
                            </select>
                        </div>
                        <div class="form-group">
                            <label for="categorySort">排序</label>
                            <input type="number" class="form-control" id="categorySort" name="sort" value="0" min="0">
                        </div>
                        <button type="submit" class="btn btn-primary btn-block">
                            <i class="fas fa-plus"></i> 添加分类
                        </button>
                    </form>
                </div>
            </div>
        </div>

        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    分类列表
                </div>
                <div class="card-body">
                    {% if categories %}
                    <table class="table table-hover category-table">
                        <thead>
                            <tr>
                                <th width="5%">ID</th>
                                <th width="40%">分类名称</th>
                                <th width="20%">父级分类</th>
                                <th width="10%">排序</th>
                                <th width="25%">操作</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for category in categories %}
                            <tr data-id="{{ category.id }}">
                                <td>{{ category.id }}</td>
                                <td>{{ category.name }}</td>
                                <td>
                                    {% if category.parent %}
                                    {{ category.parent.name }}
                                    {% else %}
                                    <span class="text-muted">无</span>
                                    {% endif %}
                                </td>
                                <td>{{ category.sort }}</td>
                                                                <td>
                                    <button class="btn btn-sm btn-primary edit-category" data-id="{{ category.id }}"
                                            data-name="{{ category.name }}"
                                            data-parent="{{ category.parent_id or '' }}"
                                            data-sort="{{ category.sort }}">
                                        <i class="fas fa-edit"></i> 编辑
                                    </button>
                                    <button class="btn btn-sm btn-danger delete-category" data-id="{{ category.id }}" data-name="{{ category.name }}">
                                        <i class="fas fa-trash"></i> 删除
                                    </button>
                                </td>
                            </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                    {% else %}
                    <div class="no-categories">
                        <i class="fas fa-exclamation-circle"></i>
                        <p>暂无分类数据</p>
                    </div>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 编辑分类模态框 -->
<div class="modal fade" id="editCategoryModal" tabindex="-1" role="dialog" aria-labelledby="editCategoryModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="editCategoryModalLabel">编辑分类</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <form id="editCategoryForm">
                <div class="modal-body">
                    <input type="hidden" id="editCategoryId">
                    <div class="form-group">
                        <label for="editCategoryName">分类名称</label>
                        <input type="text" class="form-control" id="editCategoryName" name="name" required>
                    </div>
                    <div class="form-group">
                        <label for="editParentCategory">父级分类</label>
                        <select class="form-control" id="editParentCategory" name="parent_id">
                            <option value="">无 (顶级分类)</option>
                            {% for category in categories %}
                            <option value="{{ category.id }}">{{ category.name }}</option>
                            {% endfor %}
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="editCategorySort">排序</label>
                        <input type="number" class="form-control" id="editCategorySort" name="sort" value="0" min="0">
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    <button type="submit" class="btn btn-primary">保存修改</button>
                </div>
            </form>
        </div>
    </div>
</div>

<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteCategoryModal" tabindex="-1" role="dialog" aria-labelledby="deleteCategoryModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="deleteCategoryModalLabel">确认删除</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p>您确定要删除分类 "<span id="deleteCategoryName"></span>" 吗？</p>
                <p class="text-danger">注意: 如果该分类下有图书或子分类，将无法删除！</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                <button type="button" class="btn btn-danger" id="confirmDeleteCategory">确认删除</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    $(document).ready(function() {
        // 添加分类
        $('#categoryForm').submit(function(e) {
            e.preventDefault();

            const formData = {
                name: $('#categoryName').val(),
                parent_id: $('#parentCategory').val(),
                sort: $('#categorySort').val()
            };

            $.ajax({
                url: '{{ url_for("book.add_category") }}',
                type: 'POST',
                data: formData,
                success: function(response) {
                    if (response.success) {
                        showNotification(response.message, 'success');
                        setTimeout(function() {
                            location.reload();
                        }, 1000);
                    } else {
                        showNotification(response.message, 'error');
                    }
                },
                error: function() {
                    showNotification('操作失败，请稍后重试', 'error');
                }
            });
        });

        // 打开编辑模态框
        $('.edit-category').click(function() {
            const id = $(this).data('id');
            const name = $(this).data('name');
            const parentId = $(this).data('parent');
            const sort = $(this).data('sort');

            $('#editCategoryId').val(id);
            $('#editCategoryName').val(name);
            $('#editParentCategory').val(parentId);
            $('#editCategorySort').val(sort);

            // 禁用选择自己作为父级
            $('#editParentCategory option').removeAttr('disabled');
            $(`#editParentCategory option[value="${id}"]`).attr('disabled', 'disabled');

            $('#editCategoryModal').modal('show');
        });

        // 提交编辑表单
        $('#editCategoryForm').submit(function(e) {
            e.preventDefault();

            const id = $('#editCategoryId').val();
            const formData = {
                name: $('#editCategoryName').val(),
                parent_id: $('#editParentCategory').val(),
                sort: $('#editCategorySort').val()
            };

            $.ajax({
                url: `/book/categories/edit/${id}`,
                type: 'POST',
                data: formData,
                success: function(response) {
                    if (response.success) {
                        $('#editCategoryModal').modal('hide');
                        showNotification(response.message, 'success');
                        setTimeout(function() {
                            location.reload();
                        }, 1000);
                    } else {
                        showNotification(response.message, 'error');
                    }
                },
                error: function() {
                    showNotification('操作失败，请稍后重试', 'error');
                }
            });
        });

        // 打开删除确认框
        $('.delete-category').click(function() {
            const id = $(this).data('id');
            const name = $(this).data('name');

            $('#deleteCategoryName').text(name);
            $('#confirmDeleteCategory').data('id', id);

            $('#deleteCategoryModal').modal('show');
        });

        // 确认删除
        $('#confirmDeleteCategory').click(function() {
            const id = $(this).data('id');

            $.ajax({
                url: `/book/categories/delete/${id}`,
                type: 'POST',
                success: function(response) {
                    $('#deleteCategoryModal').modal('hide');

                    if (response.success) {
                        showNotification(response.message, 'success');
                        setTimeout(function() {
                            location.reload();
                        }, 1000);
                    } else {
                        showNotification(response.message, 'error');
                    }
                },
                error: function() {
                    showNotification('操作失败，请稍后重试', 'error');
                }
            });
        });

        // 显示通知
        function showNotification(message, type) {
            const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
            const alert = `
                <div class="alert ${alertClass} alert-dismissible fade show notification-alert" role="alert">
                    ${message}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
            `;

            $('body').append(alert);

            // 5秒后自动关闭
            setTimeout(() => {
                $('.notification-alert').alert('close');
            }, 5000);
        }
    });
</script>
{% endblock %}


================================================================================
File: ./app/templates/log/list.html
================================================================================

{% extends 'base.html' %}

{% block title %}系统日志管理{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/log-list.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Dancing+Script&family=Montserrat:wght@300;400;500&display=swap" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="content-container">
    <div class="content-header">
        <h1><i class="fas fa-history sparkle"></i> 系统日志管理</h1>
        <div class="actions">
            <button id="btnExport" class="btn btn-blossom">
                <i class="fas fa-file-export"></i> 导出日志
            </button>
            <button id="btnClear" class="btn btn-danger-soft">
                <i class="fas fa-trash"></i> 清除日志
            </button>
        </div>
    </div>

    <div class="filter-panel">
        <div class="filter-panel-header">
            <span class="filter-title">日志筛选</span>
            <div class="snowflake-divider">
                <span>❄</span><span>❄</span><span>❄</span>
            </div>
        </div>
        <form method="get" action="{{ url_for('log.log_list') }}">
            <div class="filter-row">
                <div class="filter-item">
                    <label for="user_id">用户</label>
                    <select name="user_id" id="user_id" class="elegant-select">
                        <option value="">全部用户</option>
                        {% for user in users %}
                        <option value="{{ user.id }}" {% if filters.user_id == user.id %}selected{% endif %}>
                            {{ user.username }}
                        </option>
                        {% endfor %}
                    </select>
                </div>

                <div class="filter-item">
                    <label for="action">操作类型</label>
                    <select name="action" id="action" class="elegant-select">
                        <option value="">全部操作</option>
                        {% for action_type, count in action_types %}
                        <option value="{{ action_type }}" {% if filters.action == action_type %}selected{% endif %}>
                            {{ action_type }} ({{ count }})
                        </option>
                        {% endfor %}
                    </select>
                </div>

                <div class="filter-item">
                    <label for="target_type">目标类型</label>
                    <select name="target_type" id="target_type" class="elegant-select">
                        <option value="">全部类型</option>
                        {% for target_type, count in target_types %}
                        <option value="{{ target_type }}" {% if filters.target_type == target_type %}selected{% endif %}>
                            {{ target_type }} ({{ count }})
                        </option>
                        {% endfor %}
                    </select>
                </div>

                <div class="filter-item">
                    <label for="date_range">时间范围</label>
                    <select name="date_range" id="date_range" class="elegant-select">
                        <option value="1" {% if filters.date_range == '1' %}selected{% endif %}>最近1天</option>
                        <option value="7" {% if filters.date_range == '7' %}selected{% endif %}>最近7天</option>
                        <option value="30" {% if filters.date_range == '30' %}selected{% endif %}>最近30天</option>
                        <option value="custom" {% if filters.date_range == 'custom' %}selected{% endif %}>自定义</option>
                    </select>
                </div>
            </div>

            <div class="filter-row date-range-inputs" {% if filters.date_range != 'custom' %}style="display:none"{% endif %}>
                <div class="filter-item">
                    <label for="start_date">开始日期</label>
                    <input type="date" name="start_date" id="start_date" value="{{ filters.start_date }}" class="elegant-input">
                </div>
                <div class="filter-item">
                    <label for="end_date">结束日期</label>
                    <input type="date" name="end_date" id="end_date" value="{{ filters.end_date }}" class="elegant-input">
                </div>
            </div>

            <div class="filter-actions">
                <button type="submit" class="btn btn-primary-soft">应用筛选</button>
                <a href="{{ url_for('log.log_list') }}" class="btn btn-secondary-soft">重置</a>
            </div>
        </form>
    </div>

    <div class="card glass-card">
        <div class="card-body">
            <div class="table-container">
                <table class="table elegant-table">
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>用户</th>
                            <th>操作</th>
                            <th>目标类型</th>
                            <th>目标ID</th>
                            <th>IP地址</th>
                            <th>描述</th>
                            <th>时间</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for log in pagination.items %}
                        <tr class="fade-in-row">
                            <td>{{ log.id }}</td>
                            <td>
                                {% if log.user %}
                                <span class="user-badge">{{ log.user.username }}</span>
                                {% else %}
                                <span class="text-muted">未登录</span>
                                {% endif %}
                            </td>
                            <td>{{ log.action }}</td>
                            <td>{{ log.target_type }}</td>
                            <td>{{ log.target_id }}</td>
                            <td>{{ log.ip_address }}</td>
                            <td>{{ log.description }}</td>
                            <td>{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
                        </tr>
                        {% else %}
                        <tr>
                            <td colspan="8" class="text-center empty-message">
                                <div class="empty-container">
                                    <i class="fas fa-search"></i>
                                    <p>没有找到符合条件的日志记录</p>
                                </div>
                            </td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div>

            <!-- 分页部分 -->
            <div class="pagination-wrapper">
                <div class="pagination-container">
                    {% if pagination.has_prev %}
                    <a href="{{ url_for('log.log_list', page=pagination.prev_num, user_id=filters.user_id, action=filters.action, target_type=filters.target_type, date_range=filters.date_range, start_date=filters.start_date, end_date=filters.end_date) }}" class="btn btn-sm btn-outline-primary page-btn"><i class="fas fa-chevron-left"></i> 上一页</a>
                    {% endif %}

                    <span class="page-info">
                        第 {{ pagination.page }} 页，共 {{ pagination.pages }} 页，总计 {{ pagination.total }} 条记录
                    </span>

                    {% if pagination.has_next %}
                    <a href="{{ url_for('log.log_list', page=pagination.next_num, user_id=filters.user_id, action=filters.action, target_type=filters.target_type, date_range=filters.date_range, start_date=filters.start_date, end_date=filters.end_date) }}" class="btn btn-sm btn-outline-primary page-btn">下一页 <i class="fas fa-chevron-right"></i></a>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 导出日志模态框 -->
<div class="modal fade" id="exportLogModal" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-elegant" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"><i class="fas fa-file-export"></i> 导出日志</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p class="modal-message">请选择导出格式：</p>
                <div class="form-group">
                    <select id="exportFormat" class="form-control elegant-select">
                        <option value="csv">CSV 格式</option>
                        <option value="excel">Excel 格式</option>
                    </select>
                </div>
                <div class="alert alert-info elegant-alert">
                    <i class="fas fa-info-circle"></i> 将导出当前筛选条件下的所有日志记录。
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary-soft" data-dismiss="modal">取消</button>
                <button type="button" id="confirmExport" class="btn btn-primary-soft">确认导出</button>
            </div>
        </div>
    </div>
</div>

<!-- 清除日志确认框 -->
<div class="modal fade" id="clearLogModal" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-elegant" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"><i class="fas fa-trash-alt"></i> 清除日志确认</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <p class="modal-message">请选择要清除的日志范围：</p>
                <div class="form-group">
                    <select id="clearDays" class="form-control elegant-select">
                        <option value="0">清除全部日志</option>
                        <option value="30">清除30天前的日志</option>
                        <option value="90">清除90天前的日志</option>
                        <option value="180">清除半年前的日志</option>
                        <option value="365">清除一年前的日志</option>
                    </select>
                </div>
                <div class="alert alert-warning elegant-alert">
                    <i class="fas fa-exclamation-triangle"></i> 警告：此操作不可恢复！
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary-soft" data-dismiss="modal">取消</button>
                <button type="button" id="confirmClear" class="btn btn-danger-soft">确认清除</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/log-list.js') }}"></script>
<script>
    // 添加雪花动画效果
    document.addEventListener('DOMContentLoaded', function() {
        // 为表格行添加渐入效果
        const rows = document.querySelectorAll('.fade-in-row');
        rows.forEach((row, index) => {
            row.style.animationDelay = `${index * 0.05}s`;
        });

        // 为按钮添加悬停效果
        const buttons = document.querySelectorAll('.btn');
        buttons.forEach(btn => {
            btn.addEventListener('mouseover', function() {
                this.classList.add('btn-glow');
            });
            btn.addEventListener('mouseout', function() {
                this.classList.remove('btn-glow');
            });
        });
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/log/detail.html
================================================================================

{% extends 'base.html' %}

{% block title %}日志详情{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/log-detail.css') }}">
{% endblock %}

{% block content %}
<div class="content-header">
    <h1><i class="fas fa-file-alt"></i> 日志详情 #{{ log.id }}</h1>
    <div class="actions">
        <a href="{{ url_for('log.log_list') }}" class="btn btn-secondary">
            <i class="fas fa-arrow-left"></i> 返回列表
        </a>
    </div>
</div>

<div class="card">
    <div class="card-body">
        <div class="log-info">
            <div class="row">
                <div class="col-md-6">
                    <div class="info-item">
                        <div class="label">操作时间:</div>
                        <div class="value">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
                    </div>
                    <div class="info-item">
                        <div class="label">操作用户:</div>
                        <div class="value">
                            {% if log.user %}
                            {{ log.user.username }} (ID: {{ log.user_id }})
                            {% else %}
                            <span class="text-muted">未登录用户</span>
                            {% endif %}
                        </div>
                    </div>
                    <div class="info-item">
                        <div class="label">操作类型:</div>
                        <div class="value">{{ log.action }}</div>
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="info-item">
                        <div class="label">目标类型:</div>
                        <div class="value">{{ log.target_type or '无' }}</div>
                    </div>
                    <div class="info-item">
                        <div class="label">目标ID:</div>
                        <div class="value">{{ log.target_id or '无' }}</div>
                    </div>
                    <div class="info-item">
                        <div class="label">IP地址:</div>
                        <div class="value">{{ log.ip_address or '未记录' }}</div>
                    </div>
                </div>
            </div>
            <div class="info-item description">
                <div class="label">详细描述:</div>
                <div class="value">{{ log.description or '无描述' }}</div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

================================================================================
File: ./app/templates/announcement/manage.html
================================================================================

{% extends 'base.html' %}

{% block title %}公告管理 - 图书管理系统{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-manage.css') }}">
{% endblock %}

{% block content %}
<div class="announcement-manage-container">
    <div class="page-header">
        <h1>公告管理</h1>
        <a href="{{ url_for('announcement.add_announcement') }}" class="btn btn-primary">
            <i class="fas fa-plus"></i> 添加公告
        </a>
    </div>

    <!-- 搜索和筛选 -->
    <div class="filter-container">
        <form action="{{ url_for('announcement.manage_announcements') }}" method="get" class="filter-form">
            <div class="form-group">
                <input type="text" name="search" class="form-control" placeholder="搜索公告标题..." value="{{ search }}">
            </div>
            <div class="form-group">
                <select name="status" class="form-control">
                    <option value="">全部状态</option>
                    <option value="1" {% if status == 1 %}selected{% endif %}>已发布</option>
                    <option value="0" {% if status == 0 %}selected{% endif %}>已撤销</option>
                </select>
            </div>
            <button type="submit" class="btn btn-secondary">
                <i class="fas fa-search"></i> 搜索
            </button>
        </form>
    </div>

    <!-- 公告列表 -->
    <div class="announcement-list">
        {% if pagination.items %}
        <table class="table announcement-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>标题</th>
                    <th>发布者</th>
                    <th>发布时间</th>
                    <th>最后更新</th>
                    <th>状态</th>
                    <th>置顶</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                {% for announcement in pagination.items %}
                <tr>
                    <td>{{ announcement.id }}</td>
                    <td>
                        <a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}" target="_blank" class="announcement-title">
                            {{ announcement.title }}
                        </a>
                    </td>
                    <td>{{ announcement.publisher.username if announcement.publisher else '系统' }}</td>
                    <td>{{ announcement.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
                    <td>{{ announcement.updated_at.strftime('%Y-%m-%d %H:%M') }}</td>
                    <td>
                        <span class="badge badge-{{ 'success' if announcement.status == 1 else 'secondary' }}">
                            {{ '已发布' if announcement.status == 1 else '已撤销' }}
                        </span>
                    </td>
                    <td>
                        <span class="badge badge-{{ 'primary' if announcement.is_top else 'light' }}">
                            {{ '已置顶' if announcement.is_top else '未置顶' }}
                        </span>
                    </td>
                    <td>
                        <div class="btn-group">
                            <a href="{{ url_for('announcement.edit_announcement', announcement_id=announcement.id) }}" class="btn btn-sm btn-info">
                                <i class="fas fa-edit"></i> 编辑
                            </a>
                            <button class="btn btn-sm btn-{{ 'warning' if announcement.status == 1 else 'success' }}"
                                    onclick="changeStatus({{ announcement.id }}, {{ 0 if announcement.status == 1 else 1 }})">
                                <i class="fas fa-{{ 'times' if announcement.status == 1 else 'check' }}"></i>
                                {{ '撤销' if announcement.status == 1 else '发布' }}
                            </button>
                            <button class="btn btn-sm btn-{{ 'secondary' if announcement.is_top else 'primary' }}"
                                    onclick="changeTopStatus({{ announcement.id }}, {{ 'false' if announcement.is_top else 'true' }})">
                                <i class="fas fa-thumbtack"></i>
                                {{ '取消置顶' if announcement.is_top else '置顶' }}
                            </button>
                        </div>
                    </td>
                </tr>
                {% endfor %}
            </tbody>
        </table>

        <!-- 分页 -->
        <div class="pagination-container">
            {% if pagination.pages > 1 %}
            <nav aria-label="Page navigation">
                <ul class="pagination">
                    {% if pagination.has_prev %}
                    <li class="page-item">
                        <a class="page-link" href="{{ url_for('announcement.manage_announcements', page=pagination.prev_num, search=search, status=status) }}" aria-label="Previous">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>
                    {% else %}
                    <li class="page-item disabled">
                        <a class="page-link" href="#" aria-label="Previous">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>
                    {% endif %}

                    {% for page_num in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
                        {% if page_num %}
                            {% if page_num == pagination.page %}
                            <li class="page-item active">
                                <a class="page-link" href="{{ url_for('announcement.manage_announcements', page=page_num, search=search, status=status) }}">{{ page_num }}</a>
                            </li>
                            {% else %}
                            <li class="page-item">
                                <a class="page-link" href="{{ url_for('announcement.manage_announcements', page=page_num, search=search, status=status) }}">{{ page_num }}</a>
                            </li>
                            {% endif %}
                        {% else %}
                            <li class="page-item disabled">
                                <a class="page-link" href="#">...</a>
                            </li>
                        {% endif %}
                    {% endfor %}

                    {% if pagination.has_next %}
                    <li class="page-item">
                        <a class="page-link" href="{{ url_for('announcement.manage_announcements', page=pagination.next_num, search=search, status=status) }}" aria-label="Next">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                    {% else %}
                    <li class="page-item disabled">
                        <a class="page-link" href="#" aria-label="Next">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                    {% endif %}
                </ul>
            </nav>
            {% endif %}
        </div>
        {% else %}
        <div class="no-records">
            <i class="fas fa-info-circle"></i>
            <p>没有找到符合条件的公告</p>
        </div>
        {% endif %}
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='js/announcement-manage.js') }}"></script>
{% endblock %}

================================================================================
File: ./app/templates/announcement/list.html
================================================================================

{% extends 'base.html' %}

{% block title %}通知公告 - 图书管理系统{% endblock %}

{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&family=Nunito+Sans:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-list.css') }}">
{% endblock %}

{% block content %}
<div class="announcement-container">
    <div class="page-header">
        <h1><i class="fas fa-bullhorn page-icon"></i> 通知公告</h1>
        {# Optional: Add a create button if applicable for admins #}
        {# <a href="{{ url_for('announcement.create_announcement') }}" class="btn btn-fresh-create">
            <i class="fas fa-plus"></i> 发布新公告
        </a> #}
    </div>

    <div class="announcement-list">
        {% if pagination.items %}
            {% for announcement in pagination.items %}
                <div class="announcement-item {% if announcement.is_top %}pinned{% endif %}">
                    {% if announcement.is_top %}
                    <div class="pin-badge">
                        <i class="fas fa-thumbtack"></i> 置顶推荐
                    </div>
                    {% endif %}
                    <div class="announcement-header">
                        <h3><a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}">{{ announcement.title }}</a></h3>
                        <span class="date">{{ announcement.created_at.strftime('%Y年%m月%d日') }}</span>
                    </div>
                    <div class="announcement-preview">
                        {{ announcement.content|striptags|truncate(130) }}
                    </div>
                    <div class="announcement-footer">
                        <span class="publisher">
                            <i class="fas fa-user-circle"></i>
                            发布者: {{ announcement.publisher.username if announcement.publisher else '系统管理员' }}
                        </span>
                        <a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}" class="read-more">
                            阅读全文 <i class="fas fa-arrow-right"></i>
                        </a>
                    </div>
                </div>
            {% endfor %}

            <!-- 分页 -->
            <div class="pagination-container">
                {% if pagination.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        {% if pagination.has_prev %}
                        <li class="page-item">
                            <a class="page-link" href="{{ url_for('announcement.announcement_list', page=pagination.prev_num) }}" aria-label="Previous">
                                <span aria-hidden="true">&laquo;</span>
                            </a>
                        </li>
                        {% else %}
                        <li class="page-item disabled">
                            <a class="page-link" href="#" aria-label="Previous">
                                <span aria-hidden="true">&laquo;</span>
                            </a>
                        </li>
                        {% endif %}

                        {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=1) %}
                            {% if page_num %}
                                {% if page_num == pagination.page %}
                                <li class="page-item active">
                                    <a class="page-link" href="{{ url_for('announcement.announcement_list', page=page_num) }}">{{ page_num }}</a>
                                </li>
                                {% else %}
                                <li class="page-item">
                                    <a class="page-link" href="{{ url_for('announcement.announcement_list', page=page_num) }}">{{ page_num }}</a>
                                </li>
                                {% endif %}
                            {% else %}
                                <li class="page-item disabled">
                                    <a class="page-link" href="#">...</a>
                                </li>
                            {% endif %}
                        {% endfor %}

                        {% if pagination.has_next %}
                        <li class="page-item">
                            <a class="page-link" href="{{ url_for('announcement.announcement_list', page=pagination.next_num) }}" aria-label="Next">
                                <span aria-hidden="true">&raquo;</span>
                            </a>
                        </li>
                        {% else %}
                        <li class="page-item disabled">
                            <a class="page-link" href="#" aria-label="Next">
                                <span aria-hidden="true">&raquo;</span>
                            </a>
                        </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            </div>
        {% else %}
            <div class="no-records">
                 <img src="data:image/svg+xml;charset=UTF-8,%3csvg width='80' height='80' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z' fill='%23FFAAA5'/%3e%3c/svg%3e" alt="No announcements icon" class="no-records-icon">
                <p>暂时还没有新的通知公告哦，敬请期待！</p>
            </div>
        {% endif %}
    </div>
</div>
{% endblock %}

{% block scripts %}
{# <script src="{{ url_for('static', filename='js/announcement-list.js') }}"></script> #}
{# 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-form.css') }}">
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="announcement-form-container">
    <div class="page-header">
        <h1>添加公告</h1>
    </div>

    <div class="card">
        <div class="card-body">
            {% if error %}
            <div class="alert alert-danger">{{ error }}</div>
            {% endif %}

            <form method="post" action="{{ url_for('announcement.add_announcement') }}" id="announcementForm">
                <div class="form-group">
                    <label for="title">公告标题 <span class="text-danger">*</span></label>
                    <input type="text" class="form-control" id="title" name="title" required>
                </div>

                <div class="form-group">
                    <label for="editor">公告内容 <span class="text-danger">*</span></label>
                    <div id="editor" style="height: 250px;"></div>
                    <input type="hidden" name="content" id="content">
                </div>

                <div class="form-check">
                    <input type="checkbox" class="form-check-input" id="is_top" name="is_top">
                    <label class="form-check-label" for="is_top">置顶公告</label>
                </div>

                <div class="form-buttons">
                    <button type="button" class="btn btn-secondary" onclick="history.back()">取消</button>
                    <button type="submit" class="btn btn-primary" id="submitBtn">发布公告</button>
                </div>
            </form>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script src="{{ url_for('static', filename='js/announcement-form.js') }}"></script>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // 初始化富文本编辑器
        const quill = new Quill('#editor', {
            theme: 'snow',
            modules: {
                toolbar: [
                    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
                    ['bold', 'italic', 'underline', 'strike'],
                    [{ 'color': [] }, { 'background': [] }],
                    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
                    [{ 'align': [] }],
                    ['link', 'image'],
                    ['clean']
                ]
            },
            placeholder: '请输入公告内容...'
        });

        // 提交表单前获取富文本内容
        document.getElementById('announcementForm').addEventListener('submit', function(e) {
            const content = document.getElementById('content');
            content.value = quill.root.innerHTML;

            // 简单验证
            if (!document.getElementById('title').value.trim()) {
                e.preventDefault();
                alert('请输入公告标题');
                return false;
            }

            if (quill.getText().trim().length <= 1) {
                e.preventDefault();
                alert('请输入公告内容');
                return false;
            }
        });
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/announcement/notifications.html
================================================================================

{% extends 'base.html' %}

{% block title %}我的通知 - 图书管理系统{% endblock %}

{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&family=Nunito+Sans:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/notifications.css') }}">
{% endblock %}

{% block content %}
<div class="notifications-container">
    <div class="page-header">
        <h1>我的通知</h1>
        <div class="notification-actions">
            {% if pagination.items and not (unread_only and pagination.total == 0) %}
                <a href="{{ url_for('announcement.mark_all_as_read') }}" class="btn btn-fresh-action">
                    <i class="fas fa-check-double"></i> 全部标为已读
                </a>
            {% endif %}
        </div>
    </div>

    <div class="filter-tabs">
        <a href="{{ url_for('announcement.user_notifications') }}" class="filter-tab {% if not unread_only %}active{% endif %}">所有通知</a>
        <a href="{{ url_for('announcement.user_notifications', unread_only=1) }}" class="filter-tab {% if unread_only %}active{% endif %}">未读通知</a>
    </div>

    <div class="notifications-list">
        {% if pagination.items %}
            {% for notification in pagination.items %}
                <div class="notification-card {% if notification.status == 0 %}unread{% endif %}">
                    <div class="notification-icon-area">
                        <!-- Example: could use different icons based on notification.type -->
                        {% if notification.status == 0 %}
                            <i class="fas fa-envelope"></i> {# Icon for unread #}
                        {% else %}
                            <i class="fas fa-envelope-open-text"></i> {# Icon for read #}
                        {% endif %}
                    </div>
                    <div class="notification-content">
                        <h3 class="notification-title">
                            <a href="{{ url_for('announcement.view_notification', notification_id=notification.id) }}">{{ notification.title }}</a>
                            {% if notification.status == 0 %}
                                <span class="unread-badge">未读</span>
                            {% endif %}
                        </h3>
                        <div class="notification-text">{{ notification.content|striptags|truncate(120) }}</div>
                        <div class="notification-meta">
                            <span class="notification-type">{{ notification.type }}</span>
                            <span class="notification-time">{{ notification.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
                        </div>
                    </div>
                </div>
            {% endfor %}

            <!-- 分页 -->
            <div class="pagination-container">
                {% if pagination.pages > 1 %}
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        {% if pagination.has_prev %}
                        <li class="page-item">
                            <a class="page-link" href="{{ url_for('announcement.user_notifications', page=pagination.prev_num, unread_only=1 if unread_only else 0) }}" aria-label="Previous">
                                <span aria-hidden="true">&laquo;</span>
                            </a>
                        </li>
                        {% else %}
                        <li class="page-item disabled">
                            <a class="page-link" href="#" aria-label="Previous">
                                <span aria-hidden="true">&laquo;</span>
                            </a>
                        </li>
                        {% endif %}

                        {% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=1) %}
                            {% if page_num %}
                                {% if page_num == pagination.page %}
                                <li class="page-item active">
                                    <a class="page-link" href="{{ url_for('announcement.user_notifications', page=page_num, unread_only=1 if unread_only else 0) }}">{{ page_num }}</a>
                                </li>
                                {% else %}
                                <li class="page-item">
                                    <a class="page-link" href="{{ url_for('announcement.user_notifications', page=page_num, unread_only=1 if unread_only else 0) }}">{{ page_num }}</a>
                                </li>
                                {% endif %}
                            {% else %}
                                <li class="page-item disabled">
                                    <a class="page-link" href="#">...</a>
                                </li>
                            {% endif %}
                        {% endfor %}

                        {% if pagination.has_next %}
                        <li class="page-item">
                            <a class="page-link" href="{{ url_for('announcement.user_notifications', page=pagination.next_num, unread_only=1 if unread_only else 0) }}" aria-label="Next">
                                <span aria-hidden="true">&raquo;</span>
                            </a>
                        </li>
                        {% else %}
                        <li class="page-item disabled">
                            <a class="page-link" href="#" aria-label="Next">
                                <span aria-hidden="true">&raquo;</span>
                            </a>
                        </li>
                        {% endif %}
                    </ul>
                </nav>
                {% endif %}
            </div>
        {% else %}
            <div class="no-records">
                {# Suggestion: Use a more vibrant/friendly icon or illustration #}
                <img src="data:image/svg+xml;charset=UTF-8,%3csvg width='80' height='80' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z' stroke='%23A8E6CF' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M12 16.5C12.9665 16.5 13.75 15.7165 13.75 14.75C13.75 13.7835 12.9665 13 12 13C11.0335 13 10.25 13.7835 10.25 14.75C10.25 15.7165 11.0335 16.5 12 16.5Z' fill='%23A8E6CF'/%3e%3cpath d='M12 7V10' stroke='%23A8E6CF' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e" alt="No notifications icon" class="no-records-icon">
                <p>{{ '还没有未读通知哦～' if unread_only else '这里空空如也，暂时没有新通知。' }}</p>
            </div>
        {% endif %}
    </div>
</div>
{% endblock %}

{% block scripts %}
{# <script src="{{ url_for('static', filename='js/notifications.js') }}"></script> #}
{# 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 %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-form.css') }}">
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
{% endblock %}

{% block content %}
<div class="announcement-form-container">
    <div class="page-header">
        <h1>编辑公告</h1>
    </div>

    <div class="card">
        <div class="card-body">
            {% if error %}
            <div class="alert alert-danger">{{ error }}</div>
            {% endif %}

            <form method="post" action="{{ url_for('announcement.edit_announcement', announcement_id=announcement.id) }}" id="announcementForm">
                <div class="form-group">
                    <label for="title">公告标题 <span class="text-danger">*</span></label>
                    <input type="text" class="form-control" id="title" name="title" value="{{ announcement.title }}" required>
                </div>

                <div class="form-group">
                    <label for="editor">公告内容 <span class="text-danger">*</span></label>
                    <div id="editor" style="height: 250px;"></div>
                    <input type="hidden" name="content" id="content">
                </div>

                <div class="form-check">
                    <input type="checkbox" class="form-check-input" id="is_top" name="is_top" {% if announcement.is_top %}checked{% endif %}>
                    <label class="form-check-label" for="is_top">置顶公告</label>
                </div>

                <div class="form-buttons">
                    <button type="button" class="btn btn-secondary" onclick="history.back()">取消</button>
                    <button type="submit" class="btn btn-primary" id="submitBtn">更新公告</button>
                </div>
            </form>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script src="{{ url_for('static', filename='js/announcement-form.js') }}"></script>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // 初始化富文本编辑器
        const quill = new Quill('#editor', {
            theme: 'snow',
            modules: {
                toolbar: [
                    [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
                    ['bold', 'italic', 'underline', 'strike'],
                    [{ 'color': [] }, { 'background': [] }],
                    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
                    [{ 'align': [] }],
                    ['link', 'image'],
                    ['clean']
                ]
            },
            placeholder: '请输入公告内容...'
        });

        // 加载现有内容
        quill.root.innerHTML = `{{ announcement.content|safe }}`;

        // 提交表单前获取富文本内容
        document.getElementById('announcementForm').addEventListener('submit', function(e) {
            const content = document.getElementById('content');
            content.value = quill.root.innerHTML;

            // 简单验证
            if (!document.getElementById('title').value.trim()) {
                e.preventDefault();
                alert('请输入公告标题');
                return false;
            }

            if (quill.getText().trim().length <= 1) {
                e.preventDefault();
                alert('请输入公告内容');
                return false;
            }
        });
    });
</script>
{% endblock %}

================================================================================
File: ./app/templates/announcement/detail.html
================================================================================

{% extends 'base.html' %}

{% block title %}{{ announcement.title }} - 通知公告{% endblock %}

{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-detail.css') }}">
{% endblock %}

{% block content %}
<div class="announcement-detail-container">
    <div class="page-header">
        <a href="{{ url_for('announcement.announcement_list') }}" class="back-link">
            <i class="fas fa-arrow-left"></i> 返回公告列表
        </a>
        <h1>{{ announcement.title }}</h1>
    </div>

    <div class="announcement-meta">
        <div class="meta-item">
            <i class="fas fa-user"></i> 发布者: {{ announcement.publisher.username if announcement.publisher else '系统' }}
        </div>
        <div class="meta-item">
            <i class="fas fa-calendar-alt"></i> 发布时间: {{ announcement.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
        </div>
        {% if announcement.is_top %}
        <div class="meta-item pinned">
            <i class="fas fa-thumbtack"></i> 置顶公告
        </div>
        {% endif %}
    </div>

    <div class="announcement-content">
        {{ announcement.content|safe }}
    </div>
</div>
{% 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/<int:user_id>', methods=['GET', 'POST'])
@login_required
@admin_required
def user_edit(user_id):
    user = UserService.get_user_by_id(user_id)
    if not user:
        flash('用户不存在', 'error')
        return redirect(url_for('user.user_list'))

    roles = UserService.get_all_roles()

    if request.method == 'POST':
        data = {
            'email': request.form.get('email'),
            'phone': request.form.get('phone'),
            'nickname': request.form.get('nickname'),
            'role_id': int(request.form.get('role_id')),
            'status': int(request.form.get('status')),
        }

        password = request.form.get('password')
        if password:
            data['password'] = password

        success, message = UserService.update_user(user_id, data)
        if success:
            # 记录管理员编辑用户信息日志
            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/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def user_status(user_id):
    data = request.get_json()
    status = data.get('status')

    if status is None or status not in [0, 1]:
        return jsonify({'success': False, 'message': '无效的状态值'})

    # 不能修改自己的状态
    if user_id == current_user.id:
        return jsonify({'success': False, 'message': '不能修改自己的状态'})

    # 查询用户获取用户名（用于日志）
    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/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def user_delete(user_id):
    # 不能删除自己
    if user_id == current_user.id:
        return jsonify({'success': False, 'message': '不能删除自己的账号'})

    # 查询用户获取用户名（用于日志）
    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/<int:role_id>/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/<int:role_id>', 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('/user/role/<int:role_id>/count', methods=['GET'])
@login_required
@admin_required
def get_role_user_count(role_id):
    """获取指定角色的用户数量"""
    try:
        count = User.query.filter_by(role_id=role_id).count()
        return jsonify({
            'success': True,
            'count': count
        })
    except Exception as e:
        return jsonify({
            'success': False,
            'message': f"查询失败: {str(e)}",
            'count': 0
        }), 500


@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/<int:log_id>')
@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"""
    import csv
    from io import StringIO
    from flask import Response

    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')

    # 处理日期范围
    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()

    # 生成CSV文件
    si = StringIO()
    csv_writer = csv.writer(si)

    # 写入标题行
    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')
        ])

    # 设置响应头，使浏览器将其识别为下载文件
    filename = f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"

    output = si.getvalue()

    # 返回Base64编码的CSV数据
    import base64
    encoded_data = base64.b64encode(output.encode('utf-8')).decode('utf-8')

    return jsonify({
        'success': True,
        'message': f'已生成 {len(logs)} 条日志记录',
        'count': len(logs),
        'filename': filename,
        'filedata': encoded_data,
        'filetype': 'text/csv'
    })


@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
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

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/<int:book_id>')
@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/<int:book_id>', 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/<int:book_id>', 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/<int:category_id>', 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/<int:category_id>', 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')  # 替换 @admin_required
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:
                # 读取Excel文件
                df = pd.read_excel(file)
                success_count = 0
                error_count = 0
                errors = []

                # 处理每一行数据
                for index, row in df.iterrows():
                    try:
                        # 检查必填字段
                        if pd.isna(row.get('title')) or pd.isna(row.get('author')):
                            errors.append(f'第{index + 2}行: 书名或作者为空')
                            error_count += 1
                            continue

                        # 检查ISBN是否已存在
                        isbn = row.get('isbn')
                        if isbn and not pd.isna(isbn) and Book.query.filter_by(isbn=str(isbn)).first():
                            errors.append(f'第{index + 2}行: ISBN {isbn} 已存在')
                            error_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=row.get('category_id') if not pd.isna(row.get('category_id')) else None,
                            tags=row.get('tags') if not pd.isna(row.get('tags')) else None,
                            isbn=str(row.get('isbn')) if not pd.isna(row.get('isbn')) else None,
                            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=int(row.get('stock')) if not pd.isna(row.get('stock')) else 0,
                            price=float(row.get('price')) if not pd.isna(row.get('price')) else None,
                            status=1,
                            created_at=datetime.datetime.now(),
                            updated_at=datetime.datetime.now()
                        )

                        db.session.add(book)
                        # 提交以获取book的id
                        db.session.flush()

                        # 创建库存日志
                        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)

                        success_count += 1
                    except Exception as e:
                        errors.append(f'第{index + 2}行: {str(e)}')
                        error_count += 1

                db.session.commit()

                # 记录操作日志
                Log.add_log(
                    action='批量导入图书',
                    user_id=current_user.id,
                    ip_address=request.remote_addr,
                    description=f"批量导入图书: 成功{success_count}条，失败{error_count}条，文件名：{file.filename}"
                )

                flash(f'导入完成: 成功{success_count}条，失败{error_count}条', 'info')
                if errors:
                    flash('<br>'.join(errors[:10]) + (f'<br>...等共{len(errors)}个错误' if len(errors) > 10 else ''),
                          'warning')

                return redirect(url_for('book.book_list'))

            except Exception as e:
                # 记录操作失败日志
                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')
@login_required
@permission_required('import_export_books')  # 替换 @admin_required
def export_books():
    # 获取查询参数
    search = request.args.get('search', '')
    category_id = request.args.get('category_id', type=int)

    query = Book.query

    if search:
        query = query.filter(
            (Book.title.contains(search)) |
            (Book.author.contains(search)) |
            (Book.isbn.contains(search))
        )

    if category_id:
        query = query.filter_by(category_id=category_id)

    books = query.all()

    # 创建DataFrame
    data = []
    for book in books:
        category_name = book.category.name if book.category else ""
        data.append({
            'id': book.id,
            'title': book.title,
            'author': book.author,
            'publisher': book.publisher,
            'category': category_name,
            'tags': book.tags,
            'isbn': book.isbn,
            'publish_year': book.publish_year,
            'description': book.description,
            'stock': book.stock,
            'price': book.price,
            'status': '上架' if book.status == 1 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 ''
        })

    df = pd.DataFrame(data)

    # 创建临时文件
    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    filename = f'books_export_{timestamp}.xlsx'
    filepath = os.path.join(current_app.static_folder, 'temp', filename)

    # 确保目录存在
    os.makedirs(os.path.dirname(filepath), exist_ok=True)

    # 写入Excel
    df.to_excel(filepath, index=False)

    # 记录操作日志
    Log.add_log(
        action='导出图书',
        user_id=current_user.id,
        ip_address=request.remote_addr,
        description=f"导出图书数据: {len(data)}条记录, 查询条件: 搜索={search}, 分类ID={category_id}"
    )

    # 提供下载链接
    return redirect(url_for('static', filename=f'temp/{filename}'))


@book_bp.route('/test-permissions')
def test_permissions():
    """测试当前用户权限"""
    if not current_user.is_authenticated:
        return "未登录"

    return f"""
    <h1>用户权限信息</h1>
    <p>用户名: {current_user.username}</p>
    <p>角色ID: {current_user.role_id}</p>
    <p>是否管理员: {'是' if current_user.role_id == 1 else '否'}</p>
    <p><a href="/book/admin/list">尝试访问管理页面</a></p>
    """


# 图书浏览页面 - 不需要修改，已经只有@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')  # 替换 @admin_required
def download_template():
    """生成并下载Excel图书导入模板"""
    # 创建一个简单的DataFrame作为模板
    data = {
        'title': ['三体', '解忧杂货店'],
        'author': ['刘慈欣', '东野圭吾'],
        'publisher': ['重庆出版社', '南海出版公司'],
        'category_id': [2, 1],  # 对应于科幻小说和文学分类
        'tags': ['科幻,宇宙', '治愈,悬疑'],
        'isbn': ['9787229100605', '9787544270878'],
        'publish_year': ['2008', '2014'],
        'description': ['中国著名科幻小说，三体世界的故事。', '通过信件为人们解忧的杂货店故事。'],
        'cover_url': ['', ''],  # 示例中为空
        'stock': [10, 5],
        'price': [45.00, 39.80]
    }

    # 创建一个分类ID和名称的映射
    categories = Category.query.all()
    category_map = {cat.id: cat.name for cat in categories}

    # 创建一个pandas DataFrame
    df = pd.DataFrame(data)

    # 添加说明工作表
    with pd.ExcelWriter('book_import_template.xlsx', engine='openpyxl') as writer:
        df.to_excel(writer, sheet_name='图书数据示例', index=False)

        # 创建说明工作表
        info_df = pd.DataFrame({
            '字段名': ['title', 'author', 'publisher', 'category_id', 'tags',
                       'isbn', 'publish_year', 'description', 'cover_url', 'stock', 'price'],
            '说明': ['图书标题 (必填)', '作者名称 (必填)', '出版社', '分类ID (对应系统分类)',
                     '标签 (多个标签用逗号分隔)', 'ISBN编号', '出版年份', '图书简介',
                     '封面图片URL', '库存数量', '价格'],
            '示例': ['三体', '刘慈欣', '重庆出版社', '2 (科幻小说)', '科幻,宇宙',
                     '9787229100605', '2008', '中国著名科幻小说...', 'http://example.com/cover.jpg', '10', '45.00'],
            '是否必填': ['是', '是', '否', '否', '否', '否', '否', '否', '否', '否', '否']
        })
        info_df.to_excel(writer, sheet_name='填写说明', index=False)

        # 创建分类ID工作表
        category_df = pd.DataFrame({
            '分类ID': list(category_map.keys()),
            '分类名称': list(category_map.values())
        })
        category_df.to_excel(writer, sheet_name='分类对照表', index=False)

    # 设置响应
    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    filename = f'book_import_template_{timestamp}.xlsx'

    # 记录操作日志
    Log.add_log(
        action='下载图书导入模板',
        user_id=current_user.id,
        ip_address=request.remote_addr,
        description="下载图书批量导入Excel模板"
    )

    # 直接返回生成的文件
    from flask import send_file
    from io import BytesIO

    # 将文件转换为二进制流
    output = BytesIO()
    with open('book_import_template.xlsx', 'rb') as f:
        output.write(f.read())
    output.seek(0)

    # 删除临时文件
    try:
        os.remove('book_import_template.xlsx')
    except:
        pass

    # 通过send_file返回
    return send_file(
        output,
        mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        as_attachment=True,
        download_name=filename
    )

================================================================================
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')

    # 记录获取借阅趋势数据的日志
    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.due_date < day_end,
                BorrowRecord.return_date.is_(None)
            ).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()

            # 当天逾期未还的数量
            overdue_count = BorrowRecord.query.filter(
                BorrowRecord.due_date < day_end,
                BorrowRecord.return_date.is_(None)
            ).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()

            # 当月逾期未还的数量
            overdue_count = BorrowRecord.query.filter(
                BorrowRecord.due_date < month_end,
                BorrowRecord.return_date.is_(None)
            ).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'))

    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/<int:book_id>', methods=['POST'])
@login_required
def add_borrow(book_id):
    # 验证图书存在
    book = Book.query.get_or_404(book_id)

    # 默认借阅天数
    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/<int:borrow_id>', 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/<int:borrow_id>', 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/<int:borrow_id>', 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/<int:announcement_id>', 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/<int:announcement_id>', 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/<int:announcement_id>', 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/<int:announcement_id>', 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/<int:notification_id>')
@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/<int:book_id>', methods=['GET', 'POST'])
@login_required
@permission_required('manage_inventory')  # 替代 @admin_required
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/<int:book_id>/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


class UserService:
    @staticmethod
    def get_users(page=1, per_page=10, search_query=None, status=None, role_id=None):
        """
        获取用户列表，支持分页、搜索和过滤
        """
        query = User.query

        # 搜索条件
        if search_query:
            query = query.filter(or_(
                User.username.like(f'%{search_query}%'),
                User.email.like(f'%{search_query}%'),
                User.nickname.like(f'%{search_query}%'),
                User.phone.like(f'%{search_query}%')
            ))

        # 状态过滤
        if status is not None:
            query = query.filter(User.status == status)

        # 角色过滤
        if role_id is not None:
            query = query.filter(User.role_id == role_id)

        # 分页
        pagination = query.order_by(User.id.desc()).paginate(
            page=page, per_page=per_page, error_out=False
        )

        return pagination

    @staticmethod
    def get_user_by_id(user_id):
        """通过ID获取用户"""
        return User.query.get(user_id)

    @staticmethod
    def update_user(user_id, data):
        """更新用户信息"""
        user = User.query.get(user_id)
        if not user:
            return False, "用户不存在"

        try:
            # 更新可编辑字段
            if 'email' in data:
                # 检查邮箱是否已被其他用户使用
                existing = User.query.filter(User.email == data['email'], User.id != user_id).first()
                if existing:
                    return False, "邮箱已被使用"
                user.email = data['email']

            if 'phone' in data:
                # 检查手机号是否已被其他用户使用
                existing = User.query.filter(User.phone == data['phone'], User.id != user_id).first()
                if existing:
                    return False, "手机号已被使用"
                user.phone = data['phone']

            if 'nickname' in data and data['nickname']:
                user.nickname = data['nickname']

            # 只有管理员可以修改这些字段
            if 'role_id' in data:
                user.role_id = data['role_id']

            if 'status' in data:
                user.status = data['status']

            if 'password' in data and data['password']:
                user.set_password(data['password'])

            user.updated_at = datetime.now()
            db.session.commit()
            return True, "用户信息更新成功"

        except Exception as e:
            db.session.rollback()
            return False, f"更新失败: {str(e)}"

    @staticmethod
    def change_user_status(user_id, status):
        """变更用户状态 (启用/禁用)"""
        user = User.query.get(user_id)
        if not user:
            return False, "用户不存在"

        try:
            user.status = status
            user.updated_at = datetime.now()
            db.session.commit()
            status_text = "启用" if status == 1 else "禁用"
            return True, f"用户已{status_text}"
        except Exception as e:
            db.session.rollback()
            return False, f"状态变更失败: {str(e)}"

    @staticmethod
    def delete_user(user_id):
        """删除用户 (软删除，将状态设为-1)"""
        user = User.query.get(user_id)
        if not user:
            return False, "用户不存在"

        try:
            user.status = -1  # 软删除，设置状态为-1
            user.updated_at = datetime.now()
            db.session.commit()
            return True, "用户已删除"
        except Exception as e:
            db.session.rollback()
            return False, f"删除失败: {str(e)}"

    @staticmethod
    def get_all_roles():
        """获取所有角色"""
        return Role.query.all()

    @staticmethod
    def create_role(role_name, description, 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)}'

