================================================================================
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, Markup
from flask_login import LoginManager
from app.models.user import db, User
from app.controllers.user import user_bp
from app.controllers.book import book_bp
from app.controllers.borrow import borrow_bp
import os
login_manager = LoginManager()
def create_app(config=None):
    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',
        EMAIL_FROM='3399560459@qq.com',
        EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
    )
    # 实例配置,如果存在
    app.config.from_pyfile('config.py', silent=True)
    # 初始化数据库
    db.init_app(app)
    # 初始化 Flask-Login
    login_manager.init_app(app)
    login_manager.login_view = 'user.login'
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    # 注册蓝图
    app.register_blueprint(user_bp, url_prefix='/user')
    app.register_blueprint(book_bp, url_prefix='/book')
    app.register_blueprint(borrow_bp, url_prefix='/borrow')
    # 创建数据库表
    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 s:
            return Markup(s.replace('\n', '
'))
        return s
    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"""
        
        
            
                图书管理系统 - 邮箱验证
                您好,
                感谢您注册图书管理系统,您的验证码是:
                
                    {verification_code}
                
                该验证码将在10分钟内有效,请勿将验证码分享给他人。
                如果您没有请求此验证码,请忽略此邮件。
                
                    此邮件为系统自动发送,请勿回复。
                    © 2025 图书管理系统
                 
             
        
        
        """
        msg.attach(MIMEText(body, 'html'))
        logger.debug("尝试连接到SMTP服务器...")
        # 连接服务器发送邮件
        server = smtplib.SMTP(email_host, email_port)
        server.set_debuglevel(1)  # 启用详细的SMTP调试输出
        logger.debug("检查是否需要STARTTLS加密...")
        if current_app.config.get('EMAIL_ENCRYPTION') == 'starttls':
            logger.debug("启用STARTTLS...")
            server.starttls()
        logger.debug(f"尝试登录邮箱: {email_username}")
        server.login(email_username, email_password)
        logger.debug("发送邮件...")
        server.send_message(msg)
        logger.debug("关闭连接...")
        server.quit()
        logger.info(f"邮件发送成功: {to_email}")
        return True
    except Exception as e:
        logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
        return False
def generate_verification_code(length=6):
    """
    生成数字验证码
    """
    return ''.join(random.choice(string.digits) for _ in range(length))
================================================================================
File: ./app/utils/helpers.py
================================================================================
================================================================================
File: ./app/models/user.py
================================================================================
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from flask_login import UserMixin
db = SQLAlchemy()
class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(64), unique=True, nullable=False)
    password = db.Column(db.String(255), nullable=False)
    email = db.Column(db.String(128), unique=True, nullable=True)
    phone = db.Column(db.String(20), unique=True, nullable=True)
    nickname = db.Column(db.String(64), nullable=True)
    status = db.Column(db.Integer, default=1)  # 1: active, 0: disabled
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2)  # 2: 普通用户, 1: 管理员
    created_at = db.Column(db.DateTime, default=datetime.now)
    updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
    def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2):
        self.username = username
        self.set_password(password)
        self.email = email
        self.phone = phone
        self.nickname = nickname
        self.role_id = role_id
    def is_active(self):
        return self.status == 1
    def set_password(self, password):
        """设置密码,使用哈希加密"""
        self.password = generate_password_hash(password)
    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''
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''
================================================================================
File: ./app/models/borrow.py
================================================================================
from app.models.user import db
from datetime import datetime
class BorrowRecord(db.Model):
    __tablename__ = 'borrow_records'
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False)
    borrow_date = db.Column(db.DateTime, nullable=False, default=datetime.now)
    due_date = db.Column(db.DateTime, nullable=False)
    return_date = db.Column(db.DateTime, nullable=True)
    renew_count = db.Column(db.Integer, default=0)
    status = db.Column(db.Integer, default=1)  # 1: 借出, 0: 已归还
    remark = db.Column(db.String(255), nullable=True)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
    updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
    # 添加反向关系引用
    user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic'))
    book = db.relationship('Book', backref=db.backref('borrow_records', lazy='dynamic'))
    # book 关系会在后面步骤添加
    def __repr__(self):
        return f''
================================================================================
File: ./app/models/announcement.py
================================================================================
================================================================================
File: ./app/models/inventory.py
================================================================================
from app.models.user import db
from datetime import datetime
class InventoryLog(db.Model):
    __tablename__ = 'inventory_logs'
    id = db.Column(db.Integer, primary_key=True)
    book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False)
    change_type = db.Column(db.String(32), nullable=False)  # 'in' 入库, 'out' 出库
    change_amount = db.Column(db.Integer, nullable=False)
    after_stock = db.Column(db.Integer, nullable=False)
    operator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
    remark = db.Column(db.String(255), nullable=True)
    changed_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
    # 添加反向关系引用
    operator = db.relationship('User', backref=db.backref('inventory_logs', lazy='dynamic'))
    # book 关系会在后面步骤添加
    def __repr__(self):
        return f''
================================================================================
File: ./app/static/css/register.css
================================================================================
/* register.css - 注册页面专用样式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
:root {
    --primary-color: #4a89dc;
    --primary-hover: #3b78c4;
    --secondary-color: #5cb85c;
    --text-color: #333;
    --light-text: #666;
    --bg-color: #f5f7fa;
    --card-bg: #ffffff;
    --border-color: #ddd;
    --error-color: #e74c3c;
    --success-color: #2ecc71;
}
body.dark-mode {
    --primary-color: #5a9aed;
    --primary-hover: #4a89dc;
    --secondary-color: #6bc76b;
    --text-color: #f1f1f1;
    --light-text: #aaa;
    --bg-color: #1a1a1a;
    --card-bg: #2c2c2c;
    --border-color: #444;
}
body {
    background-color: var(--bg-color);
    background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
    background-size: cover;
    background-position: center;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    color: var(--text-color);
    transition: all 0.3s ease;
}
.theme-toggle {
    position: absolute;
    top: 20px;
    right: 20px;
    z-index: 10;
    cursor: pointer;
    padding: 8px;
    border-radius: 50%;
    background-color: rgba(255, 255, 255, 0.2);
    backdrop-filter: blur(5px);
    border: 1px solid rgba(255, 255, 255, 0.1);
}
.overlay {
    background-color: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(5px);
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: -1;
}
.main-container {
    display: flex;
    justify-content: center;
    align-items: center;
    flex: 1;
    padding: 20px;
}
.login-container {
    background-color: var(--card-bg);
    border-radius: 12px;
    box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
    width: 450px;
    padding: 35px;
    position: relative;
    overflow: hidden;
    animation: fadeIn 0.5s ease;
}
.register-container {
    width: 500px;
}
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}
.logo {
    text-align: center;
    margin-bottom: 25px;
    position: relative;
}
.logo img {
    width: 90px;
    height: 90px;
    border-radius: 12px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    padding: 5px;
    background-color: #fff;
    transition: transform 0.3s ease;
}
h1 {
    text-align: center;
    color: var(--text-color);
    margin-bottom: 10px;
    font-weight: 600;
    font-size: 28px;
}
.subtitle {
    text-align: center;
    color: var(--light-text);
    margin-bottom: 30px;
    font-size: 14px;
}
.form-group {
    margin-bottom: 22px;
    position: relative;
}
.form-group label {
    display: block;
    margin-bottom: 8px;
    color: var(--text-color);
    font-weight: 500;
    font-size: 14px;
}
.input-with-icon {
    position: relative;
}
.input-icon {
    position: absolute;
    left: 15px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--light-text);
}
.form-control {
    width: 100%;
    height: 48px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 0 15px 0 45px;
    font-size: 15px;
    transition: all 0.3s ease;
    background-color: var(--card-bg);
    color: var(--text-color);
}
.form-control:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
    outline: none;
}
.password-toggle {
    position: absolute;
    right: 15px;
    top: 50%;
    transform: translateY(-50%);
    cursor: pointer;
    color: var(--light-text);
}
.validation-message {
    margin-top: 6px;
    font-size: 12px;
    color: var(--error-color);
    display: none;
}
.validation-message.show {
    display: block;
    animation: shake 0.5s ease;
}
@keyframes shake {
    0%, 100% { transform: translateX(0); }
    10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
    20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.btn-login {
    width: 100%;
    height: 48px;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
    position: relative;
    overflow: hidden;
}
.btn-login:hover {
    background-color: var(--primary-hover);
}
.btn-login:active {
    transform: scale(0.98);
}
.btn-login .loading {
    display: none;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
.btn-login.loading-state {
    color: transparent;
}
.btn-login.loading-state .loading {
    display: block;
}
.signup {
    text-align: center;
    margin-top: 25px;
    font-size: 14px;
    color: var(--light-text);
}
.signup a {
    color: var(--primary-color);
    text-decoration: none;
    font-weight: 600;
    transition: color 0.3s ease;
}
.signup a:hover {
    color: var(--primary-hover);
    text-decoration: underline;
}
.alert {
    padding: 10px;
    margin-bottom: 15px;
    border-radius: 4px;
    color: #721c24;
    background-color: #f8d7da;
    border: 1px solid #f5c6cb;
}
.verification-code-container {
    display: flex;
    gap: 10px;
}
.verification-input {
    flex: 1;
    height: 48px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 0 15px;
    font-size: 15px;
    transition: all 0.3s ease;
    background-color: var(--card-bg);
    color: var(--text-color);
}
.send-code-btn {
    padding: 0 15px;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 14px;
    white-space: nowrap;
    transition: all 0.3s ease;
}
.send-code-btn:hover {
    background-color: var(--primary-hover);
}
.send-code-btn:disabled {
    background-color: #ccc;
    cursor: not-allowed;
}
footer {
    text-align: center;
    padding: 20px;
    color: rgba(255, 255, 255, 0.7);
    font-size: 12px;
}
footer a {
    color: rgba(255, 255, 255, 0.9);
    text-decoration: none;
}
@media (max-width: 576px) {
    .login-container, .register-container {
        width: 100%;
        padding: 25px;
        border-radius: 0;
    }
    .theme-toggle {
        top: 10px;
    }
    .logo img {
        width: 70px;
        height: 70px;
    }
    h1 {
        font-size: 22px;
    }
    .main-container {
        padding: 0;
    }
    .verification-code-container {
        flex-direction: column;
    }
}
================================================================================
File: ./app/static/css/user-list.css
================================================================================
/* 用户列表页面样式 */
.user-list-container {
    padding: 20px;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题和操作按钮 */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 25px;
    padding-bottom: 15px;
    border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
    font-size: 1.8rem;
    color: #333;
    margin: 0;
}
.page-header .actions {
    display: flex;
    gap: 10px;
}
/* 搜索和筛选区域 */
.search-filter-container {
    margin-bottom: 20px;
    padding: 20px;
    background-color: #f9f9f9;
    border-radius: 6px;
}
.search-filter-form .form-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 15px;
}
.search-box {
    position: relative;
    flex: 1;
    min-width: 250px;
}
.search-box input {
    padding-right: 40px;
    border-radius: 4px;
    border: 1px solid #ddd;
}
.btn-search {
    position: absolute;
    right: 5px;
    top: 5px;
    background: none;
    border: none;
    color: #666;
}
.filter-box {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}
.filter-box select {
    min-width: 120px;
    border-radius: 4px;
    border: 1px solid #ddd;
    padding: 5px 10px;
}
.btn-filter, .btn-reset {
    padding: 6px 15px;
    border-radius: 4px;
}
.btn-filter {
    background-color: #4c84ff;
    color: white;
    border: none;
}
.btn-reset {
    background-color: #f8f9fa;
    color: #333;
    border: 1px solid #ddd;
}
/* 表格样式 */
.table {
    width: 100%;
    margin-bottom: 0;
    color: #333;
    border-collapse: collapse;
}
.table th {
    background-color: #f8f9fa;
    padding: 12px 15px;
    font-weight: 600;
    text-align: left;
    border-top: 1px solid #dee2e6;
    border-bottom: 1px solid #dee2e6;
}
.table td {
    padding: 12px 15px;
    vertical-align: middle;
    border-bottom: 1px solid #f0f0f0;
}
.table tr:hover {
    background-color: #f8f9fa;
}
/* 状态标签 */
.status-badge {
    display: inline-block;
    padding: 5px 10px;
    border-radius: 20px;
    font-size: 0.85rem;
    font-weight: 500;
}
.status-badge.active {
    background-color: #e8f5e9;
    color: #43a047;
}
.status-badge.inactive {
    background-color: #ffebee;
    color: #e53935;
}
/* 操作按钮 */
.actions {
    display: flex;
    gap: 5px;
    align-items: center;
}
.actions .btn {
    padding: 5px 8px;
    line-height: 1;
}
/* 分页控件 */
.pagination-container {
    margin-top: 20px;
    display: flex;
    justify-content: center;
}
.pagination {
    display: flex;
    padding-left: 0;
    list-style: none;
    border-radius: 0.25rem;
}
.page-item {
    margin: 0 2px;
}
.page-link {
    position: relative;
    display: block;
    padding: 0.5rem 0.75rem;
    margin-left: -1px;
    color: #4c84ff;
    background-color: #fff;
    border: 1px solid #dee2e6;
    text-decoration: none;
}
.page-item.active .page-link {
    z-index: 3;
    color: #fff;
    background-color: #4c84ff;
    border-color: #4c84ff;
}
.page-item.disabled .page-link {
    color: #aaa;
    pointer-events: none;
    background-color: #f8f9fa;
    border-color: #dee2e6;
}
/* 通知样式 */
.alert-box {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 1050;
}
.alert-box .alert {
    margin-bottom: 10px;
    padding: 10px 15px;
    border-radius: 4px;
    opacity: 0;
    transition: opacity 0.3s ease-in-out;
}
.alert-box .fade-in {
    opacity: 1;
}
.alert-box .fade-out {
    opacity: 0;
}
/* 响应式调整 */
@media (max-width: 992px) {
    .search-filter-form .form-row {
        flex-direction: column;
    }
    .search-box, .filter-box {
        width: 100%;
    }
}
@media (max-width: 768px) {
    .table {
        display: block;
        overflow-x: auto;
    }
    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 15px;
    }
}
================================================================================
File: ./app/static/css/book-detail.css
================================================================================
/* 图书详情页样式 */
.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/login.css
================================================================================
/* login.css - 登录页面专用样式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
:root {
    --primary-color: #4a89dc;
    --primary-hover: #3b78c4;
    --secondary-color: #5cb85c;
    --text-color: #333;
    --light-text: #666;
    --bg-color: #f5f7fa;
    --card-bg: #ffffff;
    --border-color: #ddd;
    --error-color: #e74c3c;
    --success-color: #2ecc71;
}
body.dark-mode {
    --primary-color: #5a9aed;
    --primary-hover: #4a89dc;
    --secondary-color: #6bc76b;
    --text-color: #f1f1f1;
    --light-text: #aaa;
    --bg-color: #1a1a1a;
    --card-bg: #2c2c2c;
    --border-color: #444;
}
body {
    background-color: var(--bg-color);
    background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
    background-size: cover;
    background-position: center;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    color: var(--text-color);
    transition: all 0.3s ease;
}
.theme-toggle {
    position: absolute;
    top: 20px;
    right: 20px;
    z-index: 10;
    cursor: pointer;
    padding: 8px;
    border-radius: 50%;
    background-color: rgba(255, 255, 255, 0.2);
    backdrop-filter: blur(5px);
    border: 1px solid rgba(255, 255, 255, 0.1);
}
.overlay {
    background-color: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(5px);
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: -1;
}
.main-container {
    display: flex;
    justify-content: center;
    align-items: center;
    flex: 1;
    padding: 20px;
}
.login-container {
    background-color: var(--card-bg);
    border-radius: 12px;
    box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
    width: 450px;
    padding: 35px;
    position: relative;
    overflow: hidden;
    animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}
.logo {
    text-align: center;
    margin-bottom: 25px;
    position: relative;
}
.logo img {
    width: 90px;
    height: 90px;
    border-radius: 12px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    padding: 5px;
    background-color: #fff;
    transition: transform 0.3s ease;
}
h1 {
    text-align: center;
    color: var(--text-color);
    margin-bottom: 10px;
    font-weight: 600;
    font-size: 28px;
}
.subtitle {
    text-align: center;
    color: var(--light-text);
    margin-bottom: 30px;
    font-size: 14px;
}
.form-group {
    margin-bottom: 22px;
    position: relative;
}
.form-group label {
    display: block;
    margin-bottom: 8px;
    color: var(--text-color);
    font-weight: 500;
    font-size: 14px;
}
.input-with-icon {
    position: relative;
}
.input-icon {
    position: absolute;
    left: 15px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--light-text);
}
.form-control {
    width: 100%;
    height: 48px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 0 15px 0 45px;
    font-size: 15px;
    transition: all 0.3s ease;
    background-color: var(--card-bg);
    color: var(--text-color);
}
.form-control:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
    outline: none;
}
.password-toggle {
    position: absolute;
    right: 15px;
    top: 50%;
    transform: translateY(-50%);
    cursor: pointer;
    color: var(--light-text);
}
.validation-message {
    margin-top: 6px;
    font-size: 12px;
    color: var(--error-color);
    display: none;
}
.validation-message.show {
    display: block;
    animation: shake 0.5s ease;
}
@keyframes shake {
    0%, 100% { transform: translateX(0); }
    10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
    20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.remember-forgot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 25px;
}
.custom-checkbox {
    position: relative;
    padding-left: 30px;
    cursor: pointer;
    font-size: 14px;
    user-select: none;
    color: var(--light-text);
}
.custom-checkbox input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
    height: 0;
    width: 0;
}
.checkmark {
    position: absolute;
    top: 0;
    left: 0;
    height: 18px;
    width: 18px;
    background-color: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: 3px;
    transition: all 0.2s ease;
}
.custom-checkbox:hover input ~ .checkmark {
    border-color: var(--primary-color);
}
.custom-checkbox input:checked ~ .checkmark {
    background-color: var(--primary-color);
    border-color: var(--primary-color);
}
.checkmark:after {
    content: "";
    position: absolute;
    display: none;
}
.custom-checkbox input:checked ~ .checkmark:after {
    display: block;
}
.custom-checkbox .checkmark:after {
    left: 6px;
    top: 2px;
    width: 4px;
    height: 9px;
    border: solid white;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.forgot-password a {
    color: var(--primary-color);
    text-decoration: none;
    font-size: 14px;
    transition: color 0.3s ease;
}
.forgot-password a:hover {
    color: var(--primary-hover);
    text-decoration: underline;
}
.btn-login {
    width: 100%;
    height: 48px;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
    position: relative;
    overflow: hidden;
}
.btn-login:hover {
    background-color: var(--primary-hover);
}
.btn-login:active {
    transform: scale(0.98);
}
.btn-login .loading {
    display: none;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
.btn-login.loading-state {
    color: transparent;
}
.btn-login.loading-state .loading {
    display: block;
}
.signup {
    text-align: center;
    margin-top: 25px;
    font-size: 14px;
    color: var(--light-text);
}
.signup a {
    color: var(--primary-color);
    text-decoration: none;
    font-weight: 600;
    transition: color 0.3s ease;
}
.signup a:hover {
    color: var(--primary-hover);
    text-decoration: underline;
}
.features {
    display: flex;
    justify-content: center;
    margin-top: 25px;
    gap: 30px;
}
.feature-item {
    text-align: center;
    font-size: 12px;
    color: var(--light-text);
    display: flex;
    flex-direction: column;
    align-items: center;
}
.feature-icon {
    margin-bottom: 5px;
    font-size: 18px;
}
footer {
    text-align: center;
    padding: 20px;
    color: rgba(255, 255, 255, 0.7);
    font-size: 12px;
}
footer a {
    color: rgba(255, 255, 255, 0.9);
    text-decoration: none;
}
.alert {
    padding: 10px;
    margin-bottom: 15px;
    border-radius: 4px;
    color: #721c24;
    background-color: #f8d7da;
    border: 1px solid #f5c6cb;
}
@media (max-width: 576px) {
    .login-container {
        width: 100%;
        padding: 25px;
        border-radius: 0;
    }
    .theme-toggle {
        top: 10px;
    }
    .logo img {
        width: 70px;
        height: 70px;
    }
    h1 {
        font-size: 22px;
    }
    .main-container {
        padding: 0;
    }
}
================================================================================
File: ./app/static/css/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
================================================================================
/* ========== 基础重置和变量 ========== */
:root {
    --primary-color: #3b82f6;
    --primary-hover: #2563eb;
    --primary-light: #eff6ff;
    --danger-color: #ef4444;
    --success-color: #10b981;
    --warning-color: #f59e0b;
    --info-color: #3b82f6;
    --text-dark: #1e293b;
    --text-medium: #475569;
    --text-light: #64748b;
    --text-muted: #94a3b8;
    --border-color: #e2e8f0;
    --border-focus: #bfdbfe;
    --bg-white: #ffffff;
    --bg-light: #f8fafc;
    --bg-lightest: #f1f5f9;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    --radius-sm: 4px;
    --radius-md: 6px;
    --radius-lg: 8px;
    --radius-xl: 12px;
    --transition-fast: 0.15s ease;
    --transition-base: 0.3s ease;
    --transition-slow: 0.5s ease;
    --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
/* ========== 全局样式 ========== */
.book-form-container {
    padding: 24px;
    max-width: 1400px;
    margin: 0 auto;
    font-family: var(--font-sans);
    color: var(--text-dark);
}
/* ========== 页头样式 ========== */
.page-header-wrapper {
    margin-bottom: 24px;
    background-color: var(--bg-white);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 24px;
}
.header-title-section {
    display: flex;
    flex-direction: column;
}
.page-title {
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--text-dark);
    margin: 0;
}
.subtitle {
    margin: 8px 0 0 0;
    color: var(--text-medium);
    font-size: 0.9rem;
}
.header-actions {
    display: flex;
    align-items: center;
    gap: 16px;
}
.btn-back {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--text-medium);
    background-color: var(--bg-lightest);
    border-radius: var(--radius-md);
    padding: 8px 16px;
    font-size: 0.875rem;
    font-weight: 500;
    transition: all var(--transition-fast);
    text-decoration: none;
    box-shadow: var(--shadow-sm);
}
.btn-back:hover {
    background-color: var(--border-color);
    color: var(--text-dark);
    text-decoration: none;
}
.btn-back i {
    font-size: 14px;
}
/* 进度条样式 */
.form-progress {
    min-width: 180px;
}
.progress-bar-container {
    height: 6px;
    background-color: var(--bg-lightest);
    border-radius: 3px;
    overflow: hidden;
}
.progress-bar {
    height: 100%;
    background-color: var(--primary-color);
    border-radius: 3px;
    transition: width var(--transition-base);
}
.progress-text {
    font-size: 0.75rem;
    color: var(--text-light);
    text-align: right;
    display: block;
    margin-top: 4px;
}
/* ========== 表单布局 ========== */
.form-grid {
    display: grid;
    grid-template-columns: 1fr 360px;
    gap: 24px;
}
.form-main-content {
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.form-sidebar {
    display: flex;
    flex-direction: column;
    gap: 24px;
}
/* ========== 表单卡片样式 ========== */
.form-card {
    background-color: var(--bg-white);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    transition: box-shadow var(--transition-base);
}
.form-card:hover {
    box-shadow: var(--shadow-md);
}
.card-header {
    padding: 16px 20px;
    background-color: var(--bg-white);
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
}
.card-title {
    font-weight: 600;
    color: var(--text-dark);
    font-size: 0.9375rem;
}
.card-body {
    padding: 20px;
}
.form-section {
    padding: 0;
}
/* ========== 表单元素样式 ========== */
.form-row {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
    margin-bottom: 16px;
}
.form-group {
    margin-bottom: 20px;
}
.form-group:last-child {
    margin-bottom: 0;
}
.form-label {
    display: block;
    font-weight: 500;
    color: var(--text-dark);
    margin-bottom: 8px;
    font-size: 0.9375rem;
}
.form-control {
    display: block;
    width: 100%;
    padding: 10px 14px;
    font-size: 0.9375rem;
    line-height: 1.5;
    color: var(--text-dark);
    background-color: var(--bg-white);
    background-clip: padding-box;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.form-control:focus {
    border-color: var(--border-focus);
    outline: 0;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.form-control::placeholder {
    color: var(--text-muted);
}
.form-control:disabled, .form-control[readonly] {
    background-color: var(--bg-lightest);
    opacity: 0.6;
}
.form-help {
    margin-top: 6px;
    font-size: 0.8125rem;
    color: var(--text-light);
}
.form-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 8px;
}
.char-counter {
    font-size: 0.8125rem;
    color: var(--text-muted);
}
/* 带按钮输入框 */
.input-with-button {
    display: flex;
    align-items: center;
}
.input-with-button .form-control {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    flex-grow: 1;
}
.btn-append {
    height: 42px;
    padding: 0 14px;
    background-color: var(--bg-lightest);
    border: 1px solid var(--border-color);
    border-left: none;
    border-top-right-radius: var(--radius-md);
    border-bottom-right-radius: var(--radius-md);
    color: var(--text-medium);
    cursor: pointer;
    transition: background-color var(--transition-fast);
}
.btn-append:hover {
    background-color: var(--border-color);
    color: var(--text-dark);
}
/* 文本域 */
textarea.form-control {
    min-height: 150px;
    resize: vertical;
}
/* 数字输入控件 */
.number-control {
    display: flex;
    align-items: center;
    width: 100%;
    border-radius: var(--radius-md);
    overflow: hidden;
}
.number-btn {
    width: 42px;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--bg-lightest);
    border: 1px solid var(--border-color);
    color: var(--text-medium);
    cursor: pointer;
    transition: all var(--transition-fast);
    font-size: 1rem;
    user-select: none;
}
.number-btn:hover {
    background-color: var(--border-color);
    color: var(--text-dark);
}
.decrement {
    border-top-left-radius: var(--radius-md);
    border-bottom-left-radius: var(--radius-md);
}
.increment {
    border-top-right-radius: var(--radius-md);
    border-bottom-right-radius: var(--radius-md);
}
.number-control .form-control {
    flex: 1;
    border-radius: 0;
    border-left: none;
    border-right: none;
    text-align: center;
    padding: 10px 0;
}
/* 价格输入 */
.price-input {
    position: relative;
}
.currency-symbol {
    position: absolute;
    left: 14px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-medium);
}
.price-input .form-control {
    padding-left: 30px;
}
.price-slider {
    margin-top: 16px;
}
.range-slider {
    -webkit-appearance: none;
    width: 100%;
    height: 4px;
    border-radius: 2px;
    background-color: var(--border-color);
    outline: none;
    margin: 14px 0;
}
.range-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--primary-color);
    cursor: pointer;
    border: 2px solid var(--bg-white);
    box-shadow: var(--shadow-sm);
}
.slider-marks {
    display: flex;
    justify-content: space-between;
    font-size: 0.75rem;
    color: var(--text-light);
}
/* ========== 按钮样式 ========== */
.btn-primary {
    padding: 12px 16px;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: var(--radius-md);
    font-weight: 500;
    font-size: 0.9375rem;
    cursor: pointer;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transition: all var(--transition-fast);
    box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
    background-color: var(--primary-hover);
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
}
.btn-primary:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
.btn-primary:active {
    transform: translateY(1px);
}
.btn-secondary {
    padding: 10px 16px;
    background-color: var(--bg-white);
    color: var(--text-medium);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    font-weight: 500;
    font-size: 0.9375rem;
    cursor: pointer;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transition: all var(--transition-fast);
}
.btn-secondary:hover {
    background-color: var(--bg-lightest);
    color: var(--text-dark);
}
.btn-secondary:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(226, 232, 240, 0.5);
}
/* ========== 标签输入样式 ========== */
.tag-input-wrapper {
    display: flex;
    align-items: center;
    gap: 8px;
}
.tag-input-wrapper .form-control {
    flex-grow: 1;
}
.btn-tag-add {
    width: 42px;
    height: 42px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--primary-color);
    border: none;
    border-radius: var(--radius-md);
    color: white;
    cursor: pointer;
    transition: all var(--transition-fast);
}
.btn-tag-add:hover {
    background-color: var(--primary-hover);
}
.tags-container {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
    min-height: 32px;
}
.tag {
    display: inline-flex;
    align-items: center;
    background-color: var(--primary-light);
    border-radius: 50px;
    padding: 6px 10px 6px 14px;
    font-size: 0.8125rem;
    color: var(--primary-color);
    transition: all var(--transition-fast);
}
.tag:hover {
    background-color: rgba(59, 130, 246, 0.2);
}
.tag-text {
    margin-right: 6px;
}
.tag-remove {
    background: none;
    border: none;
    color: var(--primary-color);
    cursor: pointer;
    padding: 0;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    transition: all var(--transition-fast);
}
.tag-remove:hover {
    background-color: rgba(59, 130, 246, 0.3);
    color: white;
}
/* ========== 封面上传区域 ========== */
.cover-preview-container {
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.cover-preview {
    width: 100%;
    aspect-ratio: 5/7;
    background-color: var(--bg-lightest);
    border-radius: var(--radius-md);
    overflow: hidden;
    cursor: pointer;
    transition: all var(--transition-fast);
}
.cover-preview:hover {
    background-color: var(--bg-light);
}
.cover-preview.dragover {
    background-color: var(--primary-light);
}
.cover-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.no-cover-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: var(--text-light);
    padding: 24px;
    text-align: center;
}
.no-cover-placeholder i {
    font-size: 48px;
    margin-bottom: 16px;
    color: var(--text-muted);
}
.placeholder-tip {
    font-size: 0.8125rem;
    margin-top: 8px;
    color: var(--text-muted);
}
.upload-options {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.upload-btn-group {
    display: flex;
    gap: 8px;
}
.btn-upload {
    flex-grow: 1;
    padding: 10px 16px;
    background-color: var(--primary-color);
    color: white;
    border: none;
    border-radius: var(--radius-md);
    font-weight: 500;
    font-size: 0.875rem;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transition: all var(--transition-fast);
}
.btn-upload:hover {
    background-color: var(--primary-hover);
}
.btn-remove {
    width: 42px;
    height: 38px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--bg-white);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    color: var(--text-medium);
    cursor: pointer;
    transition: all var(--transition-fast);
}
.btn-remove:hover {
    background-color: #fee2e2;
    border-color: #fca5a5;
    color: #ef4444;
}
.upload-tips {
    text-align: center;
    font-size: 0.75rem;
    color: var(--text-muted);
    line-height: 1.5;
}
/* ========== 表单提交区域 ========== */
.form-actions {
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.secondary-actions {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
}
.form-tip {
    margin-top: 8px;
    font-size: 0.8125rem;
    color: var(--text-muted);
    text-align: center;
}
.form-tip i {
    color: var(--info-color);
    margin-right: 4px;
}
/* 必填项标记 */
.required {
    color: var(--danger-color);
    margin-left: 4px;
}
/* 无效输入状态 */
.is-invalid {
    border-color: var(--danger-color) !important;
}
.invalid-feedback {
    display: block;
    color: var(--danger-color);
    font-size: 0.8125rem;
    margin-top: 6px;
}
/* ========== Select2 定制 ========== */
.select2-container--classic .select2-selection--single {
    height: 42px;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    background-color: var(--bg-white);
}
.select2-container--classic .select2-selection--single .select2-selection__rendered {
    line-height: 40px;
    color: var(--text-dark);
    padding-left: 14px;
}
.select2-container--classic .select2-selection--single .select2-selection__arrow {
    height: 40px;
    border-left: 1px solid var(--border-color);
}
.select2-container--classic .select2-selection--single:focus {
    border-color: var(--border-focus);
    outline: 0;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
/* ========== 模态框样式 ========== */
.modal-content {
    border: none;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}
.modal-header {
    background-color: var(--bg-white);
    border-bottom: 1px solid var(--border-color);
    padding: 16px 20px;
}
.modal-title {
    font-weight: 600;
    color: var(--text-dark);
    font-size: 1.125rem;
}
.modal-body {
    padding: 20px;
}
.modal-footer {
    border-top: 1px solid var(--border-color);
    padding: 16px 20px;
}
.modal-btn {
    min-width: 100px;
}
/* 裁剪模态框 */
.img-container {
    max-height: 500px;
    overflow: hidden;
    margin-bottom: 20px;
}
#cropperImage {
    display: block;
    max-width: 100%;
}
.cropper-controls {
    display: flex;
    justify-content: center;
    gap: 20px;
    margin-top: 16px;
}
.control-group {
    display: flex;
    gap: 8px;
}
.control-btn {
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md);
    background-color: var(--bg-lightest);
    border: 1px solid var(--border-color);
    color: var(--text-medium);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all var(--transition-fast);
}
.control-btn:hover {
    background-color: var(--border-color);
    color: var(--text-dark);
}
/* 图书预览模态框 */
.preview-header {
    background-color: var(--bg-white);
    border-bottom: 1px solid var(--border-color);
}
.preview-body {
    padding: 0;
    background-color: var(--bg-lightest);
}
/* 添加到你的CSS文件中 */
.book-preview {
    display: flex;
    flex-direction: row;
    gap: 20px;
}
.preview-cover-section {
    flex: 0 0 200px;
}
.preview-details-section {
    flex: 1;
}
.book-preview-cover {
    height: 280px;
    width: 200px;
    overflow: hidden;
    border-radius: 4px;
    border: 1px solid #ddd;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f8f9fa;
}
.preview-cover-img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}
.preview-tag {
    display: inline-block;
    background: #e9ecef;
    color: #495057;
    padding: 3px 8px;
    border-radius: 12px;
    font-size: 12px;
    margin-right: 5px;
    margin-bottom: 5px;
}
.book-tags-preview {
    margin: 15px 0;
}
.book-description-preview {
    margin-top: 20px;
}
.section-title {
    font-size: 16px;
    margin-bottom: 10px;
    color: #495057;
    border-bottom: 1px solid #dee2e6;
    padding-bottom: 5px;
}
.book-meta {
    margin-top: 10px;
    text-align: center;
}
.book-price {
    font-size: 18px;
    font-weight: bold;
    color: #dc3545;
}
.book-stock {
    font-size: 14px;
    color: #6c757d;
}
/* 响应式调整 */
@media (max-width: 768px) {
    .book-preview {
        flex-direction: column;
    }
    .preview-cover-section {
        margin: 0 auto;
    }
}
.preview-details-section {
    padding: 24px;
}
.book-title {
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--text-dark);
    margin: 0 0 8px 0;
}
.book-author {
    color: var(--text-medium);
    font-size: 1rem;
    margin-bottom: 24px;
}
.book-info-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
    background-color: var(--bg-white);
    border-radius: var(--radius-md);
    padding: 16px;
    box-shadow: var(--shadow-sm);
    margin-bottom: 24px;
}
.info-item {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.info-label {
    font-size: 0.75rem;
    color: var(--text-light);
    text-transform: uppercase;
}
.info-value {
    font-weight: 500;
    color: var(--text-dark);
}
.book-tags-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 24px;
}
.preview-tag {
    display: inline-block;
    background-color: var(--primary-light);
    color: var(--primary-color);
    padding: 4px 12px;
    border-radius: 50px;
    font-size: 0.8125rem;
}
.no-tags {
    font-size: 0.875rem;
    color: var(--text-muted);
}
.book-description-preview {
    background-color: var(--bg-white);
    border-radius: var(--radius-md);
    padding: 16px;
    box-shadow: var(--shadow-sm);
}
.section-title {
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-dark);
    margin: 0 0 12px 0;
}
.description-content {
    font-size: 0.9375rem;
    color: var(--text-medium);
    line-height: 1.6;
}
.placeholder-text {
    color: var(--text-muted);
    font-style: italic;
}
.preview-footer {
    background-color: var(--bg-white);
    display: flex;
    justify-content: flex-end;
    gap: 12px;
}
/* ========== 通知样式 ========== */
.notification-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    gap: 12px;
    max-width: 320px;
}
.notification {
    background-color: var(--bg-white);
    border-radius: var(--radius-md);
    padding: 12px 16px;
    box-shadow: var(--shadow-md);
    display: flex;
    align-items: center;
    gap: 12px;
    animation-duration: 0.5s;
}
.success-notification {
    border-left: 4px solid var(--success-color);
}
.error-notification {
    border-left: 4px solid var(--danger-color);
}
.warning-notification {
    border-left: 4px solid var(--warning-color);
}
.info-notification {
    border-left: 4px solid var(--info-color);
}
.notification-icon {
    color: var(--text-light);
}
.success-notification .notification-icon {
    color: var(--success-color);
}
.error-notification .notification-icon {
    color: var(--danger-color);
}
.warning-notification .notification-icon {
    color: var(--warning-color);
}
.info-notification .notification-icon {
    color: var(--info-color);
}
.notification-content {
    flex-grow: 1;
}
.notification-content p {
    margin: 0;
    font-size: 0.875rem;
    color: var(--text-dark);
}
.notification-close {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    padding: 5px;
    transition: color var(--transition-fast);
}
.notification-close:hover {
    color: var(--text-medium);
}
/* ========== 动画效果 ========== */
@keyframes pulse {
    0% {
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
    }
    70% {
        box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
    }
    100% {
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
    }
}
.pulse {
    animation: pulse 2s infinite;
}
/* ========== 响应式样式 ========== */
@media (max-width: 1200px) {
    .form-grid {
        grid-template-columns: 1fr 320px;
        gap: 20px;
    }
}
@media (max-width: 992px) {
    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 16px;
    }
    .header-actions {
        width: 100%;
        justify-content: space-between;
    }
    .form-grid {
        grid-template-columns: 1fr;
    }
    .book-preview {
        grid-template-columns: 1fr;
    }
    .preview-cover-section {
        border-right: none;
        border-bottom: 1px solid var(--border-color);
        padding-bottom: 24px;
    }
    .book-preview-cover {
        max-width: 240px;
        margin: 0 auto;
    }
}
@media (max-width: 768px) {
    .book-form-container {
        padding: 16px 12px;
    }
    .page-header {
        padding: 20px;
    }
    .form-row {
        grid-template-columns: 1fr;
        gap: 12px;
    }
    .secondary-actions {
        grid-template-columns: 1fr;
    }
    .card-body {
        padding: 16px;
    }
    .book-info-grid {
        grid-template-columns: 1fr;
    }
}
.cover-preview {
    min-height: 250px;
    width: 100%;
    border: 1px dashed #ccc;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    position: relative;
}
.cover-preview img.cover-image {
    max-width: 100%;
    max-height: 300px;
    object-fit: contain;
}
.img-container {
    max-height: 500px;
    overflow: auto;
}
#cropperImage {
    max-width: 100%;
    display: block;
}
================================================================================
File: ./app/static/css/categories.css
================================================================================
/* 分类管理页面样式 */
.categories-container {
    padding: 20px;
}
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding-bottom: 15px;
    border-bottom: 1px solid #eee;
}
.card {
    margin-bottom: 20px;
    border: 1px solid rgba(0,0,0,0.125);
    border-radius: 0.25rem;
}
.card-header {
    padding: 0.75rem 1.25rem;
    background-color: rgba(0,0,0,0.03);
    border-bottom: 1px solid rgba(0,0,0,0.125);
    font-weight: 600;
}
.card-body {
    padding: 1.25rem;
}
.category-table {
    border: 1px solid #eee;
}
.category-table th {
    background-color: #f8f9fa;
}
.no-categories {
    text-align: center;
    padding: 30px;
    color: #888;
}
.no-categories i {
    font-size: 48px;
    color: #ddd;
    margin-bottom: 10px;
}
/* 通知弹窗 */
.notification-alert {
    position: fixed;
    top: 20px;
    right: 20px;
    min-width: 300px;
    z-index: 1050;
}
/* 响应式调整 */
@media (max-width: 768px) {
    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: 15px;
    }
}
================================================================================
File: ./app/static/css/user-edit.css
================================================================================
/* 用户编辑页面样式 */
.user-edit-container {
    padding: 20px;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题和操作按钮 */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 25px;
    padding-bottom: 15px;
    border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
    font-size: 1.8rem;
    color: #333;
    margin: 0;
}
.page-header .actions {
    display: flex;
    gap: 10px;
}
/* 卡片样式 */
.card {
    border: none;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
    margin-bottom: 20px;
}
.card-body {
    padding: 25px;
}
/* 表单样式 */
.form-group {
    margin-bottom: 20px;
}
.form-group label {
    font-weight: 500;
    margin-bottom: 8px;
    color: #333;
    display: block;
}
.form-control {
    height: auto;
    padding: 10px 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 0.95rem;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
    border-color: #4c84ff;
    box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
}
.form-control[readonly] {
    background-color: #f8f9fa;
    opacity: 0.7;
}
.form-text {
    font-size: 0.85rem;
    margin-top: 5px;
}
.form-row {
    margin-right: -15px;
    margin-left: -15px;
    display: flex;
    flex-wrap: wrap;
}
.col-md-6 {
    flex: 0 0 50%;
    max-width: 50%;
    padding-right: 15px;
    padding-left: 15px;
}
.col-md-12 {
    flex: 0 0 100%;
    max-width: 100%;
    padding-right: 15px;
    padding-left: 15px;
}
/* 用户信息框 */
.user-info-box {
    margin-top: 20px;
    margin-bottom: 20px;
    padding: 20px;
    background-color: #f8f9fa;
    border-radius: 4px;
    display: flex;
    flex-wrap: wrap;
}
.info-item {
    flex: 0 0 auto;
    margin-right: 30px;
    margin-bottom: 10px;
}
.info-label {
    font-weight: 500;
    color: #666;
    margin-right: 5px;
}
.info-value {
    color: #333;
}
/* 表单操作区域 */
.form-actions {
    display: flex;
    justify-content: flex-start;
    gap: 10px;
    margin-top: 30px;
    padding-top: 20px;
    border-top: 1px solid #f0f0f0;
}
.btn {
    padding: 8px 16px;
    border-radius: 4px;
    transition: all 0.2s ease;
}
.btn-primary {
    background-color: #4c84ff;
    border-color: #4c84ff;
}
.btn-primary:hover {
    background-color: #3a70e9;
    border-color: #3a70e9;
}
.btn-secondary {
    background-color: #f8f9fa;
    border-color: #ddd;
    color: #333;
}
.btn-secondary:hover {
    background-color: #e9ecef;
    border-color: #ccc;
}
.btn-outline-secondary {
    color: #6c757d;
    border-color: #6c757d;
}
.btn-outline-secondary:hover {
    color: #fff;
    background-color: #6c757d;
    border-color: #6c757d;
}
/* 表单分隔线 */
.form-divider {
    height: 1px;
    background-color: #f0f0f0;
    margin: 30px 0;
}
/* 警告和错误状态 */
.is-invalid {
    border-color: #dc3545 !important;
}
.invalid-feedback {
    display: block;
    width: 100%;
    margin-top: 5px;
    font-size: 0.85rem;
    color: #dc3545;
}
/* 成功消息样式 */
.alert-success {
    color: #155724;
    background-color: #d4edda;
    border-color: #c3e6cb;
    padding: 15px;
    margin-bottom: 20px;
    border-radius: 4px;
}
/* 错误消息样式 */
.alert-error, .alert-danger {
    color: #721c24;
    background-color: #f8d7da;
    border-color: #f5c6cb;
    padding: 15px;
    margin-bottom: 20px;
    border-radius: 4px;
}
/* 响应式调整 */
@media (max-width: 768px) {
    .form-row {
        flex-direction: column;
    }
    .col-md-6, .col-md-12 {
        flex: 0 0 100%;
        max-width: 100%;
    }
    .page-header {
        flex-direction: column;
        align-items: flex-start;
    }
    .page-header .actions {
        margin-top: 15px;
    }
    .user-info-box {
        flex-direction: column;
    }
    .info-item {
        margin-right: 0;
    }
}
================================================================================
File: ./app/static/css/user-profile.css
================================================================================
/* 用户个人中心页面样式 */
.profile-container {
    padding: 20px;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题 */
.page-header {
    margin-bottom: 25px;
    padding-bottom: 15px;
    border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
    font-size: 1.8rem;
    color: #333;
    margin: 0;
}
/* 个人中心内容布局 */
.profile-content {
    display: flex;
    gap: 30px;
}
/* 左侧边栏 */
.profile-sidebar {
    flex: 0 0 300px;
    background-color: #f8f9fa;
    border-radius: 8px;
    padding: 25px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 右侧主要内容 */
.profile-main {
    flex: 1;
    min-width: 0; /* 防止内容溢出 */
}
/* 用户头像容器 */
.user-avatar-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-bottom: 25px;
    padding-bottom: 25px;
    border-bottom: 1px solid #e9ecef;
}
/* 大头像样式 */
.user-avatar.large {
    width: 120px;
    height: 120px;
    border-radius: 50%;
    background-color: #4c84ff;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 3rem;
    margin-bottom: 15px;
    box-shadow: 0 4px 8px rgba(76, 132, 255, 0.2);
}
.user-name {
    font-size: 1.5rem;
    margin: 10px 0 5px;
    color: #333;
}
.user-role {
    font-size: 0.9rem;
    color: #6c757d;
    margin: 0;
}
/* 用户统计信息 */
.user-stats {
    display: flex;
    justify-content: space-between;
    margin-bottom: 25px;
    padding-bottom: 25px;
    border-bottom: 1px solid #e9ecef;
}
.stat-item {
    text-align: center;
    flex: 1;
}
.stat-value {
    font-size: 1.8rem;
    font-weight: 600;
    color: #4c84ff;
    line-height: 1;
    margin-bottom: 5px;
}
.stat-label {
    font-size: 0.85rem;
    color: #6c757d;
}
/* 账户信息样式 */
.account-info {
    margin-bottom: 10px;
}
.account-info .info-row {
    display: flex;
    justify-content: space-between;
    margin-bottom: 15px;
    font-size: 0.95rem;
}
.account-info .info-label {
    color: #6c757d;
    font-weight: 500;
}
.account-info .info-value {
    color: #333;
    text-align: right;
    word-break: break-all;
}
/* 选项卡导航样式 */
.nav-tabs {
    border-bottom: 1px solid #dee2e6;
    margin-bottom: 25px;
}
.nav-tabs .nav-link {
    border: none;
    color: #6c757d;
    padding: 12px 15px;
    margin-right: 5px;
    border-bottom: 2px solid transparent;
    transition: all 0.2s ease;
}
.nav-tabs .nav-link:hover {
    color: #4c84ff;
    border-bottom-color: #4c84ff;
}
.nav-tabs .nav-link.active {
    font-weight: 500;
    color: #4c84ff;
    border-bottom: 2px solid #4c84ff;
    background-color: transparent;
}
.nav-tabs .nav-link i {
    margin-right: 5px;
}
/* 表单区域 */
.form-section {
    padding: 20px;
    background-color: #f9f9fb;
    border-radius: 8px;
    margin-bottom: 20px;
}
.form-section h4 {
    margin-top: 0;
    margin-bottom: 20px;
    color: #333;
    font-size: 1.2rem;
    font-weight: 500;
}
.form-group {
    margin-bottom: 20px;
}
.form-group label {
    font-weight: 500;
    margin-bottom: 8px;
    color: #333;
    display: block;
}
.form-control {
    height: auto;
    padding: 10px 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 0.95rem;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
    border-color: #4c84ff;
    box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
}
.form-text {
    font-size: 0.85rem;
    margin-top: 5px;
}
/* 表单操作区域 */
.form-actions {
    margin-top: 25px;
    display: flex;
    justify-content: flex-start;
}
.btn {
    padding: 10px 20px;
    border-radius: 4px;
    transition: all 0.2s ease;
}
.btn-primary {
    background-color: #4c84ff;
    border-color: #4c84ff;
}
.btn-primary:hover {
    background-color: #3a70e9;
    border-color: #3a70e9;
}
/* 活动记录选项卡 */
.activity-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}
.activity-filter {
    display: flex;
    align-items: center;
    gap: 10px;
}
.activity-filter label {
    margin-bottom: 0;
}
.activity-filter select {
    width: auto;
}
/* 活动时间线 */
.activity-timeline {
    padding: 20px;
    background-color: #f9f9fb;
    border-radius: 8px;
    min-height: 300px;
    position: relative;
}
.timeline-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 250px;
}
.timeline-loading p {
    margin-top: 15px;
    color: #6c757d;
}
.timeline-item {
    position: relative;
    padding-left: 30px;
    padding-bottom: 25px;
    border-left: 2px solid #dee2e6;
}
.timeline-item:last-child {
    border-left: none;
}
.timeline-icon {
    position: absolute;
    left: -10px;
    top: 0;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: #4c84ff;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-size: 10px;
}
.timeline-content {
    background-color: white;
    border-radius: 6px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
    padding: 15px;
}
.timeline-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
}
.timeline-title {
    font-weight: 500;
    color: #333;
    margin: 0;
}
.timeline-time {
    font-size: 0.85rem;
    color: #6c757d;
}
.timeline-details {
    color: #555;
    font-size: 0.95rem;
}
.timeline-type-login .timeline-icon {
    background-color: #4caf50;
}
.timeline-type-borrow .timeline-icon {
    background-color: #2196f3;
}
.timeline-type-return .timeline-icon {
    background-color: #ff9800;
}
/* 通知样式 */
.alert {
    padding: 15px;
    margin-bottom: 20px;
    border-radius: 4px;
}
.alert-success {
    color: #155724;
    background-color: #d4edda;
    border: 1px solid #c3e6cb;
}
.alert-error, .alert-danger {
    color: #721c24;
    background-color: #f8d7da;
    border: 1px solid #f5c6cb;
}
/* 响应式调整 */
@media (max-width: 992px) {
    .profile-content {
        flex-direction: column;
    }
    .profile-sidebar {
        flex: none;
        width: 100%;
        margin-bottom: 20px;
    }
    .user-stats {
        justify-content: space-around;
    }
}
================================================================================
File: ./app/static/css/user-roles.css
================================================================================
/* 角色管理页面样式 */
.roles-container {
    padding: 20px;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 页面标题 */
.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 25px;
    padding-bottom: 15px;
    border-bottom: 1px solid #f0f0f0;
}
.page-header h1 {
    font-size: 1.8rem;
    color: #333;
    margin: 0;
}
.page-header .actions {
    display: flex;
    gap: 10px;
}
/* 角色列表 */
.role-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
    margin-bottom: 30px;
}
/* 角色卡片 */
.role-card {
    background-color: #f9f9fb;
    border-radius: 8px;
    padding: 20px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.role-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.role-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 15px;
}
.role-name {
    font-size: 1.3rem;
    color: #333;
    margin: 0;
    font-weight: 600;
}
.role-actions {
    display: flex;
    gap: 5px;
}
.role-actions .btn {
    padding: 5px 8px;
    color: #6c757d;
    background: none;
    border: none;
}
.role-actions .btn:hover {
    color: #4c84ff;
}
.role-actions .btn-delete-role:hover {
    color: #dc3545;
}
.role-description {
    color: #555;
    margin-bottom: 20px;
    min-height: 50px;
    line-height: 1.5;
}
.role-stats {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 0.9rem;
    color: #6c757d;
}
.stat-item {
    display: flex;
    align-items: center;
    gap: 5px;
}
.stat-item i {
    color: #4c84ff;
}
/* 角色标签 */
.role-badge {
    display: inline-block;
    padding: 5px 12px;
    border-radius: 20px;
    font-size: 0.85rem;
    font-weight: 500;
}
.role-badge.admin {
    background-color: #e3f2fd;
    color: #1976d2;
}
.role-badge.user {
    background-color: #e8f5e9;
    color: #43a047;
}
.role-badge.custom {
    background-color: #fff3e0;
    color: #ef6c00;
}
/* 无数据提示 */
.no-data-message {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 50px 0;
    color: #6c757d;
}
.no-data-message i {
    font-size: 3rem;
    margin-bottom: 15px;
    opacity: 0.5;
}
/* 权限信息部分 */
.permissions-info {
    margin-top: 30px;
}
.permissions-info h3 {
    font-size: 1.4rem;
    margin-bottom: 15px;
    color: #333;
}
.permission-table {
    width: 100%;
    margin-bottom: 0;
}
.permission-table th {
    background-color: #f8f9fa;
    font-weight: 600;
}
.permission-table td, .permission-table th {
    padding: 12px 15px;
    text-align: center;
}
.permission-table td:first-child {
    text-align: left;
    font-weight: 500;
}
.text-success {
    color: #28a745;
}
.text-danger {
    color: #dc3545;
}
/* 模态框样式 */
.modal-header {
    background-color: #f8f9fa;
    border-bottom: 1px solid #f0f0f0;
}
.modal-title {
    font-weight: 600;
    color: #333;
}
.modal-footer {
    border-top: 1px solid #f0f0f0;
    padding: 15px;
}
/* 表单样式 */
.form-group {
    margin-bottom: 20px;
}
.form-group label {
    font-weight: 500;
    margin-bottom: 8px;
    color: #333;
    display: block;
}
.form-control {
    height: auto;
    padding: 10px 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 0.95rem;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
    border-color: #4c84ff;
    box-shadow: 0 0 0 0.2rem rgba(76, 132, 255, 0.25);
}
/* 响应式调整 */
@media (max-width: 992px) {
    .role-list {
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    }
}
@media (max-width: 576px) {
    .role-list {
        grid-template-columns: 1fr;
    }
    .page-header {
        flex-direction: column;
        align-items: flex-start;
    }
    .page-header .actions {
        margin-top: 15px;
    }
}
================================================================================
File: ./app/static/js/user-edit.js
================================================================================
// 用户编辑页面交互
document.addEventListener('DOMContentLoaded', function() {
    const passwordField = document.getElementById('password');
    const confirmPasswordGroup = document.getElementById('confirmPasswordGroup');
    const confirmPasswordField = document.getElementById('confirm_password');
    const userEditForm = document.getElementById('userEditForm');
    // 如果输入密码,显示确认密码字段
    passwordField.addEventListener('input', function() {
        if (this.value.trim() !== '') {
            confirmPasswordGroup.style.display = 'block';
        } else {
            confirmPasswordGroup.style.display = 'none';
            confirmPasswordField.value = '';
        }
    });
    // 表单提交验证
    userEditForm.addEventListener('submit', function(event) {
        let valid = true;
        // 清除之前的错误提示
        const invalidFields = document.querySelectorAll('.is-invalid');
        const feedbackElements = document.querySelectorAll('.invalid-feedback');
        invalidFields.forEach(field => {
            field.classList.remove('is-invalid');
        });
        feedbackElements.forEach(element => {
            element.parentNode.removeChild(element);
        });
        // 邮箱格式验证
        const emailField = document.getElementById('email');
        if (emailField.value.trim() !== '') {
            const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            if (!emailPattern.test(emailField.value.trim())) {
                showError(emailField, '请输入有效的邮箱地址');
                valid = false;
            }
        }
        // 手机号码格式验证
        const phoneField = document.getElementById('phone');
        if (phoneField.value.trim() !== '') {
            const phonePattern = /^1[3456789]\d{9}$/;
            if (!phonePattern.test(phoneField.value.trim())) {
                showError(phoneField, '请输入有效的手机号码');
                valid = false;
            }
        }
        // 密码验证
        if (passwordField.value.trim() !== '') {
            if (passwordField.value.length < 6) {
                showError(passwordField, '密码长度至少为6个字符');
                valid = false;
            }
            if (passwordField.value !== confirmPasswordField.value) {
                showError(confirmPasswordField, '两次输入的密码不一致');
                valid = false;
            }
        }
        if (!valid) {
            event.preventDefault();
        }
    });
    // 显示表单字段错误
    function showError(field, message) {
        field.classList.add('is-invalid');
        const feedback = document.createElement('div');
        feedback.className = 'invalid-feedback';
        feedback.innerText = message;
        field.parentNode.appendChild(feedback);
    }
    // 处理表单提交后的成功反馈
    const successAlert = document.querySelector('.alert-success');
    if (successAlert) {
        // 如果有成功消息,显示成功对话框
        setTimeout(() => {
            $('#successModal').modal('show');
        }, 500);
    }
});
================================================================================
File: ./app/static/js/user-roles.js
================================================================================
// 角色管理页面交互
document.addEventListener('DOMContentLoaded', function() {
    // 获取DOM元素
    const addRoleBtn = document.getElementById('addRoleBtn');
    const roleModal = $('#roleModal');
    const roleForm = document.getElementById('roleForm');
    const roleIdInput = document.getElementById('roleId');
    const roleNameInput = document.getElementById('roleName');
    const roleDescriptionInput = document.getElementById('roleDescription');
    const saveRoleBtn = document.getElementById('saveRoleBtn');
    const deleteModal = $('#deleteModal');
    const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
    let roleIdToDelete = null;
    // 加载角色用户统计
    fetchRoleUserCounts();
    // 添加角色按钮点击事件
    if (addRoleBtn) {
        addRoleBtn.addEventListener('click', function() {
            // 重置表单
            roleIdInput.value = '';
            roleNameInput.value = '';
            roleDescriptionInput.value = '';
            // 更新模态框标题
            document.getElementById('roleModalLabel').textContent = '添加角色';
            // 显示模态框
            roleModal.modal('show');
        });
    }
    // 编辑角色按钮点击事件
    const editButtons = document.querySelectorAll('.btn-edit-role');
    editButtons.forEach(button => {
        button.addEventListener('click', function() {
            const roleCard = this.closest('.role-card');
            const roleId = roleCard.getAttribute('data-id');
            const roleName = roleCard.querySelector('.role-name').textContent;
            let roleDescription = roleCard.querySelector('.role-description').textContent;
            // 移除"暂无描述"文本
            if (roleDescription.trim() === '暂无描述') {
                roleDescription = '';
            }
            // 填充表单
            roleIdInput.value = roleId;
            roleNameInput.value = roleName;
            roleDescriptionInput.value = roleDescription.trim();
            // 更新模态框标题
            document.getElementById('roleModalLabel').textContent = '编辑角色';
            // 显示模态框
            roleModal.modal('show');
        });
    });
    // 删除角色按钮点击事件
    const deleteButtons = document.querySelectorAll('.btn-delete-role');
    deleteButtons.forEach(button => {
        button.addEventListener('click', function() {
            const roleCard = this.closest('.role-card');
            roleIdToDelete = roleCard.getAttribute('data-id');
            // 显示确认删除模态框
            deleteModal.modal('show');
        });
    });
    // 保存角色按钮点击事件
    if (saveRoleBtn) {
        saveRoleBtn.addEventListener('click', function() {
            if (!roleNameInput.value.trim()) {
                showAlert('角色名称不能为空', 'error');
                return;
            }
            const roleData = {
                id: roleIdInput.value || null,
                role_name: roleNameInput.value.trim(),
                description: roleDescriptionInput.value.trim() || null
            };
            saveRole(roleData);
        });
    }
    // 确认删除按钮点击事件
    if (confirmDeleteBtn) {
        confirmDeleteBtn.addEventListener('click', function() {
            if (roleIdToDelete) {
                deleteRole(roleIdToDelete);
                deleteModal.modal('hide');
            }
        });
    }
    // 保存角色
    function saveRole(roleData) {
        // 实际应用中应从后端保存
        // fetch('/user/role/save', {
        //     method: 'POST',
        //     headers: {
        //         'Content-Type': 'application/json',
        //         'X-Requested-With': 'XMLHttpRequest'
        //     },
        //     body: JSON.stringify(roleData)
        // })
        // .then(response => response.json())
        // .then(data => {
        //     if (data.success) {
        //         showAlert(data.message, 'success');
        //         setTimeout(() => {
        //             window.location.reload();
        //         }, 1500);
        //     } else {
        //         showAlert(data.message, 'error');
        //     }
        // });
        // 模拟成功响应
        setTimeout(() => {
            showAlert('角色保存成功!', 'success');
            setTimeout(() => {
                window.location.reload();
            }, 1500);
        }, 500);
    }
    // 删除角色
    function deleteRole(roleId) {
        // 实际应用中应从后端删除
        // fetch(`/user/role/delete/${roleId}`, {
        //     method: 'POST',
        //     headers: {
        //         'X-Requested-With': 'XMLHttpRequest'
        //     }
        // })
        // .then(response => response.json())
        // .then(data => {
        //     if (data.success) {
        //         showAlert(data.message, 'success');
        //         setTimeout(() => {
        //             window.location.reload();
        //         }, 1500);
        //     } else {
        //         showAlert(data.message, 'error');
        //     }
        // });
        // 模拟成功响应
        setTimeout(() => {
            showAlert('角色删除成功!', 'success');
            setTimeout(() => {
                window.location.reload();
            }, 1500);
        }, 500);
    }
    // 获取角色用户数量
    function fetchRoleUserCounts() {
        const roleCards = document.querySelectorAll('.role-card');
        roleCards.forEach(card => {
            const roleId = card.getAttribute('data-id');
            const countElement = document.getElementById(`userCount-${roleId}`);
            // 实际应用中应从后端获取
            // fetch(`/api/role/${roleId}/user-count`)
            // .then(response => response.json())
            // .then(data => {
            //     countElement.textContent = data.count;
            // });
            // 模拟数据
            setTimeout(() => {
                const count = roleId == 1 ? 1 : (roleId == 2 ? 42 : Math.floor(Math.random() * 10));
                if (countElement) countElement.textContent = count;
            }, 300);
        });
    }
    // 显示通知
    function showAlert(message, type) {
        // 检查是否已有通知元素
        let alertBox = document.querySelector('.alert-box');
        if (!alertBox) {
            alertBox = document.createElement('div');
            alertBox.className = 'alert-box';
            document.body.appendChild(alertBox);
        }
        // 创建新的通知
        const alert = document.createElement('div');
        alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
        alert.innerHTML = message;
        // 添加到通知框中
        alertBox.appendChild(alert);
        // 自动关闭
        setTimeout(() => {
            alert.classList.add('fade-out');
            setTimeout(() => {
                alertBox.removeChild(alert);
            }, 500);
        }, 3000);
    }
});
================================================================================
File: ./app/static/js/user-list.js
================================================================================
// 用户列表页面交互
document.addEventListener('DOMContentLoaded', function() {
    // 处理状态切换按钮
    const toggleStatusButtons = document.querySelectorAll('.toggle-status');
    toggleStatusButtons.forEach(button => {
        button.addEventListener('click', function() {
            const userId = this.getAttribute('data-id');
            const newStatus = parseInt(this.getAttribute('data-status'));
            const statusText = newStatus === 1 ? '启用' : '禁用';
            if (confirm(`确定要${statusText}该用户吗?`)) {
                toggleUserStatus(userId, newStatus);
            }
        });
    });
    // 处理删除按钮
    const deleteButtons = document.querySelectorAll('.delete-user');
    const deleteModal = $('#deleteModal');
    let userIdToDelete = null;
    deleteButtons.forEach(button => {
        button.addEventListener('click', function() {
            userIdToDelete = this.getAttribute('data-id');
            deleteModal.modal('show');
        });
    });
    // 确认删除按钮
    document.getElementById('confirmDelete').addEventListener('click', function() {
        if (userIdToDelete) {
            deleteUser(userIdToDelete);
            deleteModal.modal('hide');
        }
    });
    // 自动提交表单的下拉菜单
    const autoSubmitSelects = document.querySelectorAll('select[name="status"], select[name="role_id"]');
    autoSubmitSelects.forEach(select => {
        select.addEventListener('change', function() {
            this.closest('form').submit();
        });
    });
});
// 切换用户状态
function toggleUserStatus(userId, status) {
    fetch(`/user/status/${userId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        },
        body: JSON.stringify({ status: status })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            showAlert(data.message, 'success');
            setTimeout(() => {
                window.location.reload();
            }, 1500);
        } else {
            showAlert(data.message, 'error');
        }
    })
    .catch(error => {
        console.error('Error:', error);
        showAlert('操作失败,请稍后重试', 'error');
    });
}
// 删除用户
function deleteUser(userId) {
    fetch(`/user/delete/${userId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            showAlert(data.message, 'success');
            setTimeout(() => {
                window.location.reload();
            }, 1500);
        } else {
            showAlert(data.message, 'error');
        }
    })
    .catch(error => {
        console.error('Error:', error);
        showAlert('操作失败,请稍后重试', 'error');
    });
}
// 显示通知
function showAlert(message, type) {
    // 检查是否已有通知元素
    let alertBox = document.querySelector('.alert-box');
    if (!alertBox) {
        alertBox = document.createElement('div');
        alertBox.className = 'alert-box';
        document.body.appendChild(alertBox);
    }
    // 创建新的通知
    const alert = document.createElement('div');
    alert.className = `alert alert-${type === 'success' ? 'success' : 'danger'} fade-in`;
    alert.innerHTML = message;
    // 添加到通知框中
    alertBox.appendChild(alert);
    // 自动关闭
    setTimeout(() => {
        alert.classList.add('fade-out');
        setTimeout(() => {
            alertBox.removeChild(alert);
        }, 500);
    }, 3000);
}
================================================================================
File: ./app/static/js/main.js
================================================================================
// 主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 = `
            ${icon}
            
        `;
        document.body.appendChild(notification);
        setTimeout(() => {
            notification.classList.add('show');
        }, 10);
        setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => {
                document.body.removeChild(notification);
            }, 300);
        }, 3000);
    }
});
================================================================================
File: ./app/static/js/book-add.js
================================================================================
/**
 * 图书添加页面脚本
 * 处理图书表单的交互、验证和预览功能
 */
let isSubmitting = false;
$(document).ready(function() {
    // 全局变量
    let cropper;
    let coverBlob;
    let tags = [];
    const coverPreview = $('#coverPreview');
    const coverInput = $('#cover');
    const tagInput = $('#tagInput');
    const tagsContainer = $('#tagsContainer');
    const tagsHiddenInput = $('#tags');
    // 初始化函数
    function initialize() {
        initSelect2();
        initFormProgress();
        initTagsFromInput();
        initCoverHandlers();
        initNumberControls();
        initPriceSlider();
        initCharCounter();
        initFormValidation();
        attachEventListeners();
    }
    // ========== 组件初始化 ==========
    // 初始化Select2
    function initSelect2() {
        $('.select2').select2({
            placeholder: "选择分类...",
            allowClear: true,
            theme: "classic",
            width: '100%'
        });
    }
    // 初始化表单进度条
    function initFormProgress() {
        updateFormProgress();
        $('input, textarea, select').on('change keyup', function() {
            updateFormProgress();
        });
    }
    // 初始化标签(从隐藏输入字段)
    function initTagsFromInput() {
        const tagsValue = $('#tags').val();
        if (tagsValue) {
            tags = tagsValue.split(',');
            renderTags();
        }
    }
    // 初始化封面处理
    function initCoverHandlers() {
        // 拖放上传功能
        coverPreview.on('dragover', function(e) {
            e.preventDefault();
            $(this).addClass('dragover');
        }).on('dragleave drop', function(e) {
            e.preventDefault();
            $(this).removeClass('dragover');
        }).on('drop', function(e) {
            e.preventDefault();
            const file = e.originalEvent.dataTransfer.files[0];
            if (file && file.type.match('image.*')) {
                coverInput[0].files = e.originalEvent.dataTransfer.files;
                coverInput.trigger('change');
            }
        }).on('click', function() {
            if (!$(this).find('img').length) {
                coverInput.click();
            }
        });
        // 重置页面加载完后的字符计数
        if ($('#description').val()) {
            $('#charCount').text($('#description').val().length);
        }
    }
    // 初始化数字控制
    function initNumberControls() {
        $('#stockDecrement').on('click', function() {
            const input = $('#stock');
            const value = parseInt(input.val());
            if (value > parseInt(input.attr('min'))) {
                input.val(value - 1).trigger('change');
            }
        });
        $('#stockIncrement').on('click', function() {
            const input = $('#stock');
            const value = parseInt(input.val());
            input.val(value + 1).trigger('change');
        });
    }
    // 初始化价格滑块
    function initPriceSlider() {
        $('#priceRange').on('input', function() {
            $('#price').val($(this).val());
        });
        $('#price').on('input', function() {
            const value = parseFloat($(this).val()) || 0;
            $('#priceRange').val(Math.min(value, 500));
        });
    }
    // 初始化字符计数器
    function initCharCounter() {
        $('#description').on('input', function() {
            const count = $(this).val().length;
            $('#charCount').text(count);
            if (count > 2000) {
                $('#charCount').addClass('text-danger');
            } else {
                $('#charCount').removeClass('text-danger');
            }
        });
    }
    // 初始化表单验证
    ffunction initFormValidation() {
        $('#bookForm').on('submit', function(e) {
            // 如果表单正在提交中,阻止重复提交
            if (isSubmitting) {
                e.preventDefault();
                showNotification('表单正在提交中,请勿重复点击', 'warning');
                return false;
            }
            let isValid = true;
            $('[required]').each(function() {
                if (!$(this).val().trim()) {
                    isValid = false;
                    $(this).addClass('is-invalid');
                    // 添加错误提示
                    if (!$(this).next('.invalid-feedback').length) {
                        $(this).after(`此字段不能为空
`);
                    }
                } else {
                    $(this).removeClass('is-invalid').next('.invalid-feedback').remove();
                }
            });
            // 验证ISBN格式(如果已填写)
            const isbn = $('#isbn').val().trim();
            if (isbn) {
                // 移除所有非数字、X和x字符后检查
                const cleanIsbn = isbn.replace(/[^0-9Xx]/g, '');
                const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/;
                if (!isbnRegex.test(cleanIsbn)) {
                    isValid = false;
                    $('#isbn').addClass('is-invalid');
                    if (!$('#isbn').next('.invalid-feedback').length) {
                        $('#isbn').after(`ISBN格式不正确,应为10位或13位
`);
                    }
                }
            }
            if (!isValid) {
                e.preventDefault();
                // 滚动到第一个错误字段
                $('html, body').animate({
                    scrollTop: $('.is-invalid:first').offset().top - 100
                }, 500);
                showNotification('请正确填写所有标记的字段', 'error');
            } else {
                // 设置表单锁定状态
                isSubmitting = true;
                // 修改提交按钮样式
                const submitBtn = $(this).find('button[type="submit"]');
                const originalHtml = submitBtn.html();
                submitBtn.prop('disabled', true)
                        .html(' 保存中...');
                // 显示提交中通知
                showNotification('表单提交中...', 'info');
                // 如果表单提交时间过长,30秒后自动解锁
                setTimeout(function() {
                    if (isSubmitting) {
                        isSubmitting = false;
                        submitBtn.prop('disabled', false).html(originalHtml);
                        showNotification('提交超时,请重试', 'warning');
                    }
                }, 30000);
            }
        });
        // 输入时移除错误样式
        $('input, textarea, select').on('input change', function() {
            $(this).removeClass('is-invalid').next('.invalid-feedback').remove();
        });
    }
    // 还需要在服务端处理成功后重置状态
    // 在页面加载完成时,添加监听服务器重定向事件
    $(window).on('pageshow', function(event) {
        if (event.originalEvent.persisted ||
            (window.performance && window.performance.navigation.type === 2)) {
            // 如果页面是从缓存加载的或通过后退按钮回到的
            isSubmitting = false;
            $('button[type="submit"]').prop('disabled', false)
                                     .html(' 保存图书');
        }
    });
    // 绑定事件监听器
    function attachEventListeners() {
        // 文件选择处理
        coverInput.on('change', handleCoverSelect);
        // 裁剪控制
        $('#rotateLeft').on('click', function() { cropper && cropper.rotate(-90); });
        $('#rotateRight').on('click', function() { cropper && cropper.rotate(90); });
        $('#zoomIn').on('click', function() { cropper && cropper.zoom(0.1); });
        $('#zoomOut').on('click', function() { cropper && cropper.zoom(-0.1); });
        $('#cropImage').on('click', applyCrop);
        $('#removeCover').on('click', removeCover);
        // 标签处理
        tagInput.on('keydown', handleTagKeydown);
        $('#addTagBtn').on('click', addTag);
        $(document).on('click', '.tag-remove', removeTag);
        // ISBN查询
        $('#isbnLookup').on('click', lookupISBN);
        // 预览按钮
        $('#previewBtn').on('click', showPreview);
        // 表单重置
        $('#resetBtn').on('click', confirmReset);
    }
    // ========== 功能函数 ==========
    // 更新表单进度条
    function updateFormProgress() {
        const requiredFields = $('[required]');
        const filledFields = requiredFields.filter(function() {
            return $(this).val() !== '';
        });
        const otherFields = $('input:not([required]), textarea:not([required]), select:not([required])').not('[type="file"]');
        const filledOtherFields = otherFields.filter(function() {
            return $(this).val() !== '';
        });
        let requiredWeight = 70; // 必填字段权重70%
        let otherWeight = 30;    // 非必填字段权重30%
        let requiredProgress = requiredFields.length ? (filledFields.length / requiredFields.length) * requiredWeight : requiredWeight;
        let otherProgress = otherFields.length ? (filledOtherFields.length / otherFields.length) * otherWeight : 0;
        let totalProgress = Math.floor(requiredProgress + otherProgress);
        $('#formProgress').css('width', totalProgress + '%').attr('aria-valuenow', totalProgress);
        $('#progressText').text('完成 ' + totalProgress + '%');
        if (totalProgress >= 100) {
            $('.btn-primary').addClass('pulse');
        } else {
            $('.btn-primary').removeClass('pulse');
        }
    }
    // 处理封面选择
    function handleCoverSelect(e) {
        const file = e.target.files[0];
        if (!file) return;
        // 验证文件类型
        if (!file.type.match('image.*')) {
            showNotification('请选择图片文件', 'warning');
            return;
        }
        // 验证文件大小(最大5MB)
        if (file.size > 5 * 1024 * 1024) {
            showNotification('图片大小不能超过5MB', 'warning');
            return;
        }
        const reader = new FileReader();
        reader.onload = function(e) {
            // 先显示在预览框中,确保用户能立即看到上传的图片
            coverPreview.html(`
`);
            // 准备裁剪图片
            $('#cropperImage').attr('src', e.target.result);
            // 确保图片加载完成后再显示模态框
            $('#cropperImage').on('load', function() {
                // 打开模态框
                $('#cropperModal').modal('show');
                // 在模态框完全显示后初始化裁剪器
                $('#cropperModal').on('shown.bs.modal', function() {
                    if (cropper) {
                        cropper.destroy();
                    }
                    try {
                        cropper = new Cropper(document.getElementById('cropperImage'), {
                            aspectRatio: 5 / 7,
                            viewMode: 2,
                            responsive: true,
                            guides: true,
                            background: true,
                            ready: function() {
                                console.log('Cropper初始化成功');
                            }
                        });
                    } catch (err) {
                        console.error('Cropper初始化失败:', err);
                        showNotification('图片处理工具初始化失败,请重试', 'error');
                    }
                });
            });
        };
        // 处理读取错误
        reader.onerror = function() {
            showNotification('读取图片失败,请重试', 'error');
        };
        reader.readAsDataURL(file);
    }
    // 应用裁剪
    function applyCrop() {
        if (!cropper) {
            showNotification('图片处理工具未就绪,请重新上传', 'error');
            $('#cropperModal').modal('hide');
            return;
        }
        try {
            const canvas = cropper.getCroppedCanvas({
                width: 500,
                height: 700,
                fillColor: '#fff',
                imageSmoothingEnabled: true,
                imageSmoothingQuality: 'high',
            });
            if (!canvas) {
                throw new Error('无法生成裁剪后的图片');
            }
            canvas.toBlob(function(blob) {
                if (!blob) {
                    showNotification('图片处理失败,请重试', 'error');
                    return;
                }
                const url = URL.createObjectURL(blob);
                coverPreview.html(`
`);
                coverBlob = blob;
                // 模拟File对象
                const fileList = new DataTransfer();
                const file = new File([blob], "cover.jpg", {type: "image/jpeg"});
                fileList.items.add(file);
                document.getElementById('cover').files = fileList.files;
                $('#cropperModal').modal('hide');
                showNotification('封面图片已更新', 'success');
            }, 'image/jpeg', 0.95);
        } catch (err) {
            console.error('裁剪失败:', err);
            showNotification('图片裁剪失败,请重试', 'error');
            $('#cropperModal').modal('hide');
        }
    }
    // 移除封面
    function removeCover() {
        coverPreview.html(`
            
        `);
        coverInput.val('');
        coverBlob = null;
    }
    // 渲染标签
    function renderTags() {
        tagsContainer.empty();
        tags.forEach(tag => {
            tagsContainer.append(`
                
                    ${tag}
                    
                
            `);
        });
        tagsHiddenInput.val(tags.join(','));
    }
    // 添加标签
    function addTag() {
        const tag = tagInput.val().trim();
        if (tag && !tags.includes(tag)) {
            tags.push(tag);
            renderTags();
            tagInput.val('').focus();
        }
    }
    // 处理标签输入键盘事件
    function handleTagKeydown(e) {
        if (e.key === 'Enter' || e.key === ',') {
            e.preventDefault();
            addTag();
        }
    }
    // 移除标签
    function removeTag() {
        const tagToRemove = $(this).data('tag');
        tags = tags.filter(t => t !== tagToRemove);
        renderTags();
    }
    // ISBN查询
    function lookupISBN() {
        const isbn = $('#isbn').val().trim();
        if (!isbn) {
            showNotification('请先输入ISBN', 'warning');
            return;
        }
        // 验证ISBN格式
        const cleanIsbn = isbn.replace(/[^0-9Xx]/g, '');
        const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/;
        if (!isbnRegex.test(cleanIsbn)) {
            showNotification('ISBN格式不正确,应为10位或13位', 'warning');
            return;
        }
        $(this).html('');
        // 先检查ISBN是否已存在
        $.get('/book/api/check-isbn', {isbn: isbn}, function(data) {
            if (data.exists) {
                $('#isbnLookup').html('');
                showNotification(`ISBN "${isbn}" 已存在: 《${data.book_title}》`, 'warning');
                $('#isbn').addClass('is-invalid');
                if (!$('#isbn').next('.invalid-feedback').length) {
                    $('#isbn').after(`此ISBN已被图书《${data.book_title}》使用
`);
                }
            } else {
                // 继续查询外部API(模拟)
                simulateISBNLookup(isbn);
            }
        }).fail(function() {
            $('#isbnLookup').html('');
            showNotification('服务器查询失败,请稍后再试', 'error');
        });
    }
    // 模拟ISBN查询
    function simulateISBNLookup(isbn) {
        // 模拟API查询延迟
        setTimeout(() => {
            // 模拟查到的数据
            if (isbn === '9787020002207') {
                $('#title').val('红楼梦').trigger('blur');
                $('#author').val('曹雪芹').trigger('blur');
                $('#publisher').val('人民文学出版社').trigger('blur');
                $('#publish_year').val('1996').trigger('blur');
                $('#category_id').val('1').trigger('change');
                tags = ['中国文学', '古典', '名著'];
                renderTags();
                $('#description').val('《红楼梦》是中国古代章回体长篇小说,中国古典四大名著之一,通行本共120回,一般认为前80回是清代作家曹雪芹所著,后40回作者有争议。小说以贾、史、王、薛四大家族的兴衰为背景,以贾府的家庭琐事、闺阁闲情为脉络,以贾宝玉、林黛玉、薛宝钗的爱情婚姻悲剧为主线,刻画了以贾宝玉和金陵十二钗为中心的正邪两赋有情人的人性美和悲剧美。').trigger('input');
                $('#price').val('59.70').trigger('input');
                $('#priceRange').val('59.70');
                showNotification('ISBN查询成功', 'success');
            } else if (isbn === '9787544270878') {
                $('#title').val('挪威的森林').trigger('blur');
                $('#author').val('村上春树').trigger('blur');
                $('#publisher').val('南海出版社').trigger('blur');
                $('#publish_year').val('2017').trigger('blur');
                $('#category_id').val('2').trigger('change');
                tags = ['外国文学', '日本', '小说'];
                renderTags();
                $('#description').val('《挪威的森林》是日本作家村上春树创作的长篇小说,首次出版于1987年。小说讲述了一个悲伤的爱情故事,背景设定在20世纪60年代末的日本。主人公渡边纠缠在与平静的直子和开朗的绿子两人的感情中,最终选择了生活。').trigger('input');
                $('#price').val('39.50').trigger('input');
                $('#priceRange').val('39.50');
                showNotification('ISBN查询成功', 'success');
            } else {
                showNotification('未找到相关图书信息', 'warning');
            }
            $('#isbnLookup').html('');
            updateFormProgress();
        }, 1500);
    }
    // 显示预览
    function showPreview() {
        // 检查必填字段
        if (!$('#title').val().trim() || !$('#author').val().trim()) {
            showNotification('请至少填写书名和作者后再预览', 'warning');
            return;
        }
        try {
            // 确保所有值都有默认值,防止undefined错误
            const title = $('#title').val() || '未填写标题';
            const author = $('#author').val() || '未填写作者';
            const publisher = $('#publisher').val() || '-';
            const isbn = $('#isbn').val() || '-';
            const publishYear = $('#publish_year').val() || '-';
            const description = $('#description').val() || '';
            const stock = $('#stock').val() || '0';
            let price = parseFloat($('#price').val()) || 0;
            // 填充预览内容
            $('#previewTitle').text(title);
            $('#previewAuthor').text(author ? '作者: ' + author : '未填写作者');
            $('#previewPublisher').text(publisher);
            $('#previewISBN').text(isbn);
            $('#previewYear').text(publishYear);
            // 获取分类文本
            const categoryId = $('#category_id').val();
            const categoryText = categoryId ? $('#category_id option:selected').text() : '-';
            $('#previewCategory').text(categoryText);
            // 价格和库存
            $('#previewPrice').text(price ? '¥' + price.toFixed(2) : '¥0.00');
            $('#previewStock').text('库存: ' + stock);
            // 标签
            const previewTags = $('#previewTags');
            previewTags.empty();
            if (tags && tags.length > 0) {
                tags.forEach(tag => {
                    previewTags.append(`${tag}`);
                });
            } else {
                previewTags.append('暂无标签');
            }
            // 描述
            if (description) {
                $('#previewDescription').html(`${description.replace(/\n/g, '
')}
`);
            } else {
                $('#previewDescription').html(`暂无简介内容
`);
            }
            // 封面
            const previewCover = $('#previewCover');
            previewCover.empty(); // 清空现有内容
            if ($('#coverPreview img').length) {
                const coverSrc = $('#coverPreview img').attr('src');
                previewCover.html(`
`);
            } else {
                previewCover.html(`
                    
                        
                        暂无封面
                    
                `);
            }
            // 显示预览模态框
            $('#previewModal').modal('show');
            console.log('预览模态框已显示');
        } catch (err) {
            console.error('生成预览时发生错误:', err);
            showNotification('生成预览时出错,请重试', 'error');
        }
    }
    // 确认重置表单
    function confirmReset() {
        if (confirm('确定要重置表单吗?所有已填写的内容将被清空。')) {
            $('#bookForm')[0].reset();
            removeCover();
            tags = [];
            renderTags();
            updateFormProgress();
            $('.select2').val(null).trigger('change');
            $('#charCount').text('0');
            showNotification('表单已重置', 'info');
        }
    }
    // 通知提示函数
    function showNotification(message, type) {
        // 创建通知元素
        const notification = $(`
            
        `);
        // 添加到页面
        if ($('.notification-container').length === 0) {
            $('body').append('');
        }
        $('.notification-container').append(notification);
        // 自动关闭
        setTimeout(() => {
            notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
            setTimeout(() => {
                notification.remove();
            }, 500);
        }, 5000);
        // 点击关闭
        notification.find('.notification-close').on('click', function() {
            notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
            setTimeout(() => {
                notification.remove();
            }, 500);
        });
    }
    function getIconForType(type) {
        switch(type) {
            case 'success': return 'fa-check-circle';
            case 'warning': return 'fa-exclamation-triangle';
            case 'error': return 'fa-times-circle';
            case 'info':
            default: return 'fa-info-circle';
        }
    }
    // 初始化页面
    initialize();
});
================================================================================
File: ./app/static/js/user-profile.js
================================================================================
// 用户个人中心页面交互
document.addEventListener('DOMContentLoaded', function() {
    // 获取表单对象
    const profileForm = document.getElementById('profileForm');
    const passwordForm = document.getElementById('passwordForm');
    // 表单验证逻辑
    if (profileForm) {
        profileForm.addEventListener('submit', function(event) {
            let valid = true;
            // 清除之前的错误提示
            clearValidationErrors();
            // 验证邮箱
            const emailField = document.getElementById('email');
            if (emailField.value.trim() !== '') {
                const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (!emailPattern.test(emailField.value.trim())) {
                    showError(emailField, '请输入有效的邮箱地址');
                    valid = false;
                }
            }
            // 验证手机号
            const phoneField = document.getElementById('phone');
            if (phoneField.value.trim() !== '') {
                const phonePattern = /^1[3456789]\d{9}$/;
                if (!phonePattern.test(phoneField.value.trim())) {
                    showError(phoneField, '请输入有效的手机号码');
                    valid = false;
                }
            }
            if (!valid) {
                event.preventDefault();
            }
        });
    }
    // 密码修改表单验证
    if (passwordForm) {
        passwordForm.addEventListener('submit', function(event) {
            let valid = true;
            // 清除之前的错误提示
            clearValidationErrors();
            // 验证当前密码
            const currentPasswordField = document.getElementById('current_password');
            if (currentPasswordField.value.trim() === '') {
                showError(currentPasswordField, '请输入当前密码');
                valid = false;
            }
            // 验证新密码
            const newPasswordField = document.getElementById('new_password');
            if (newPasswordField.value.trim() === '') {
                showError(newPasswordField, '请输入新密码');
                valid = false;
            } else if (newPasswordField.value.length < 6) {
                showError(newPasswordField, '密码长度至少为6个字符');
                valid = false;
            }
            // 验证确认密码
            const confirmPasswordField = document.getElementById('confirm_password');
            if (confirmPasswordField.value.trim() === '') {
                showError(confirmPasswordField, '请确认新密码');
                valid = false;
            } else if (confirmPasswordField.value !== newPasswordField.value) {
                showError(confirmPasswordField, '两次输入的密码不一致');
                valid = false;
            }
            if (!valid) {
                event.preventDefault();
            }
        });
    }
    // 获取用户统计数据
    fetchUserStats();
    // 获取用户活动记录
    const activityFilter = document.getElementById('activityFilter');
    if (activityFilter) {
        // 初始加载
        fetchUserActivities('all');
        // 监听过滤器变化
        activityFilter.addEventListener('change', function() {
            fetchUserActivities(this.value);
        });
    }
    // 处理URL中的tab参数
    const urlParams = new URLSearchParams(window.location.search);
    const tabParam = urlParams.get('tab');
    if (tabParam) {
        const tabElement = document.getElementById(`${tabParam}-tab`);
        if (tabElement) {
            $('#profileTabs a[href="#' + tabParam + '"]').tab('show');
        }
    }
    // 清除表单验证错误
    function clearValidationErrors() {
        const invalidFields = document.querySelectorAll('.is-invalid');
        const feedbackElements = document.querySelectorAll('.invalid-feedback');
        invalidFields.forEach(field => {
            field.classList.remove('is-invalid');
        });
        feedbackElements.forEach(element => {
            element.parentNode.removeChild(element);
        });
    }
    // 显示错误消息
    function showError(field, message) {
        field.classList.add('is-invalid');
        const feedback = document.createElement('div');
        feedback.className = 'invalid-feedback';
        feedback.innerText = message;
        field.parentNode.appendChild(feedback);
    }
    // 获取用户统计数据
    function fetchUserStats() {
        // 这里使用虚拟数据,实际应用中应当从后端获取
        // fetch('/api/user/stats')
        // .then(response => response.json())
        // .then(data => {
        //     updateUserStats(data);
        // });
        // 模拟数据
        setTimeout(() => {
            const mockData = {
                borrow: 2,
                returned: 15,
                overdue: 0
            };
            updateUserStats(mockData);
        }, 500);
    }
    // 更新用户统计显示
    function updateUserStats(data) {
        const borrowCount = document.getElementById('borrowCount');
        const returnedCount = document.getElementById('returnedCount');
        const overdueCount = document.getElementById('overdueCount');
        if (borrowCount) borrowCount.textContent = data.borrow;
        if (returnedCount) returnedCount.textContent = data.returned;
        if (overdueCount) overdueCount.textContent = data.overdue;
    }
    // 获取用户活动记录
    function fetchUserActivities(type) {
        const timelineContainer = document.getElementById('activityTimeline');
        if (!timelineContainer) return;
        // 显示加载中
        timelineContainer.innerHTML = `
            
        `;
        // 实际应用中应当从后端获取
        // fetch(`/api/user/activities?type=${type}`)
        // .then(response => response.json())
        // .then(data => {
        //     renderActivityTimeline(data, timelineContainer);
        // });
        // 模拟数据
        setTimeout(() => {
            const mockActivities = [
                {
                    id: 1,
                    type: 'login',
                    title: '系统登录',
                    details: '成功登录系统',
                    time: '2023-04-28 15:30:22',
                    ip: '192.168.1.1'
                },
                {
                    id: 2,
                    type: 'borrow',
                    title: '借阅图书',
                    details: '借阅《JavaScript高级编程》',
                    time: '2023-04-27 11:45:10',
                    book_id: 101
                },
                {
                    id: 3,
                    type: 'return',
                    title: '归还图书',
                    details: '归还《Python数据分析》',
                    time: '2023-04-26 09:15:33',
                    book_id: 95
                },
                {
                    id: 4,
                    type: 'login',
                    title: '系统登录',
                    details: '成功登录系统',
                    time: '2023-04-25 08:22:15',
                    ip: '192.168.1.1'
                }
            ];
            // 根据筛选条件过滤活动
            let filteredActivities = mockActivities;
            if (type !== 'all') {
                filteredActivities = mockActivities.filter(activity => activity.type === type);
            }
            renderActivityTimeline(filteredActivities, timelineContainer);
        }, 800);
    }
    // 渲染活动时间线
    function renderActivityTimeline(activities, container) {
        if (!activities || activities.length === 0) {
            container.innerHTML = '暂无活动记录
';
            return;
        }
        let timelineHTML = '';
        activities.forEach((activity, index) => {
            let iconClass = 'fas fa-info';
            if (activity.type === 'login') {
                iconClass = 'fas fa-sign-in-alt';
            } else if (activity.type === 'borrow') {
                iconClass = 'fas fa-book';
            } else if (activity.type === 'return') {
                iconClass = 'fas fa-undo';
            }
            const isLast = index === activities.length - 1;
            timelineHTML += `
                
                    
                        
                    
                    
                        
                        
                            ${activity.details}
                            ${activity.ip ? `
IP: ${activity.ip}
` : ''}
                        
 
                     
                 
            `;
        });
        container.innerHTML = timelineHTML;
    }
});
================================================================================
File: ./app/static/js/book-list.js
================================================================================
// 图书列表页面脚本
$(document).ready(function() {
  // 处理分类筛选
  function setFilter(button, categoryId) {
    // 移除所有按钮的活跃状态
    $('.filter-btn').removeClass('active');
    // 为当前点击的按钮添加活跃状态
    $(button).addClass('active');
    // 设置隐藏的分类ID输入值
    $('#category_id').val(categoryId);
    // 提交表单
    $(button).closest('form').submit();
  }
  // 处理排序方向切换
  function toggleSortDirection(button) {
    const $button = $(button);
    const isAsc = $button.hasClass('asc');
    // 切换方向类
    $button.toggleClass('asc desc');
    // 更新图标
    if (isAsc) {
      $button.find('i').removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down');
      $('#sort_order').val('desc');
    } else {
      $button.find('i').removeClass('fa-sort-amount-down').addClass('fa-sort-amount-up');
      $('#sort_order').val('asc');
    }
    // 提交表单
    $button.closest('form').submit();
  }
  // 将函数暴露到全局作用域
  window.setFilter = setFilter;
  window.toggleSortDirection = toggleSortDirection;
  // 处理删除图书
  let bookIdToDelete = null;
  $('.delete-book').click(function(e) {
    e.preventDefault();
    bookIdToDelete = $(this).data('id');
    const bookTitle = $(this).data('title');
    $('#deleteBookTitle').text(bookTitle);
    $('#deleteModal').modal('show');
  });
  $('#confirmDelete').click(function() {
    if (!bookIdToDelete) return;
    $.ajax({
      url: `/book/delete/${bookIdToDelete}`,
      type: 'POST',
      success: function(response) {
        if (response.success) {
          $('#deleteModal').modal('hide');
          // 显示成功消息
          showNotification(response.message, 'success');
          // 移除图书卡片
          $(`.book-card[data-id="${bookIdToDelete}"]`).fadeOut(300, function() {
            $(this).remove();
          });
          setTimeout(() => {
            if ($('.book-card').length === 0) {
              location.reload(); // 如果没有图书了,刷新页面显示"无图书"提示
            }
          }, 500);
        } else {
          $('#deleteModal').modal('hide');
          showNotification(response.message, 'error');
        }
      },
      error: function() {
        $('#deleteModal').modal('hide');
        showNotification('删除操作失败,请稍后重试', 'error');
      }
    });
  });
  // 处理借阅图书
  $('.borrow-book').click(function(e) {
    e.preventDefault();
    const bookId = $(this).data('id');
    $.ajax({
      url: `/borrow/add/${bookId}`,
      type: 'POST',
      success: function(response) {
        if (response.success) {
          showNotification(response.message, 'success');
          // 可以更新UI显示,比如更新库存或禁用借阅按钮
          setTimeout(() => {
            location.reload();
          }, 800);
        } else {
          showNotification(response.message, 'error');
        }
      },
      error: function() {
        showNotification('借阅操作失败,请稍后重试', 'error');
      }
    });
  });
  // 显示通知
  function showNotification(message, type) {
    // 移除可能存在的旧通知
    $('.notification-alert').remove();
    const alertClass = type === 'success' ? 'notification-success' : 'notification-error';
    const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
    const notification = `
      
    `;
    $('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);
      }
    }
  `;
  // 将通知样式添加到头部
  $('
    
        
        
        
        
        404
        噢!页面不见了~
        
            抱歉,您要找的页面似乎藏起来了,或者从未存在过。
            请检查您输入的网址是否正确,或者回到首页继续浏览吧!
         
        返回首页