5724 lines
177 KiB
Plaintext
5724 lines
177 KiB
Plaintext
|
||
================================================================================
|
||
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>© 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">×</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">×</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">×</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">×</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
|
||
================================================================================
|
||
|