
================================================================================
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')

# 数据库连接字符串
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
from app.models.user import db, User
from app.controllers.user import user_bp
from app.controllers.book import book_bp  # 引入图书蓝图
import os


def create_app():
    app = Flask(__name__)

    # 配置应用
    app.config.from_mapping(
        SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'),
        SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system',
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
        PERMANENT_SESSION_LIFETIME=86400 * 7,  # 7天

        # 邮件配置
        EMAIL_HOST='smtp.qq.com',
        EMAIL_PORT=587,
        EMAIL_ENCRYPTION='starttls',
        EMAIL_USERNAME='3399560459@qq.com',
        EMAIL_PASSWORD='fzwhyirhbqdzcjgf',  # 这是你的SMTP授权码，不是邮箱密码
        EMAIL_FROM='3399560459@qq.com',
        EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
    )

    # 实例配置，如果存在
    app.config.from_pyfile('config.py', silent=True)

    # 初始化数据库
    db.init_app(app)

    # 注册蓝图
    app.register_blueprint(user_bp, url_prefix='/user')
    app.register_blueprint(book_bp, url_prefix='/book')  # 注册图书蓝图

    # 创建数据库表
    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

        # 现在添加反向关系
        # 这样可以确保所有类都已经定义好
        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():
        if not g.user:
            return render_template('login.html')
        return render_template('index.html', current_user=g.user)

    # 错误处理
    @app.errorhandler(404)
    def page_not_found(e):
        return render_template('404.html'), 404

    # 模板过滤器
    @app.template_filter('nl2br')
    def nl2br_filter(s):
        if not s:
            return s
        return s.replace('\n', '<br>')

    return app

================================================================================
File: ./app/utils/auth.py
================================================================================

from functools import wraps
from flask import g, redirect, url_for, flash, request

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            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):
        if g.user is None:
            flash('请先登录', 'warning')
            return redirect(url_for('user.login', next=request.url))
        if g.user.role_id != 1:  # 假设role_id=1是管理员
            flash('权限不足', 'danger')
            return redirect(url_for('index'))
        return f(*args, **kwargs)
    return decorated_function

================================================================================
File: ./app/utils/db.py
================================================================================


================================================================================
File: ./app/utils/__init__.py
================================================================================


================================================================================
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 flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime

db = SQLAlchemy()


class User(db.Model):
    __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):
        self.username = username
        self.set_password(password)
        self.email = email
        self.phone = phone
        self.nickname = nickname
        self.role_id = role_id

    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))

    users = db.relationship('User', backref='role')

================================================================================
File: ./app/models/log.py
================================================================================


================================================================================
File: ./app/models/notification.py
================================================================================


================================================================================
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)

    # 移除所有关系引用

    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 关系会在后面步骤添加

    def __repr__(self):
        return f'<BorrowRecord {self.id}>'

================================================================================
File: ./app/models/announcement.py
================================================================================


================================================================================
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/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/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/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: 15px;
    min-width: 280px;
    position: relative;
}

.rank-badge {
    position: absolute;
    top: -10px;
    left: 10px;
    background-color: #4a89dc;
    color: white;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    font-size: 0.8rem;
    font-weight: bold;
}

.book-cover.small {
    width: 60px;
    height: 90px;
    min-width: 60px;
    margin-right: 15px;
}

.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/book-form.css
================================================================================

/* 图书表单页面样式 */
.book-form-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-form {
    margin-bottom: 30px;
}

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

/* 必填项标记 */
.required {
    color: #dc3545;
    margin-left: 2px;
}

/* 封面预览区域 */
.cover-preview-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 15px;
}

.cover-preview {
    width: 100%;
    max-width: 200px;
    height: 280px;
    border: 1px dashed #ccc;
    border-radius: 4px;
    overflow: hidden;
    background-color: #f8f9fa;
    margin-bottom: 10px;
}

.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: #aaa;
}

.no-cover-placeholder i {
    font-size: 48px;
    margin-bottom: 10px;
}

.upload-container {
    width: 100%;
    max-width: 200px;
}

/* 提交按钮容器 */
.form-submit-container {
    margin-top: 30px;
}

/* 响应式调整 */
@media (max-width: 768px) {
    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 15px;
    }

    .actions {
        width: 100%;
    }
}

================================================================================
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/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-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-btn').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');
          // 移除图书卡片
          setTimeout(() => {
            location.reload();
          }, 800);
        } else {
          showNotification(response.message, 'error');
        }
      },
      error: function() {
        showNotification('删除操作失败，请稍后重试', 'error');
      }
    });
  });

  // 处理借阅图书
  $('.borrow-btn').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/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">8,567</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">1,245</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">352</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">{{ 5 }}</p>
        </div>
    </div>
</div>

<!-- 主要内容区 -->
<div class="main-sections">
    <!-- 最新图书 -->
    <div class="content-section book-section">
        <div class="section-header">
            <h2>最新图书</h2>
            <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
        </div>
        <div class="book-grid">
            {% 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 %}
        </div>
    </div>

    <!-- 通知公告 -->
    <div class="content-section notice-section">
        <div class="section-header">
            <h2>通知公告</h2>
            <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
        </div>
        <div class="notice-list">
            <div class="notice-item">
                <div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
                <div class="notice-content">
                    <h3>关于五一假期图书馆开放时间调整的通知</h3>
                    <p>五一期间(5月1日-5日)，图书馆开放时间调整为上午9:00-下午5:00。</p>
                    <div class="notice-meta">
                        <span class="notice-time">2023-04-28</span>
                    </div>
                </div>
            </div>
            <div class="notice-item">
                <div class="notice-icon"><i class="fas fa-bell"></i></div>
                <div class="notice-content">
                    <h3>您有2本图书即将到期</h3>
                    <p>《Python编程》《算法导论》将于3天后到期，请及时归还或办理续借。</p>
                    <div class="notice-meta">
                        <span class="notice-time">2023-04-27</span>
                        <button class="renew-btn">一键续借</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 热门图书区域 -->
<div class="content-section popular-section">
    <div class="section-header">
        <h2>热门图书</h2>
        <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
    </div>
    <div class="popular-books">
        {% 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 %}
    </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://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.book_list') }}"><i class="fas fa-book"></i> 图书浏览</a>
                </li>
                <li class="{% if '/borrow' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a>
                </li>
                <li class="{% if '/announcement' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-bell"></i> 通知公告</a>
                </li>
                {% if current_user.role_id == 1 %}
                <li class="nav-category">管理功能</li>
                <li class="{% if '/user/manage' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-users"></i> 用户管理</a>
                </li>
                <li class="{% if '/book/list' in request.path %}active{% endif %}">
                    <a href="{{ url_for('book.book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
                </li>
                <li class="{% if '/borrow/manage' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a>
                </li>
                <li class="{% if '/inventory' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-warehouse"></i> 库存管理</a>
                </li>
                <li class="{% if '/statistics' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a>
                </li>
                <li class="{% if '/log' in request.path %}active{% endif %}">
                    <a href="#"><i class="fas fa-history"></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">
                        <i class="fas fa-bell"></i>
                        <span class="badge">3</span>
                    </div>
                    <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="#"><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>
                </div>
            </header>

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

    <!-- 通用JavaScript -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 用户菜单下拉
            const userInfo = document.querySelector('.user-info');
            userInfo.addEventListener('click', function(e) {
                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/main.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>© 2025 图书管理系统 - 版权所有</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') }}">
    <style>
        .error-container {
            text-align: center;
            padding: 50px 20px;
        }
        .error-code {
            font-size: 100px;
            font-weight: bold;
            color: #4a89dc;
            margin-bottom: 20px;
        }
        .error-message {
            font-size: 24px;
            color: #333;
            margin-bottom: 30px;
        }
        .back-button {
            display: inline-block;
            padding: 10px 20px;
            background-color: #4a89dc;
            color: white;
            text-decoration: none;
            border-radius: 5px;
            font-weight: 500;
        }
        .back-button:hover {
            background-color: #3b78c4;
        }
    </style>
</head>
<body>
    <div class="main-container">
        <div class="error-container">
            <div class="error-code">404</div>
            <div class="error-message">页面未找到</div>
            <p>抱歉，您访问的页面不存在或已被移除。</p>
            <p style="margin-bottom: 30px;">请检查URL是否正确，或返回首页。</p>
            <a href="{{ url_for('index') }}" class="back-button">返回首页</a>
        </div>
    </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/main.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>© 2025 图书管理系统 - 版权所有</p>
    </footer>

    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
================================================================================
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">
    <!-- 添加泡泡动画元素 -->
    <!-- 这些泡泡会通过 JS 动态创建 -->

    <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 %}
        <div class="book-card">
            <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>


{% 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') }}">
{% endblock %}

{% block content %}
<div class="book-form-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>

    <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-row">
                            <div class="form-group col-md-12">
                                <label for="title">书名 <span class="required">*</span></label>
                                <input type="text" class="form-control" id="title" name="title" required>
                            </div>
                        </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" required>
                            </div>
                            <div class="form-group col-md-6">
                                <label for="publisher">出版社</label>
                                <input type="text" class="form-control" id="publisher" name="publisher">
                            </div>
                        </div>

                        <div class="form-row">
                            <div class="form-group col-md-6">
                                <label for="isbn">ISBN</label>
                                <input type="text" class="form-control" id="isbn" name="isbn">
                            </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">
                            </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 }}">{{ 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" placeholder="多个标签用逗号分隔">
                            </div>
                        </div>
                    </div>
                </div>

                <div class="card mt-4">
                    <div class="card-header">图书简介</div>
                    <div class="card-body">
                        <div class="form-group">
                            <textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介"></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-container">
                            <div class="cover-preview" id="coverPreview">
                                <div class="no-cover-placeholder">
                                    <i class="fas fa-image"></i>
                                    <span>暂无封面</span>
                                </div>
                            </div>
                            <div class="upload-container">
                                <label for="cover" class="btn btn-outline-primary btn-block">
                                    <i class="fas fa-upload"></i> 上传封面
                                </label>
                                <input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
                            </div>
                        </div>
                    </div>
                </div>

                <div class="card mt-4">
                    <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="0">
                        </div>
                        <div class="form-group">
                            <label for="price">价格</label>
                            <div class="input-group">
                                <div class="input-group-prepend">
                                    <span class="input-group-text">¥</span>
                                </div>
                                <input type="number" class="form-control" id="price" name="price" step="0.01" min="0">
                            </div>
                        </div>
                    </div>
                </div>

                <div class="form-submit-container">
                    <button type="submit" class="btn btn-primary btn-lg btn-block">
                        <i class="fas fa-save"></i> 保存图书
                    </button>
                </div>
            </div>
        </div>
    </form>
</div>
{% endblock %}

{% block scripts %}
<script>
    $(document).ready(function() {
        // 封面预览
        $('#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">`);
                }
                reader.readAsDataURL(file);
            } else {
                $('#coverPreview').html(`
                    <div class="no-cover-placeholder">
                        <i class="fas fa-image"></i>
                        <span>暂无封面</span>
                    </div>
                `);
            }
        });
    });
</script>
{% endblock %}

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

{% extends 'base.html' %}

{% block title %}编辑图书 - {{ book.title }}{% endblock %}

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

{% block content %}
<div class="book-form-container">
    <div class="page-header">
        <h1>编辑图书</h1>
        <div class="actions">
            <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info">
                <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-row">
                            <div class="form-group col-md-12">
                                <label for="title">书名 <span class="required">*</span></label>
                                <input type="text" class="form-control" id="title" name="title" value="{{ book.title }}" required>
                            </div>
                        </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" id="isbn" name="isbn" value="{{ book.isbn or '' }}">
                            </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 mt-4">
                    <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-container">
                            <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-image"></i>
                                    <span>暂无封面</span>
                                </div>
                                {% endif %}
                            </div>
                            <div class="upload-container">
                                <label for="cover" class="btn btn-outline-primary btn-block">
                                    <i class="fas fa-upload"></i> 更换封面
                                </label>
                                <input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
                            </div>
                        </div>
                    </div>
                </div>

                <div class="card mt-4">
                    <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>
                            <div class="input-group">
                                <div class="input-group-prepend">
                                    <span class="input-group-text">¥</span>
                                </div>
                                <input type="number" class="form-control" id="price" name="price" step="0.01" min="0" value="{{ book.price or '' }}">
                            </div>
                        </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">
                        <i class="fas fa-save"></i> 保存修改
                    </button>
                </div>
            </div>
        </div>
    </form>
</div>
{% endblock %}

{% block scripts %}
<script>
    $(document).ready(function() {
        // 封面预览
        $('#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">`);
                }
                reader.readAsDataURL(file);
            } else {
                $('#coverPreview').html(`
                    {% 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-image"></i>
                        <span>暂无封面</span>
                    </div>
                    {% endif %}
                `);
            }
        });
    });
</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') }}">
{% endblock %}

{% block content %}
<div class="import-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-8 offset-md-2">
            <div class="card">
                <div class="card-header">
                    <h4>Excel文件导入</h4>
                </div>
                <div class="card-body">
                    <form method="POST" enctype="multipart/form-data">
                        <div class="form-group">
                            <label for="file">选择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">
                            <i class="fas fa-upload"></i> 开始导入
                        </button>
                    </form>

                    <hr>

                    <div class="import-instructions">
                        <h5>导入说明:</h5>
                        <ul>
                            <li>Excel文件须包含以下列 (标题行必须与下列完全一致):</li>
                            <li class="required-field">title - 图书标题 (必填)</li>
                            <li class="required-field">author - 作者名称 (必填)</li>
                            <li>publisher - 出版社</li>
                            <li>category_id - 分类ID (对应系统中的分类ID)</li>
                            <li>tags - 标签 (多个标签用逗号分隔)</li>
                            <li>isbn - ISBN编号 (建议唯一)</li>
                            <li>publish_year - 出版年份</li>
                            <li>description - 图书简介</li>
                            <li>cover_url - 封面图片URL</li>
                            <li>stock - 库存数量</li>
                            <li>price - 价格</li>
                        </ul>

                        <div class="template-download">
                            <p>下载导入模板:</p>
                            <a href="{{ url_for('static', filename='templates/book_import_template.xlsx') }}" class="btn btn-outline-primary">
                                <i class="fas fa-download"></i> 下载Excel模板
                            </a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    $(document).ready(function() {
        // 显示选择的文件名
        $('.custom-file-input').on('change', function() {
            const fileName = $(this).val().split('\\').pop();
            $(this).next('.custom-file-label').html(fileName);
        });
    });
</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">
            <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
                <i class="fas fa-arrow-left"></i> 返回列表
            </a>
            {% if 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>
            {% 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.role_id == 1 %}
        <div class="book-borrow-history">
            <h3>借阅历史</h3>
            {% set borrow_records = book.borrow_records.order_by(BorrowRecord.borrow_date.desc()).limit(10).all() %}

            {% 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">
    <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/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.utils.email import send_verification_email, generate_verification_code
import logging
from functools import wraps
import time
from datetime import datetime, timedelta

# 创建蓝图
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 login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            return redirect(url_for('user.login'))
        return f(*args, **kwargs)

    return decorated_function


@user_bp.route('/login', methods=['GET', 'POST'])
def login():
    # 保持原代码不变
    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):
            return render_template('login.html', error='用户名或密码错误')

        if user.status == 0:
            return render_template('login.html', error='账号已被禁用，请联系管理员')

        # 登录成功，保存用户信息到会话
        session['user_id'] = user.id
        session['username'] = user.username
        session['role_id'] = user.role_id

        if remember_me:
            # 设置会话过期时间为7天
            session.permanent = True

        # 记录登录日志（可选）
        # log_user_action('用户登录')

        # 重定向到首页
        return redirect(url_for('index'))

    return render_template('login.html')


@user_bp.route('/register', methods=['GET', 'POST'])
def register():
    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()

            # 清除验证码
            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')
def logout():
    # 清除会话数据
    session.pop('user_id', None)
    session.pop('username', None)
    session.pop('role_id', None)
    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):
        return jsonify({'success': True, 'message': '验证码已发送'})
    else:
        return jsonify({'success': False, 'message': '邮件发送失败，请稍后重试'})

================================================================================
File: ./app/controllers/log.py
================================================================================


================================================================================
File: ./app/controllers/__init__.py
================================================================================


================================================================================
File: ./app/controllers/book.py
================================================================================

from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, g, jsonify
from app.models.book import Book, Category
from app.models.user import db
from app.utils.auth import login_required, admin_required
import os
from werkzeug.utils import secure_filename
import datetime
import pandas as pd
import uuid

book_bp = Blueprint('book', __name__)


# 图书列表页面
@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)

    query = Book.query

    # 搜索功能
    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()

    return render_template('book/list.html',
                           books=books,
                           pagination=pagination,
                           search=search,
                           categories=categories,
                           category_id=category_id,
                           sort=sort,
                           order=order,
                           current_user=g.user)


# 图书详情页面
@book_bp.route('/detail/<int:book_id>')
@login_required
def book_detail(book_id):
    book = Book.query.get_or_404(book_id)
    return render_template('book/detail.html', book=book, current_user=g.user)


# 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST'])
@login_required
@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)
        price = request.form.get('price')

        if not title or not author:
            flash('书名和作者不能为空', 'danger')
            categories = Category.query.all()
            return render_template('book/add.html', categories=categories, current_user=g.user)

        # 处理封面图片上传
        cover_url = None
        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, 'uploads/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}'

        # 创建新图书
        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)

        # 记录库存日志
        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=g.user.id,
                remark='新书入库',
                changed_at=datetime.datetime.now()
            )
            db.session.add(inventory_log)

        db.session.commit()

        flash('图书添加成功', 'success')
        return redirect(url_for('book.book_list'))

    categories = Category.query.all()
    return render_template('book/add.html', categories=categories, current_user=g.user)


# 编辑图书
@book_bp.route('/edit/<int:book_id>', methods=['GET', 'POST'])
@login_required
@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=g.user)

        # 处理库存变更
        new_stock = request.form.get('stock', type=int)
        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=g.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, 'uploads/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}'

        # 更新图书信息
        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()

        db.session.commit()

        flash('图书信息更新成功', 'success')
        return redirect(url_for('book.book_list'))

    categories = Category.query.all()
    return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)


# 删除图书
@book_bp.route('/delete/<int:book_id>', methods=['POST'])
@login_required
@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:
        return jsonify({'success': False, 'message': '该图书有未归还的借阅记录，无法删除'})

    # 考虑软删除而不是物理删除
    book.status = 0  # 0表示已删除/下架
    book.updated_at = datetime.datetime.now()
    db.session.commit()

    return jsonify({'success': True, 'message': '图书已成功下架'})


# 图书分类管理
@book_bp.route('/categories', methods=['GET'])
@login_required
@admin_required
def category_list():
    categories = Category.query.all()
    return render_template('book/categories.html', categories=categories, current_user=g.user)


# 添加分类
@book_bp.route('/categories/add', methods=['POST'])
@login_required
@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()

    return jsonify({'success': True, 'message': '分类添加成功', 'id': category.id, 'name': category.name})


# 编辑分类
@book_bp.route('/categories/edit/<int:category_id>', methods=['POST'])
@login_required
@admin_required
def edit_category(category_id):
    category = Category.query.get_or_404(category_id)

    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()

    return jsonify({'success': True, 'message': '分类更新成功'})


# 删除分类
@book_bp.route('/categories/delete/<int:category_id>', methods=['POST'])
@login_required
@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:
        return jsonify({'success': False, 'message': f'该分类下有{books_count}本图书，无法删除'})

    # 检查是否有子分类
    children_count = Category.query.filter_by(parent_id=category_id).count()
    if children_count > 0:
        return jsonify({'success': False, 'message': f'该分类下有{children_count}个子分类，无法删除'})

    db.session.delete(category)
    db.session.commit()

    return jsonify({'success': True, 'message': '分类删除成功'})


# 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST'])
@login_required
@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=g.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()
                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:
                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=g.user)


# 导出图书
@book_bp.route('/export')
@login_required
@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)

    # 提供下载链接
    return redirect(url_for('static', filename=f'temp/{filename}'))

================================================================================
File: ./app/controllers/statistics.py
================================================================================


================================================================================
File: ./app/controllers/borrow.py
================================================================================


================================================================================
File: ./app/controllers/announcement.py
================================================================================


================================================================================
File: ./app/controllers/inventory.py
================================================================================


================================================================================
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
================================================================================

