Book_system/code_collection.txt
2025-04-30 16:23:05 +08:00

5724 lines
177 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

================================================================================
File: ./config.py
================================================================================
import os
# 数据库配置
DB_HOST = os.environ.get('DB_HOST', '27.124.22.104')
DB_PORT = os.environ.get('DB_PORT', '3306')
DB_USER = os.environ.get('DB_USER', 'book20250428')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'booksystem')
DB_NAME = os.environ.get('DB_NAME', 'book_system')
# 数据库连接字符串
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 应用密钥
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key_replace_in_production')
# 邮件配置
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls')
EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com')
EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf')
EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com')
EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL')
# 会话配置
PERMANENT_SESSION_LIFETIME = 86400 * 7
================================================================================
File: ./all_file_output.py
================================================================================
import os
import sys
def collect_code_files(output_file="code_collection.txt"):
# 定义代码文件扩展名
code_extensions = [
'.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs',
'.js', '.html', '.css', '.php', '.go', '.rb',
'.swift', '.kt', '.ts', '.sh', '.pl', '.r'
]
# 定义要排除的目录
excluded_dirs = [
'venv', 'env', '.venv', '.env', 'virtualenv',
'__pycache__', 'node_modules', '.git', '.idea',
'dist', 'build', 'target', 'bin'
]
# 计数器
file_count = 0
# 打开输出文件
with open(output_file, 'w', encoding='utf-8') as out_file:
# 遍历当前目录及所有子目录
for root, dirs, files in os.walk('.'):
# 从dirs中移除排除的目录这会阻止os.walk进入这些目录
dirs[:] = [d for d in dirs if d not in excluded_dirs]
for file in files:
# 获取文件扩展名
_, ext = os.path.splitext(file)
# 检查是否为代码文件
if ext.lower() in code_extensions:
file_path = os.path.join(root, file)
file_count += 1
# 写入文件路径作为分隔
out_file.write(f"\n{'=' * 80}\n")
out_file.write(f"File: {file_path}\n")
out_file.write(f"{'=' * 80}\n\n")
# 尝试读取文件内容并写入
try:
with open(file_path, 'r', encoding='utf-8') as code_file:
out_file.write(code_file.read())
except UnicodeDecodeError:
# 尝试用不同的编码
try:
with open(file_path, 'r', encoding='latin-1') as code_file:
out_file.write(code_file.read())
except Exception as e:
out_file.write(f"无法读取文件内容: {str(e)}\n")
except Exception as e:
out_file.write(f"读取文件时出错: {str(e)}\n")
print(f"已成功收集 {file_count} 个代码文件到 {output_file}")
if __name__ == "__main__":
# 如果提供了命令行参数,则使用它作为输出文件名
output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt"
collect_code_files(output_file)
================================================================================
File: ./app.py
================================================================================
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=49666)
================================================================================
File: ./main.py
================================================================================
# 这是一个示例 Python 脚本。
# 按 ⌃R 执行或将其替换为您的代码。
# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
def print_hi(name):
# 在下面的代码行中使用断点来调试脚本。
print(f'Hi, {name}') # 按 ⌘F8 切换断点。
# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
print_hi('PyCharm')
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
================================================================================
File: ./app/__init__.py
================================================================================
from flask import Flask, render_template, session, g
from app.models.user import db, User
from app.controllers.user import user_bp
from app.controllers.book import book_bp # 引入图书蓝图
import os
def create_app():
app = Flask(__name__)
# 配置应用
app.config.from_mapping(
SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'),
SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system',
SQLALCHEMY_TRACK_MODIFICATIONS=False,
PERMANENT_SESSION_LIFETIME=86400 * 7, # 7天
# 邮件配置
EMAIL_HOST='smtp.qq.com',
EMAIL_PORT=587,
EMAIL_ENCRYPTION='starttls',
EMAIL_USERNAME='3399560459@qq.com',
EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码不是邮箱密码
EMAIL_FROM='3399560459@qq.com',
EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
)
# 实例配置,如果存在
app.config.from_pyfile('config.py', silent=True)
# 初始化数据库
db.init_app(app)
# 注册蓝图
app.register_blueprint(user_bp, url_prefix='/user')
app.register_blueprint(book_bp, url_prefix='/book') # 注册图书蓝图
# 创建数据库表
with app.app_context():
# 先导入基础模型
from app.models.user import User, Role
from app.models.book import Book, Category
# 创建表
db.create_all()
# 再导入依赖模型
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
# 现在添加反向关系
# 这样可以确保所有类都已经定义好
Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic')
Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic')
Category.books = db.relationship('Book', backref='category', lazy='dynamic')
# 创建默认角色
from app.models.user import Role
if not Role.query.filter_by(id=1).first():
admin_role = Role(id=1, role_name='管理员', description='系统管理员')
db.session.add(admin_role)
if not Role.query.filter_by(id=2).first():
user_role = Role(id=2, role_name='普通用户', description='普通用户')
db.session.add(user_role)
# 创建管理员账号
if not User.query.filter_by(username='admin').first():
admin = User(
username='admin',
password='admin123',
email='admin@example.com',
role_id=1,
nickname='系统管理员'
)
db.session.add(admin)
# 创建基础分类
from app.models.book import Category
if not Category.query.first():
categories = [
Category(name='文学', sort=1),
Category(name='计算机', sort=2),
Category(name='历史', sort=3),
Category(name='科学', sort=4),
Category(name='艺术', sort=5),
Category(name='经济', sort=6),
Category(name='哲学', sort=7),
Category(name='教育', sort=8)
]
db.session.add_all(categories)
db.session.commit()
# 请求前处理
@app.before_request
def load_logged_in_user():
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = User.query.get(user_id)
# 首页路由
@app.route('/')
def index():
if not g.user:
return render_template('login.html')
return render_template('index.html', current_user=g.user)
# 错误处理
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
# 模板过滤器
@app.template_filter('nl2br')
def nl2br_filter(s):
if not s:
return s
return s.replace('\n', '<br>')
return app
================================================================================
File: ./app/utils/auth.py
================================================================================
from functools import wraps
from flask import g, redirect, url_for, flash, request
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
flash('请先登录', 'warning')
return redirect(url_for('user.login', next=request.url))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
flash('请先登录', 'warning')
return redirect(url_for('user.login', next=request.url))
if g.user.role_id != 1: # 假设role_id=1是管理员
flash('权限不足', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
================================================================================
File: ./app/utils/db.py
================================================================================
================================================================================
File: ./app/utils/__init__.py
================================================================================
================================================================================
File: ./app/utils/email.py
================================================================================
import smtplib
import random
import string
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from flask import current_app
import logging
# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# 配置邮件发送功能
def send_verification_email(to_email, verification_code):
"""
发送验证码邮件
"""
try:
# 从应用配置获取邮件设置
email_host = current_app.config['EMAIL_HOST']
email_port = current_app.config['EMAIL_PORT']
email_username = current_app.config['EMAIL_USERNAME']
email_password = current_app.config['EMAIL_PASSWORD']
email_from = current_app.config['EMAIL_FROM']
email_from_name = current_app.config['EMAIL_FROM_NAME']
logger.info(f"准备发送邮件到: {to_email}, 验证码: {verification_code}")
logger.debug(f"邮件配置: 主机={email_host}, 端口={email_port}")
# 邮件内容
msg = MIMEMultipart()
msg['From'] = f"{email_from_name} <{email_from}>"
msg['To'] = to_email
msg['Subject'] = "图书管理系统 - 验证码"
# 邮件正文
body = f"""
<html>
<body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e1e1e1; border-radius: 5px;">
<h2 style="color: #4a89dc;">图书管理系统 - 邮箱验证</h2>
<p>您好,</p>
<p>感谢您注册图书管理系统,您的验证码是:</p>
<div style="background-color: #f5f5f5; padding: 10px; border-radius: 5px; text-align: center; font-size: 24px; letter-spacing: 5px; font-weight: bold; margin: 20px 0;">
{verification_code}
</div>
<p>该验证码将在10分钟内有效请勿将验证码分享给他人。</p>
<p>如果您没有请求此验证码,请忽略此邮件。</p>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e1e1e1; font-size: 12px; color: #888;">
<p>此邮件为系统自动发送,请勿回复。</p>
<p>&copy; 2025 图书管理系统</p>
</div>
</div>
</body>
</html>
"""
msg.attach(MIMEText(body, 'html'))
logger.debug("尝试连接到SMTP服务器...")
# 连接服务器发送邮件
server = smtplib.SMTP(email_host, email_port)
server.set_debuglevel(1) # 启用详细的SMTP调试输出
logger.debug("检查是否需要STARTTLS加密...")
if current_app.config.get('EMAIL_ENCRYPTION') == 'starttls':
logger.debug("启用STARTTLS...")
server.starttls()
logger.debug(f"尝试登录邮箱: {email_username}")
server.login(email_username, email_password)
logger.debug("发送邮件...")
server.send_message(msg)
logger.debug("关闭连接...")
server.quit()
logger.info(f"邮件发送成功: {to_email}")
return True
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
return False
def generate_verification_code(length=6):
"""
生成数字验证码
"""
return ''.join(random.choice(string.digits) for _ in range(length))
================================================================================
File: ./app/utils/helpers.py
================================================================================
================================================================================
File: ./app/models/user.py
================================================================================
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
email = db.Column(db.String(128), unique=True, nullable=True)
phone = db.Column(db.String(20), unique=True, nullable=True)
nickname = db.Column(db.String(64), nullable=True)
status = db.Column(db.Integer, default=1) # 1: active, 0: disabled
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2) # 2: 普通用户, 1: 管理员
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2):
self.username = username
self.set_password(password)
self.email = email
self.phone = phone
self.nickname = nickname
self.role_id = role_id
def set_password(self, password):
"""设置密码,使用哈希加密"""
self.password = generate_password_hash(password)
def check_password(self, password):
"""验证密码"""
return check_password_hash(self.password, password)
def to_dict(self):
"""转换为字典格式"""
return {
'id': self.id,
'username': self.username,
'email': self.email,
'phone': self.phone,
'nickname': self.nickname,
'status': self.status,
'role_id': self.role_id,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
}
@classmethod
def create_user(cls, username, password, email=None, phone=None, nickname=None, role_id=2):
"""创建新用户"""
user = User(
username=username,
password=password,
email=email,
phone=phone,
nickname=nickname,
role_id=role_id
)
db.session.add(user)
db.session.commit()
return user
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
role_name = db.Column(db.String(32), unique=True, nullable=False)
description = db.Column(db.String(128))
users = db.relationship('User', backref='role')
================================================================================
File: ./app/models/log.py
================================================================================
================================================================================
File: ./app/models/notification.py
================================================================================
================================================================================
File: ./app/models/__init__.py
================================================================================
def create_app():
app = Flask(__name__)
# ... 配置代码 ...
# 初始化数据库
db.init_app(app)
# 导入模型,确保所有模型在创建表之前被加载
from app.models.user import User, Role
from app.models.book import Book, Category
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
# 创建数据库表
with app.app_context():
db.create_all()
# ... 其余代码 ...
================================================================================
File: ./app/models/book.py
================================================================================
from app.models.user import db
from datetime import datetime
class Category(db.Model):
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
sort = db.Column(db.Integer, default=0)
# 关系 - 只保留与自身的关系
parent = db.relationship('Category', remote_side=[id], backref='children')
def __repr__(self):
return f'<Category {self.name}>'
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
author = db.Column(db.String(128), nullable=False)
publisher = db.Column(db.String(128), nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
tags = db.Column(db.String(255), nullable=True)
isbn = db.Column(db.String(32), unique=True, nullable=True)
publish_year = db.Column(db.String(16), nullable=True)
description = db.Column(db.Text, nullable=True)
cover_url = db.Column(db.String(255), nullable=True)
stock = db.Column(db.Integer, default=0)
price = db.Column(db.Numeric(10, 2), nullable=True)
status = db.Column(db.Integer, default=1) # 1:可用, 0:不可用
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# 移除所有关系引用
def __repr__(self):
return f'<Book {self.title}>'
================================================================================
File: ./app/models/borrow.py
================================================================================
from app.models.user import db
from datetime import datetime
class BorrowRecord(db.Model):
__tablename__ = 'borrow_records'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False)
borrow_date = db.Column(db.DateTime, nullable=False, default=datetime.now)
due_date = db.Column(db.DateTime, nullable=False)
return_date = db.Column(db.DateTime, nullable=True)
renew_count = db.Column(db.Integer, default=0)
status = db.Column(db.Integer, default=1) # 1: 借出, 0: 已归还
remark = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# 添加反向关系引用
user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic'))
# book 关系会在后面步骤添加
def __repr__(self):
return f'<BorrowRecord {self.id}>'
================================================================================
File: ./app/models/announcement.py
================================================================================
================================================================================
File: ./app/models/inventory.py
================================================================================
from app.models.user import db
from datetime import datetime
class InventoryLog(db.Model):
__tablename__ = 'inventory_logs'
id = db.Column(db.Integer, primary_key=True)
book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False)
change_type = db.Column(db.String(32), nullable=False) # 'in' 入库, 'out' 出库
change_amount = db.Column(db.Integer, nullable=False)
after_stock = db.Column(db.Integer, nullable=False)
operator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
remark = db.Column(db.String(255), nullable=True)
changed_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# 添加反向关系引用
operator = db.relationship('User', backref=db.backref('inventory_logs', lazy='dynamic'))
# book 关系会在后面步骤添加
def __repr__(self):
return f'<InventoryLog {self.id}>'
================================================================================
File: ./app/static/css/book-detail.css
================================================================================
/* 图书详情页样式 */
.book-detail-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.actions {
display: flex;
gap: 10px;
}
.book-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
overflow: hidden;
}
.book-header {
display: flex;
padding: 25px;
border-bottom: 1px solid #f0f0f0;
background-color: #f9f9f9;
}
.book-cover-large {
flex: 0 0 200px;
height: 300px;
background-color: #f0f0f0;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
margin-right: 30px;
}
.book-cover-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-cover-large {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #aaa;
}
.no-cover-large i {
font-size: 48px;
margin-bottom: 10px;
}
.book-main-info {
flex: 1;
}
.book-title {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 15px;
color: #333;
}
.book-author {
font-size: 1.1rem;
color: #555;
margin-bottom: 20px;
}
.book-meta-info {
margin-bottom: 25px;
}
.meta-item {
display: flex;
align-items: center;
margin-bottom: 12px;
color: #666;
}
.meta-item i {
width: 20px;
margin-right: 10px;
text-align: center;
color: #555;
}
.meta-value {
font-weight: 500;
color: #444;
}
.tag {
display: inline-block;
background-color: #e9ecef;
color: #495057;
padding: 2px 8px;
border-radius: 3px;
margin-right: 5px;
margin-bottom: 5px;
font-size: 0.85rem;
}
.book-status-info {
display: flex;
align-items: center;
gap: 20px;
margin-top: 20px;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9rem;
}
.status-badge.available {
background-color: #d4edda;
color: #155724;
}
.status-badge.unavailable {
background-color: #f8d7da;
color: #721c24;
}
.stock-info {
font-size: 0.95rem;
color: #555;
}
.book-details-section {
padding: 25px;
}
.book-details-section h3 {
font-size: 1.3rem;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
color: #444;
}
.book-description {
color: #555;
line-height: 1.6;
}
.no-description {
color: #888;
font-style: italic;
}
.book-borrow-history {
padding: 0 25px 25px;
}
.book-borrow-history h3 {
font-size: 1.3rem;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
color: #444;
}
.borrow-table {
border: 1px solid #eee;
}
.no-records {
color: #888;
font-style: italic;
text-align: center;
padding: 20px;
background-color: #f9f9f9;
border-radius: 4px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.book-header {
flex-direction: column;
}
.book-cover-large {
margin-right: 0;
margin-bottom: 20px;
max-width: 200px;
align-self: center;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.actions {
width: 100%;
}
}
================================================================================
File: ./app/static/css/book.css
================================================================================
/* 图书列表页面样式 - 女性友好版 */
/* 背景和泡泡动画 */
.book-list-container {
padding: 24px;
background-color: #ffeef2; /* 淡粉色背景 */
min-height: calc(100vh - 60px);
position: relative;
overflow: hidden;
}
/* 泡泡动画 */
.book-list-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 0;
}
@keyframes bubble {
0% {
transform: translateY(100%) scale(0);
opacity: 0;
}
50% {
opacity: 0.6;
}
100% {
transform: translateY(-100vh) scale(1);
opacity: 0;
}
}
.bubble {
position: absolute;
bottom: -50px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
z-index: 1;
animation: bubble 15s infinite ease-in;
}
/* 为页面添加15个泡泡 */
.bubble:nth-child(1) { left: 5%; width: 30px; height: 30px; animation-duration: 20s; animation-delay: 0s; }
.bubble:nth-child(2) { left: 15%; width: 20px; height: 20px; animation-duration: 18s; animation-delay: 1s; }
.bubble:nth-child(3) { left: 25%; width: 25px; height: 25px; animation-duration: 16s; animation-delay: 2s; }
.bubble:nth-child(4) { left: 35%; width: 15px; height: 15px; animation-duration: 15s; animation-delay: 0.5s; }
.bubble:nth-child(5) { left: 45%; width: 30px; height: 30px; animation-duration: 14s; animation-delay: 3s; }
.bubble:nth-child(6) { left: 55%; width: 20px; height: 20px; animation-duration: 13s; animation-delay: 2.5s; }
.bubble:nth-child(7) { left: 65%; width: 25px; height: 25px; animation-duration: 12s; animation-delay: 1.5s; }
.bubble:nth-child(8) { left: 75%; width: 15px; height: 15px; animation-duration: 11s; animation-delay: 4s; }
.bubble:nth-child(9) { left: 85%; width: 30px; height: 30px; animation-duration: 10s; animation-delay: 3.5s; }
.bubble:nth-child(10) { left: 10%; width: 18px; height: 18px; animation-duration: 19s; animation-delay: 0.5s; }
.bubble:nth-child(11) { left: 20%; width: 22px; height: 22px; animation-duration: 17s; animation-delay: 2.5s; }
.bubble:nth-child(12) { left: 30%; width: 28px; height: 28px; animation-duration: 16s; animation-delay: 1.2s; }
.bubble:nth-child(13) { left: 40%; width: 17px; height: 17px; animation-duration: 15s; animation-delay: 3.7s; }
.bubble:nth-child(14) { left: 60%; width: 23px; height: 23px; animation-duration: 13s; animation-delay: 2.1s; }
.bubble:nth-child(15) { left: 80%; width: 19px; height: 19px; animation-duration: 12s; animation-delay: 1.7s; }
/* 页面标题部分 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(233, 152, 174, 0.3);
position: relative;
z-index: 2;
}
.page-header h1 {
color: #d23f6e;
font-size: 1.9rem;
font-weight: 600;
margin: 0;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
}
/* 更漂亮的顶部按钮 */
.action-buttons {
display: flex;
gap: 12px;
position: relative;
z-index: 2;
}
.action-buttons .btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: 50px;
font-weight: 500;
padding: 9px 18px;
transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06);
border: none;
font-size: 0.95rem;
position: relative;
overflow: hidden;
}
.action-buttons .btn::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), transparent);
pointer-events: none;
}
.action-buttons .btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08);
}
.action-buttons .btn:active {
transform: translateY(1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* 按钮颜色 */
.btn-primary {
background: linear-gradient(135deg, #5c88da, #4a73c7);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #56c596, #41b384);
color: white;
}
.btn-info {
background: linear-gradient(135deg, #5bc0de, #46b8da);
color: white;
}
.btn-secondary {
background: linear-gradient(135deg, #f0ad4e, #ec971f);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #ff7676, #ff5252);
color: white;
}
/* 过滤和搜索部分 */
.filter-section {
margin-bottom: 25px;
padding: 18px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 16px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 2;
backdrop-filter: blur(5px);
}
.search-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.search-row {
margin-bottom: 5px;
width: 100%;
}
.search-group {
display: flex;
width: 100%;
max-width: 800px;
}
.search-group .form-control {
border: 1px solid #f9c0d0;
border-right: none;
border-radius: 25px 0 0 25px;
padding: 10px 20px;
height: 42px;
font-size: 0.95rem;
background-color: rgba(255, 255, 255, 0.9);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
flex: 1;
}
.search-group .form-control:focus {
outline: none;
border-color: #e67e9f;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2);
}
.search-group .btn {
border-radius: 50%;
width: 42px;
height: 42px;
min-width: 42px;
padding: 0;
background: linear-gradient(135deg, #e67e9f 60%, #ffd3e1 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
margin-left: -1px; /* 防止和输入框间有缝隙 */
font-size: 1.1rem;
box-shadow: 0 2px 6px rgba(230, 126, 159, 0.10);
transition: background 0.2s, box-shadow 0.2s;
}
.search-group .btn:hover {
background: linear-gradient(135deg, #d23f6e 80%, #efb6c6 100%);
color: #fff;
box-shadow: 0 4px 12px rgba(230, 126, 159, 0.14);
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
width: 100%;
}
.filter-group {
flex: 1;
min-width: 130px;
}
.filter-section .form-control {
border: 1px solid #f9c0d0;
border-radius: 25px;
height: 42px;
padding: 10px 20px;
background-color: rgba(255, 255, 255, 0.9);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23e67e9f' d='M6 8.825L1.175 4 2.238 2.938 6 6.7 9.763 2.937 10.825 4z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 12px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
width: 100%;
}
.filter-section .form-control:focus {
outline: none;
border-color: #e67e9f;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2);
}
/* 图书网格布局 */
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
margin-bottom: 30px;
position: relative;
z-index: 2;
}
/* 图书卡片样式 */
.book-card {
display: flex;
flex-direction: column;
border-radius: 16px;
overflow: hidden;
background-color: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
height: 100%;
position: relative;
border: 1px solid rgba(233, 152, 174, 0.2);
}
.book-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 25px rgba(0, 0, 0, 0.1);
}
.book-cover {
width: 100%;
height: 180px;
background-color: #faf3f5;
overflow: hidden;
position: relative;
}
.book-cover::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, transparent 60%, rgba(249, 219, 227, 0.4));
pointer-events: none;
}
.book-cover img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.book-card:hover .book-cover img {
transform: scale(1.05);
}
.no-cover {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #ffeef2 0%, #ffd9e2 100%);
color: #e67e9f;
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 1;
pointer-events: none;
}
.no-cover i {
font-size: 36px;
margin-bottom: 10px;
}
.book-info {
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.book-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 10px;
color: #d23f6e;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.book-author {
font-size: 0.95rem;
color: #888;
margin-bottom: 15px;
}
.book-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.book-category {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
background-color: #ffebf0;
color: #e67e9f;
font-weight: 500;
}
.book-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.book-status.available {
background-color: #dffff6;
color: #26a69a;
}
.book-status.unavailable {
background-color: #ffeeee;
color: #e57373;
}
.book-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
font-size: 0.9rem;
color: #777;
}
.book-details p {
margin: 0;
display: flex;
}
.book-details strong {
min-width: 65px;
color: #999;
font-weight: 600;
}
/* 按钮组样式 */
.book-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: auto;
}
.book-actions .btn {
padding: 8px 0;
font-size: 0.9rem;
text-align: center;
border-radius: 25px;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border: none;
font-weight: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.book-actions .btn:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}
.book-actions .btn i {
font-size: 0.85rem;
}
/* 具体按钮颜色 */
.book-actions .btn-primary {
background: linear-gradient(135deg, #5c88da, #4a73c7);
}
.book-actions .btn-info {
background: linear-gradient(135deg, #5bc0de, #46b8da);
}
.book-actions .btn-success {
background: linear-gradient(135deg, #56c596, #41b384);
}
.book-actions .btn-danger {
background: linear-gradient(135deg, #ff7676, #ff5252);
}
/* 无图书状态 */
.no-books {
grid-column: 1 / -1;
padding: 50px 30px;
text-align: center;
background-color: white;
border-radius: 16px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 2;
}
.no-books i {
font-size: 60px;
color: #f9c0d0;
margin-bottom: 20px;
}
.no-books p {
font-size: 1.1rem;
color: #e67e9f;
font-weight: 500;
}
/* 分页容器 */
.pagination-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30px;
position: relative;
z-index: 2;
}
.pagination {
display: flex;
list-style: none;
padding: 0;
margin: 0 0 15px 0;
background-color: white;
border-radius: 30px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.pagination .page-item {
margin: 0;
}
.pagination .page-link {
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 40px;
padding: 0 15px;
border: none;
color: #777;
font-weight: 500;
transition: all 0.2s;
position: relative;
}
.pagination .page-link:hover {
color: #e67e9f;
background-color: #fff9fb;
}
.pagination .page-item.active .page-link {
background-color: #e67e9f;
color: white;
box-shadow: none;
}
.pagination .page-item.disabled .page-link {
color: #bbb;
background-color: #f9f9f9;
}
.pagination-info {
color: #999;
font-size: 0.9rem;
}
/* 优化模态框样式 */
.modal-content {
border-radius: 20px;
border: none;
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
overflow: hidden;
}
.modal-header {
padding: 20px 25px;
background-color: #ffeef2;
border-bottom: 1px solid #ffe0e9;
}
.modal-title {
color: #d23f6e;
font-size: 1.2rem;
font-weight: 600;
}
.modal-body {
padding: 25px;
}
.modal-footer {
padding: 15px 25px;
border-top: 1px solid #ffe0e9;
background-color: #ffeef2;
}
.modal-body p {
color: #666;
font-size: 1rem;
line-height: 1.6;
}
.modal-body p.text-danger {
color: #ff5252 !important;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.modal-body p.text-danger::before {
content: "\f06a";
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
.modal .close {
font-size: 1.5rem;
color: #e67e9f;
opacity: 0.8;
text-shadow: none;
transition: all 0.2s;
}
.modal .close:hover {
opacity: 1;
color: #d23f6e;
}
.modal .btn {
border-radius: 25px;
padding: 8px 20px;
font-weight: 500;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
border: none;
}
.modal .btn-secondary {
background: linear-gradient(135deg, #a0a0a0, #808080);
color: white;
}
.modal .btn-danger {
background: linear-gradient(135deg, #ff7676, #ff5252);
color: white;
}
/* 封面标题栏 */
.cover-title-bar {
position: absolute;
left: 0; right: 0; bottom: 0;
background: linear-gradient(0deg, rgba(233,152,174,0.92) 0%, rgba(255,255,255,0.08) 90%);
color: #fff;
font-size: 1rem;
font-weight: bold;
padding: 10px 14px 7px 14px;
text-shadow: 0 2px 6px rgba(180,0,80,0.14);
line-height: 1.3;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: flex-end;
min-height: 38px;
z-index: 2;
}
.book-card:hover .cover-title-bar {
background: linear-gradient(0deg, #d23f6e 0%, rgba(255,255,255,0.1) 100%);
font-size: 1.07rem;
letter-spacing: .5px;
}
/* 响应式调整 */
@media (max-width: 992px) {
.filter-row {
flex-wrap: wrap;
}
.filter-group {
flex: 1 0 180px;
}
}
@media (max-width: 768px) {
.book-list-container {
padding: 16px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.action-buttons {
width: 100%;
overflow-x: auto;
padding-bottom: 8px;
flex-wrap: nowrap;
justify-content: flex-start;
}
.filter-section {
padding: 15px;
}
.search-form {
flex-direction: column;
gap: 12px;
}
.search-group {
max-width: 100%;
}
.filter-row {
gap: 12px;
}
.books-grid {
grid-template-columns: 1fr;
}
.book-actions {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 600px) {
.cover-title-bar {
font-size: 0.95rem;
min-height: 27px;
padding: 8px 8px 5px 10px;
}
.book-actions {
grid-template-columns: 1fr;
}
}
================================================================================
File: ./app/static/css/index.css
================================================================================
/* index.css - 仅用于图书管理系统首页/仪表板 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
font-size: 16px;
line-height: 1.6;
}
a {
text-decoration: none;
color: #4a89dc;
}
ul {
list-style: none;
}
/* 应用容器 */
.app-container {
display: flex;
min-height: 100vh;
}
/* 侧边导航栏 */
.sidebar {
width: 250px;
background-color: #2c3e50;
color: #ecf0f1;
padding: 20px 0;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
position: fixed;
height: 100vh;
overflow-y: auto;
}
.logo-container {
padding: 0 20px 20px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.logo {
width: 60px;
height: auto;
margin-bottom: 10px;
}
.logo-container h2 {
font-size: 1.2rem;
margin: 10px 0;
color: #ecf0f1;
font-weight: 500;
}
.nav-links li {
margin-bottom: 5px;
}
.nav-links li a {
padding: 10px 20px;
display: flex;
align-items: center;
color: #bdc3c7;
transition: all 0.3s ease;
}
.nav-links li a i {
margin-right: 10px;
font-size: 1.1rem;
width: 20px;
text-align: center;
}
.nav-links li a:hover, .nav-links li.active a {
background-color: #34495e;
color: #ecf0f1;
border-left: 3px solid #4a89dc;
}
.nav-category {
padding: 10px 20px;
font-size: 0.85rem;
text-transform: uppercase;
color: #7f8c8d;
margin-top: 15px;
margin-bottom: 5px;
}
/* 主内容区 */
.main-content {
flex: 1;
margin-left: 250px;
padding: 20px;
}
/* 顶部导航栏 */
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.search-container {
position: relative;
width: 300px;
}
.search-input {
padding: 10px 15px 10px 40px;
width: 100%;
border: 1px solid #e1e4e8;
border-radius: 20px;
font-size: 14px;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: #4a89dc;
box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.1);
outline: none;
}
.search-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #8492a6;
}
.user-menu {
display: flex;
align-items: center;
}
.notifications {
margin-right: 20px;
position: relative;
cursor: pointer;
}
.notifications i {
font-size: 1.2rem;
color: #606266;
}
.badge {
position: absolute;
top: -8px;
right: -8px;
background-color: #f56c6c;
color: white;
font-size: 0.7rem;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.user-info {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #4a89dc;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
font-size: 1.2rem;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 500;
color: #333;
}
.user-role {
font-size: 0.8rem;
color: #8492a6;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
padding: 10px 0;
min-width: 150px;
display: none;
z-index: 10;
}
.user-info.active .dropdown-menu {
display: block;
}
.dropdown-menu a {
display: block;
padding: 8px 15px;
color: #606266;
transition: all 0.3s ease;
}
.dropdown-menu a:hover {
background-color: #f5f7fa;
}
.dropdown-menu a i {
margin-right: 8px;
width: 16px;
text-align: center;
}
/* 欢迎区域 */
.welcome-section {
background: linear-gradient(to right, #4a89dc, #5d9cec);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.welcome-section h1 {
font-size: 1.8rem;
margin-bottom: 5px;
}
.welcome-section p {
font-size: 1rem;
opacity: 0.9;
}
/* 统计卡片样式 */
.stats-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
padding: 20px;
display: flex;
align-items: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.stat-icon {
font-size: 2rem;
color: #4a89dc;
margin-right: 15px;
width: 40px;
text-align: center;
}
.stat-info h3 {
font-size: 0.9rem;
color: #606266;
margin-bottom: 5px;
}
.stat-number {
font-size: 1.8rem;
font-weight: 600;
color: #2c3e50;
}
/* 主要内容区域 */
.main-sections {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.content-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
padding: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #edf2f7;
}
.section-header h2 {
font-size: 1.2rem;
color: #2c3e50;
}
.view-all {
font-size: 0.85rem;
color: #4a89dc;
display: flex;
align-items: center;
}
.view-all i {
margin-left: 5px;
transition: transform 0.3s ease;
}
.view-all:hover i {
transform: translateX(3px);
}
/* 图书卡片样式 */
.book-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.book-card {
display: flex;
border: 1px solid #edf2f7;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.book-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.book-cover {
width: 100px;
height: 140px;
min-width: 100px;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.book-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.book-info {
padding: 15px;
flex: 1;
display: flex;
flex-direction: column;
}
.book-title {
font-size: 1rem;
margin-bottom: 5px;
color: #2c3e50;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.book-author {
font-size: 0.85rem;
color: #606266;
margin-bottom: 10px;
}
.book-meta {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.book-category {
background-color: #e5f1ff;
color: #4a89dc;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
}
.book-status {
font-size: 0.75rem;
font-weight: 500;
}
.book-status.available {
color: #67c23a;
}
.book-status.borrowed {
color: #e6a23c;
}
.borrow-btn {
background-color: #4a89dc;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
margin-top: auto;
transition: background-color 0.3s ease;
}
.borrow-btn:hover {
background-color: #357bc8;
}
/* 通知公告样式 */
.notice-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #edf2f7;
}
.notice-item:last-child {
border-bottom: none;
}
.notice-icon {
font-size: 1.5rem;
color: #4a89dc;
margin-right: 15px;
display: flex;
align-items: flex-start;
padding-top: 5px;
}
.notice-content h3 {
font-size: 1rem;
color: #2c3e50;
margin-bottom: 5px;
}
.notice-content p {
font-size: 0.9rem;
color: #606266;
margin-bottom: 10px;
}
.notice-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.notice-time {
font-size: 0.8rem;
color: #8492a6;
}
.renew-btn {
background-color: #ecf5ff;
color: #4a89dc;
border: 1px solid #d9ecff;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
}
.renew-btn:hover {
background-color: #4a89dc;
color: white;
border-color: #4a89dc;
}
/* 热门图书区域 */
.popular-section {
margin-top: 20px;
}
.popular-books {
display: flex;
overflow-x: auto;
gap: 15px;
padding-bottom: 10px;
}
.popular-book-item {
display: flex;
background-color: #f8fafc;
border-radius: 8px;
padding: 15px;
min-width: 280px;
position: relative;
}
.rank-badge {
position: absolute;
top: -10px;
left: 10px;
background-color: #4a89dc;
color: white;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 0.8rem;
font-weight: bold;
}
.book-cover.small {
width: 60px;
height: 90px;
min-width: 60px;
margin-right: 15px;
}
.book-details {
flex: 1;
}
.book-stats {
display: flex;
flex-direction: column;
gap: 5px;
margin-top: 10px;
}
.book-stats span {
font-size: 0.8rem;
color: #8492a6;
}
.book-stats i {
margin-right: 5px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
.main-sections {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.sidebar {
width: 70px;
overflow: hidden;
}
.logo-container {
padding: 10px;
}
.logo-container h2 {
display: none;
}
.nav-links li a span {
display: none;
}
.nav-links li a i {
margin-right: 0;
}
.nav-category {
display: none;
}
.main-content {
margin-left: 70px;
}
.search-container {
width: 180px;
}
.book-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 576px) {
.stats-container {
grid-template-columns: 1fr;
}
.top-bar {
flex-direction: column;
gap: 15px;
}
.search-container {
width: 100%;
}
.user-details {
display: none;
}
}
================================================================================
File: ./app/static/css/main.css
================================================================================
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f2f5;
color: #333;
}
.app-container {
display: flex;
min-height: 100vh;
}
/* 侧边栏样式 */
.sidebar {
width: 250px;
background-color: #2c3e50;
color: white;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
position: fixed;
height: 100vh;
overflow-y: auto;
z-index: 1000;
}
.logo-container {
display: flex;
align-items: center;
padding: 20px 15px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.logo {
width: 40px;
height: 40px;
margin-right: 10px;
}
.logo-container h2 {
font-size: 1.2rem;
font-weight: 600;
}
.nav-links {
list-style: none;
padding: 15px 0;
}
.nav-category {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 1px;
padding: 15px 20px 5px;
color: #adb5bd;
}
.nav-links li {
position: relative;
}
.nav-links li.active {
background-color: rgba(255,255,255,0.1);
}
.nav-links li a {
display: flex;
align-items: center;
padding: 12px 20px;
color: #ecf0f1;
text-decoration: none;
transition: all 0.3s;
}
.nav-links li a:hover {
background-color: rgba(255,255,255,0.05);
}
.nav-links li a i {
margin-right: 10px;
width: 20px;
text-align: center;
}
/* 主内容区样式 */
.main-content {
flex: 1;
margin-left: 250px;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 25px;
background-color: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 900;
}
.search-container {
position: relative;
width: 350px;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #adb5bd;
}
.search-input {
width: 100%;
padding: 10px 10px 10px 35px;
border: 1px solid #dee2e6;
border-radius: 20px;
font-size: 0.9rem;
}
.search-input:focus {
outline: none;
border-color: #4a6cf7;
}
.user-menu {
display: flex;
align-items: center;
}
.notifications {
position: relative;
margin-right: 20px;
cursor: pointer;
}
.badge {
position: absolute;
top: -5px;
right: -5px;
background-color: #e74c3c;
color: white;
font-size: 0.7rem;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.user-info {
display: flex;
align-items: center;
cursor: pointer;
position: relative;
}
.user-avatar {
width: 40px;
height: 40px;
background-color: #4a6cf7;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 600;
font-size: 0.9rem;
}
.user-role {
font-size: 0.8rem;
color: #6c757d;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background-color: white;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
border-radius: 5px;
width: 200px;
padding: 10px 0;
display: none;
z-index: 1000;
}
.user-info.active .dropdown-menu {
display: block;
}
.dropdown-menu a {
display: block;
padding: 8px 15px;
color: #333;
text-decoration: none;
transition: background-color 0.3s;
}
.dropdown-menu a:hover {
background-color: #f8f9fa;
}
.dropdown-menu a i {
width: 20px;
margin-right: 10px;
text-align: center;
}
/* 内容区域 */
.content-wrapper {
flex: 1;
padding: 20px;
background-color: #f0f2f5;
}
/* 响应式适配 */
@media (max-width: 768px) {
.sidebar {
width: 70px;
overflow: visible;
}
.logo-container h2 {
display: none;
}
.nav-links li a span {
display: none;
}
.main-content {
margin-left: 70px;
}
.user-details {
display: none;
}
}
================================================================================
File: ./app/static/css/book-form.css
================================================================================
/* 图书表单页面样式 */
.book-form-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.actions {
display: flex;
gap: 10px;
}
.book-form {
margin-bottom: 30px;
}
.card {
margin-bottom: 20px;
border: 1px solid rgba(0,0,0,0.125);
border-radius: 0.25rem;
}
.card-header {
padding: 0.75rem 1.25rem;
background-color: rgba(0,0,0,0.03);
border-bottom: 1px solid rgba(0,0,0,0.125);
font-weight: 600;
}
.card-body {
padding: 1.25rem;
}
/* 必填项标记 */
.required {
color: #dc3545;
margin-left: 2px;
}
/* 封面预览区域 */
.cover-preview-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.cover-preview {
width: 100%;
max-width: 200px;
height: 280px;
border: 1px dashed #ccc;
border-radius: 4px;
overflow: hidden;
background-color: #f8f9fa;
margin-bottom: 10px;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-cover-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #aaa;
}
.no-cover-placeholder i {
font-size: 48px;
margin-bottom: 10px;
}
.upload-container {
width: 100%;
max-width: 200px;
}
/* 提交按钮容器 */
.form-submit-container {
margin-top: 30px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.actions {
width: 100%;
}
}
================================================================================
File: ./app/static/css/categories.css
================================================================================
/* 分类管理页面样式 */
.categories-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.card {
margin-bottom: 20px;
border: 1px solid rgba(0,0,0,0.125);
border-radius: 0.25rem;
}
.card-header {
padding: 0.75rem 1.25rem;
background-color: rgba(0,0,0,0.03);
border-bottom: 1px solid rgba(0,0,0,0.125);
font-weight: 600;
}
.card-body {
padding: 1.25rem;
}
.category-table {
border: 1px solid #eee;
}
.category-table th {
background-color: #f8f9fa;
}
.no-categories {
text-align: center;
padding: 30px;
color: #888;
}
.no-categories i {
font-size: 48px;
color: #ddd;
margin-bottom: 10px;
}
/* 通知弹窗 */
.notification-alert {
position: fixed;
top: 20px;
right: 20px;
min-width: 300px;
z-index: 1050;
}
/* 响应式调整 */
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
}
================================================================================
File: ./app/static/js/main.js
================================================================================
// 主JS文件 - 包含登录和注册功能
document.addEventListener('DOMContentLoaded', function() {
// 主题切换
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
const body = document.body;
themeToggle.addEventListener('click', function() {
body.classList.toggle('dark-mode');
const isDarkMode = body.classList.contains('dark-mode');
localStorage.setItem('dark-mode', isDarkMode);
themeToggle.innerHTML = isDarkMode ? '🌙' : '☀️';
});
// 从本地存储中加载主题首选项
const savedDarkMode = localStorage.getItem('dark-mode') === 'true';
if (savedDarkMode) {
body.classList.add('dark-mode');
themeToggle.innerHTML = '🌙';
}
}
// 密码可见性切换
const passwordToggle = document.getElementById('password-toggle');
if (passwordToggle) {
const passwordInput = document.getElementById('password');
passwordToggle.addEventListener('click', function() {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️‍🗨️';
});
}
// 登录表单验证
const loginForm = document.getElementById('login-form');
if (loginForm) {
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const usernameError = document.getElementById('username-error');
const passwordError = document.getElementById('password-error');
const loginButton = document.getElementById('login-button');
if (usernameInput && usernameError) {
usernameInput.addEventListener('input', function() {
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
} else {
usernameError.classList.remove('show');
}
});
}
if (passwordInput && passwordError) {
passwordInput.addEventListener('input', function() {
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
} else {
passwordError.classList.remove('show');
}
});
}
loginForm.addEventListener('submit', function(e) {
let isValid = true;
// 验证用户名
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
isValid = false;
}
// 验证密码
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
isValid = false;
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
isValid = false;
}
if (!isValid) {
e.preventDefault();
} else if (loginButton) {
loginButton.classList.add('loading-state');
}
});
}
// 注册表单验证
const registerForm = document.getElementById('register-form');
if (registerForm) {
const usernameInput = document.getElementById('username');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const confirmPasswordInput = document.getElementById('confirm_password');
const verificationCodeInput = document.getElementById('verification_code');
const usernameError = document.getElementById('username-error');
const emailError = document.getElementById('email-error');
const passwordError = document.getElementById('password-error');
const confirmPasswordError = document.getElementById('confirm-password-error');
const verificationCodeError = document.getElementById('verification-code-error');
const registerButton = document.getElementById('register-button');
const sendCodeBtn = document.getElementById('send-code-btn');
// 用户名验证
if (usernameInput && usernameError) {
usernameInput.addEventListener('input', function() {
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
} else if (usernameInput.value.length < 3) {
usernameError.textContent = '用户名至少3个字符';
usernameError.classList.add('show');
} else {
usernameError.classList.remove('show');
}
});
}
// 邮箱验证
if (emailInput && emailError) {
emailInput.addEventListener('input', function() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailInput.value.trim() === '') {
emailError.textContent = '邮箱不能为空';
emailError.classList.add('show');
} else if (!emailRegex.test(emailInput.value)) {
emailError.textContent = '请输入有效的邮箱地址';
emailError.classList.add('show');
} else {
emailError.classList.remove('show');
}
});
}
// 密码验证
if (passwordInput && passwordError) {
passwordInput.addEventListener('input', function() {
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
} else {
passwordError.classList.remove('show');
}
// 检查确认密码是否匹配
if (confirmPasswordInput && confirmPasswordInput.value) {
if (confirmPasswordInput.value !== passwordInput.value) {
confirmPasswordError.textContent = '两次输入的密码不匹配';
confirmPasswordError.classList.add('show');
} else {
confirmPasswordError.classList.remove('show');
}
}
});
}
// 确认密码验证
if (confirmPasswordInput && confirmPasswordError) {
confirmPasswordInput.addEventListener('input', function() {
if (confirmPasswordInput.value.trim() === '') {
confirmPasswordError.textContent = '请确认密码';
confirmPasswordError.classList.add('show');
} else if (confirmPasswordInput.value !== passwordInput.value) {
confirmPasswordError.textContent = '两次输入的密码不匹配';
confirmPasswordError.classList.add('show');
} else {
confirmPasswordError.classList.remove('show');
}
});
}
// 发送验证码按钮
if (sendCodeBtn) {
sendCodeBtn.addEventListener('click', function() {
const email = emailInput.value.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) {
emailError.textContent = '请输入邮箱地址';
emailError.classList.add('show');
return;
} else if (!emailRegex.test(email)) {
emailError.textContent = '请输入有效的邮箱地址';
emailError.classList.add('show');
return;
}
// 禁用按钮并显示倒计时
let countdown = 60;
sendCodeBtn.disabled = true;
const originalText = sendCodeBtn.textContent;
sendCodeBtn.textContent = `${countdown}秒后重试`;
const timer = setInterval(() => {
countdown--;
sendCodeBtn.textContent = `${countdown}秒后重试`;
if (countdown <= 0) {
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = originalText;
}
}, 1000);
// 发送请求获取验证码
fetch('/user/send_verification_code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
.then(response => response.json())
.then(data => {
console.log("验证码发送响应:", data); // 添加调试日志
if (data.success) {
showMessage('验证码已发送', '请检查您的邮箱', 'success');
} else {
showMessage('发送失败', data.message || '请稍后重试', 'error');
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = originalText;
}
})
.catch(error => {
console.error('Error:', error);
showMessage('发送失败', '网络错误,请稍后重试', 'error');
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = originalText;
});
});
}
// 表单提交验证
registerForm.addEventListener('submit', function(e) {
let isValid = true;
// 验证用户名
if (usernameInput.value.trim() === '') {
usernameError.textContent = '用户名不能为空';
usernameError.classList.add('show');
isValid = false;
} else if (usernameInput.value.length < 3) {
usernameError.textContent = '用户名至少3个字符';
usernameError.classList.add('show');
isValid = false;
}
// 验证邮箱
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailInput.value.trim() === '') {
emailError.textContent = '邮箱不能为空';
emailError.classList.add('show');
isValid = false;
} else if (!emailRegex.test(emailInput.value)) {
emailError.textContent = '请输入有效的邮箱地址';
emailError.classList.add('show');
isValid = false;
}
// 验证密码
if (passwordInput.value.trim() === '') {
passwordError.textContent = '密码不能为空';
passwordError.classList.add('show');
isValid = false;
} else if (passwordInput.value.length < 6) {
passwordError.textContent = '密码长度至少6位';
passwordError.classList.add('show');
isValid = false;
}
// 验证确认密码
if (confirmPasswordInput.value.trim() === '') {
confirmPasswordError.textContent = '请确认密码';
confirmPasswordError.classList.add('show');
isValid = false;
} else if (confirmPasswordInput.value !== passwordInput.value) {
confirmPasswordError.textContent = '两次输入的密码不匹配';
confirmPasswordError.classList.add('show');
isValid = false;
}
// 验证验证码
if (verificationCodeInput.value.trim() === '') {
verificationCodeError.textContent = '请输入验证码';
verificationCodeError.classList.add('show');
isValid = false;
}
if (!isValid) {
e.preventDefault();
} else if (registerButton) {
registerButton.classList.add('loading-state');
}
});
}
// 通知消息显示函数
function showMessage(title, message, type) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const icon = type === 'success' ? '✓' : '✗';
notification.innerHTML = `
<div class="notification-icon">${icon}</div>
<div class="notification-content">
<h3>${title}</h3>
<p>${message}</p>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 10);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
});
================================================================================
File: ./app/static/js/book-list.js
================================================================================
// 图书列表页面脚本
$(document).ready(function() {
// 处理分类筛选
function setFilter(button, categoryId) {
// 移除所有按钮的活跃状态
$('.filter-btn').removeClass('active');
// 为当前点击的按钮添加活跃状态
$(button).addClass('active');
// 设置隐藏的分类ID输入值
$('#category_id').val(categoryId);
// 提交表单
$(button).closest('form').submit();
}
// 处理排序方向切换
function toggleSortDirection(button) {
const $button = $(button);
const isAsc = $button.hasClass('asc');
// 切换方向类
$button.toggleClass('asc desc');
// 更新图标
if (isAsc) {
$button.find('i').removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down');
$('#sort_order').val('desc');
} else {
$button.find('i').removeClass('fa-sort-amount-down').addClass('fa-sort-amount-up');
$('#sort_order').val('asc');
}
// 提交表单
$button.closest('form').submit();
}
// 将函数暴露到全局作用域
window.setFilter = setFilter;
window.toggleSortDirection = toggleSortDirection;
// 处理删除图书
let bookIdToDelete = null;
$('.delete-btn').click(function(e) {
e.preventDefault();
bookIdToDelete = $(this).data('id');
const bookTitle = $(this).data('title');
$('#deleteBookTitle').text(bookTitle);
$('#deleteModal').modal('show');
});
$('#confirmDelete').click(function() {
if (!bookIdToDelete) return;
$.ajax({
url: `/book/delete/${bookIdToDelete}`,
type: 'POST',
success: function(response) {
if (response.success) {
$('#deleteModal').modal('hide');
// 显示成功消息
showNotification(response.message, 'success');
// 移除图书卡片
setTimeout(() => {
location.reload();
}, 800);
} else {
showNotification(response.message, 'error');
}
},
error: function() {
showNotification('删除操作失败,请稍后重试', 'error');
}
});
});
// 处理借阅图书
$('.borrow-btn').click(function(e) {
e.preventDefault();
const bookId = $(this).data('id');
$.ajax({
url: `/borrow/add/${bookId}`,
type: 'POST',
success: function(response) {
if (response.success) {
showNotification(response.message, 'success');
// 可以更新UI显示比如更新库存或禁用借阅按钮
setTimeout(() => {
location.reload();
}, 800);
} else {
showNotification(response.message, 'error');
}
},
error: function() {
showNotification('借阅操作失败,请稍后重试', 'error');
}
});
});
// 显示通知
function showNotification(message, type) {
// 移除可能存在的旧通知
$('.notification-alert').remove();
const alertClass = type === 'success' ? 'notification-success' : 'notification-error';
const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
const notification = `
<div class="notification-alert ${alertClass}">
<div class="notification-icon">
<i class="fas ${iconClass}"></i>
</div>
<div class="notification-message">${message}</div>
<button class="notification-close">
<i class="fas fa-times"></i>
</button>
</div>
`;
$('body').append(notification);
// 显示通知
setTimeout(() => {
$('.notification-alert').addClass('show');
}, 10);
// 通知自动关闭
setTimeout(() => {
$('.notification-alert').removeClass('show');
setTimeout(() => {
$('.notification-alert').remove();
}, 300);
}, 4000);
// 点击关闭按钮
$('.notification-close').click(function() {
$(this).closest('.notification-alert').removeClass('show');
setTimeout(() => {
$(this).closest('.notification-alert').remove();
}, 300);
});
}
// 添加通知样式
const notificationCSS = `
.notification-alert {
position: fixed;
top: 20px;
right: 20px;
min-width: 280px;
max-width: 350px;
background-color: white;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
padding: 15px;
transform: translateX(calc(100% + 20px));
transition: transform 0.3s ease;
z-index: 9999;
}
.notification-alert.show {
transform: translateX(0);
}
.notification-success {
border-left: 4px solid var(--success-color);
}
.notification-error {
border-left: 4px solid var(--danger-color);
}
.notification-icon {
margin-right: 15px;
font-size: 24px;
}
.notification-success .notification-icon {
color: var(--success-color);
}
.notification-error .notification-icon {
color: var(--danger-color);
}
.notification-message {
flex: 1;
font-size: 0.95rem;
color: var(--text-color);
}
.notification-close {
background: none;
border: none;
color: var(--text-lighter);
cursor: pointer;
padding: 5px;
margin-left: 10px;
font-size: 0.8rem;
}
.notification-close:hover {
color: var(--text-color);
}
@media (max-width: 576px) {
.notification-alert {
top: auto;
bottom: 20px;
left: 20px;
right: 20px;
min-width: auto;
max-width: none;
transform: translateY(calc(100% + 20px));
}
.notification-alert.show {
transform: translateY(0);
}
}
`;
// 将通知样式添加到头部
$('<style>').text(notificationCSS).appendTo('head');
// 修复图书卡片布局的高度问题
function adjustCardHeights() {
// 重置所有卡片高度
$('.book-card').css('height', 'auto');
// 在大屏幕上应用等高布局
if (window.innerWidth >= 768) {
// 分组按行
const rows = {};
$('.book-card').each(function() {
const offsetTop = $(this).offset().top;
if (!rows[offsetTop]) {
rows[offsetTop] = [];
}
rows[offsetTop].push($(this));
});
// 为每行设置相同高度
Object.keys(rows).forEach(offsetTop => {
const cards = rows[offsetTop];
let maxHeight = 0;
// 找出最大高度
cards.forEach(card => {
const height = card.outerHeight();
if (height > maxHeight) {
maxHeight = height;
}
});
// 应用最大高度
cards.forEach(card => {
card.css('height', maxHeight + 'px');
});
});
}
}
// 初始调整高度
$(window).on('load', adjustCardHeights);
// 窗口大小变化时调整高度
$(window).on('resize', adjustCardHeights);
// 为封面图片添加加载错误处理
$('.book-cover').on('error', function() {
const $this = $(this);
const title = $this.attr('alt') || '图书';
// 替换为默认封面
$this.replaceWith(`
<div class="default-cover">
<i class="fas fa-book"></i>
<span class="default-cover-text">${title.charAt(0)}</span>
</div>
`);
});
// 添加初始动画效果
$('.book-card').each(function(index) {
$(this).css({
'opacity': '0',
'transform': 'translateY(20px)'
});
setTimeout(() => {
$(this).css({
'opacity': '1',
'transform': 'translateY(0)',
'transition': 'opacity 0.5s ease, transform 0.5s ease'
});
}, 50 * index);
});
});
================================================================================
File: ./app/templates/index.html
================================================================================
{% extends 'base.html' %}
{% block title %}首页 - 图书管理系统{% endblock %}
{% block head %}
<!-- 只引用index页面的专用样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
{% endblock %}
{% block content %}
<!-- 欢迎区域 -->
<div class="welcome-section">
<h1>欢迎回来,{{ current_user.username }}</h1>
<p>今天是 <span id="current-date"></span>,祝您使用愉快。</p>
</div>
<!-- 快速统计 -->
<div class="stats-container">
<div class="stat-card">
<i class="fas fa-book stat-icon"></i>
<div class="stat-info">
<h3>馆藏总量</h3>
<p class="stat-number">8,567</p>
</div>
</div>
<div class="stat-card">
<i class="fas fa-users stat-icon"></i>
<div class="stat-info">
<h3>注册用户</h3>
<p class="stat-number">1,245</p>
</div>
</div>
<div class="stat-card">
<i class="fas fa-exchange-alt stat-icon"></i>
<div class="stat-info">
<h3>当前借阅</h3>
<p class="stat-number">352</p>
</div>
</div>
<div class="stat-card">
<i class="fas fa-clock stat-icon"></i>
<div class="stat-info">
<h3>待还图书</h3>
<p class="stat-number">{{ 5 }}</p>
</div>
</div>
</div>
<!-- 主要内容区 -->
<div class="main-sections">
<!-- 最新图书 -->
<div class="content-section book-section">
<div class="section-header">
<h2>最新图书</h2>
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
</div>
<div class="book-grid">
{% for i in range(4) %}
<div class="book-card">
<div class="book-cover">
<img src="https://via.placeholder.com/150x210?text=No+Cover" alt="Book Cover">
</div>
<div class="book-info">
<h3 class="book-title">示例图书标题</h3>
<p class="book-author">作者名</p>
<div class="book-meta">
<span class="book-category">计算机</span>
<span class="book-status available">可借阅</span>
</div>
<button class="borrow-btn">借阅</button>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 通知公告 -->
<div class="content-section notice-section">
<div class="section-header">
<h2>通知公告</h2>
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
</div>
<div class="notice-list">
<div class="notice-item">
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
<div class="notice-content">
<h3>关于五一假期图书馆开放时间调整的通知</h3>
<p>五一期间(5月1日-5日)图书馆开放时间调整为上午9:00-下午5:00。</p>
<div class="notice-meta">
<span class="notice-time">2023-04-28</span>
</div>
</div>
</div>
<div class="notice-item">
<div class="notice-icon"><i class="fas fa-bell"></i></div>
<div class="notice-content">
<h3>您有2本图书即将到期</h3>
<p>《Python编程》《算法导论》将于3天后到期请及时归还或办理续借。</p>
<div class="notice-meta">
<span class="notice-time">2023-04-27</span>
<button class="renew-btn">一键续借</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 热门图书区域 -->
<div class="content-section popular-section">
<div class="section-header">
<h2>热门图书</h2>
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
</div>
<div class="popular-books">
{% for i in range(5) %}
<div class="popular-book-item">
<div class="rank-badge">{{ i+1 }}</div>
<div class="book-cover small">
<img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
</div>
<div class="book-details">
<h3 class="book-title">热门图书标题示例</h3>
<p class="book-author">知名作者</p>
<div class="book-stats">
<span><i class="fas fa-eye"></i> 1024 次浏览</span>
<span><i class="fas fa-bookmark"></i> 89 次借阅</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 设置当前日期
const now = new Date();
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', options);
});
</script>
{% endblock %}
================================================================================
File: ./app/templates/base.html
================================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}图书管理系统{% endblock %}</title>
<!-- 通用CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<!-- 页面特定CSS -->
{% block head %}{% endblock %}
</head>
<body>
<div class="app-container">
<!-- 侧边导航栏 -->
<nav class="sidebar">
<div class="logo-container">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo">
<h2>图书管理系统</h2>
</div>
<ul class="nav-links">
<li class="{% if request.path == '/' %}active{% endif %}">
<a href="{{ url_for('index') }}"><i class="fas fa-home"></i> 首页</a>
</li>
<li class="{% if '/book/list' in request.path %}active{% endif %}">
<a href="{{ url_for('book.book_list') }}"><i class="fas fa-book"></i> 图书浏览</a>
</li>
<li class="{% if '/borrow' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a>
</li>
<li class="{% if '/announcement' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-bell"></i> 通知公告</a>
</li>
{% if current_user.role_id == 1 %}
<li class="nav-category">管理功能</li>
<li class="{% if '/user/manage' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-users"></i> 用户管理</a>
</li>
<li class="{% if '/book/list' in request.path %}active{% endif %}">
<a href="{{ url_for('book.book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
</li>
<li class="{% if '/borrow/manage' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a>
</li>
<li class="{% if '/inventory' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-warehouse"></i> 库存管理</a>
</li>
<li class="{% if '/statistics' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a>
</li>
<li class="{% if '/log' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-history"></i> 日志管理</a>
</li>
{% endif %}
</ul>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部导航 -->
<header class="top-bar">
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" placeholder="搜索图书..." class="search-input">
</div>
<div class="user-menu">
<div class="notifications">
<i class="fas fa-bell"></i>
<span class="badge">3</span>
</div>
<div class="user-info">
<div class="user-avatar">
{{ current_user.username[0] }}
</div>
<div class="user-details">
<span class="user-name">{{ current_user.username }}</span>
<span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
</div>
<div class="dropdown-menu">
<a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
<a href="#"><i class="fas fa-cog"></i> 设置</a>
<a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
</div>
</div>
</div>
</header>
<!-- 内容区 - 这里是核心变化 -->
<div class="content-wrapper">
{% block content %}
<!-- 子模板将在这里添加内容 -->
{% endblock %}
</div>
</main>
</div>
<!-- 通用JavaScript -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 用户菜单下拉
const userInfo = document.querySelector('.user-info');
userInfo.addEventListener('click', function(e) {
userInfo.classList.toggle('active');
});
// 点击其他区域关闭下拉菜单
document.addEventListener('click', function(e) {
if (!userInfo.contains(e.target)) {
userInfo.classList.remove('active');
}
});
});
</script>
<!-- 页面特定JavaScript -->
{% block scripts %}{% endblock %}
</body>
</html>
================================================================================
File: ./app/templates/register.html
================================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<div class="overlay"></div>
<div class="theme-toggle" id="theme-toggle">☀️</div>
<div class="main-container">
<div class="login-container register-container">
<div class="logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
</div>
<h1>图书管理系统</h1>
<p class="subtitle">创建您的新账户</p>
<div id="register-form-container">
<form id="register-form" action="{{ url_for('user.register') }}" method="post">
<div class="form-group">
<label for="username">用户名</label>
<div class="input-with-icon">
<span class="input-icon">👤</span>
<input type="text" id="username" name="username" class="form-control" placeholder="请输入用户名" required>
</div>
<div class="validation-message" id="username-error"></div>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<div class="input-with-icon">
<span class="input-icon">📧</span>
<input type="email" id="email" name="email" class="form-control" placeholder="请输入邮箱" required>
</div>
<div class="validation-message" id="email-error"></div>
</div>
<div class="form-group">
<label for="verification_code">邮箱验证码</label>
<div class="verification-code-container">
<input type="text" id="verification_code" name="verification_code" class="verification-input" placeholder="请输入验证码" required>
<button type="button" id="send-code-btn" class="send-code-btn">发送验证码</button>
</div>
<div class="validation-message" id="verification-code-error"></div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" id="password" name="password" class="form-control" placeholder="请设置密码" required>
<span class="password-toggle" id="password-toggle">👁️</span>
</div>
<div class="validation-message" id="password-error"></div>
</div>
<div class="form-group">
<label for="confirm_password">确认密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" placeholder="请再次输入密码" required>
</div>
<div class="validation-message" id="confirm-password-error"></div>
</div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<button type="submit" class="btn-login" id="register-button">
<span>注册</span>
<span class="loading">⟳</span>
</button>
</form>
<div class="signup">
已有账号? <a href="{{ url_for('user.login') }}">返回登录</a>
</div>
</div>
</div>
</div>
<footer>
<p>© 2025 图书管理系统 - 版权所有</p>
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
================================================================================
File: ./app/templates/404.html
================================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面未找到 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<style>
.error-container {
text-align: center;
padding: 50px 20px;
}
.error-code {
font-size: 100px;
font-weight: bold;
color: #4a89dc;
margin-bottom: 20px;
}
.error-message {
font-size: 24px;
color: #333;
margin-bottom: 30px;
}
.back-button {
display: inline-block;
padding: 10px 20px;
background-color: #4a89dc;
color: white;
text-decoration: none;
border-radius: 5px;
font-weight: 500;
}
.back-button:hover {
background-color: #3b78c4;
}
</style>
</head>
<body>
<div class="main-container">
<div class="error-container">
<div class="error-code">404</div>
<div class="error-message">页面未找到</div>
<p>抱歉,您访问的页面不存在或已被移除。</p>
<p style="margin-bottom: 30px;">请检查URL是否正确或返回首页。</p>
<a href="{{ url_for('index') }}" class="back-button">返回首页</a>
</div>
</div>
</body>
</html>
================================================================================
File: ./app/templates/login.html
================================================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录 - 图书管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<div class="overlay"></div>
<div class="theme-toggle" id="theme-toggle">☀️</div>
<div class="main-container">
<div class="login-container">
<div class="logo">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
</div>
<h1>图书管理系统</h1>
<p class="subtitle">欢迎回来,请登录您的账户</p>
<div id="account-login">
<form id="login-form" action="{{ url_for('user.login') }}" method="post">
<div class="form-group">
<label for="username">用户名/邮箱</label>
<div class="input-with-icon">
<span class="input-icon">👤</span>
<input type="text" id="username" name="username" class="form-control" placeholder="请输入账号">
</div>
<div class="validation-message" id="username-error"></div>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="input-with-icon">
<span class="input-icon">🔒</span>
<input type="password" id="password" name="password" class="form-control" placeholder="请输入密码">
<span class="password-toggle" id="password-toggle">👁️</span>
</div>
<div class="validation-message" id="password-error"></div>
</div>
<div class="remember-forgot">
<label class="custom-checkbox">
<input type="checkbox" name="remember_me">
<span class="checkmark"></span>
记住我 (7天内免登录)
</label>
<div class="forgot-password">
<a href="#">忘记密码?</a>
</div>
</div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<button type="submit" class="btn-login" id="login-button">
<span>登录</span>
<span class="loading">⟳</span>
</button>
</form>
<div class="signup">
还没有账号? <a href="{{ url_for('user.register') }}">立即注册</a>
</div>
<div class="features">
<div class="feature-item">
<span class="feature-icon">🔒</span>
<span>安全登录</span>
</div>
<div class="feature-item">
<span class="feature-icon">🔐</span>
<span>数据加密</span>
</div>
<div class="feature-item">
<span class="feature-icon">📚</span>
<span>图书管理</span>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>© 2025 图书管理系统 - 版权所有</p>
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
================================================================================
File: ./app/templates/book/list.html
================================================================================
{% extends 'base.html' %}
{% block title %}图书列表 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book.css') }}">
{% endblock %}
{% block content %}
<div class="book-list-container">
<!-- 添加泡泡动画元素 -->
<!-- 这些泡泡会通过 JS 动态创建 -->
<div class="page-header">
<h1>图书管理</h1>
{% if current_user.role_id == 1 %}
<div class="action-buttons">
<a href="{{ url_for('book.add_book') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> 添加图书
</a>
<a href="{{ url_for('book.import_books') }}" class="btn btn-success">
<i class="fas fa-file-upload"></i> 批量导入
</a>
<a href="{{ url_for('book.export_books') }}" class="btn btn-info">
<i class="fas fa-file-download"></i> 导出图书
</a>
<a href="{{ url_for('book.category_list') }}" class="btn btn-secondary">
<i class="fas fa-tags"></i> 分类管理
</a>
</div>
{% endif %}
</div>
<div class="filter-section">
<form method="GET" action="{{ url_for('book.book_list') }}" class="search-form">
<div class="search-row">
<div class="form-group search-group">
<input type="text" name="search" class="form-control" placeholder="搜索书名/作者/ISBN" value="{{ search }}">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="filter-row">
<div class="form-group filter-group">
<select name="category_id" class="form-control" onchange="this.form.submit()">
<option value="">全部分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group filter-group">
<select name="sort" class="form-control" onchange="this.form.submit()">
<option value="id" {% if sort == 'id' %}selected{% endif %}>默认排序</option>
<option value="created_at" {% if sort == 'created_at' %}selected{% endif %}>入库时间</option>
<option value="title" {% if sort == 'title' %}selected{% endif %}>书名</option>
<option value="stock" {% if sort == 'stock' %}selected{% endif %}>库存</option>
</select>
</div>
<div class="form-group filter-group">
<select name="order" class="form-control" onchange="this.form.submit()">
<option value="desc" {% if order == 'desc' %}selected{% endif %}>降序</option>
<option value="asc" {% if order == 'asc' %}selected{% endif %}>升序</option>
</select>
</div>
</div>
</form>
</div>
<div class="books-grid">
{% for book in books %}
<div class="book-card">
<div class="book-cover">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}">
{% else %}
<div class="no-cover">
<i class="fas fa-book"></i>
<span>无封面</span>
</div>
{% endif %}
<!-- 添加书名覆盖层 -->
<div class="cover-title-bar">{{ book.title }}</div>
</div>
<div class="book-info">
<h3 class="book-title">{{ book.title }}</h3>
<p class="book-author">{{ book.author }}</p>
<div class="book-meta">
{% if book.category %}
<span class="book-category">{{ book.category.name }}</span>
{% endif %}
<span class="book-status {{ 'available' if book.stock > 0 else 'unavailable' }}">
{{ '可借阅' if book.stock > 0 else '无库存' }}
</span>
</div>
<div class="book-details">
<p><strong>ISBN:</strong> <span>{{ book.isbn or '无' }}</span></p>
<p><strong>出版社:</strong> <span>{{ book.publisher or '无' }}</span></p>
<p><strong>库存:</strong> <span>{{ book.stock }}</span></p>
</div>
<div class="book-actions">
<a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info btn-sm">
<i class="fas fa-info-circle"></i> 详情
</a>
{% if current_user.role_id == 1 %}
<a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary btn-sm">
<i class="fas fa-edit"></i> 编辑
</a>
<button class="btn btn-danger btn-sm delete-book" data-id="{{ book.id }}" data-title="{{ book.title }}">
<i class="fas fa-trash"></i> 删除
</button>
{% endif %}
{% if book.stock > 0 %}
<a href="#" class="btn btn-success btn-sm borrow-book" data-id="{{ book.id }}">
<i class="fas fa-hand-holding"></i> 借阅
</a>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="no-books">
<i class="fas fa-exclamation-circle"></i>
<p>没有找到符合条件的图书</p>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if pagination.pages > 1 %}
<div class="pagination-container">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('book.book_list', page=pagination.prev_num, search=search, category_id=category_id, sort=sort, order=order) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for p in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if p %}
{% if p == pagination.page %}
<li class="page-item active">
<span class="page-link">{{ p }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('book.book_list', page=p, search=search, category_id=category_id, sort=sort, order=order) }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('book.book_list', page=pagination.next_num, search=search, category_id=category_id, sort=sort, order=order) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
<div class="pagination-info">
显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }}
到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/book-list.js') }}"></script>
{{ super() }}
{% endblock %}
================================================================================
File: ./app/templates/book/add.html
================================================================================
{% extends 'base.html' %}
{% block title %}添加图书 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-form.css') }}">
{% endblock %}
{% block content %}
<div class="book-form-container">
<div class="page-header">
<h1>添加新图书</h1>
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
</div>
<form method="POST" enctype="multipart/form-data" class="book-form">
<div class="form-row">
<div class="col-md-8">
<div class="card">
<div class="card-header">基本信息</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-12">
<label for="title">书名 <span class="required">*</span></label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="author">作者 <span class="required">*</span></label>
<input type="text" class="form-control" id="author" name="author" required>
</div>
<div class="form-group col-md-6">
<label for="publisher">出版社</label>
<input type="text" class="form-control" id="publisher" name="publisher">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="isbn">ISBN</label>
<input type="text" class="form-control" id="isbn" name="isbn">
</div>
<div class="form-group col-md-6">
<label for="publish_year">出版年份</label>
<input type="text" class="form-control" id="publish_year" name="publish_year">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="category_id">分类</label>
<select class="form-control" id="category_id" name="category_id">
<option value="">未分类</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-md-6">
<label for="tags">标签</label>
<input type="text" class="form-control" id="tags" name="tags" placeholder="多个标签用逗号分隔">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">图书简介</div>
<div class="card-body">
<div class="form-group">
<textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介"></textarea>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">封面图片</div>
<div class="card-body">
<div class="cover-preview-container">
<div class="cover-preview" id="coverPreview">
<div class="no-cover-placeholder">
<i class="fas fa-image"></i>
<span>暂无封面</span>
</div>
</div>
<div class="upload-container">
<label for="cover" class="btn btn-outline-primary btn-block">
<i class="fas fa-upload"></i> 上传封面
</label>
<input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">库存和价格</div>
<div class="card-body">
<div class="form-group">
<label for="stock">库存数量</label>
<input type="number" class="form-control" id="stock" name="stock" min="0" value="0">
</div>
<div class="form-group">
<label for="price">价格</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">¥</span>
</div>
<input type="number" class="form-control" id="price" name="price" step="0.01" min="0">
</div>
</div>
</div>
</div>
<div class="form-submit-container">
<button type="submit" class="btn btn-primary btn-lg btn-block">
<i class="fas fa-save"></i> 保存图书
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 封面预览
$('#cover').change(function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
$('#coverPreview').html(`<img src="${e.target.result}" class="cover-image">`);
}
reader.readAsDataURL(file);
} else {
$('#coverPreview').html(`
<div class="no-cover-placeholder">
<i class="fas fa-image"></i>
<span>暂无封面</span>
</div>
`);
}
});
});
</script>
{% endblock %}
================================================================================
File: ./app/templates/book/edit.html
================================================================================
{% extends 'base.html' %}
{% block title %}编辑图书 - {{ book.title }}{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-form.css') }}">
{% endblock %}
{% block content %}
<div class="book-form-container">
<div class="page-header">
<h1>编辑图书</h1>
<div class="actions">
<a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info">
<i class="fas fa-eye"></i> 查看详情
</a>
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
</div>
</div>
<form method="POST" enctype="multipart/form-data" class="book-form">
<div class="form-row">
<div class="col-md-8">
<div class="card">
<div class="card-header">基本信息</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-12">
<label for="title">书名 <span class="required">*</span></label>
<input type="text" class="form-control" id="title" name="title" value="{{ book.title }}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="author">作者 <span class="required">*</span></label>
<input type="text" class="form-control" id="author" name="author" value="{{ book.author }}" required>
</div>
<div class="form-group col-md-6">
<label for="publisher">出版社</label>
<input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="isbn">ISBN</label>
<input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn or '' }}">
</div>
<div class="form-group col-md-6">
<label for="publish_year">出版年份</label>
<input type="text" class="form-control" id="publish_year" name="publish_year" value="{{ book.publish_year or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="category_id">分类</label>
<select class="form-control" id="category_id" name="category_id">
<option value="">未分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if book.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group col-md-6">
<label for="tags">标签</label>
<input type="text" class="form-control" id="tags" name="tags" value="{{ book.tags or '' }}" placeholder="多个标签用逗号分隔">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">图书简介</div>
<div class="card-body">
<div class="form-group">
<textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介">{{ book.description or '' }}</textarea>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">封面图片</div>
<div class="card-body">
<div class="cover-preview-container">
<div class="cover-preview" id="coverPreview">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
{% else %}
<div class="no-cover-placeholder">
<i class="fas fa-image"></i>
<span>暂无封面</span>
</div>
{% endif %}
</div>
<div class="upload-container">
<label for="cover" class="btn btn-outline-primary btn-block">
<i class="fas fa-upload"></i> 更换封面
</label>
<input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">库存和价格</div>
<div class="card-body">
<div class="form-group">
<label for="stock">库存数量</label>
<input type="number" class="form-control" id="stock" name="stock" min="0" value="{{ book.stock }}">
</div>
<div class="form-group">
<label for="price">价格</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">¥</span>
</div>
<input type="number" class="form-control" id="price" name="price" step="0.01" min="0" value="{{ book.price or '' }}">
</div>
</div>
<div class="form-group">
<label for="status">状态</label>
<select class="form-control" id="status" name="status">
<option value="1" {% if book.status == 1 %}selected{% endif %}>上架</option>
<option value="0" {% if book.status == 0 %}selected{% endif %}>下架</option>
</select>
</div>
</div>
</div>
<div class="form-submit-container">
<button type="submit" class="btn btn-primary btn-lg btn-block">
<i class="fas fa-save"></i> 保存修改
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 封面预览
$('#cover').change(function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
$('#coverPreview').html(`<img src="${e.target.result}" class="cover-image">`);
}
reader.readAsDataURL(file);
} else {
$('#coverPreview').html(`
{% if book.cover_url %}
<img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
{% else %}
<div class="no-cover-placeholder">
<i class="fas fa-image"></i>
<span>暂无封面</span>
</div>
{% endif %}
`);
}
});
});
</script>
{% endblock %}
================================================================================
File: ./app/templates/book/import.html
================================================================================
{% extends 'base.html' %}
{% block title %}批量导入图书 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-import.css') }}">
{% endblock %}
{% block content %}
<div class="import-container">
<div class="page-header">
<h1>批量导入图书</h1>
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回图书列表
</a>
</div>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h4>Excel文件导入</h4>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="file">选择Excel文件</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="file" name="file" accept=".xlsx, .xls" required>
<label class="custom-file-label" for="file">选择文件...</label>
</div>
<small class="form-text text-muted">支持的文件格式: .xlsx, .xls</small>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">
<i class="fas fa-upload"></i> 开始导入
</button>
</form>
<hr>
<div class="import-instructions">
<h5>导入说明:</h5>
<ul>
<li>Excel文件须包含以下列 (标题行必须与下列完全一致):</li>
<li class="required-field">title - 图书标题 (必填)</li>
<li class="required-field">author - 作者名称 (必填)</li>
<li>publisher - 出版社</li>
<li>category_id - 分类ID (对应系统中的分类ID)</li>
<li>tags - 标签 (多个标签用逗号分隔)</li>
<li>isbn - ISBN编号 (建议唯一)</li>
<li>publish_year - 出版年份</li>
<li>description - 图书简介</li>
<li>cover_url - 封面图片URL</li>
<li>stock - 库存数量</li>
<li>price - 价格</li>
</ul>
<div class="template-download">
<p>下载导入模板:</p>
<a href="{{ url_for('static', filename='templates/book_import_template.xlsx') }}" class="btn btn-outline-primary">
<i class="fas fa-download"></i> 下载Excel模板
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 显示选择的文件名
$('.custom-file-input').on('change', function() {
const fileName = $(this).val().split('\\').pop();
$(this).next('.custom-file-label').html(fileName);
});
});
</script>
{% endblock %}
================================================================================
File: ./app/templates/book/detail.html
================================================================================
{% extends 'base.html' %}
{% block title %}{{ book.title }} - 图书详情{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-detail.css') }}">
{% endblock %}
{% block content %}
<div class="book-detail-container">
<div class="page-header">
<h1>图书详情</h1>
<div class="actions">
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
{% if current_user.role_id == 1 %}
<a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> 编辑图书
</a>
{% endif %}
{% if book.stock > 0 %}
<a href="#" class="btn btn-success" id="borrowBtn">
<i class="fas fa-hand-holding"></i> 借阅此书
</a>
{% endif %}
</div>
</div>
<div class="book-content">
<div class="book-header">
<div class="book-cover-large">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}">
{% else %}
<div class="no-cover-large">
<i class="fas fa-book"></i>
<span>无封面</span>
</div>
{% endif %}
</div>
<div class="book-main-info">
<h2 class="book-title">{{ book.title }}</h2>
<p class="book-author"><i class="fas fa-user-edit"></i> 作者: {{ book.author }}</p>
<div class="book-meta-info">
<div class="meta-item">
<i class="fas fa-building"></i>
<span>出版社: </span>
<span class="meta-value">{{ book.publisher or '未知' }}</span>
</div>
<div class="meta-item">
<i class="fas fa-calendar-alt"></i>
<span>出版年份: </span>
<span class="meta-value">{{ book.publish_year or '未知' }}</span>
</div>
<div class="meta-item">
<i class="fas fa-barcode"></i>
<span>ISBN: </span>
<span class="meta-value">{{ book.isbn or '未知' }}</span>
</div>
<div class="meta-item">
<i class="fas fa-layer-group"></i>
<span>分类: </span>
<span class="meta-value">{{ book.category.name if book.category else '未分类' }}</span>
</div>
{% if book.tags %}
<div class="meta-item">
<i class="fas fa-tags"></i>
<span>标签: </span>
<span class="meta-value">
{% for tag in book.tags.split(',') %}
<span class="tag">{{ tag.strip() }}</span>
{% endfor %}
</span>
</div>
{% endif %}
<div class="meta-item">
<i class="fas fa-yuan-sign"></i>
<span>价格: </span>
<span class="meta-value">{{ book.price or '未知' }}</span>
</div>
</div>
<div class="book-status-info">
<div class="status-badge {{ 'available' if book.stock > 0 else 'unavailable' }}">
{{ '可借阅' if book.stock > 0 else '无库存' }}
</div>
<div class="stock-info">
<i class="fas fa-cubes"></i> 库存: {{ book.stock }}
</div>
</div>
</div>
</div>
<div class="book-details-section">
<h3>图书简介</h3>
<div class="book-description">
{% if book.description %}
<p>{{ book.description|nl2br }}</p>
{% else %}
<p class="no-description">暂无图书简介</p>
{% endif %}
</div>
</div>
<!-- 借阅历史 (仅管理员可见) -->
{% if current_user.role_id == 1 %}
<div class="book-borrow-history">
<h3>借阅历史</h3>
{% set borrow_records = book.borrow_records.order_by(BorrowRecord.borrow_date.desc()).limit(10).all() %}
{% if borrow_records %}
<table class="table borrow-table">
<thead>
<tr>
<th>借阅用户</th>
<th>借阅日期</th>
<th>应还日期</th>
<th>实际归还</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for record in borrow_records %}
<tr>
<td>{{ record.user.username }}</td>
<td>{{ record.borrow_date.strftime('%Y-%m-%d') }}</td>
<td>{{ record.due_date.strftime('%Y-%m-%d') }}</td>
<td>{{ record.return_date.strftime('%Y-%m-%d') if record.return_date else '-' }}</td>
<td>
{% if record.status == 1 and record.due_date < now %}
<span class="badge badge-danger">已逾期</span>
{% elif record.status == 1 %}
<span class="badge badge-warning">借阅中</span>
{% else %}
<span class="badge badge-success">已归还</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="no-records">暂无借阅记录</p>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- 借阅确认模态框 -->
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="borrowModalLabel">借阅确认</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="borrowForm" action="{{ url_for('borrow.borrow_book') }}" method="POST">
<div class="modal-body">
<p>您确定要借阅《{{ book.title }}》吗?</p>
<input type="hidden" name="book_id" value="{{ book.id }}">
<div class="form-group">
<label for="borrow_days">借阅天数</label>
<select class="form-control" id="borrow_days" name="borrow_days">
<option value="7">7天</option>
<option value="14" selected>14天</option>
<option value="30">30天</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-success">确认借阅</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 借阅按钮点击事件
$('#borrowBtn').click(function(e) {
e.preventDefault();
$('#borrowModal').modal('show');
});
});
</script>
{% endblock %}
================================================================================
File: ./app/templates/book/categories.html
================================================================================
{% extends 'base.html' %}
{% block title %}图书分类管理 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/categories.css') }}">
{% endblock %}
{% block content %}
<div class="categories-container">
<div class="page-header">
<h1>图书分类管理</h1>
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回图书列表
</a>
</div>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
添加新分类
</div>
<div class="card-body">
<form id="categoryForm">
<div class="form-group">
<label for="categoryName">分类名称</label>
<input type="text" class="form-control" id="categoryName" name="name" required>
</div>
<div class="form-group">
<label for="parentCategory">父级分类</label>
<select class="form-control" id="parentCategory" name="parent_id">
<option value="">无 (顶级分类)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="categorySort">排序</label>
<input type="number" class="form-control" id="categorySort" name="sort" value="0" min="0">
</div>
<button type="submit" class="btn btn-primary btn-block">
<i class="fas fa-plus"></i> 添加分类
</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">
分类列表
</div>
<div class="card-body">
{% if categories %}
<table class="table table-hover category-table">
<thead>
<tr>
<th width="5%">ID</th>
<th width="40%">分类名称</th>
<th width="20%">父级分类</th>
<th width="10%">排序</th>
<th width="25%">操作</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr data-id="{{ category.id }}">
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
<td>
{% if category.parent %}
{{ category.parent.name }}
{% else %}
<span class="text-muted">无</span>
{% endif %}
</td>
<td>{{ category.sort }}</td>
<td>
<button class="btn btn-sm btn-primary edit-category" data-id="{{ category.id }}"
data-name="{{ category.name }}"
data-parent="{{ category.parent_id or '' }}"
data-sort="{{ category.sort }}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn btn-sm btn-danger delete-category" data-id="{{ category.id }}" data-name="{{ category.name }}">
<i class="fas fa-trash"></i> 删除
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="no-categories">
<i class="fas fa-exclamation-circle"></i>
<p>暂无分类数据</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 编辑分类模态框 -->
<div class="modal fade" id="editCategoryModal" tabindex="-1" role="dialog" aria-labelledby="editCategoryModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editCategoryModalLabel">编辑分类</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="editCategoryForm">
<div class="modal-body">
<input type="hidden" id="editCategoryId">
<div class="form-group">
<label for="editCategoryName">分类名称</label>
<input type="text" class="form-control" id="editCategoryName" name="name" required>
</div>
<div class="form-group">
<label for="editParentCategory">父级分类</label>
<select class="form-control" id="editParentCategory" name="parent_id">
<option value="">无 (顶级分类)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="editCategorySort">排序</label>
<input type="number" class="form-control" id="editCategorySort" name="sort" value="0" min="0">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">保存修改</button>
</div>
</form>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteCategoryModal" tabindex="-1" role="dialog" aria-labelledby="deleteCategoryModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteCategoryModalLabel">确认删除</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>您确定要删除分类 "<span id="deleteCategoryName"></span>" 吗?</p>
<p class="text-danger">注意: 如果该分类下有图书或子分类,将无法删除!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDeleteCategory">确认删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 添加分类
$('#categoryForm').submit(function(e) {
e.preventDefault();
const formData = {
name: $('#categoryName').val(),
parent_id: $('#parentCategory').val(),
sort: $('#categorySort').val()
};
$.ajax({
url: '{{ url_for("book.add_category") }}',
type: 'POST',
data: formData,
success: function(response) {
if (response.success) {
showNotification(response.message, 'success');
setTimeout(function() {
location.reload();
}, 1000);
} else {
showNotification(response.message, 'error');
}
},
error: function() {
showNotification('操作失败,请稍后重试', 'error');
}
});
});
// 打开编辑模态框
$('.edit-category').click(function() {
const id = $(this).data('id');
const name = $(this).data('name');
const parentId = $(this).data('parent');
const sort = $(this).data('sort');
$('#editCategoryId').val(id);
$('#editCategoryName').val(name);
$('#editParentCategory').val(parentId);
$('#editCategorySort').val(sort);
// 禁用选择自己作为父级
$('#editParentCategory option').removeAttr('disabled');
$(`#editParentCategory option[value="${id}"]`).attr('disabled', 'disabled');
$('#editCategoryModal').modal('show');
});
// 提交编辑表单
$('#editCategoryForm').submit(function(e) {
e.preventDefault();
const id = $('#editCategoryId').val();
const formData = {
name: $('#editCategoryName').val(),
parent_id: $('#editParentCategory').val(),
sort: $('#editCategorySort').val()
};
$.ajax({
url: `/book/categories/edit/${id}`,
type: 'POST',
data: formData,
success: function(response) {
if (response.success) {
$('#editCategoryModal').modal('hide');
showNotification(response.message, 'success');
setTimeout(function() {
location.reload();
}, 1000);
} else {
showNotification(response.message, 'error');
}
},
error: function() {
showNotification('操作失败,请稍后重试', 'error');
}
});
});
// 打开删除确认框
$('.delete-category').click(function() {
const id = $(this).data('id');
const name = $(this).data('name');
$('#deleteCategoryName').text(name);
$('#confirmDeleteCategory').data('id', id);
$('#deleteCategoryModal').modal('show');
});
// 确认删除
$('#confirmDeleteCategory').click(function() {
const id = $(this).data('id');
$.ajax({
url: `/book/categories/delete/${id}`,
type: 'POST',
success: function(response) {
$('#deleteCategoryModal').modal('hide');
if (response.success) {
showNotification(response.message, 'success');
setTimeout(function() {
location.reload();
}, 1000);
} else {
showNotification(response.message, 'error');
}
},
error: function() {
showNotification('操作失败,请稍后重试', 'error');
}
});
});
// 显示通知
function showNotification(message, type) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const alert = `
<div class="alert ${alertClass} alert-dismissible fade show notification-alert" role="alert">
${message}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
`;
$('body').append(alert);
// 5秒后自动关闭
setTimeout(() => {
$('.notification-alert').alert('close');
}, 5000);
}
});
</script>
{% endblock %}
================================================================================
File: ./app/controllers/user.py
================================================================================
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
from app.models.user import User, db
from app.utils.email import send_verification_email, generate_verification_code
import logging
from functools import wraps
import time
from datetime import datetime, timedelta
# 创建蓝图
user_bp = Blueprint('user', __name__)
# 使用内存字典代替Redis存储验证码
class VerificationStore:
def __init__(self):
self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}}
def setex(self, email, seconds, code):
"""设置验证码并指定过期时间"""
expiry = datetime.now() + timedelta(seconds=seconds)
self.codes[email] = {'code': code, 'expires': expiry}
return True
def get(self, email):
"""获取验证码如果过期则返回None"""
if email not in self.codes:
return None
data = self.codes[email]
if datetime.now() > data['expires']:
# 验证码已过期,删除它
self.delete(email)
return None
return data['code']
def delete(self, email):
"""删除验证码"""
if email in self.codes:
del self.codes[email]
return True
# 使用内存存储验证码
verification_codes = VerificationStore()
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('user.login'))
return f(*args, **kwargs)
return decorated_function
@user_bp.route('/login', methods=['GET', 'POST'])
def login():
# 保持原代码不变
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
remember_me = request.form.get('remember_me') == 'on'
if not username or not password:
return render_template('login.html', error='用户名和密码不能为空')
# 检查用户是否存在
user = User.query.filter((User.username == username) | (User.email == username)).first()
if not user or not user.check_password(password):
return render_template('login.html', error='用户名或密码错误')
if user.status == 0:
return render_template('login.html', error='账号已被禁用,请联系管理员')
# 登录成功,保存用户信息到会话
session['user_id'] = user.id
session['username'] = user.username
session['role_id'] = user.role_id
if remember_me:
# 设置会话过期时间为7天
session.permanent = True
# 记录登录日志(可选)
# log_user_action('用户登录')
# 重定向到首页
return redirect(url_for('index'))
return render_template('login.html')
@user_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
verification_code = request.form.get('verification_code')
# 验证表单数据
if not username or not email or not password or not confirm_password or not verification_code:
return render_template('register.html', error='所有字段都是必填项')
if password != confirm_password:
return render_template('register.html', error='两次输入的密码不匹配')
# 检查用户名和邮箱是否已存在
if User.query.filter_by(username=username).first():
return render_template('register.html', error='用户名已存在')
if User.query.filter_by(email=email).first():
return render_template('register.html', error='邮箱已被注册')
# 验证验证码
stored_code = verification_codes.get(email)
if not stored_code or stored_code != verification_code:
return render_template('register.html', error='验证码无效或已过期')
# 创建新用户
try:
new_user = User(
username=username,
password=password, # 密码会在模型中自动哈希
email=email,
nickname=username # 默认昵称与用户名相同
)
db.session.add(new_user)
db.session.commit()
# 清除验证码
verification_codes.delete(email)
flash('注册成功,请登录', 'success')
return redirect(url_for('user.login'))
except Exception as e:
db.session.rollback()
logging.error(f"User registration failed: {str(e)}")
return render_template('register.html', error='注册失败,请稍后重试')
return render_template('register.html')
@user_bp.route('/logout')
def logout():
# 清除会话数据
session.pop('user_id', None)
session.pop('username', None)
session.pop('role_id', None)
return redirect(url_for('user.login'))
@user_bp.route('/send_verification_code', methods=['POST'])
def send_verification_code():
data = request.get_json()
email = data.get('email')
if not email:
return jsonify({'success': False, 'message': '请提供邮箱地址'})
# 检查邮箱格式
import re
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
return jsonify({'success': False, 'message': '邮箱格式不正确'})
# 生成验证码
code = generate_verification_code()
# 存储验证码(10分钟有效)
verification_codes.setex(email, 600, code) # 10分钟过期
# 发送验证码邮件
if send_verification_email(email, code):
return jsonify({'success': True, 'message': '验证码已发送'})
else:
return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'})
================================================================================
File: ./app/controllers/log.py
================================================================================
================================================================================
File: ./app/controllers/__init__.py
================================================================================
================================================================================
File: ./app/controllers/book.py
================================================================================
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, g, jsonify
from app.models.book import Book, Category
from app.models.user import db
from app.utils.auth import login_required, admin_required
import os
from werkzeug.utils import secure_filename
import datetime
import pandas as pd
import uuid
book_bp = Blueprint('book', __name__)
# 图书列表页面
@book_bp.route('/list')
@login_required
def book_list():
print("访问图书列表页面") # 调试输出
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = Book.query
# 搜索功能
search = request.args.get('search', '')
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
)
# 分类筛选
category_id = request.args.get('category_id', type=int)
if category_id:
query = query.filter_by(category_id=category_id)
# 排序
sort = request.args.get('sort', 'id')
order = request.args.get('order', 'desc')
if order == 'desc':
query = query.order_by(getattr(Book, sort).desc())
else:
query = query.order_by(getattr(Book, sort))
pagination = query.paginate(page=page, per_page=per_page)
books = pagination.items
# 获取所有分类供筛选使用
categories = Category.query.all()
return render_template('book/list.html',
books=books,
pagination=pagination,
search=search,
categories=categories,
category_id=category_id,
sort=sort,
order=order,
current_user=g.user)
# 图书详情页面
@book_bp.route('/detail/<int:book_id>')
@login_required
def book_detail(book_id):
book = Book.query.get_or_404(book_id)
return render_template('book/detail.html', book=book, current_user=g.user)
# 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST'])
@login_required
@admin_required
def add_book():
if request.method == 'POST':
title = request.form.get('title')
author = request.form.get('author')
publisher = request.form.get('publisher')
category_id = request.form.get('category_id')
tags = request.form.get('tags')
isbn = request.form.get('isbn')
publish_year = request.form.get('publish_year')
description = request.form.get('description')
stock = request.form.get('stock', type=int)
price = request.form.get('price')
if not title or not author:
flash('书名和作者不能为空', 'danger')
categories = Category.query.all()
return render_template('book/add.html', categories=categories, current_user=g.user)
# 处理封面图片上传
cover_url = None
if 'cover' in request.files:
cover_file = request.files['cover']
if cover_file and cover_file.filename != '':
filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}")
upload_folder = os.path.join(current_app.static_folder, 'uploads/covers')
# 确保上传目录存在
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
file_path = os.path.join(upload_folder, filename)
cover_file.save(file_path)
cover_url = f'/static/covers/{filename}'
# 创建新图书
book = Book(
title=title,
author=author,
publisher=publisher,
category_id=category_id,
tags=tags,
isbn=isbn,
publish_year=publish_year,
description=description,
cover_url=cover_url,
stock=stock,
price=price,
status=1,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now()
)
db.session.add(book)
# 记录库存日志
if stock and int(stock) > 0:
from app.models.inventory import InventoryLog
inventory_log = InventoryLog(
book_id=book.id,
change_type='入库',
change_amount=stock,
after_stock=stock,
operator_id=g.user.id,
remark='新书入库',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
db.session.commit()
flash('图书添加成功', 'success')
return redirect(url_for('book.book_list'))
categories = Category.query.all()
return render_template('book/add.html', categories=categories, current_user=g.user)
# 编辑图书
@book_bp.route('/edit/<int:book_id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_book(book_id):
book = Book.query.get_or_404(book_id)
if request.method == 'POST':
title = request.form.get('title')
author = request.form.get('author')
publisher = request.form.get('publisher')
category_id = request.form.get('category_id')
tags = request.form.get('tags')
isbn = request.form.get('isbn')
publish_year = request.form.get('publish_year')
description = request.form.get('description')
price = request.form.get('price')
status = request.form.get('status', type=int)
if not title or not author:
flash('书名和作者不能为空', 'danger')
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
# 处理库存变更
new_stock = request.form.get('stock', type=int)
if new_stock != book.stock:
from app.models.inventory import InventoryLog
change_amount = new_stock - book.stock
change_type = '入库' if change_amount > 0 else '出库'
inventory_log = InventoryLog(
book_id=book.id,
change_type=change_type,
change_amount=abs(change_amount),
after_stock=new_stock,
operator_id=g.user.id,
remark=f'管理员编辑图书库存 - {book.title}',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
book.stock = new_stock
# 处理封面图片上传
if 'cover' in request.files:
cover_file = request.files['cover']
if cover_file and cover_file.filename != '':
filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}")
upload_folder = os.path.join(current_app.static_folder, 'uploads/covers')
# 确保上传目录存在
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
file_path = os.path.join(upload_folder, filename)
cover_file.save(file_path)
book.cover_url = f'/static/covers/{filename}'
# 更新图书信息
book.title = title
book.author = author
book.publisher = publisher
book.category_id = category_id
book.tags = tags
book.isbn = isbn
book.publish_year = publish_year
book.description = description
book.price = price
book.status = status
book.updated_at = datetime.datetime.now()
db.session.commit()
flash('图书信息更新成功', 'success')
return redirect(url_for('book.book_list'))
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
# 删除图书
@book_bp.route('/delete/<int:book_id>', methods=['POST'])
@login_required
@admin_required
def delete_book(book_id):
book = Book.query.get_or_404(book_id)
# 检查该书是否有借阅记录
from app.models.borrow import BorrowRecord
active_borrows = BorrowRecord.query.filter_by(book_id=book_id, status=1).count()
if active_borrows > 0:
return jsonify({'success': False, 'message': '该图书有未归还的借阅记录,无法删除'})
# 考虑软删除而不是物理删除
book.status = 0 # 0表示已删除/下架
book.updated_at = datetime.datetime.now()
db.session.commit()
return jsonify({'success': True, 'message': '图书已成功下架'})
# 图书分类管理
@book_bp.route('/categories', methods=['GET'])
@login_required
@admin_required
def category_list():
categories = Category.query.all()
return render_template('book/categories.html', categories=categories, current_user=g.user)
# 添加分类
@book_bp.route('/categories/add', methods=['POST'])
@login_required
@admin_required
def add_category():
name = request.form.get('name')
parent_id = request.form.get('parent_id') or None
sort = request.form.get('sort', 0, type=int)
if not name:
return jsonify({'success': False, 'message': '分类名称不能为空'})
category = Category(name=name, parent_id=parent_id, sort=sort)
db.session.add(category)
db.session.commit()
return jsonify({'success': True, 'message': '分类添加成功', 'id': category.id, 'name': category.name})
# 编辑分类
@book_bp.route('/categories/edit/<int:category_id>', methods=['POST'])
@login_required
@admin_required
def edit_category(category_id):
category = Category.query.get_or_404(category_id)
name = request.form.get('name')
parent_id = request.form.get('parent_id') or None
sort = request.form.get('sort', 0, type=int)
if not name:
return jsonify({'success': False, 'message': '分类名称不能为空'})
category.name = name
category.parent_id = parent_id
category.sort = sort
db.session.commit()
return jsonify({'success': True, 'message': '分类更新成功'})
# 删除分类
@book_bp.route('/categories/delete/<int:category_id>', methods=['POST'])
@login_required
@admin_required
def delete_category(category_id):
category = Category.query.get_or_404(category_id)
# 检查是否有书籍使用此分类
books_count = Book.query.filter_by(category_id=category_id).count()
if books_count > 0:
return jsonify({'success': False, 'message': f'该分类下有{books_count}本图书,无法删除'})
# 检查是否有子分类
children_count = Category.query.filter_by(parent_id=category_id).count()
if children_count > 0:
return jsonify({'success': False, 'message': f'该分类下有{children_count}个子分类,无法删除'})
db.session.delete(category)
db.session.commit()
return jsonify({'success': True, 'message': '分类删除成功'})
# 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST'])
@login_required
@admin_required
def import_books():
if request.method == 'POST':
if 'file' not in request.files:
flash('未选择文件', 'danger')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('未选择文件', 'danger')
return redirect(request.url)
if file and file.filename.endswith(('.xlsx', '.xls')):
try:
# 读取Excel文件
df = pd.read_excel(file)
success_count = 0
error_count = 0
errors = []
# 处理每一行数据
for index, row in df.iterrows():
try:
# 检查必填字段
if pd.isna(row.get('title')) or pd.isna(row.get('author')):
errors.append(f'第{index + 2}行: 书名或作者为空')
error_count += 1
continue
# 检查ISBN是否已存在
isbn = row.get('isbn')
if isbn and not pd.isna(isbn) and Book.query.filter_by(isbn=str(isbn)).first():
errors.append(f'第{index + 2}行: ISBN {isbn} 已存在')
error_count += 1
continue
# 创建新书籍记录
book = Book(
title=row.get('title'),
author=row.get('author'),
publisher=row.get('publisher') if not pd.isna(row.get('publisher')) else None,
category_id=row.get('category_id') if not pd.isna(row.get('category_id')) else None,
tags=row.get('tags') if not pd.isna(row.get('tags')) else None,
isbn=str(row.get('isbn')) if not pd.isna(row.get('isbn')) else None,
publish_year=str(row.get('publish_year')) if not pd.isna(row.get('publish_year')) else None,
description=row.get('description') if not pd.isna(row.get('description')) else None,
cover_url=row.get('cover_url') if not pd.isna(row.get('cover_url')) else None,
stock=int(row.get('stock')) if not pd.isna(row.get('stock')) else 0,
price=float(row.get('price')) if not pd.isna(row.get('price')) else None,
status=1,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now()
)
db.session.add(book)
# 提交以获取book的id
db.session.flush()
# 创建库存日志
if book.stock > 0:
from app.models.inventory import InventoryLog
inventory_log = InventoryLog(
book_id=book.id,
change_type='入库',
change_amount=book.stock,
after_stock=book.stock,
operator_id=g.user.id,
remark='批量导入图书',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
success_count += 1
except Exception as e:
errors.append(f'第{index + 2}行: {str(e)}')
error_count += 1
db.session.commit()
flash(f'导入完成: 成功{success_count}条,失败{error_count}条', 'info')
if errors:
flash('<br>'.join(errors[:10]) + (f'<br>...等共{len(errors)}个错误' if len(errors) > 10 else ''),
'warning')
return redirect(url_for('book.book_list'))
except Exception as e:
flash(f'导入失败: {str(e)}', 'danger')
return redirect(request.url)
else:
flash('只支持Excel文件(.xlsx, .xls)', 'danger')
return redirect(request.url)
return render_template('book/import.html', current_user=g.user)
# 导出图书
@book_bp.route('/export')
@login_required
@admin_required
def export_books():
# 获取查询参数
search = request.args.get('search', '')
category_id = request.args.get('category_id', type=int)
query = Book.query
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
)
if category_id:
query = query.filter_by(category_id=category_id)
books = query.all()
# 创建DataFrame
data = []
for book in books:
category_name = book.category.name if book.category else ""
data.append({
'id': book.id,
'title': book.title,
'author': book.author,
'publisher': book.publisher,
'category': category_name,
'tags': book.tags,
'isbn': book.isbn,
'publish_year': book.publish_year,
'description': book.description,
'stock': book.stock,
'price': book.price,
'status': '上架' if book.status == 1 else '下架',
'created_at': book.created_at.strftime('%Y-%m-%d %H:%M:%S') if book.created_at else '',
'updated_at': book.updated_at.strftime('%Y-%m-%d %H:%M:%S') if book.updated_at else ''
})
df = pd.DataFrame(data)
# 创建临时文件
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
filename = f'books_export_{timestamp}.xlsx'
filepath = os.path.join(current_app.static_folder, 'temp', filename)
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
# 写入Excel
df.to_excel(filepath, index=False)
# 提供下载链接
return redirect(url_for('static', filename=f'temp/{filename}'))
================================================================================
File: ./app/controllers/statistics.py
================================================================================
================================================================================
File: ./app/controllers/borrow.py
================================================================================
================================================================================
File: ./app/controllers/announcement.py
================================================================================
================================================================================
File: ./app/controllers/inventory.py
================================================================================
================================================================================
File: ./app/services/borrow_service.py
================================================================================
================================================================================
File: ./app/services/inventory_service.py
================================================================================
================================================================================
File: ./app/services/__init__.py
================================================================================
================================================================================
File: ./app/services/book_service.py
================================================================================
================================================================================
File: ./app/services/user_service.py
================================================================================