================================================================================ File: ./config.py ================================================================================ import os # 数据库配置 DB_HOST = os.environ.get('DB_HOST', '27.124.22.104') DB_PORT = os.environ.get('DB_PORT', '3306') DB_USER = os.environ.get('DB_USER', 'book20250428') DB_PASSWORD = os.environ.get('DB_PASSWORD', 'booksystem') DB_NAME = os.environ.get('DB_NAME', 'book_system') # 数据库连接字符串 SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}' SQLALCHEMY_TRACK_MODIFICATIONS = False # 应用密钥 SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key_replace_in_production') # 邮件配置 EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com') EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587)) EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls') EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com') EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf') EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com') EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL') # 会话配置 PERMANENT_SESSION_LIFETIME = 86400 * 7 ================================================================================ File: ./all_file_output.py ================================================================================ import os import sys def collect_code_files(output_file="code_collection.txt"): # 定义代码文件扩展名 code_extensions = [ '.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs', '.js', '.html', '.css', '.php', '.go', '.rb', '.swift', '.kt', '.ts', '.sh', '.pl', '.r' ] # 定义要排除的目录 excluded_dirs = [ 'venv', 'env', '.venv', '.env', 'virtualenv', '__pycache__', 'node_modules', '.git', '.idea', 'dist', 'build', 'target', 'bin' ] # 计数器 file_count = 0 # 打开输出文件 with open(output_file, 'w', encoding='utf-8') as out_file: # 遍历当前目录及所有子目录 for root, dirs, files in os.walk('.'): # 从dirs中移除排除的目录,这会阻止os.walk进入这些目录 dirs[:] = [d for d in dirs if d not in excluded_dirs] for file in files: # 获取文件扩展名 _, ext = os.path.splitext(file) # 检查是否为代码文件 if ext.lower() in code_extensions: file_path = os.path.join(root, file) file_count += 1 # 写入文件路径作为分隔 out_file.write(f"\n{'=' * 80}\n") out_file.write(f"File: {file_path}\n") out_file.write(f"{'=' * 80}\n\n") # 尝试读取文件内容并写入 try: with open(file_path, 'r', encoding='utf-8') as code_file: out_file.write(code_file.read()) except UnicodeDecodeError: # 尝试用不同的编码 try: with open(file_path, 'r', encoding='latin-1') as code_file: out_file.write(code_file.read()) except Exception as e: out_file.write(f"无法读取文件内容: {str(e)}\n") except Exception as e: out_file.write(f"读取文件时出错: {str(e)}\n") print(f"已成功收集 {file_count} 个代码文件到 {output_file}") if __name__ == "__main__": # 如果提供了命令行参数,则使用它作为输出文件名 output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt" collect_code_files(output_file) ================================================================================ File: ./app.py ================================================================================ from app import create_app app = create_app() if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=49666) ================================================================================ File: ./main.py ================================================================================ # 这是一个示例 Python 脚本。 # 按 ⌃R 执行或将其替换为您的代码。 # 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。 def print_hi(name): # 在下面的代码行中使用断点来调试脚本。 print(f'Hi, {name}') # 按 ⌘F8 切换断点。 # 按间距中的绿色按钮以运行脚本。 if __name__ == '__main__': print_hi('PyCharm') # 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 ================================================================================ File: ./app/__init__.py ================================================================================ from flask import Flask, render_template, session, g from app.models.user import db, User from app.controllers.user import user_bp from app.controllers.book import book_bp # 引入图书蓝图 import os def create_app(): app = Flask(__name__) # 配置应用 app.config.from_mapping( SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'), SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system', SQLALCHEMY_TRACK_MODIFICATIONS=False, PERMANENT_SESSION_LIFETIME=86400 * 7, # 7天 # 邮件配置 EMAIL_HOST='smtp.qq.com', EMAIL_PORT=587, EMAIL_ENCRYPTION='starttls', EMAIL_USERNAME='3399560459@qq.com', EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码,不是邮箱密码 EMAIL_FROM='3399560459@qq.com', EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL' ) # 实例配置,如果存在 app.config.from_pyfile('config.py', silent=True) # 初始化数据库 db.init_app(app) # 注册蓝图 app.register_blueprint(user_bp, url_prefix='/user') app.register_blueprint(book_bp, url_prefix='/book') # 注册图书蓝图 # 创建数据库表 with app.app_context(): # 先导入基础模型 from app.models.user import User, Role from app.models.book import Book, Category # 创建表 db.create_all() # 再导入依赖模型 from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog # 现在添加反向关系 # 这样可以确保所有类都已经定义好 Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic') Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') Category.books = db.relationship('Book', backref='category', lazy='dynamic') # 创建默认角色 from app.models.user import Role if not Role.query.filter_by(id=1).first(): admin_role = Role(id=1, role_name='管理员', description='系统管理员') db.session.add(admin_role) if not Role.query.filter_by(id=2).first(): user_role = Role(id=2, role_name='普通用户', description='普通用户') db.session.add(user_role) # 创建管理员账号 if not User.query.filter_by(username='admin').first(): admin = User( username='admin', password='admin123', email='admin@example.com', role_id=1, nickname='系统管理员' ) db.session.add(admin) # 创建基础分类 from app.models.book import Category if not Category.query.first(): categories = [ Category(name='文学', sort=1), Category(name='计算机', sort=2), Category(name='历史', sort=3), Category(name='科学', sort=4), Category(name='艺术', sort=5), Category(name='经济', sort=6), Category(name='哲学', sort=7), Category(name='教育', sort=8) ] db.session.add_all(categories) db.session.commit() # 请求前处理 @app.before_request def load_logged_in_user(): user_id = session.get('user_id') if user_id is None: g.user = None else: g.user = User.query.get(user_id) # 首页路由 @app.route('/') def index(): if not g.user: return render_template('login.html') return render_template('index.html', current_user=g.user) # 错误处理 @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 # 模板过滤器 @app.template_filter('nl2br') def nl2br_filter(s): if not s: return s return s.replace('\n', '
') 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 db = SQLAlchemy() class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True, autoincrement=True) username = db.Column(db.String(64), unique=True, nullable=False) password = db.Column(db.String(255), nullable=False) email = db.Column(db.String(128), unique=True, nullable=True) phone = db.Column(db.String(20), unique=True, nullable=True) nickname = db.Column(db.String(64), nullable=True) status = db.Column(db.Integer, default=1) # 1: active, 0: disabled role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2) # 2: 普通用户, 1: 管理员 created_at = db.Column(db.DateTime, default=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2): self.username = username self.set_password(password) self.email = email self.phone = phone self.nickname = nickname self.role_id = role_id def set_password(self, password): """设置密码,使用哈希加密""" self.password = generate_password_hash(password) def check_password(self, password): """验证密码""" return check_password_hash(self.password, password) def to_dict(self): """转换为字典格式""" return { 'id': self.id, 'username': self.username, 'email': self.email, 'phone': self.phone, 'nickname': self.nickname, 'status': self.status, 'role_id': self.role_id, 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'), 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') } @classmethod def create_user(cls, username, password, email=None, phone=None, nickname=None, role_id=2): """创建新用户""" user = User( username=username, password=password, email=email, phone=phone, nickname=nickname, role_id=role_id ) db.session.add(user) db.session.commit() return user class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True, autoincrement=True) role_name = db.Column(db.String(32), unique=True, nullable=False) description = db.Column(db.String(128)) users = db.relationship('User', backref='role') ================================================================================ File: ./app/models/log.py ================================================================================ ================================================================================ File: ./app/models/notification.py ================================================================================ ================================================================================ File: ./app/models/__init__.py ================================================================================ def create_app(): app = Flask(__name__) # ... 配置代码 ... # 初始化数据库 db.init_app(app) # 导入模型,确保所有模型在创建表之前被加载 from app.models.user import User, Role from app.models.book import Book, Category from app.models.borrow import BorrowRecord from app.models.inventory import InventoryLog # 创建数据库表 with app.app_context(): db.create_all() # ... 其余代码 ... ================================================================================ File: ./app/models/book.py ================================================================================ from app.models.user import db from datetime import datetime class Category(db.Model): __tablename__ = 'categories' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False) parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) sort = db.Column(db.Integer, default=0) # 关系 - 只保留与自身的关系 parent = db.relationship('Category', remote_side=[id], backref='children') def __repr__(self): return f'' 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 关系会在后面步骤添加 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/book-detail.css ================================================================================ /* 图书详情页样式 */ .book-detail-container { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .actions { display: flex; gap: 10px; } .book-content { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); overflow: hidden; } .book-header { display: flex; padding: 25px; border-bottom: 1px solid #f0f0f0; background-color: #f9f9f9; } .book-cover-large { flex: 0 0 200px; height: 300px; background-color: #f0f0f0; border-radius: 5px; overflow: hidden; box-shadow: 0 4px 8px rgba(0,0,0,0.1); margin-right: 30px; } .book-cover-large img { width: 100%; height: 100%; object-fit: cover; } .no-cover-large { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #aaa; } .no-cover-large i { font-size: 48px; margin-bottom: 10px; } .book-main-info { flex: 1; } .book-title { font-size: 1.8rem; font-weight: 600; margin-bottom: 15px; color: #333; } .book-author { font-size: 1.1rem; color: #555; margin-bottom: 20px; } .book-meta-info { margin-bottom: 25px; } .meta-item { display: flex; align-items: center; margin-bottom: 12px; color: #666; } .meta-item i { width: 20px; margin-right: 10px; text-align: center; color: #555; } .meta-value { font-weight: 500; color: #444; } .tag { display: inline-block; background-color: #e9ecef; color: #495057; padding: 2px 8px; border-radius: 3px; margin-right: 5px; margin-bottom: 5px; font-size: 0.85rem; } .book-status-info { display: flex; align-items: center; gap: 20px; margin-top: 20px; } .status-badge { display: inline-block; padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 0.9rem; } .status-badge.available { background-color: #d4edda; color: #155724; } .status-badge.unavailable { background-color: #f8d7da; color: #721c24; } .stock-info { font-size: 0.95rem; color: #555; } .book-details-section { padding: 25px; } .book-details-section h3 { font-size: 1.3rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; color: #444; } .book-description { color: #555; line-height: 1.6; } .no-description { color: #888; font-style: italic; } .book-borrow-history { padding: 0 25px 25px; } .book-borrow-history h3 { font-size: 1.3rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; color: #444; } .borrow-table { border: 1px solid #eee; } .no-records { color: #888; font-style: italic; text-align: center; padding: 20px; background-color: #f9f9f9; border-radius: 4px; } /* 响应式调整 */ @media (max-width: 768px) { .book-header { flex-direction: column; } .book-cover-large { margin-right: 0; margin-bottom: 20px; max-width: 200px; align-self: center; } .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } .actions { width: 100%; } } ================================================================================ File: ./app/static/css/book.css ================================================================================ /* 图书列表页面样式 - 女性友好版 */ /* 背景和泡泡动画 */ .book-list-container { padding: 24px; background-color: #ffeef2; /* 淡粉色背景 */ min-height: calc(100vh - 60px); position: relative; overflow: hidden; } /* 泡泡动画 */ .book-list-container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 0; } @keyframes bubble { 0% { transform: translateY(100%) scale(0); opacity: 0; } 50% { opacity: 0.6; } 100% { transform: translateY(-100vh) scale(1); opacity: 0; } } .bubble { position: absolute; bottom: -50px; background-color: rgba(255, 255, 255, 0.5); border-radius: 50%; z-index: 1; animation: bubble 15s infinite ease-in; } /* 为页面添加15个泡泡 */ .bubble:nth-child(1) { left: 5%; width: 30px; height: 30px; animation-duration: 20s; animation-delay: 0s; } .bubble:nth-child(2) { left: 15%; width: 20px; height: 20px; animation-duration: 18s; animation-delay: 1s; } .bubble:nth-child(3) { left: 25%; width: 25px; height: 25px; animation-duration: 16s; animation-delay: 2s; } .bubble:nth-child(4) { left: 35%; width: 15px; height: 15px; animation-duration: 15s; animation-delay: 0.5s; } .bubble:nth-child(5) { left: 45%; width: 30px; height: 30px; animation-duration: 14s; animation-delay: 3s; } .bubble:nth-child(6) { left: 55%; width: 20px; height: 20px; animation-duration: 13s; animation-delay: 2.5s; } .bubble:nth-child(7) { left: 65%; width: 25px; height: 25px; animation-duration: 12s; animation-delay: 1.5s; } .bubble:nth-child(8) { left: 75%; width: 15px; height: 15px; animation-duration: 11s; animation-delay: 4s; } .bubble:nth-child(9) { left: 85%; width: 30px; height: 30px; animation-duration: 10s; animation-delay: 3.5s; } .bubble:nth-child(10) { left: 10%; width: 18px; height: 18px; animation-duration: 19s; animation-delay: 0.5s; } .bubble:nth-child(11) { left: 20%; width: 22px; height: 22px; animation-duration: 17s; animation-delay: 2.5s; } .bubble:nth-child(12) { left: 30%; width: 28px; height: 28px; animation-duration: 16s; animation-delay: 1.2s; } .bubble:nth-child(13) { left: 40%; width: 17px; height: 17px; animation-duration: 15s; animation-delay: 3.7s; } .bubble:nth-child(14) { left: 60%; width: 23px; height: 23px; animation-duration: 13s; animation-delay: 2.1s; } .bubble:nth-child(15) { left: 80%; width: 19px; height: 19px; animation-duration: 12s; animation-delay: 1.7s; } /* 页面标题部分 */ .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid rgba(233, 152, 174, 0.3); position: relative; z-index: 2; } .page-header h1 { color: #d23f6e; font-size: 1.9rem; font-weight: 600; margin: 0; text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8); } /* 更漂亮的顶部按钮 */ .action-buttons { display: flex; gap: 12px; position: relative; z-index: 2; } .action-buttons .btn { display: flex; align-items: center; justify-content: center; gap: 8px; border-radius: 50px; font-weight: 500; padding: 9px 18px; transition: all 0.3s ease; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06); border: none; font-size: 0.95rem; position: relative; overflow: hidden; } .action-buttons .btn::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), transparent); pointer-events: none; } .action-buttons .btn:hover { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08); } .action-buttons .btn:active { transform: translateY(1px); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } /* 按钮颜色 */ .btn-primary { background: linear-gradient(135deg, #5c88da, #4a73c7); color: white; } .btn-success { background: linear-gradient(135deg, #56c596, #41b384); color: white; } .btn-info { background: linear-gradient(135deg, #5bc0de, #46b8da); color: white; } .btn-secondary { background: linear-gradient(135deg, #f0ad4e, #ec971f); color: white; } .btn-danger { background: linear-gradient(135deg, #ff7676, #ff5252); color: white; } /* 过滤和搜索部分 */ .filter-section { margin-bottom: 25px; padding: 18px; background-color: rgba(255, 255, 255, 0.8); border-radius: 16px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); position: relative; z-index: 2; backdrop-filter: blur(5px); } .search-form { display: flex; flex-direction: column; gap: 16px; } .search-row { margin-bottom: 5px; width: 100%; } .search-group { display: flex; width: 100%; max-width: 800px; } .search-group .form-control { border: 1px solid #f9c0d0; border-right: none; border-radius: 25px 0 0 25px; padding: 10px 20px; height: 42px; font-size: 0.95rem; background-color: rgba(255, 255, 255, 0.9); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s; flex: 1; } .search-group .form-control:focus { outline: none; border-color: #e67e9f; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2); } .search-group .btn { border-radius: 50%; width: 42px; height: 42px; min-width: 42px; padding: 0; background: linear-gradient(135deg, #e67e9f 60%, #ffd3e1 100%); color: white; display: flex; align-items: center; justify-content: center; margin-left: -1px; /* 防止和输入框间有缝隙 */ font-size: 1.1rem; box-shadow: 0 2px 6px rgba(230, 126, 159, 0.10); transition: background 0.2s, box-shadow 0.2s; } .search-group .btn:hover { background: linear-gradient(135deg, #d23f6e 80%, #efb6c6 100%); color: #fff; box-shadow: 0 4px 12px rgba(230, 126, 159, 0.14); } .filter-row { display: flex; flex-wrap: wrap; gap: 15px; width: 100%; } .filter-group { flex: 1; min-width: 130px; } .filter-section .form-control { border: 1px solid #f9c0d0; border-radius: 25px; height: 42px; padding: 10px 20px; background-color: rgba(255, 255, 255, 0.9); appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23e67e9f' d='M6 8.825L1.175 4 2.238 2.938 6 6.7 9.763 2.937 10.825 4z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 15px center; background-size: 12px; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); width: 100%; } .filter-section .form-control:focus { outline: none; border-color: #e67e9f; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2); } /* 图书网格布局 */ .books-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24px; margin-bottom: 30px; position: relative; z-index: 2; } /* 图书卡片样式 */ .book-card { display: flex; flex-direction: column; border-radius: 16px; overflow: hidden; background-color: white; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.06); transition: all 0.3s ease; height: 100%; position: relative; border: 1px solid rgba(233, 152, 174, 0.2); } .book-card:hover { transform: translateY(-8px); box-shadow: 0 12px 25px rgba(0, 0, 0, 0.1); } .book-cover { width: 100%; height: 180px; background-color: #faf3f5; overflow: hidden; position: relative; } .book-cover::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, transparent 60%, rgba(249, 219, 227, 0.4)); pointer-events: none; } .book-cover img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; } .book-card:hover .book-cover img { transform: scale(1.05); } .no-cover { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background: linear-gradient(135deg, #ffeef2 0%, #ffd9e2 100%); color: #e67e9f; position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 1; pointer-events: none; } .no-cover i { font-size: 36px; margin-bottom: 10px; } .book-info { padding: 20px; display: flex; flex-direction: column; flex: 1; } .book-title { font-size: 1.1rem; font-weight: 600; margin: 0 0 10px; color: #d23f6e; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .book-author { font-size: 0.95rem; color: #888; margin-bottom: 15px; } .book-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; } .book-category { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; background-color: #ffebf0; color: #e67e9f; font-weight: 500; } .book-status { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 500; } .book-status.available { background-color: #dffff6; color: #26a69a; } .book-status.unavailable { background-color: #ffeeee; color: #e57373; } .book-details { flex: 1; display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; font-size: 0.9rem; color: #777; } .book-details p { margin: 0; display: flex; } .book-details strong { min-width: 65px; color: #999; font-weight: 600; } /* 按钮组样式 */ .book-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: auto; } .book-actions .btn { padding: 8px 0; font-size: 0.9rem; text-align: center; border-radius: 25px; transition: all 0.3s; display: flex; align-items: center; justify-content: center; gap: 6px; border: none; font-weight: 500; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .book-actions .btn:hover { transform: translateY(-3px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12); } .book-actions .btn i { font-size: 0.85rem; } /* 具体按钮颜色 */ .book-actions .btn-primary { background: linear-gradient(135deg, #5c88da, #4a73c7); } .book-actions .btn-info { background: linear-gradient(135deg, #5bc0de, #46b8da); } .book-actions .btn-success { background: linear-gradient(135deg, #56c596, #41b384); } .book-actions .btn-danger { background: linear-gradient(135deg, #ff7676, #ff5252); } /* 无图书状态 */ .no-books { grid-column: 1 / -1; padding: 50px 30px; text-align: center; background-color: white; border-radius: 16px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); position: relative; z-index: 2; } .no-books i { font-size: 60px; color: #f9c0d0; margin-bottom: 20px; } .no-books p { font-size: 1.1rem; color: #e67e9f; font-weight: 500; } /* 分页容器 */ .pagination-container { display: flex; flex-direction: column; align-items: center; margin-top: 30px; position: relative; z-index: 2; } .pagination { display: flex; list-style: none; padding: 0; margin: 0 0 15px 0; background-color: white; border-radius: 30px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); overflow: hidden; } .pagination .page-item { margin: 0; } .pagination .page-link { display: flex; align-items: center; justify-content: center; min-width: 40px; height: 40px; padding: 0 15px; border: none; color: #777; font-weight: 500; transition: all 0.2s; position: relative; } .pagination .page-link:hover { color: #e67e9f; background-color: #fff9fb; } .pagination .page-item.active .page-link { background-color: #e67e9f; color: white; box-shadow: none; } .pagination .page-item.disabled .page-link { color: #bbb; background-color: #f9f9f9; } .pagination-info { color: #999; font-size: 0.9rem; } /* 优化模态框样式 */ .modal-content { border-radius: 20px; border: none; box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07); overflow: hidden; } .modal-header { padding: 20px 25px; background-color: #ffeef2; border-bottom: 1px solid #ffe0e9; } .modal-title { color: #d23f6e; font-size: 1.2rem; font-weight: 600; } .modal-body { padding: 25px; } .modal-footer { padding: 15px 25px; border-top: 1px solid #ffe0e9; background-color: #ffeef2; } .modal-body p { color: #666; font-size: 1rem; line-height: 1.6; } .modal-body p.text-danger { color: #ff5252 !important; font-weight: 500; display: flex; align-items: center; gap: 8px; } .modal-body p.text-danger::before { content: "\f06a"; font-family: "Font Awesome 5 Free"; font-weight: 900; } .modal .close { font-size: 1.5rem; color: #e67e9f; opacity: 0.8; text-shadow: none; transition: all 0.2s; } .modal .close:hover { opacity: 1; color: #d23f6e; } .modal .btn { border-radius: 25px; padding: 8px 20px; font-weight: 500; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); border: none; } .modal .btn-secondary { background: linear-gradient(135deg, #a0a0a0, #808080); color: white; } .modal .btn-danger { background: linear-gradient(135deg, #ff7676, #ff5252); color: white; } /* 封面标题栏 */ .cover-title-bar { position: absolute; left: 0; right: 0; bottom: 0; background: linear-gradient(0deg, rgba(233,152,174,0.92) 0%, rgba(255,255,255,0.08) 90%); color: #fff; font-size: 1rem; font-weight: bold; padding: 10px 14px 7px 14px; text-shadow: 0 2px 6px rgba(180,0,80,0.14); line-height: 1.3; width: 100%; box-sizing: border-box; display: flex; align-items: flex-end; min-height: 38px; z-index: 2; } .book-card:hover .cover-title-bar { background: linear-gradient(0deg, #d23f6e 0%, rgba(255,255,255,0.1) 100%); font-size: 1.07rem; letter-spacing: .5px; } /* 响应式调整 */ @media (max-width: 992px) { .filter-row { flex-wrap: wrap; } .filter-group { flex: 1 0 180px; } } @media (max-width: 768px) { .book-list-container { padding: 16px; } .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } .action-buttons { width: 100%; overflow-x: auto; padding-bottom: 8px; flex-wrap: nowrap; justify-content: flex-start; } .filter-section { padding: 15px; } .search-form { flex-direction: column; gap: 12px; } .search-group { max-width: 100%; } .filter-row { gap: 12px; } .books-grid { grid-template-columns: 1fr; } .book-actions { grid-template-columns: 1fr 1fr; } } @media (max-width: 600px) { .cover-title-bar { font-size: 0.95rem; min-height: 27px; padding: 8px 8px 5px 10px; } .book-actions { grid-template-columns: 1fr; } } ================================================================================ File: ./app/static/css/index.css ================================================================================ /* index.css - 仅用于图书管理系统首页/仪表板 */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; } body { background-color: #f5f7fa; color: #333; font-size: 16px; line-height: 1.6; } a { text-decoration: none; color: #4a89dc; } ul { list-style: none; } /* 应用容器 */ .app-container { display: flex; min-height: 100vh; } /* 侧边导航栏 */ .sidebar { width: 250px; background-color: #2c3e50; color: #ecf0f1; padding: 20px 0; box-shadow: 0 0 10px rgba(0,0,0,0.1); position: fixed; height: 100vh; overflow-y: auto; } .logo-container { padding: 0 20px 20px; display: flex; align-items: center; justify-content: center; flex-direction: column; margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.1); } .logo { width: 60px; height: auto; margin-bottom: 10px; } .logo-container h2 { font-size: 1.2rem; margin: 10px 0; color: #ecf0f1; font-weight: 500; } .nav-links li { margin-bottom: 5px; } .nav-links li a { padding: 10px 20px; display: flex; align-items: center; color: #bdc3c7; transition: all 0.3s ease; } .nav-links li a i { margin-right: 10px; font-size: 1.1rem; width: 20px; text-align: center; } .nav-links li a:hover, .nav-links li.active a { background-color: #34495e; color: #ecf0f1; border-left: 3px solid #4a89dc; } .nav-category { padding: 10px 20px; font-size: 0.85rem; text-transform: uppercase; color: #7f8c8d; margin-top: 15px; margin-bottom: 5px; } /* 主内容区 */ .main-content { flex: 1; margin-left: 250px; padding: 20px; } /* 顶部导航栏 */ .top-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 30px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 20px; } .search-container { position: relative; width: 300px; } .search-input { padding: 10px 15px 10px 40px; width: 100%; border: 1px solid #e1e4e8; border-radius: 20px; font-size: 14px; transition: all 0.3s ease; } .search-input:focus { border-color: #4a89dc; box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.1); outline: none; } .search-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #8492a6; } .user-menu { display: flex; align-items: center; } .notifications { margin-right: 20px; position: relative; cursor: pointer; } .notifications i { font-size: 1.2rem; color: #606266; } .badge { position: absolute; top: -8px; right: -8px; background-color: #f56c6c; color: white; font-size: 0.7rem; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .user-info { display: flex; align-items: center; position: relative; cursor: pointer; } .user-avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #4a89dc; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px; font-size: 1.2rem; } .user-details { display: flex; flex-direction: column; } .user-name { font-weight: 500; color: #333; } .user-role { font-size: 0.8rem; color: #8492a6; } .dropdown-menu { position: absolute; top: 100%; right: 0; background-color: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); padding: 10px 0; min-width: 150px; display: none; z-index: 10; } .user-info.active .dropdown-menu { display: block; } .dropdown-menu a { display: block; padding: 8px 15px; color: #606266; transition: all 0.3s ease; } .dropdown-menu a:hover { background-color: #f5f7fa; } .dropdown-menu a i { margin-right: 8px; width: 16px; text-align: center; } /* 欢迎区域 */ .welcome-section { background: linear-gradient(to right, #4a89dc, #5d9cec); color: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } .welcome-section h1 { font-size: 1.8rem; margin-bottom: 5px; } .welcome-section p { font-size: 1rem; opacity: 0.9; } /* 统计卡片样式 */ .stats-container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; } .stat-card { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px; display: flex; align-items: center; transition: transform 0.3s ease, box-shadow 0.3s ease; } .stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .stat-icon { font-size: 2rem; color: #4a89dc; margin-right: 15px; width: 40px; text-align: center; } .stat-info h3 { font-size: 0.9rem; color: #606266; margin-bottom: 5px; } .stat-number { font-size: 1.8rem; font-weight: 600; color: #2c3e50; } /* 主要内容区域 */ .main-sections { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; } .content-section { background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 20px; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #edf2f7; } .section-header h2 { font-size: 1.2rem; color: #2c3e50; } .view-all { font-size: 0.85rem; color: #4a89dc; display: flex; align-items: center; } .view-all i { margin-left: 5px; transition: transform 0.3s ease; } .view-all:hover i { transform: translateX(3px); } /* 图书卡片样式 */ .book-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; } .book-card { display: flex; border: 1px solid #edf2f7; border-radius: 8px; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; } .book-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.05); } .book-cover { width: 100px; height: 140px; min-width: 100px; background-color: #f5f7fa; display: flex; align-items: center; justify-content: center; overflow: hidden; } .book-cover img { width: 100%; height: 100%; object-fit: cover; } .book-info { padding: 15px; flex: 1; display: flex; flex-direction: column; } .book-title { font-size: 1rem; margin-bottom: 5px; color: #2c3e50; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; } .book-author { font-size: 0.85rem; color: #606266; margin-bottom: 10px; } .book-meta { display: flex; justify-content: space-between; margin-bottom: 15px; } .book-category { background-color: #e5f1ff; color: #4a89dc; padding: 3px 8px; border-radius: 4px; font-size: 0.75rem; } .book-status { font-size: 0.75rem; font-weight: 500; } .book-status.available { color: #67c23a; } .book-status.borrowed { color: #e6a23c; } .borrow-btn { background-color: #4a89dc; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.85rem; margin-top: auto; transition: background-color 0.3s ease; } .borrow-btn:hover { background-color: #357bc8; } /* 通知公告样式 */ .notice-item { display: flex; padding: 15px 0; border-bottom: 1px solid #edf2f7; } .notice-item:last-child { border-bottom: none; } .notice-icon { font-size: 1.5rem; color: #4a89dc; margin-right: 15px; display: flex; align-items: flex-start; padding-top: 5px; } .notice-content h3 { font-size: 1rem; color: #2c3e50; margin-bottom: 5px; } .notice-content p { font-size: 0.9rem; color: #606266; margin-bottom: 10px; } .notice-meta { display: flex; justify-content: space-between; align-items: center; } .notice-time { font-size: 0.8rem; color: #8492a6; } .renew-btn { background-color: #ecf5ff; color: #4a89dc; border: 1px solid #d9ecff; padding: 5px 10px; border-radius: 4px; font-size: 0.8rem; cursor: pointer; transition: all 0.3s ease; } .renew-btn:hover { background-color: #4a89dc; color: white; border-color: #4a89dc; } /* 热门图书区域 */ .popular-section { margin-top: 20px; } .popular-books { display: flex; overflow-x: auto; gap: 15px; padding-bottom: 10px; } .popular-book-item { display: flex; background-color: #f8fafc; border-radius: 8px; padding: 15px; min-width: 280px; position: relative; } .rank-badge { position: absolute; top: -10px; left: 10px; background-color: #4a89dc; color: white; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 0.8rem; font-weight: bold; } .book-cover.small { width: 60px; height: 90px; min-width: 60px; margin-right: 15px; } .book-details { flex: 1; } .book-stats { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; } .book-stats span { font-size: 0.8rem; color: #8492a6; } .book-stats i { margin-right: 5px; } /* 响应式调整 */ @media (max-width: 1200px) { .stats-container { grid-template-columns: repeat(2, 1fr); } .main-sections { grid-template-columns: 1fr; } } @media (max-width: 768px) { .sidebar { width: 70px; overflow: hidden; } .logo-container { padding: 10px; } .logo-container h2 { display: none; } .nav-links li a span { display: none; } .nav-links li a i { margin-right: 0; } .nav-category { display: none; } .main-content { margin-left: 70px; } .search-container { width: 180px; } .book-grid { grid-template-columns: 1fr; } } @media (max-width: 576px) { .stats-container { grid-template-columns: 1fr; } .top-bar { flex-direction: column; gap: 15px; } .search-container { width: 100%; } .user-details { display: none; } } ================================================================================ File: ./app/static/css/main.css ================================================================================ /* 基础样式 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f0f2f5; color: #333; } .app-container { display: flex; min-height: 100vh; } /* 侧边栏样式 */ .sidebar { width: 250px; background-color: #2c3e50; color: white; box-shadow: 2px 0 5px rgba(0,0,0,0.1); position: fixed; height: 100vh; overflow-y: auto; z-index: 1000; } .logo-container { display: flex; align-items: center; padding: 20px 15px; border-bottom: 1px solid rgba(255,255,255,0.1); } .logo { width: 40px; height: 40px; margin-right: 10px; } .logo-container h2 { font-size: 1.2rem; font-weight: 600; } .nav-links { list-style: none; padding: 15px 0; } .nav-category { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; padding: 15px 20px 5px; color: #adb5bd; } .nav-links li { position: relative; } .nav-links li.active { background-color: rgba(255,255,255,0.1); } .nav-links li a { display: flex; align-items: center; padding: 12px 20px; color: #ecf0f1; text-decoration: none; transition: all 0.3s; } .nav-links li a:hover { background-color: rgba(255,255,255,0.05); } .nav-links li a i { margin-right: 10px; width: 20px; text-align: center; } /* 主内容区样式 */ .main-content { flex: 1; margin-left: 250px; display: flex; flex-direction: column; min-height: 100vh; } .top-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 25px; background-color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 900; } .search-container { position: relative; width: 350px; } .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: #adb5bd; } .search-input { width: 100%; padding: 10px 10px 10px 35px; border: 1px solid #dee2e6; border-radius: 20px; font-size: 0.9rem; } .search-input:focus { outline: none; border-color: #4a6cf7; } .user-menu { display: flex; align-items: center; } .notifications { position: relative; margin-right: 20px; cursor: pointer; } .badge { position: absolute; top: -5px; right: -5px; background-color: #e74c3c; color: white; font-size: 0.7rem; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .user-info { display: flex; align-items: center; cursor: pointer; position: relative; } .user-avatar { width: 40px; height: 40px; background-color: #4a6cf7; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px; } .user-details { display: flex; flex-direction: column; } .user-name { font-weight: 600; font-size: 0.9rem; } .user-role { font-size: 0.8rem; color: #6c757d; } .dropdown-menu { position: absolute; top: 100%; right: 0; background-color: white; box-shadow: 0 3px 10px rgba(0,0,0,0.1); border-radius: 5px; width: 200px; padding: 10px 0; display: none; z-index: 1000; } .user-info.active .dropdown-menu { display: block; } .dropdown-menu a { display: block; padding: 8px 15px; color: #333; text-decoration: none; transition: background-color 0.3s; } .dropdown-menu a:hover { background-color: #f8f9fa; } .dropdown-menu a i { width: 20px; margin-right: 10px; text-align: center; } /* 内容区域 */ .content-wrapper { flex: 1; padding: 20px; background-color: #f0f2f5; } /* 响应式适配 */ @media (max-width: 768px) { .sidebar { width: 70px; overflow: visible; } .logo-container h2 { display: none; } .nav-links li a span { display: none; } .main-content { margin-left: 70px; } .user-details { display: none; } } ================================================================================ File: ./app/static/css/book-form.css ================================================================================ /* 图书表单页面样式 */ .book-form-container { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .actions { display: flex; gap: 10px; } .book-form { margin-bottom: 30px; } .card { margin-bottom: 20px; border: 1px solid rgba(0,0,0,0.125); border-radius: 0.25rem; } .card-header { padding: 0.75rem 1.25rem; background-color: rgba(0,0,0,0.03); border-bottom: 1px solid rgba(0,0,0,0.125); font-weight: 600; } .card-body { padding: 1.25rem; } /* 必填项标记 */ .required { color: #dc3545; margin-left: 2px; } /* 封面预览区域 */ .cover-preview-container { display: flex; flex-direction: column; align-items: center; gap: 15px; } .cover-preview { width: 100%; max-width: 200px; height: 280px; border: 1px dashed #ccc; border-radius: 4px; overflow: hidden; background-color: #f8f9fa; margin-bottom: 10px; } .cover-image { width: 100%; height: 100%; object-fit: cover; } .no-cover-placeholder { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #aaa; } .no-cover-placeholder i { font-size: 48px; margin-bottom: 10px; } .upload-container { width: 100%; max-width: 200px; } /* 提交按钮容器 */ .form-submit-container { margin-top: 30px; } /* 响应式调整 */ @media (max-width: 768px) { .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } .actions { width: 100%; } } ================================================================================ File: ./app/static/css/categories.css ================================================================================ /* 分类管理页面样式 */ .categories-container { padding: 20px; } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .card { margin-bottom: 20px; border: 1px solid rgba(0,0,0,0.125); border-radius: 0.25rem; } .card-header { padding: 0.75rem 1.25rem; background-color: rgba(0,0,0,0.03); border-bottom: 1px solid rgba(0,0,0,0.125); font-weight: 600; } .card-body { padding: 1.25rem; } .category-table { border: 1px solid #eee; } .category-table th { background-color: #f8f9fa; } .no-categories { text-align: center; padding: 30px; color: #888; } .no-categories i { font-size: 48px; color: #ddd; margin-bottom: 10px; } /* 通知弹窗 */ .notification-alert { position: fixed; top: 20px; right: 20px; min-width: 300px; z-index: 1050; } /* 响应式调整 */ @media (max-width: 768px) { .page-header { flex-direction: column; align-items: flex-start; gap: 15px; } } ================================================================================ File: ./app/static/js/main.js ================================================================================ // 主JS文件 - 包含登录和注册功能 document.addEventListener('DOMContentLoaded', function() { // 主题切换 const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) { const body = document.body; themeToggle.addEventListener('click', function() { body.classList.toggle('dark-mode'); const isDarkMode = body.classList.contains('dark-mode'); localStorage.setItem('dark-mode', isDarkMode); themeToggle.innerHTML = isDarkMode ? '🌙' : '☀️'; }); // 从本地存储中加载主题首选项 const savedDarkMode = localStorage.getItem('dark-mode') === 'true'; if (savedDarkMode) { body.classList.add('dark-mode'); themeToggle.innerHTML = '🌙'; } } // 密码可见性切换 const passwordToggle = document.getElementById('password-toggle'); if (passwordToggle) { const passwordInput = document.getElementById('password'); passwordToggle.addEventListener('click', function() { const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; passwordInput.setAttribute('type', type); passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️‍🗨️'; }); } // 登录表单验证 const loginForm = document.getElementById('login-form'); if (loginForm) { const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); const usernameError = document.getElementById('username-error'); const passwordError = document.getElementById('password-error'); const loginButton = document.getElementById('login-button'); if (usernameInput && usernameError) { usernameInput.addEventListener('input', function() { if (usernameInput.value.trim() === '') { usernameError.textContent = '用户名不能为空'; usernameError.classList.add('show'); } else { usernameError.classList.remove('show'); } }); } if (passwordInput && passwordError) { passwordInput.addEventListener('input', function() { if (passwordInput.value.trim() === '') { passwordError.textContent = '密码不能为空'; passwordError.classList.add('show'); } else if (passwordInput.value.length < 6) { passwordError.textContent = '密码长度至少6位'; passwordError.classList.add('show'); } else { passwordError.classList.remove('show'); } }); } loginForm.addEventListener('submit', function(e) { let isValid = true; // 验证用户名 if (usernameInput.value.trim() === '') { usernameError.textContent = '用户名不能为空'; usernameError.classList.add('show'); isValid = false; } // 验证密码 if (passwordInput.value.trim() === '') { passwordError.textContent = '密码不能为空'; passwordError.classList.add('show'); isValid = false; } else if (passwordInput.value.length < 6) { passwordError.textContent = '密码长度至少6位'; passwordError.classList.add('show'); isValid = false; } if (!isValid) { e.preventDefault(); } else if (loginButton) { loginButton.classList.add('loading-state'); } }); } // 注册表单验证 const registerForm = document.getElementById('register-form'); if (registerForm) { const usernameInput = document.getElementById('username'); const emailInput = document.getElementById('email'); const passwordInput = document.getElementById('password'); const confirmPasswordInput = document.getElementById('confirm_password'); const verificationCodeInput = document.getElementById('verification_code'); const usernameError = document.getElementById('username-error'); const emailError = document.getElementById('email-error'); const passwordError = document.getElementById('password-error'); const confirmPasswordError = document.getElementById('confirm-password-error'); const verificationCodeError = document.getElementById('verification-code-error'); const registerButton = document.getElementById('register-button'); const sendCodeBtn = document.getElementById('send-code-btn'); // 用户名验证 if (usernameInput && usernameError) { usernameInput.addEventListener('input', function() { if (usernameInput.value.trim() === '') { usernameError.textContent = '用户名不能为空'; usernameError.classList.add('show'); } else if (usernameInput.value.length < 3) { usernameError.textContent = '用户名至少3个字符'; usernameError.classList.add('show'); } else { usernameError.classList.remove('show'); } }); } // 邮箱验证 if (emailInput && emailError) { emailInput.addEventListener('input', function() { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (emailInput.value.trim() === '') { emailError.textContent = '邮箱不能为空'; emailError.classList.add('show'); } else if (!emailRegex.test(emailInput.value)) { emailError.textContent = '请输入有效的邮箱地址'; emailError.classList.add('show'); } else { emailError.classList.remove('show'); } }); } // 密码验证 if (passwordInput && passwordError) { passwordInput.addEventListener('input', function() { if (passwordInput.value.trim() === '') { passwordError.textContent = '密码不能为空'; passwordError.classList.add('show'); } else if (passwordInput.value.length < 6) { passwordError.textContent = '密码长度至少6位'; passwordError.classList.add('show'); } else { passwordError.classList.remove('show'); } // 检查确认密码是否匹配 if (confirmPasswordInput && confirmPasswordInput.value) { if (confirmPasswordInput.value !== passwordInput.value) { confirmPasswordError.textContent = '两次输入的密码不匹配'; confirmPasswordError.classList.add('show'); } else { confirmPasswordError.classList.remove('show'); } } }); } // 确认密码验证 if (confirmPasswordInput && confirmPasswordError) { confirmPasswordInput.addEventListener('input', function() { if (confirmPasswordInput.value.trim() === '') { confirmPasswordError.textContent = '请确认密码'; confirmPasswordError.classList.add('show'); } else if (confirmPasswordInput.value !== passwordInput.value) { confirmPasswordError.textContent = '两次输入的密码不匹配'; confirmPasswordError.classList.add('show'); } else { confirmPasswordError.classList.remove('show'); } }); } // 发送验证码按钮 if (sendCodeBtn) { sendCodeBtn.addEventListener('click', function() { const email = emailInput.value.trim(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email) { emailError.textContent = '请输入邮箱地址'; emailError.classList.add('show'); return; } else if (!emailRegex.test(email)) { emailError.textContent = '请输入有效的邮箱地址'; emailError.classList.add('show'); return; } // 禁用按钮并显示倒计时 let countdown = 60; sendCodeBtn.disabled = true; const originalText = sendCodeBtn.textContent; sendCodeBtn.textContent = `${countdown}秒后重试`; const timer = setInterval(() => { countdown--; sendCodeBtn.textContent = `${countdown}秒后重试`; if (countdown <= 0) { clearInterval(timer); sendCodeBtn.disabled = false; sendCodeBtn.textContent = originalText; } }, 1000); // 发送请求获取验证码 fetch('/user/send_verification_code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: email }), }) .then(response => response.json()) .then(data => { console.log("验证码发送响应:", data); // 添加调试日志 if (data.success) { showMessage('验证码已发送', '请检查您的邮箱', 'success'); } else { showMessage('发送失败', data.message || '请稍后重试', 'error'); clearInterval(timer); sendCodeBtn.disabled = false; sendCodeBtn.textContent = originalText; } }) .catch(error => { console.error('Error:', error); showMessage('发送失败', '网络错误,请稍后重试', 'error'); clearInterval(timer); sendCodeBtn.disabled = false; sendCodeBtn.textContent = originalText; }); }); } // 表单提交验证 registerForm.addEventListener('submit', function(e) { let isValid = true; // 验证用户名 if (usernameInput.value.trim() === '') { usernameError.textContent = '用户名不能为空'; usernameError.classList.add('show'); isValid = false; } else if (usernameInput.value.length < 3) { usernameError.textContent = '用户名至少3个字符'; usernameError.classList.add('show'); isValid = false; } // 验证邮箱 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (emailInput.value.trim() === '') { emailError.textContent = '邮箱不能为空'; emailError.classList.add('show'); isValid = false; } else if (!emailRegex.test(emailInput.value)) { emailError.textContent = '请输入有效的邮箱地址'; emailError.classList.add('show'); isValid = false; } // 验证密码 if (passwordInput.value.trim() === '') { passwordError.textContent = '密码不能为空'; passwordError.classList.add('show'); isValid = false; } else if (passwordInput.value.length < 6) { passwordError.textContent = '密码长度至少6位'; passwordError.classList.add('show'); isValid = false; } // 验证确认密码 if (confirmPasswordInput.value.trim() === '') { confirmPasswordError.textContent = '请确认密码'; confirmPasswordError.classList.add('show'); isValid = false; } else if (confirmPasswordInput.value !== passwordInput.value) { confirmPasswordError.textContent = '两次输入的密码不匹配'; confirmPasswordError.classList.add('show'); isValid = false; } // 验证验证码 if (verificationCodeInput.value.trim() === '') { verificationCodeError.textContent = '请输入验证码'; verificationCodeError.classList.add('show'); isValid = false; } if (!isValid) { e.preventDefault(); } else if (registerButton) { registerButton.classList.add('loading-state'); } }); } // 通知消息显示函数 function showMessage(title, message, type) { const notification = document.createElement('div'); notification.className = `notification ${type}`; const icon = type === 'success' ? '✓' : '✗'; notification.innerHTML = `
${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-list.js ================================================================================ // 图书列表页面脚本 $(document).ready(function() { // 处理分类筛选 function setFilter(button, categoryId) { // 移除所有按钮的活跃状态 $('.filter-btn').removeClass('active'); // 为当前点击的按钮添加活跃状态 $(button).addClass('active'); // 设置隐藏的分类ID输入值 $('#category_id').val(categoryId); // 提交表单 $(button).closest('form').submit(); } // 处理排序方向切换 function toggleSortDirection(button) { const $button = $(button); const isAsc = $button.hasClass('asc'); // 切换方向类 $button.toggleClass('asc desc'); // 更新图标 if (isAsc) { $button.find('i').removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down'); $('#sort_order').val('desc'); } else { $button.find('i').removeClass('fa-sort-amount-down').addClass('fa-sort-amount-up'); $('#sort_order').val('asc'); } // 提交表单 $button.closest('form').submit(); } // 将函数暴露到全局作用域 window.setFilter = setFilter; window.toggleSortDirection = toggleSortDirection; // 处理删除图书 let bookIdToDelete = null; $('.delete-btn').click(function(e) { e.preventDefault(); bookIdToDelete = $(this).data('id'); const bookTitle = $(this).data('title'); $('#deleteBookTitle').text(bookTitle); $('#deleteModal').modal('show'); }); $('#confirmDelete').click(function() { if (!bookIdToDelete) return; $.ajax({ url: `/book/delete/${bookIdToDelete}`, type: 'POST', success: function(response) { if (response.success) { $('#deleteModal').modal('hide'); // 显示成功消息 showNotification(response.message, 'success'); // 移除图书卡片 setTimeout(() => { location.reload(); }, 800); } else { showNotification(response.message, 'error'); } }, error: function() { showNotification('删除操作失败,请稍后重试', 'error'); } }); }); // 处理借阅图书 $('.borrow-btn').click(function(e) { e.preventDefault(); const bookId = $(this).data('id'); $.ajax({ url: `/borrow/add/${bookId}`, type: 'POST', success: function(response) { if (response.success) { showNotification(response.message, 'success'); // 可以更新UI显示,比如更新库存或禁用借阅按钮 setTimeout(() => { location.reload(); }, 800); } else { showNotification(response.message, 'error'); } }, error: function() { showNotification('借阅操作失败,请稍后重试', 'error'); } }); }); // 显示通知 function showNotification(message, type) { // 移除可能存在的旧通知 $('.notification-alert').remove(); const alertClass = type === 'success' ? 'notification-success' : 'notification-error'; const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'; const notification = `
${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
页面未找到

抱歉,您访问的页面不存在或已被移除。

请检查URL是否正确,或返回首页。

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

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

================================================================================ 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 %}
基本信息
图书简介
封面图片
暂无封面
库存和价格
¥
{% 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 %}

借阅历史

{% set borrow_records = book.borrow_records.order_by(BorrowRecord.borrow_date.desc()).limit(10).all() %} {% 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 # 创建蓝图 user_bp = Blueprint('user', __name__) # 使用内存字典代替Redis存储验证码 class VerificationStore: def __init__(self): self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}} def setex(self, email, seconds, code): """设置验证码并指定过期时间""" expiry = datetime.now() + timedelta(seconds=seconds) self.codes[email] = {'code': code, 'expires': expiry} return True def get(self, email): """获取验证码,如果过期则返回None""" if email not in self.codes: return None data = self.codes[email] if datetime.now() > data['expires']: # 验证码已过期,删除它 self.delete(email) return None return data['code'] def delete(self, email): """删除验证码""" if email in self.codes: del self.codes[email] return True # 使用内存存储验证码 verification_codes = VerificationStore() def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('user.login')) return f(*args, **kwargs) return decorated_function @user_bp.route('/login', methods=['GET', 'POST']) def login(): # 保持原代码不变 if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') remember_me = request.form.get('remember_me') == 'on' if not username or not password: return render_template('login.html', error='用户名和密码不能为空') # 检查用户是否存在 user = User.query.filter((User.username == username) | (User.email == username)).first() if not user or not user.check_password(password): return render_template('login.html', error='用户名或密码错误') if user.status == 0: return render_template('login.html', error='账号已被禁用,请联系管理员') # 登录成功,保存用户信息到会话 session['user_id'] = user.id session['username'] = user.username session['role_id'] = user.role_id if remember_me: # 设置会话过期时间为7天 session.permanent = True # 记录登录日志(可选) # log_user_action('用户登录') # 重定向到首页 return redirect(url_for('index')) return render_template('login.html') @user_bp.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') confirm_password = request.form.get('confirm_password') verification_code = request.form.get('verification_code') # 验证表单数据 if not username or not email or not password or not confirm_password or not verification_code: return render_template('register.html', error='所有字段都是必填项') if password != confirm_password: return render_template('register.html', error='两次输入的密码不匹配') # 检查用户名和邮箱是否已存在 if User.query.filter_by(username=username).first(): return render_template('register.html', error='用户名已存在') if User.query.filter_by(email=email).first(): return render_template('register.html', error='邮箱已被注册') # 验证验证码 stored_code = verification_codes.get(email) if not stored_code or stored_code != verification_code: return render_template('register.html', error='验证码无效或已过期') # 创建新用户 try: new_user = User( username=username, password=password, # 密码会在模型中自动哈希 email=email, nickname=username # 默认昵称与用户名相同 ) db.session.add(new_user) db.session.commit() # 清除验证码 verification_codes.delete(email) flash('注册成功,请登录', 'success') return redirect(url_for('user.login')) except Exception as e: db.session.rollback() logging.error(f"User registration failed: {str(e)}") return render_template('register.html', error='注册失败,请稍后重试') return render_template('register.html') @user_bp.route('/logout') def logout(): # 清除会话数据 session.pop('user_id', None) session.pop('username', None) session.pop('role_id', None) return redirect(url_for('user.login')) @user_bp.route('/send_verification_code', methods=['POST']) def send_verification_code(): data = request.get_json() email = data.get('email') if not email: return jsonify({'success': False, 'message': '请提供邮箱地址'}) # 检查邮箱格式 import re if not re.match(r"[^@]+@[^@]+\.[^@]+", email): return jsonify({'success': False, 'message': '邮箱格式不正确'}) # 生成验证码 code = generate_verification_code() # 存储验证码(10分钟有效) verification_codes.setex(email, 600, code) # 10分钟过期 # 发送验证码邮件 if send_verification_email(email, code): return jsonify({'success': True, 'message': '验证码已发送'}) else: return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'}) ================================================================================ File: ./app/controllers/log.py ================================================================================ ================================================================================ File: ./app/controllers/__init__.py ================================================================================ ================================================================================ File: ./app/controllers/book.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, g, jsonify from app.models.book import Book, Category from app.models.user import db from app.utils.auth import login_required, admin_required import os from werkzeug.utils import secure_filename import datetime import pandas as pd import uuid book_bp = Blueprint('book', __name__) # 图书列表页面 @book_bp.route('/list') @login_required def book_list(): print("访问图书列表页面") # 调试输出 page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) query = Book.query # 搜索功能 search = request.args.get('search', '') if search: query = query.filter( (Book.title.contains(search)) | (Book.author.contains(search)) | (Book.isbn.contains(search)) ) # 分类筛选 category_id = request.args.get('category_id', type=int) if category_id: query = query.filter_by(category_id=category_id) # 排序 sort = request.args.get('sort', 'id') order = request.args.get('order', 'desc') if order == 'desc': query = query.order_by(getattr(Book, sort).desc()) else: query = query.order_by(getattr(Book, sort)) pagination = query.paginate(page=page, per_page=per_page) books = pagination.items # 获取所有分类供筛选使用 categories = Category.query.all() return render_template('book/list.html', books=books, pagination=pagination, search=search, categories=categories, category_id=category_id, sort=sort, order=order, current_user=g.user) # 图书详情页面 @book_bp.route('/detail/') @login_required def book_detail(book_id): book = Book.query.get_or_404(book_id) return render_template('book/detail.html', book=book, current_user=g.user) # 添加图书页面 @book_bp.route('/add', methods=['GET', 'POST']) @login_required @admin_required def add_book(): if request.method == 'POST': title = request.form.get('title') author = request.form.get('author') publisher = request.form.get('publisher') category_id = request.form.get('category_id') tags = request.form.get('tags') isbn = request.form.get('isbn') publish_year = request.form.get('publish_year') description = request.form.get('description') stock = request.form.get('stock', type=int) price = request.form.get('price') if not title or not author: flash('书名和作者不能为空', 'danger') categories = Category.query.all() return render_template('book/add.html', categories=categories, current_user=g.user) # 处理封面图片上传 cover_url = None if 'cover' in request.files: cover_file = request.files['cover'] if cover_file and cover_file.filename != '': filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}") upload_folder = os.path.join(current_app.static_folder, 'uploads/covers') # 确保上传目录存在 if not os.path.exists(upload_folder): os.makedirs(upload_folder) file_path = os.path.join(upload_folder, filename) cover_file.save(file_path) cover_url = f'/static/covers/{filename}' # 创建新图书 book = Book( title=title, author=author, publisher=publisher, category_id=category_id, tags=tags, isbn=isbn, publish_year=publish_year, description=description, cover_url=cover_url, stock=stock, price=price, status=1, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now() ) db.session.add(book) # 记录库存日志 if stock and int(stock) > 0: from app.models.inventory import InventoryLog inventory_log = InventoryLog( book_id=book.id, change_type='入库', change_amount=stock, after_stock=stock, operator_id=g.user.id, remark='新书入库', changed_at=datetime.datetime.now() ) db.session.add(inventory_log) db.session.commit() flash('图书添加成功', 'success') return redirect(url_for('book.book_list')) categories = Category.query.all() return render_template('book/add.html', categories=categories, current_user=g.user) # 编辑图书 @book_bp.route('/edit/', 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 ================================================================================ ================================================================================ 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 ================================================================================