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

${title}

${message}

`; 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 = $(`

${message}

`); // 添加到页面 if ($('.notification-container').length === 0) { $('body').append('
'); } $('.notification-container').append(notification); // 自动关闭 setTimeout(() => { notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); setTimeout(() => { notification.remove(); }, 500); }, 5000); // 点击关闭 notification.find('.notification-close').on('click', function() { notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight'); setTimeout(() => { notification.remove(); }, 500); }); } function getIconForType(type) { switch(type) { case 'success': return 'fa-check-circle'; case 'warning': return 'fa-exclamation-triangle'; case 'error': return 'fa-times-circle'; case 'info': default: return 'fa-info-circle'; } } // 初始化页面 initialize(); }); ================================================================================ File: ./app/static/js/user-profile.js ================================================================================ // 用户个人中心页面交互 document.addEventListener('DOMContentLoaded', function() { // 获取表单对象 const profileForm = document.getElementById('profileForm'); const passwordForm = document.getElementById('passwordForm'); // 表单验证逻辑 if (profileForm) { profileForm.addEventListener('submit', function(event) { let valid = true; // 清除之前的错误提示 clearValidationErrors(); // 验证邮箱 const emailField = document.getElementById('email'); if (emailField.value.trim() !== '') { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(emailField.value.trim())) { showError(emailField, '请输入有效的邮箱地址'); valid = false; } } // 验证手机号 const phoneField = document.getElementById('phone'); if (phoneField.value.trim() !== '') { const phonePattern = /^1[3456789]\d{9}$/; if (!phonePattern.test(phoneField.value.trim())) { showError(phoneField, '请输入有效的手机号码'); valid = false; } } if (!valid) { event.preventDefault(); } }); } // 密码修改表单验证 if (passwordForm) { passwordForm.addEventListener('submit', function(event) { let valid = true; // 清除之前的错误提示 clearValidationErrors(); // 验证当前密码 const currentPasswordField = document.getElementById('current_password'); if (currentPasswordField.value.trim() === '') { showError(currentPasswordField, '请输入当前密码'); valid = false; } // 验证新密码 const newPasswordField = document.getElementById('new_password'); if (newPasswordField.value.trim() === '') { showError(newPasswordField, '请输入新密码'); valid = false; } else if (newPasswordField.value.length < 6) { showError(newPasswordField, '密码长度至少为6个字符'); valid = false; } // 验证确认密码 const confirmPasswordField = document.getElementById('confirm_password'); if (confirmPasswordField.value.trim() === '') { showError(confirmPasswordField, '请确认新密码'); valid = false; } else if (confirmPasswordField.value !== newPasswordField.value) { showError(confirmPasswordField, '两次输入的密码不一致'); valid = false; } if (!valid) { event.preventDefault(); } }); } // 获取用户统计数据 fetchUserStats(); // 获取用户活动记录 const activityFilter = document.getElementById('activityFilter'); if (activityFilter) { // 初始加载 fetchUserActivities('all'); // 监听过滤器变化 activityFilter.addEventListener('change', function() { fetchUserActivities(this.value); }); } // 处理URL中的tab参数 const urlParams = new URLSearchParams(window.location.search); const tabParam = urlParams.get('tab'); if (tabParam) { const tabElement = document.getElementById(`${tabParam}-tab`); if (tabElement) { $('#profileTabs a[href="#' + tabParam + '"]').tab('show'); } } // 清除表单验证错误 function clearValidationErrors() { const invalidFields = document.querySelectorAll('.is-invalid'); const feedbackElements = document.querySelectorAll('.invalid-feedback'); invalidFields.forEach(field => { field.classList.remove('is-invalid'); }); feedbackElements.forEach(element => { element.parentNode.removeChild(element); }); } // 显示错误消息 function showError(field, message) { field.classList.add('is-invalid'); const feedback = document.createElement('div'); feedback.className = 'invalid-feedback'; feedback.innerText = message; field.parentNode.appendChild(feedback); } // 获取用户统计数据 function fetchUserStats() { // 这里使用虚拟数据,实际应用中应当从后端获取 // fetch('/api/user/stats') // .then(response => response.json()) // .then(data => { // updateUserStats(data); // }); // 模拟数据 setTimeout(() => { const mockData = { borrow: 2, returned: 15, overdue: 0 }; updateUserStats(mockData); }, 500); } // 更新用户统计显示 function updateUserStats(data) { const borrowCount = document.getElementById('borrowCount'); const returnedCount = document.getElementById('returnedCount'); const overdueCount = document.getElementById('overdueCount'); if (borrowCount) borrowCount.textContent = data.borrow; if (returnedCount) returnedCount.textContent = data.returned; if (overdueCount) overdueCount.textContent = data.overdue; } // 获取用户活动记录 function fetchUserActivities(type) { const timelineContainer = document.getElementById('activityTimeline'); if (!timelineContainer) return; // 显示加载中 timelineContainer.innerHTML = `
Loading...

加载中...

`; // 实际应用中应当从后端获取 // fetch(`/api/user/activities?type=${type}`) // .then(response => response.json()) // .then(data => { // renderActivityTimeline(data, timelineContainer); // }); // 模拟数据 setTimeout(() => { const mockActivities = [ { id: 1, type: 'login', title: '系统登录', details: '成功登录系统', time: '2023-04-28 15:30:22', ip: '192.168.1.1' }, { id: 2, type: 'borrow', title: '借阅图书', details: '借阅《JavaScript高级编程》', time: '2023-04-27 11:45:10', book_id: 101 }, { id: 3, type: 'return', title: '归还图书', details: '归还《Python数据分析》', time: '2023-04-26 09:15:33', book_id: 95 }, { id: 4, type: 'login', title: '系统登录', details: '成功登录系统', time: '2023-04-25 08:22:15', ip: '192.168.1.1' } ]; // 根据筛选条件过滤活动 let filteredActivities = mockActivities; if (type !== 'all') { filteredActivities = mockActivities.filter(activity => activity.type === type); } renderActivityTimeline(filteredActivities, timelineContainer); }, 800); } // 渲染活动时间线 function renderActivityTimeline(activities, container) { if (!activities || activities.length === 0) { container.innerHTML = '
暂无活动记录
'; return; } let timelineHTML = ''; activities.forEach((activity, index) => { let iconClass = 'fas fa-info'; if (activity.type === 'login') { iconClass = 'fas fa-sign-in-alt'; } else if (activity.type === 'borrow') { iconClass = 'fas fa-book'; } else if (activity.type === 'return') { iconClass = 'fas fa-undo'; } const isLast = index === activities.length - 1; timelineHTML += `
${activity.title}
${activity.time}
${activity.details} ${activity.ip ? `
IP: ${activity.ip}
` : ''}
`; }); container.innerHTML = timelineHTML; } }); ================================================================================ File: ./app/static/js/book-list.js ================================================================================ // 图书列表页面脚本 $(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 = `
${message}
`; $('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
噢!页面不见了~

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

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

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

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

================================================================================ File: ./app/templates/user/profile.html ================================================================================ {% extends "base.html" %} {% block title %}个人中心 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{{ user.username[0] }}

{{ user.nickname or user.username }}

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

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

个人信息

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

修改密码

密码长度至少为6个字符

最近活动

Loading...

加载中...

{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/user/list.html ================================================================================ {% extends "base.html" %} {% block title %}用户管理 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
重置
{% for user in pagination.items %} {% else %} {% endfor %}
ID 用户名 昵称 邮箱 手机号 角色 状态 注册时间 操作
{{ user.id }} {{ user.username }} {{ user.nickname or '-' }} {{ user.email or '-' }} {{ user.phone or '-' }} {% for role in roles %} {% if role.id == user.role_id %} {{ role.role_name }} {% endif %} {% endfor %} {{ '正常' if user.status == 1 else '禁用' }} {{ user.created_at }} {% if user.id != session.get('user_id') %} {% if user.status == 1 %} {% else %} {% endif %} {% else %} (当前用户) {% endif %}
暂无用户数据
{% if pagination.pages > 1 %}
    {% if pagination.has_prev %}
  • {% endif %} {% for page in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %} {% if page %} {% if page == pagination.page %}
  • {{ page }}
  • {% else %}
  • {{ page }}
  • {% endif %} {% else %}
  • ...
  • {% endif %} {% endfor %} {% if pagination.has_next %}
  • {% endif %}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/user/edit.html ================================================================================ {% extends "base.html" %} {% block title %}编辑用户 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
用户名不可修改
留空表示不修改密码
取消
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/user/roles.html ================================================================================ {% extends "base.html" %} {% block title %}角色管理 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% for role in roles %}

{{ role.role_name }}

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

暂无角色数据

{% endfor %}

角色权限说明

功能模块 管理员 普通用户 自定义角色
图书浏览
借阅图书
图书管理 可配置
用户管理 可配置
借阅管理 可配置
库存管理 可配置
统计分析 可配置
系统设置 可配置
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/list.html ================================================================================ {% extends 'base.html' %} {% block title %}图书列表 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}
{% for book in books %}
{% if book.cover_url %} {{ book.title }} {% else %}
无封面
{% endif %}
{{ book.title }}

{{ book.title }}

{{ book.author }}

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

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

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

库存: {{ book.stock }}

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

没有找到符合条件的图书

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

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

推荐尺寸: 500×700px (竖版封面)
支持格式: JPG, PNG, WebP
库存和价格
¥
¥0 ¥250 ¥500
* 的字段为必填项
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/edit.html ================================================================================ {% extends 'base.html' %} {% block title %}编辑图书 - {{ book.title }}{% endblock %} {% block head %} {% endblock %} {% block content %}
基本信息
图书简介
封面图片
{% if book.cover_url %} {{ book.title }} {% else %}
暂无封面
{% endif %}
库存和价格
¥
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/templates/book/import.html ================================================================================ {% extends 'base.html' %} {% block title %}批量导入图书 - 图书管理系统{% endblock %} {% block head %} {% endblock %} {% block content %}

Excel文件导入

支持的文件格式: .xlsx, .xls

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

下载导入模板:

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

{{ book.title }}

作者: {{ book.author }}

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

图书简介

{% if book.description %}

{{ book.description|nl2br }}

{% else %}

暂无图书简介

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

借阅历史

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

暂无借阅记录

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

暂无分类数据

{% endif %}
{% endblock %} {% block scripts %} {% endblock %} ================================================================================ File: ./app/controllers/user.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify from werkzeug.security import generate_password_hash, check_password_hash from app.models.user import User, db from app.utils.email import send_verification_email, generate_verification_code import logging from functools import wraps import time from datetime import datetime, timedelta from app.services.user_service import UserService from flask_login import login_user, logout_user, current_user, login_required # 创建蓝图 user_bp = Blueprint('user', __name__) # 使用内存字典代替Redis存储验证码 class VerificationStore: def __init__(self): self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}} def setex(self, email, seconds, code): """设置验证码并指定过期时间""" expiry = datetime.now() + timedelta(seconds=seconds) self.codes[email] = {'code': code, 'expires': expiry} return True def get(self, email): """获取验证码,如果过期则返回None""" if email not in self.codes: return None data = self.codes[email] if datetime.now() > data['expires']: # 验证码已过期,删除它 self.delete(email) return None return data['code'] def delete(self, email): """删除验证码""" if email in self.codes: del self.codes[email] return True # 使用内存存储验证码 verification_codes = VerificationStore() # 添加管理员权限检查装饰器 def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated or current_user.role_id != 1: flash('您没有管理员权限', 'error') return redirect(url_for('index')) return f(*args, **kwargs) return decorated_function @user_bp.route('/login', methods=['GET', 'POST']) def login(): # 如果用户已经登录,直接重定向到首页 if current_user.is_authenticated: return redirect(url_for('index')) if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') remember_me = request.form.get('remember_me') == 'on' if not username or not password: return render_template('login.html', error='用户名和密码不能为空') # 检查用户是否存在 user = User.query.filter((User.username == username) | (User.email == username)).first() if not user or not user.check_password(password): return render_template('login.html', error='用户名或密码错误') if user.status == 0: return render_template('login.html', error='账号已被禁用,请联系管理员') # 使用 Flask-Login 的 login_user 函数 login_user(user, remember=remember_me) # 这些session信息仍然可以保留,但不再用于认证 session['username'] = user.username session['role_id'] = user.role_id # 获取登录后要跳转的页面 next_page = request.args.get('next') if not next_page or not next_page.startswith('/'): next_page = url_for('index') # 重定向到首页或其他请求的页面 return redirect(next_page) return render_template('login.html') @user_bp.route('/register', methods=['GET', 'POST']) def register(): # 如果用户已登录,重定向到首页 if current_user.is_authenticated: return redirect(url_for('index')) if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') confirm_password = request.form.get('confirm_password') verification_code = request.form.get('verification_code') # 验证表单数据 if not username or not email or not password or not confirm_password or not verification_code: return render_template('register.html', error='所有字段都是必填项') if password != confirm_password: return render_template('register.html', error='两次输入的密码不匹配') # 检查用户名和邮箱是否已存在 if User.query.filter_by(username=username).first(): return render_template('register.html', error='用户名已存在') if User.query.filter_by(email=email).first(): return render_template('register.html', error='邮箱已被注册') # 验证验证码 stored_code = verification_codes.get(email) if not stored_code or stored_code != verification_code: return render_template('register.html', error='验证码无效或已过期') # 创建新用户 try: new_user = User( username=username, password=password, # 密码会在模型中自动哈希 email=email, nickname=username # 默认昵称与用户名相同 ) db.session.add(new_user) db.session.commit() # 清除验证码 verification_codes.delete(email) flash('注册成功,请登录', 'success') return redirect(url_for('user.login')) except Exception as e: db.session.rollback() logging.error(f"User registration failed: {str(e)}") return render_template('register.html', error='注册失败,请稍后重试') return render_template('register.html') @user_bp.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('user.login')) @user_bp.route('/send_verification_code', methods=['POST']) def send_verification_code(): data = request.get_json() email = data.get('email') if not email: return jsonify({'success': False, 'message': '请提供邮箱地址'}) # 检查邮箱格式 import re if not re.match(r"[^@]+@[^@]+\.[^@]+", email): return jsonify({'success': False, 'message': '邮箱格式不正确'}) # 生成验证码 code = generate_verification_code() # 存储验证码(10分钟有效) verification_codes.setex(email, 600, code) # 10分钟过期 # 发送验证码邮件 if send_verification_email(email, code): return jsonify({'success': True, 'message': '验证码已发送'}) else: return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'}) # 用户管理列表 @user_bp.route('/manage') @login_required @admin_required def user_list(): page = request.args.get('page', 1, type=int) search = request.args.get('search', '') status = request.args.get('status', type=int) role_id = request.args.get('role_id', type=int) pagination = UserService.get_users( page=page, per_page=10, search_query=search, status=status, role_id=role_id ) roles = UserService.get_all_roles() return render_template( 'user/list.html', pagination=pagination, search=search, status=status, role_id=role_id, roles=roles ) # 用户详情/编辑页面 @user_bp.route('/edit/', methods=['GET', 'POST']) @login_required @admin_required def user_edit(user_id): user = UserService.get_user_by_id(user_id) if not user: flash('用户不存在', 'error') return redirect(url_for('user.user_list')) roles = UserService.get_all_roles() if request.method == 'POST': data = { 'email': request.form.get('email'), 'phone': request.form.get('phone'), 'nickname': request.form.get('nickname'), 'role_id': int(request.form.get('role_id')), 'status': int(request.form.get('status')), } password = request.form.get('password') if password: data['password'] = password success, message = UserService.update_user(user_id, data) if success: flash(message, 'success') return redirect(url_for('user.user_list')) else: flash(message, 'error') return render_template('user/edit.html', user=user, roles=roles) # 用户状态管理API @user_bp.route('/status/', methods=['POST']) @login_required @admin_required def user_status(user_id): data = request.get_json() status = data.get('status') if status is None or status not in [0, 1]: return jsonify({'success': False, 'message': '无效的状态值'}) # 不能修改自己的状态 if user_id == current_user.id: return jsonify({'success': False, 'message': '不能修改自己的状态'}) success, message = UserService.change_user_status(user_id, status) return jsonify({'success': success, 'message': message}) # 用户删除API @user_bp.route('/delete/', methods=['POST']) @login_required @admin_required def user_delete(user_id): # 不能删除自己 if user_id == current_user.id: return jsonify({'success': False, 'message': '不能删除自己的账号'}) success, message = UserService.delete_user(user_id) return jsonify({'success': success, 'message': message}) # 个人中心页面 @user_bp.route('/profile', methods=['GET', 'POST']) @login_required def user_profile(): user = current_user if request.method == 'POST': data = { 'email': request.form.get('email'), 'phone': request.form.get('phone'), 'nickname': request.form.get('nickname') } current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') # 如果用户想要修改密码 if current_password and new_password: if not user.check_password(current_password): flash('当前密码不正确', 'error') return render_template('user/profile.html', user=user) if new_password != confirm_password: flash('两次输入的新密码不匹配', 'error') return render_template('user/profile.html', user=user) data['password'] = new_password success, message = UserService.update_user(user.id, data) if success: flash(message, 'success') else: flash(message, 'error') return render_template('user/profile.html', user=user) # 角色管理页面 @user_bp.route('/roles', methods=['GET']) @login_required @admin_required def role_list(): roles = UserService.get_all_roles() return render_template('user/roles.html', roles=roles) # 创建/编辑角色API @user_bp.route('/role/save', methods=['POST']) @login_required @admin_required def role_save(): data = request.get_json() role_id = data.get('id') role_name = data.get('role_name') description = data.get('description') if not role_name: return jsonify({'success': False, 'message': '角色名不能为空'}) if role_id: # 更新 success, message = UserService.update_role(role_id, role_name, description) else: # 创建 success, message = UserService.create_role(role_name, description) return jsonify({'success': success, 'message': message}) ================================================================================ File: ./app/controllers/log.py ================================================================================ ================================================================================ File: ./app/controllers/__init__.py ================================================================================ ================================================================================ File: ./app/controllers/book.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, g, jsonify from app.models.book import Book, Category from app.models.user import db from app.utils.auth import login_required, admin_required import os from werkzeug.utils import secure_filename import datetime import pandas as pd import uuid book_bp = Blueprint('book', __name__) # 图书列表页面 @book_bp.route('/list') @login_required def book_list(): print("访问图书列表页面") # 调试输出 page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) # 只显示状态为1的图书(未下架的图书) query = Book.query.filter_by(status=1) # 搜索功能 search = request.args.get('search', '') if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) # 分类筛选 category_id = request.args.get('category_id', type=int) if category_id: query = query.filter_by(category_id=category_id) # 排序 sort = request.args.get('sort', 'id') order = request.args.get('order', 'desc') if order == 'desc': query = query.order_by(getattr(Book, sort).desc()) else: query = query.order_by(getattr(Book, sort)) pagination = query.paginate(page=page, per_page=per_page) books = pagination.items # 获取所有分类供筛选使用 categories = Category.query.all() return render_template('book/list.html', books=books, pagination=pagination, search=search, categories=categories, category_id=category_id, sort=sort, order=order, current_user=g.user) # 图书详情页面 @book_bp.route('/detail/') @login_required def book_detail(book_id): book = Book.query.get_or_404(book_id) # 添加当前时间用于判断借阅是否逾期 now = datetime.datetime.now() # 如果用户是管理员,预先查询并排序借阅记录 borrow_records = [] if g.user.role_id == 1: # 假设 role_id 1 为管理员 from app.models.borrow import BorrowRecord borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit( 10).all() return render_template( 'book/detail.html', book=book, current_user=g.user, borrow_records=borrow_records, now=now ) # 添加图书页面 # 添加图书页面 @book_bp.route('/add', methods=['GET', 'POST']) @login_required @admin_required def add_book(): if request.method == 'POST': title = request.form.get('title') author = request.form.get('author') publisher = request.form.get('publisher') category_id = request.form.get('category_id') tags = request.form.get('tags') isbn = request.form.get('isbn') publish_year = request.form.get('publish_year') description = request.form.get('description') stock = request.form.get('stock', type=int, default=0) price = request.form.get('price') # 表单验证 errors = [] if not title: errors.append('书名不能为空') if not author: errors.append('作者不能为空') # 检查ISBN是否已存在(如果提供了ISBN) if isbn: existing_book = Book.query.filter_by(isbn=isbn).first() if existing_book: errors.append(f'ISBN "{isbn}" 已存在,请检查ISBN或查找现有图书') if errors: for error in errors: flash(error, 'danger') categories = Category.query.all() # 保留已填写的表单数据 book_data = { 'title': title, 'author': author, 'publisher': publisher, 'category_id': category_id, 'tags': tags, 'isbn': isbn, 'publish_year': publish_year, 'description': description, 'stock': stock, 'price': price } return render_template('book/add.html', categories=categories, current_user=g.user, book=book_data) # 处理封面图片上传 cover_url = None if 'cover' in request.files: cover_file = request.files['cover'] if cover_file and cover_file.filename != '': try: # 更清晰的文件命名 original_filename = secure_filename(cover_file.filename) # 保留原始文件扩展名 _, ext = os.path.splitext(original_filename) if not ext: ext = '.jpg' # 默认扩展名 filename = f"{uuid.uuid4()}{ext}" upload_folder = os.path.join(current_app.static_folder, 'uploads', 'covers') # 确保上传目录存在 if not os.path.exists(upload_folder): os.makedirs(upload_folder) file_path = os.path.join(upload_folder, filename) cover_file.save(file_path) cover_url = f'/static/uploads/covers/{filename}' except Exception as e: current_app.logger.error(f"封面上传失败: {str(e)}") flash(f"封面上传失败: {str(e)}", 'warning') try: # 创建新图书 book = Book( title=title, author=author, publisher=publisher, category_id=category_id, tags=tags, isbn=isbn, publish_year=publish_year, description=description, cover_url=cover_url, stock=stock, price=price, status=1, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now() ) db.session.add(book) # 先提交以获取book的id db.session.commit() # 记录库存日志 - 在获取 book.id 后 if stock and int(stock) > 0: from app.models.inventory import InventoryLog inventory_log = InventoryLog( book_id=book.id, change_type='入库', change_amount=stock, after_stock=stock, operator_id=g.user.id, remark='新书入库', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) db.session.commit() flash(f'《{title}》添加成功', 'success') return redirect(url_for('book.book_list')) except Exception as e: db.session.rollback() error_msg = str(e) # 记录详细错误日志 current_app.logger.error(f"添加图书失败: {error_msg}") flash(f'添加图书失败: {error_msg}', 'danger') categories = Category.query.all() # 保留已填写的表单数据 book_data = { 'title': title, 'author': author, 'publisher': publisher, 'category_id': category_id, 'tags': tags, 'isbn': isbn, 'publish_year': publish_year, 'description': description, 'stock': stock, 'price': price } return render_template('book/add.html', categories=categories, current_user=g.user, book=book_data) categories = Category.query.all() return render_template('book/add.html', categories=categories, current_user=g.user) # 编辑图书 @book_bp.route('/edit/', methods=['GET', 'POST']) @login_required @admin_required def edit_book(book_id): book = Book.query.get_or_404(book_id) if request.method == 'POST': title = request.form.get('title') author = request.form.get('author') publisher = request.form.get('publisher') category_id = request.form.get('category_id') tags = request.form.get('tags') isbn = request.form.get('isbn') publish_year = request.form.get('publish_year') description = request.form.get('description') price = request.form.get('price') status = request.form.get('status', type=int) if not title or not author: flash('书名和作者不能为空', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) # 处理库存变更 new_stock = request.form.get('stock', type=int) if new_stock != book.stock: from app.models.inventory import InventoryLog change_amount = new_stock - book.stock change_type = '入库' if change_amount > 0 else '出库' inventory_log = InventoryLog( book_id=book.id, change_type=change_type, change_amount=abs(change_amount), after_stock=new_stock, operator_id=g.user.id, remark=f'管理员编辑图书库存 - {book.title}', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) book.stock = new_stock # 处理封面图片上传 if 'cover' in request.files: cover_file = request.files['cover'] if cover_file and cover_file.filename != '': filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}") upload_folder = os.path.join(current_app.static_folder, 'uploads/covers') # 确保上传目录存在 if not os.path.exists(upload_folder): os.makedirs(upload_folder) file_path = os.path.join(upload_folder, filename) cover_file.save(file_path) book.cover_url = f'/static/covers/{filename}' # 更新图书信息 book.title = title book.author = author book.publisher = publisher book.category_id = category_id book.tags = tags book.isbn = isbn book.publish_year = publish_year book.description = description book.price = price book.status = status book.updated_at = datetime.datetime.now() db.session.commit() flash('图书信息更新成功', 'success') return redirect(url_for('book.book_list')) categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) # 删除图书 @book_bp.route('/delete/', methods=['POST']) @login_required @admin_required def delete_book(book_id): book = Book.query.get_or_404(book_id) # 检查该书是否有借阅记录 from app.models.borrow import BorrowRecord active_borrows = BorrowRecord.query.filter_by(book_id=book_id, status=1).count() if active_borrows > 0: return jsonify({'success': False, 'message': '该图书有未归还的借阅记录,无法删除'}) # 考虑软删除而不是物理删除 book.status = 0 # 0表示已删除/下架 book.updated_at = datetime.datetime.now() db.session.commit() return jsonify({'success': True, 'message': '图书已成功下架'}) # 图书分类管理 @book_bp.route('/categories', methods=['GET']) @login_required @admin_required def category_list(): categories = Category.query.all() return render_template('book/categories.html', categories=categories, current_user=g.user) # 添加分类 @book_bp.route('/categories/add', methods=['POST']) @login_required @admin_required def add_category(): name = request.form.get('name') parent_id = request.form.get('parent_id') or None sort = request.form.get('sort', 0, type=int) if not name: return jsonify({'success': False, 'message': '分类名称不能为空'}) category = Category(name=name, parent_id=parent_id, sort=sort) db.session.add(category) db.session.commit() return jsonify({'success': True, 'message': '分类添加成功', 'id': category.id, 'name': category.name}) # 编辑分类 @book_bp.route('/categories/edit/', methods=['POST']) @login_required @admin_required def edit_category(category_id): category = Category.query.get_or_404(category_id) name = request.form.get('name') parent_id = request.form.get('parent_id') or None sort = request.form.get('sort', 0, type=int) if not name: return jsonify({'success': False, 'message': '分类名称不能为空'}) category.name = name category.parent_id = parent_id category.sort = sort db.session.commit() return jsonify({'success': True, 'message': '分类更新成功'}) # 删除分类 @book_bp.route('/categories/delete/', methods=['POST']) @login_required @admin_required def delete_category(category_id): category = Category.query.get_or_404(category_id) # 检查是否有书籍使用此分类 books_count = Book.query.filter_by(category_id=category_id).count() if books_count > 0: return jsonify({'success': False, 'message': f'该分类下有{books_count}本图书,无法删除'}) # 检查是否有子分类 children_count = Category.query.filter_by(parent_id=category_id).count() if children_count > 0: return jsonify({'success': False, 'message': f'该分类下有{children_count}个子分类,无法删除'}) db.session.delete(category) db.session.commit() return jsonify({'success': True, 'message': '分类删除成功'}) # 批量导入图书 @book_bp.route('/import', methods=['GET', 'POST']) @login_required @admin_required def import_books(): if request.method == 'POST': if 'file' not in request.files: flash('未选择文件', 'danger') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('未选择文件', 'danger') return redirect(request.url) if file and file.filename.endswith(('.xlsx', '.xls')): try: # 读取Excel文件 df = pd.read_excel(file) success_count = 0 error_count = 0 errors = [] # 处理每一行数据 for index, row in df.iterrows(): try: # 检查必填字段 if pd.isna(row.get('title')) or pd.isna(row.get('author')): errors.append(f'第{index + 2}行: 书名或作者为空') error_count += 1 continue # 检查ISBN是否已存在 isbn = row.get('isbn') if isbn and not pd.isna(isbn) and Book.query.filter_by(isbn=str(isbn)).first(): errors.append(f'第{index + 2}行: ISBN {isbn} 已存在') error_count += 1 continue # 创建新书籍记录 book = Book( title=row.get('title'), author=row.get('author'), publisher=row.get('publisher') if not pd.isna(row.get('publisher')) else None, category_id=row.get('category_id') if not pd.isna(row.get('category_id')) else None, tags=row.get('tags') if not pd.isna(row.get('tags')) else None, isbn=str(row.get('isbn')) if not pd.isna(row.get('isbn')) else None, publish_year=str(row.get('publish_year')) if not pd.isna(row.get('publish_year')) else None, description=row.get('description') if not pd.isna(row.get('description')) else None, cover_url=row.get('cover_url') if not pd.isna(row.get('cover_url')) else None, stock=int(row.get('stock')) if not pd.isna(row.get('stock')) else 0, price=float(row.get('price')) if not pd.isna(row.get('price')) else None, status=1, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now() ) db.session.add(book) # 提交以获取book的id db.session.flush() # 创建库存日志 if book.stock > 0: from app.models.inventory import InventoryLog inventory_log = InventoryLog( book_id=book.id, change_type='入库', change_amount=book.stock, after_stock=book.stock, operator_id=g.user.id, remark='批量导入图书', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) success_count += 1 except Exception as e: errors.append(f'第{index + 2}行: {str(e)}') error_count += 1 db.session.commit() flash(f'导入完成: 成功{success_count}条,失败{error_count}条', 'info') if errors: flash('
'.join(errors[:10]) + (f'
...等共{len(errors)}个错误' if len(errors) > 10 else ''), 'warning') return redirect(url_for('book.book_list')) except Exception as e: flash(f'导入失败: {str(e)}', 'danger') return redirect(request.url) else: flash('只支持Excel文件(.xlsx, .xls)', 'danger') return redirect(request.url) return render_template('book/import.html', current_user=g.user) # 导出图书 @book_bp.route('/export') @login_required @admin_required def export_books(): # 获取查询参数 search = request.args.get('search', '') category_id = request.args.get('category_id', type=int) query = Book.query if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) if category_id: query = query.filter_by(category_id=category_id) books = query.all() # 创建DataFrame data = [] for book in books: category_name = book.category.name if book.category else "" data.append({ 'id': book.id, 'title': book.title, 'author': book.author, 'publisher': book.publisher, 'category': category_name, 'tags': book.tags, 'isbn': book.isbn, 'publish_year': book.publish_year, 'description': book.description, 'stock': book.stock, 'price': book.price, 'status': '上架' if book.status == 1 else '下架', 'created_at': book.created_at.strftime('%Y-%m-%d %H:%M:%S') if book.created_at else '', 'updated_at': book.updated_at.strftime('%Y-%m-%d %H:%M:%S') if book.updated_at else '' }) df = pd.DataFrame(data) # 创建临时文件 timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') filename = f'books_export_{timestamp}.xlsx' filepath = os.path.join(current_app.static_folder, 'temp', filename) # 确保目录存在 os.makedirs(os.path.dirname(filepath), exist_ok=True) # 写入Excel df.to_excel(filepath, index=False) # 提供下载链接 return redirect(url_for('static', filename=f'temp/{filename}')) ================================================================================ File: ./app/controllers/statistics.py ================================================================================ ================================================================================ File: ./app/controllers/borrow.py ================================================================================ from flask import Blueprint, request, redirect, url_for, flash, g from app.models.book import Book from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog from app.models.user import db # 修正:从 user 模型导入 db from app.utils.auth import login_required import datetime # 创建借阅蓝图 borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow') @borrow_bp.route('/book', methods=['POST']) @login_required def borrow_book(): book_id = request.form.get('book_id', type=int) borrow_days = request.form.get('borrow_days', type=int, default=14) if not book_id: flash('请选择要借阅的图书', 'danger') return redirect(url_for('book.book_list')) book = Book.query.get_or_404(book_id) # 检查库存 if book.stock <= 0: flash(f'《{book.title}》当前无库存,无法借阅', 'danger') return redirect(url_for('book.book_detail', book_id=book_id)) # 检查当前用户是否已借阅此书 existing_borrow = BorrowRecord.query.filter_by( user_id=g.user.id, book_id=book_id, status=1 # 1表示借阅中 ).first() if existing_borrow: flash(f'您已借阅《{book.title}》,请勿重复借阅', 'warning') return redirect(url_for('book.book_detail', book_id=book_id)) try: # 创建借阅记录 now = datetime.datetime.now() due_date = now + datetime.timedelta(days=borrow_days) borrow_record = BorrowRecord( user_id=g.user.id, book_id=book_id, borrow_date=now, due_date=due_date, status=1, # 1表示借阅中 created_at=now, updated_at=now ) # 更新图书库存 book.stock -= 1 book.updated_at = now db.session.add(borrow_record) db.session.commit() # 添加库存变更日志 inventory_log = InventoryLog( book_id=book_id, change_type='借出', change_amount=-1, after_stock=book.stock, operator_id=g.user.id, remark='用户借书', changed_at=now ) db.session.add(inventory_log) db.session.commit() flash(f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还', 'success') except Exception as e: db.session.rollback() flash(f'借阅失败: {str(e)}', 'danger') return redirect(url_for('book.book_detail', book_id=book_id)) ================================================================================ File: ./app/controllers/announcement.py ================================================================================ ================================================================================ File: ./app/controllers/inventory.py ================================================================================ ================================================================================ File: ./app/services/borrow_service.py ================================================================================ ================================================================================ File: ./app/services/inventory_service.py ================================================================================ ================================================================================ File: ./app/services/__init__.py ================================================================================ ================================================================================ File: ./app/services/book_service.py ================================================================================ ================================================================================ File: ./app/services/user_service.py ================================================================================ # app/services/user_service.py from app.models.user import User, Role, db from sqlalchemy import or_ from datetime import datetime class UserService: @staticmethod def get_users(page=1, per_page=10, search_query=None, status=None, role_id=None): """ 获取用户列表,支持分页、搜索和过滤 """ query = User.query # 搜索条件 if search_query: query = query.filter(or_( User.username.like(f'%{search_query}%'), User.email.like(f'%{search_query}%'), User.nickname.like(f'%{search_query}%'), User.phone.like(f'%{search_query}%') )) # 状态过滤 if status is not None: query = query.filter(User.status == status) # 角色过滤 if role_id is not None: query = query.filter(User.role_id == role_id) # 分页 pagination = query.order_by(User.id.desc()).paginate( page=page, per_page=per_page, error_out=False ) return pagination @staticmethod def get_user_by_id(user_id): """通过ID获取用户""" return User.query.get(user_id) @staticmethod def update_user(user_id, data): """更新用户信息""" user = User.query.get(user_id) if not user: return False, "用户不存在" try: # 更新可编辑字段 if 'email' in data: # 检查邮箱是否已被其他用户使用 existing = User.query.filter(User.email == data['email'], User.id != user_id).first() if existing: return False, "邮箱已被使用" user.email = data['email'] if 'phone' in data: # 检查手机号是否已被其他用户使用 existing = User.query.filter(User.phone == data['phone'], User.id != user_id).first() if existing: return False, "手机号已被使用" user.phone = data['phone'] if 'nickname' in data and data['nickname']: user.nickname = data['nickname'] # 只有管理员可以修改这些字段 if 'role_id' in data: user.role_id = data['role_id'] if 'status' in data: user.status = data['status'] if 'password' in data and data['password']: user.set_password(data['password']) user.updated_at = datetime.now() db.session.commit() return True, "用户信息更新成功" except Exception as e: db.session.rollback() return False, f"更新失败: {str(e)}" @staticmethod def change_user_status(user_id, status): """变更用户状态 (启用/禁用)""" user = User.query.get(user_id) if not user: return False, "用户不存在" try: user.status = status user.updated_at = datetime.now() db.session.commit() status_text = "启用" if status == 1 else "禁用" return True, f"用户已{status_text}" except Exception as e: db.session.rollback() return False, f"状态变更失败: {str(e)}" @staticmethod def delete_user(user_id): """删除用户 (软删除,将状态设为-1)""" user = User.query.get(user_id) if not user: return False, "用户不存在" try: user.status = -1 # 软删除,设置状态为-1 user.updated_at = datetime.now() db.session.commit() return True, "用户已删除" except Exception as e: db.session.rollback() return False, f"删除失败: {str(e)}" @staticmethod def get_all_roles(): """获取所有角色""" return Role.query.all() @staticmethod def create_role(role_name, description=None): """创建新角色""" existing = Role.query.filter_by(role_name=role_name).first() if existing: return False, "角色名已存在" try: role = Role(role_name=role_name, description=description) db.session.add(role) db.session.commit() return True, "角色创建成功" except Exception as e: db.session.rollback() return False, f"创建失败: {str(e)}" @staticmethod def update_role(role_id, role_name, description=None): """更新角色信息""" role = Role.query.get(role_id) if not role: return False, "角色不存在" # 检查角色名是否已被使用 existing = Role.query.filter(Role.role_name == role_name, Role.id != role_id).first() if existing: return False, "角色名已存在" try: role.role_name = role_name if description is not None: role.description = description db.session.commit() return True, "角色更新成功" except Exception as e: db.session.rollback() return False, f"更新失败: {str(e)}"