diff --git a/app/__init__.py b/app/__init__.py
index c84812c..0ea45e4 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -4,6 +4,7 @@ from app.models.user import db, User
from app.controllers.user import user_bp
from app.controllers.book import book_bp
from app.controllers.borrow import borrow_bp
+from app.controllers.inventory import inventory_bp
from flask_login import LoginManager, current_user
import os
@@ -48,6 +49,7 @@ def create_app(config=None):
app.register_blueprint(user_bp, url_prefix='/user')
app.register_blueprint(book_bp, url_prefix='/book')
app.register_blueprint(borrow_bp, url_prefix='/borrow')
+ app.register_blueprint(inventory_bp)
# 创建数据库表
with app.app_context():
@@ -132,3 +134,7 @@ def create_app(config=None):
return s
return app
+
+ @app.context_processor
+ def inject_now():
+ return {'now': datetime.datetime.now()}
diff --git a/app/controllers/book.py b/app/controllers/book.py
index 19ec822..0c75f7d 100644
--- a/app/controllers/book.py
+++ b/app/controllers/book.py
@@ -116,7 +116,8 @@ def book_detail(book_id):
# 如果用户是管理员,预先查询并排序借阅记录
borrow_records = []
- if g.user.role_id == 1: # 假设 role_id 1 为管理员
+ # 防御性编程:确保 g.user 存在且有 role_id 属性
+ if hasattr(g, 'user') and g.user is not None and hasattr(g.user, 'role_id') and g.user.role_id == 1:
from app.models.borrow import BorrowRecord
borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit(
10).all()
@@ -124,12 +125,13 @@ def book_detail(book_id):
return render_template(
'book/detail.html',
book=book,
- current_user=g.user,
+ current_user=current_user, # 使用 flask_login 的 current_user 而不是 g.user
borrow_records=borrow_records,
now=now
)
+
# 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST'])
@login_required
@@ -283,6 +285,7 @@ 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')
@@ -294,13 +297,72 @@ def edit_book(book_id):
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)
+ # ISBN验证
+ if isbn and isbn.strip(): # 确保ISBN不是空字符串
+ # 移除连字符和空格
+ clean_isbn = isbn.replace('-', '').replace(' ', '')
+
+ # 长度检查
+ if len(clean_isbn) != 10 and len(clean_isbn) != 13:
+ flash('ISBN必须是10位或13位', 'danger')
+ categories = Category.query.all()
+ return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
+
+ # ISBN-10验证
+ if len(clean_isbn) == 10:
+ # 检查前9位是否为数字
+ if not clean_isbn[:9].isdigit():
+ flash('ISBN-10的前9位必须是数字', 'danger')
+ categories = Category.query.all()
+ return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
+
+ # 检查最后一位是否为数字或'X'
+ if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'):
+ flash('ISBN-10的最后一位必须是数字或X', 'danger')
+ categories = Category.query.all()
+ return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
+
+ # 校验和验证
+ sum = 0
+ for i in range(9):
+ sum += int(clean_isbn[i]) * (10 - i)
+
+ check_digit = 10 if clean_isbn[9].upper() == 'X' else int(clean_isbn[9])
+ sum += check_digit
+
+ if sum % 11 != 0:
+ flash('ISBN-10校验和无效', 'danger')
+ categories = Category.query.all()
+ return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
+
+ # ISBN-13验证
+ if len(clean_isbn) == 13:
+ # 检查是否全是数字
+ if not clean_isbn.isdigit():
+ flash('ISBN-13必须全是数字', 'danger')
+ categories = Category.query.all()
+ return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
+
+ # 校验和验证
+ sum = 0
+ for i in range(12):
+ sum += int(clean_isbn[i]) * (1 if i % 2 == 0 else 3)
+
+ check_digit = (10 - (sum % 10)) % 10
+
+ if check_digit != int(clean_isbn[12]):
+ flash('ISBN-13校验和无效', '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)
+ new_stock = request.form.get('stock', type=int) or 0 # 默认为0而非None
if new_stock != book.stock:
from app.models.inventory import InventoryLog
change_amount = new_stock - book.stock
@@ -346,11 +408,17 @@ def edit_book(book_id):
book.status = status
book.updated_at = datetime.datetime.now()
- db.session.commit()
-
- flash('图书信息更新成功', 'success')
- return redirect(url_for('book.book_list'))
+ try:
+ db.session.commit()
+ flash('图书信息更新成功', 'success')
+ return redirect(url_for('book.book_list'))
+ except Exception as e:
+ db.session.rollback()
+ flash(f'保存失败: {str(e)}', 'danger')
+ categories = Category.query.all()
+ return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
+ # GET 请求
categories = Category.query.all()
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
@@ -622,3 +690,53 @@ def test_permissions():
是否管理员: {'是' if current_user.role_id == 1 else '否'}
尝试访问管理页面
"""
+
+# 添加到app/controllers/book.py文件中
+
+@book_bp.route('/browse')
+@login_required
+def browse_books():
+ """图书浏览页面 - 面向普通用户的友好界面"""
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 12, type=int) # 增加每页数量
+
+ # 只显示状态为1的图书(未下架的图书)
+ query = Book.query.filter_by(status=1)
+
+ # 搜索功能
+ search = request.args.get('search', '')
+ if search:
+ query = query.filter(
+ (Book.title.contains(search)) |
+ (Book.author.contains(search)) |
+ (Book.isbn.contains(search))
+ )
+
+ # 分类筛选
+ category_id = request.args.get('category_id', type=int)
+ if category_id:
+ query = query.filter_by(category_id=category_id)
+
+ # 排序
+ sort = request.args.get('sort', 'id')
+ order = request.args.get('order', 'desc')
+
+ if order == 'desc':
+ query = query.order_by(getattr(Book, sort).desc())
+ else:
+ query = query.order_by(getattr(Book, sort))
+
+ pagination = query.paginate(page=page, per_page=per_page)
+ books = pagination.items
+
+ # 获取所有分类供筛选使用
+ categories = Category.query.all()
+
+ return render_template('book/browse.html',
+ books=books,
+ pagination=pagination,
+ search=search,
+ categories=categories,
+ category_id=category_id,
+ sort=sort,
+ order=order,)
diff --git a/app/controllers/borrow.py b/app/controllers/borrow.py
index f68acb1..52b2f6e 100644
--- a/app/controllers/borrow.py
+++ b/app/controllers/borrow.py
@@ -1,11 +1,11 @@
-from flask import Blueprint, request, redirect, url_for, flash, g, jsonify
+from flask import Blueprint, request, redirect, url_for, flash, render_template, jsonify
from flask_login import current_user, login_required
from app.models.book import Book
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
-from app.models.user import db # 修正:从 user 模型导入 db
-from app.utils.auth import login_required
+from app.models.user import db, User
import datetime
+from app.utils.auth import admin_required
# 创建借阅蓝图
borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow')
@@ -30,7 +30,7 @@ def borrow_book():
# 检查当前用户是否已借阅此书
existing_borrow = BorrowRecord.query.filter_by(
- user_id=g.user.id,
+ user_id=current_user.id,
book_id=book_id,
status=1 # 1表示借阅中
).first()
@@ -45,7 +45,7 @@ def borrow_book():
due_date = now + datetime.timedelta(days=borrow_days)
borrow_record = BorrowRecord(
- user_id=g.user.id,
+ user_id=current_user.id,
book_id=book_id,
borrow_date=now,
due_date=due_date,
@@ -67,7 +67,7 @@ def borrow_book():
change_type='借出',
change_amount=-1,
after_stock=book.stock,
- operator_id=g.user.id,
+ operator_id=current_user.id,
remark='用户借书',
changed_at=now
)
@@ -101,7 +101,7 @@ def add_borrow(book_id):
# 检查是否已借阅
existing_borrow = BorrowRecord.query.filter_by(
- user_id=current_user.id, # 使用current_user
+ user_id=current_user.id,
book_id=book_id,
status=1 # 1表示借阅中
).first()
@@ -118,7 +118,7 @@ def add_borrow(book_id):
due_date = now + datetime.timedelta(days=borrow_days)
borrow_record = BorrowRecord(
- user_id=current_user.id, # 使用current_user
+ user_id=current_user.id,
book_id=book_id,
borrow_date=now,
due_date=due_date,
@@ -140,7 +140,7 @@ def add_borrow(book_id):
change_type='借出',
change_amount=-1,
after_stock=book.stock,
- operator_id=current_user.id, # 使用current_user
+ operator_id=current_user.id,
remark='用户借书',
changed_at=now
)
@@ -158,3 +158,382 @@ def add_borrow(book_id):
'success': False,
'message': f'借阅失败: {str(e)}'
})
+
+
+@borrow_bp.route('/return/', methods=['POST'])
+@login_required
+def return_book(borrow_id):
+ """还书操作"""
+ # 查找借阅记录
+ borrow_record = BorrowRecord.query.get_or_404(borrow_id)
+
+ # 检查是否是自己的借阅记录或者是管理员
+ if borrow_record.user_id != current_user.id and current_user.role_id != 1:
+ return jsonify({
+ 'success': False,
+ 'message': '您无权执行此操作'
+ })
+
+ # 检查是否已还
+ if borrow_record.status != 1:
+ return jsonify({
+ 'success': False,
+ 'message': '此书已归还,请勿重复操作'
+ })
+
+ try:
+ book = Book.query.get(borrow_record.book_id)
+ now = datetime.datetime.now()
+
+ # 更新借阅记录
+ borrow_record.status = 0 # 0表示已归还
+ borrow_record.return_date = now
+ borrow_record.updated_at = now
+
+ # 更新图书库存
+ book.stock += 1
+ book.updated_at = now
+
+ db.session.commit()
+
+ # 添加库存变更日志
+ inventory_log = InventoryLog(
+ book_id=borrow_record.book_id,
+ change_type='归还',
+ change_amount=1,
+ after_stock=book.stock,
+ operator_id=current_user.id,
+ remark='用户还书',
+ changed_at=now
+ )
+ db.session.add(inventory_log)
+ db.session.commit()
+
+ return jsonify({
+ 'success': True,
+ 'message': f'成功归还《{book.title}》'
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ return jsonify({
+ 'success': False,
+ 'message': f'归还失败: {str(e)}'
+ })
+
+
+@borrow_bp.route('/renew/', methods=['POST'])
+@login_required
+def renew_book(borrow_id):
+ """续借操作"""
+ # 查找借阅记录
+ borrow_record = BorrowRecord.query.get_or_404(borrow_id)
+
+ # 检查是否是自己的借阅记录或者是管理员
+ if borrow_record.user_id != current_user.id and current_user.role_id != 1:
+ return jsonify({
+ 'success': False,
+ 'message': '您无权执行此操作'
+ })
+
+ # 检查是否已还
+ if borrow_record.status != 1:
+ return jsonify({
+ 'success': False,
+ 'message': '此书已归还,无法续借'
+ })
+
+ # 检查续借次数限制(最多续借2次)
+ if borrow_record.renew_count >= 2:
+ return jsonify({
+ 'success': False,
+ 'message': '此书已达到最大续借次数,无法继续续借'
+ })
+
+ try:
+ now = datetime.datetime.now()
+
+ # 检查是否已逾期
+ if now > borrow_record.due_date:
+ return jsonify({
+ 'success': False,
+ 'message': '此书已逾期,请先归还并处理逾期情况'
+ })
+
+ # 续借14天
+ new_due_date = borrow_record.due_date + datetime.timedelta(days=14)
+
+ # 更新借阅记录
+ borrow_record.due_date = new_due_date
+ borrow_record.renew_count += 1
+ borrow_record.updated_at = now
+
+ db.session.commit()
+
+ return jsonify({
+ 'success': True,
+ 'message': f'续借成功,新的归还日期为 {new_due_date.strftime("%Y-%m-%d")}'
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ return jsonify({
+ 'success': False,
+ 'message': f'续借失败: {str(e)}'
+ })
+
+
+@borrow_bp.route('/my_borrows')
+@login_required
+def my_borrows():
+ """用户查看自己的借阅记录"""
+ page = request.args.get('page', 1, type=int)
+ status = request.args.get('status', default=None, type=int)
+
+ # 构建查询
+ query = BorrowRecord.query.filter_by(user_id=current_user.id)
+
+ # 根据状态筛选
+ if status is not None:
+ query = query.filter_by(status=status)
+
+ # 按借阅日期倒序排列
+ query = query.order_by(BorrowRecord.borrow_date.desc())
+
+ # 分页
+ pagination = query.paginate(page=page, per_page=10, error_out=False)
+
+ # 获取当前借阅数量和历史借阅数量(用于标签显示)
+ current_borrows_count = BorrowRecord.query.filter_by(user_id=current_user.id, status=1).count()
+ history_borrows_count = BorrowRecord.query.filter_by(user_id=current_user.id, status=0).count()
+
+ return render_template(
+ 'borrow/my_borrows.html',
+ pagination=pagination,
+ current_borrows_count=current_borrows_count,
+ history_borrows_count=history_borrows_count,
+ status=status,
+ now=datetime.datetime.now() # 添加当前时间变量
+ )
+
+
+
+@borrow_bp.route('/manage')
+@login_required
+@admin_required
+def manage_borrows():
+ """管理员查看所有借阅记录"""
+ page = request.args.get('page', 1, type=int)
+ status = request.args.get('status', default=None, type=int)
+ user_id = request.args.get('user_id', default=None, type=int)
+ book_id = request.args.get('book_id', default=None, type=int)
+ search = request.args.get('search', default='')
+
+ # 构建查询
+ query = BorrowRecord.query
+
+ # 根据状态筛选
+ if status is not None:
+ query = query.filter_by(status=status)
+
+ # 根据用户筛选
+ if user_id:
+ query = query.filter_by(user_id=user_id)
+
+ # 根据图书筛选
+ if book_id:
+ query = query.filter_by(book_id=book_id)
+
+ # 根据搜索条件筛选(用户名或图书名)
+ if search:
+ query = query.join(User, BorrowRecord.user_id == User.id) \
+ .join(Book, BorrowRecord.book_id == Book.id) \
+ .filter((User.username.like(f'%{search}%')) |
+ (Book.title.like(f'%{search}%')))
+
+ # 按借阅日期倒序排列
+ query = query.order_by(BorrowRecord.borrow_date.desc())
+
+ # 分页
+ pagination = query.paginate(page=page, per_page=10, error_out=False)
+
+ # 获取统计数据
+ current_borrows_count = BorrowRecord.query.filter_by(status=1).count()
+ history_borrows_count = BorrowRecord.query.filter_by(status=0).count()
+
+ # 获取所有用户(用于筛选)
+ users = User.query.all()
+
+ return render_template(
+ 'borrow/borrow_management.html',
+ pagination=pagination,
+ current_borrows_count=current_borrows_count,
+ history_borrows_count=history_borrows_count,
+ status=status,
+ user_id=user_id,
+ book_id=book_id,
+ search=search,
+ users=users,
+ now=datetime.datetime.now() # 添加当前时间变量
+ )
+
+
+
+@borrow_bp.route('/admin/add', methods=['POST'])
+@login_required
+@admin_required
+def admin_add_borrow():
+ """管理员为用户添加借阅记录"""
+ user_id = request.form.get('user_id', type=int)
+ book_id = request.form.get('book_id', type=int)
+ borrow_days = request.form.get('borrow_days', type=int, default=14)
+
+ if not user_id or not book_id:
+ flash('用户ID和图书ID不能为空', 'danger')
+ return redirect(url_for('borrow.manage_borrows'))
+
+ # 验证用户和图书是否存在
+ user = User.query.get_or_404(user_id)
+ book = Book.query.get_or_404(book_id)
+
+ # 检查库存
+ if book.stock <= 0:
+ flash(f'《{book.title}》当前无库存,无法借阅', 'danger')
+ return redirect(url_for('borrow.manage_borrows'))
+
+ # 检查用户是否已借阅此书
+ existing_borrow = BorrowRecord.query.filter_by(
+ user_id=user_id,
+ book_id=book_id,
+ status=1 # 1表示借阅中
+ ).first()
+
+ if existing_borrow:
+ flash(f'用户 {user.username} 已借阅《{book.title}》,请勿重复借阅', 'warning')
+ return redirect(url_for('borrow.manage_borrows'))
+
+ try:
+ # 创建借阅记录
+ now = datetime.datetime.now()
+ due_date = now + datetime.timedelta(days=borrow_days)
+
+ borrow_record = BorrowRecord(
+ user_id=user_id,
+ book_id=book_id,
+ borrow_date=now,
+ due_date=due_date,
+ status=1, # 1表示借阅中
+ created_at=now,
+ updated_at=now
+ )
+
+ # 更新图书库存
+ book.stock -= 1
+ book.updated_at = now
+
+ db.session.add(borrow_record)
+ db.session.commit()
+
+ # 添加库存变更日志
+ inventory_log = InventoryLog(
+ book_id=book_id,
+ change_type='借出',
+ change_amount=-1,
+ after_stock=book.stock,
+ operator_id=current_user.id,
+ remark=f'管理员 {current_user.username} 为用户 {user.username} 借书',
+ changed_at=now
+ )
+ db.session.add(inventory_log)
+ db.session.commit()
+
+ flash(f'成功为用户 {user.username} 借阅《{book.title}》,归还日期: {due_date.strftime("%Y-%m-%d")}', 'success')
+
+ except Exception as e:
+ db.session.rollback()
+ flash(f'借阅失败: {str(e)}', 'danger')
+
+ return redirect(url_for('borrow.manage_borrows'))
+
+
+@borrow_bp.route('/overdue')
+@login_required
+@admin_required
+def overdue_borrows():
+ """查看逾期借阅"""
+ page = request.args.get('page', 1, type=int)
+ now = datetime.datetime.now()
+
+ # 查询所有已逾期且未归还的借阅记录
+ query = BorrowRecord.query.filter(
+ BorrowRecord.status == 1, # 借阅中
+ BorrowRecord.due_date < now # 已过期
+ ).order_by(BorrowRecord.due_date) # 按到期日期排序,最早到期的排在前面
+
+ pagination = query.paginate(page=page, per_page=10, error_out=False)
+
+ # 计算逾期总数
+ overdue_count = query.count()
+
+ return render_template(
+ 'borrow/overdue.html',
+ pagination=pagination,
+ overdue_count=overdue_count
+ )
+
+
+@borrow_bp.route('/overdue/notify/', methods=['POST'])
+@login_required
+@admin_required
+def notify_overdue(borrow_id):
+ """发送逾期通知"""
+ from app.models.notification import Notification
+
+ borrow_record = BorrowRecord.query.get_or_404(borrow_id)
+
+ # 检查是否已还
+ if borrow_record.status != 1:
+ return jsonify({
+ 'success': False,
+ 'message': '此书已归还,无需发送逾期通知'
+ })
+
+ now = datetime.datetime.now()
+
+ # 检查是否确实逾期
+ if borrow_record.due_date > now:
+ return jsonify({
+ 'success': False,
+ 'message': '此借阅记录尚未逾期'
+ })
+
+ try:
+ # 创建通知
+ notification = Notification(
+ user_id=borrow_record.user_id,
+ title='图书逾期提醒',
+ content=f'您借阅的《{borrow_record.book.title}》已逾期,请尽快归还。应还日期: {borrow_record.due_date.strftime("%Y-%m-%d")}',
+ type='overdue',
+ sender_id=current_user.id,
+ created_at=now
+ )
+
+ db.session.add(notification)
+ db.session.commit()
+
+ # 更新借阅记录备注
+ borrow_record.remark = f'{borrow_record.remark or ""}[{now.strftime("%Y-%m-%d")} 已发送逾期通知]'
+ borrow_record.updated_at = now
+ db.session.commit()
+
+ return jsonify({
+ 'success': True,
+ 'message': '已成功发送逾期通知'
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ return jsonify({
+ 'success': False,
+ 'message': f'发送通知失败: {str(e)}'
+ })
diff --git a/app/controllers/inventory.py b/app/controllers/inventory.py
index e69de29..fb33411 100644
--- a/app/controllers/inventory.py
+++ b/app/controllers/inventory.py
@@ -0,0 +1,161 @@
+# app/controllers/inventory.py
+from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for
+from flask_login import login_required, current_user
+from app.models.book import Book
+from app.models.inventory import InventoryLog
+from app.models.user import db
+from app.utils.auth import admin_required
+from datetime import datetime
+
+inventory_bp = Blueprint('inventory', __name__, url_prefix='/inventory')
+
+
+@inventory_bp.route('/')
+@login_required
+@admin_required
+def inventory_list():
+ """库存管理页面 - 只有管理员有权限进入"""
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 20, type=int)
+
+ # 搜索功能
+ search = request.args.get('search', '')
+ query = Book.query
+
+ if search:
+ query = query.filter(
+ (Book.title.contains(search)) |
+ (Book.author.contains(search)) |
+ (Book.isbn.contains(search))
+ )
+
+ # 排序
+ sort = request.args.get('sort', 'id')
+ order = request.args.get('order', 'asc')
+ 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
+
+ return render_template('inventory/list.html',
+ books=books,
+ pagination=pagination,
+ search=search,
+ sort=sort,
+ order=order)
+
+
+@inventory_bp.route('/adjust/', methods=['GET', 'POST'])
+@login_required
+@admin_required
+def adjust_inventory(book_id):
+ """调整图书库存"""
+ book = Book.query.get_or_404(book_id)
+
+ if request.method == 'POST':
+ change_type = request.form.get('change_type')
+ change_amount = int(request.form.get('change_amount', 0))
+ remark = request.form.get('remark', '')
+
+ if change_amount <= 0:
+ flash('调整数量必须大于0', 'danger')
+ return redirect(url_for('inventory.adjust_inventory', book_id=book_id))
+
+ # 计算库存变化
+ original_stock = book.stock
+ if change_type == 'in':
+ book.stock += change_amount
+ after_stock = book.stock
+ elif change_type == 'out':
+ if book.stock < change_amount:
+ flash('出库数量不能大于当前库存', 'danger')
+ return redirect(url_for('inventory.adjust_inventory', book_id=book_id))
+ book.stock -= change_amount
+ after_stock = book.stock
+ else:
+ flash('无效的操作类型', 'danger')
+ return redirect(url_for('inventory.adjust_inventory', book_id=book_id))
+
+ # 创建库存日志
+ log = InventoryLog(
+ book_id=book.id,
+ change_type=change_type,
+ change_amount=change_amount,
+ after_stock=after_stock,
+ operator_id=current_user.id,
+ remark=remark,
+ changed_at=datetime.now()
+ )
+
+ try:
+ db.session.add(log)
+ db.session.commit()
+ flash(f'图书《{book.title}》库存调整成功!原库存:{original_stock},现库存:{after_stock}', 'success')
+ return redirect(url_for('inventory.inventory_list'))
+ except Exception as e:
+ db.session.rollback()
+ flash(f'操作失败:{str(e)}', 'danger')
+ return redirect(url_for('inventory.adjust_inventory', book_id=book_id))
+
+ return render_template('inventory/adjust.html', book=book)
+
+
+@inventory_bp.route('/logs')
+@login_required
+@admin_required
+def inventory_logs():
+ """查看库存变动日志"""
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 20, type=int)
+ # 搜索和筛选
+ book_id = request.args.get('book_id', type=int)
+ change_type = request.args.get('change_type', '')
+ date_from = request.args.get('date_from', '')
+ date_to = request.args.get('date_to', '')
+ query = InventoryLog.query
+ if book_id:
+ query = query.filter_by(book_id=book_id)
+ if change_type:
+ query = query.filter_by(change_type=change_type)
+ if date_from:
+ query = query.filter(InventoryLog.changed_at >= datetime.strptime(date_from, '%Y-%m-%d'))
+ if date_to:
+ query = query.filter(InventoryLog.changed_at <= datetime.strptime(date_to + ' 23:59:59', '%Y-%m-%d %H:%M:%S'))
+ # 默认按时间倒序
+ query = query.order_by(InventoryLog.changed_at.desc())
+ pagination = query.paginate(page=page, per_page=per_page)
+ logs = pagination.items
+ # 获取所有图书用于筛选
+ books = Book.query.all()
+ # 如果特定 book_id 被指定,也获取该书的详细信息
+ book = Book.query.get(book_id) if book_id else None
+ return render_template('inventory/logs.html',
+ logs=logs,
+ pagination=pagination,
+ books=books,
+ book=book, # 添加这个变量
+ book_id=book_id,
+ change_type=change_type,
+ date_from=date_from,
+ date_to=date_to)
+
+@inventory_bp.route('/book//logs')
+@login_required
+@admin_required
+def book_inventory_logs(book_id):
+ """查看特定图书的库存变动日志"""
+ book = Book.query.get_or_404(book_id)
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 20, type=int)
+
+ logs = InventoryLog.query.filter_by(book_id=book_id) \
+ .order_by(InventoryLog.changed_at.desc()) \
+ .paginate(page=page, per_page=per_page)
+
+ return render_template('inventory/book_logs.html',
+ book=book,
+ logs=logs.items,
+ pagination=logs)
diff --git a/app/controllers/user.py b/app/controllers/user.py
index 85a9bd9..dd7289f 100644
--- a/app/controllers/user.py
+++ b/app/controllers/user.py
@@ -405,3 +405,70 @@ def get_role_user_count(role_id):
'message': f"查询失败: {str(e)}",
'count': 0
}), 500
+
+
+@user_bp.route('/add', methods=['GET', 'POST'])
+@login_required
+@admin_required
+def add_user():
+ roles = UserService.get_all_roles()
+
+ 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')
+ nickname = request.form.get('nickname')
+ phone = request.form.get('phone')
+ if phone == '':
+ phone = None
+ nickname = request.form.get('nickname')
+ role_id = request.form.get('role_id', 2, type=int) # 默认为普通用户
+ status = request.form.get('status', 1, type=int) # 默认为启用状态
+
+ # 验证表单数据
+ if not username or not email or not password or not confirm_password or not verification_code:
+ return render_template('user/add.html', error='所有必填字段不能为空', roles=roles)
+
+ if password != confirm_password:
+ return render_template('user/add.html', error='两次输入的密码不匹配', roles=roles)
+
+ # 检查用户名和邮箱是否已存在
+ if User.query.filter_by(username=username).first():
+ return render_template('user/add.html', error='用户名已存在', roles=roles)
+
+ if User.query.filter_by(email=email).first():
+ return render_template('user/add.html', error='邮箱已被注册', roles=roles)
+
+ # 验证验证码
+ stored_code = verification_codes.get(email)
+ if not stored_code or stored_code != verification_code:
+ return render_template('user/add.html', error='验证码无效或已过期', roles=roles)
+
+ # 创建新用户
+ try:
+ new_user = User(
+ username=username,
+ password=password, # 密码会在模型中自动哈希
+ email=email,
+ nickname=nickname or username, # 如果未提供昵称,使用用户名
+ phone=phone,
+ role_id=role_id,
+ status=status
+ )
+ db.session.add(new_user)
+ db.session.commit()
+
+ # 清除验证码
+ verification_codes.delete(email)
+
+ flash('用户添加成功', 'success')
+ return redirect(url_for('user.user_list'))
+ except Exception as e:
+ db.session.rollback()
+ logging.error(f"用户添加失败: {str(e)}")
+ return render_template('user/add.html', error=f'添加用户失败: {str(e)}', roles=roles)
+
+ # GET请求,显示添加用户表单
+ return render_template('user/add.html', roles=roles)
diff --git a/app/models/book.py b/app/models/book.py
index d09d37f..28dc9a5 100644
--- a/app/models/book.py
+++ b/app/models/book.py
@@ -36,7 +36,8 @@ class Book(db.Model):
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
- # 移除所有关系引用
+ # 添加与 InventoryLog 的关系
+ inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic')
def __repr__(self):
return f''
diff --git a/app/models/user.py b/app/models/user.py
index ba7fa6c..069c2a7 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -19,13 +19,14 @@ class User(db.Model, UserMixin):
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):
+ def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2, status=1):
self.username = username
self.set_password(password)
self.email = email
self.phone = phone
self.nickname = nickname
self.role_id = role_id
+ self.status = status # 新增
@property
def is_active(self):
diff --git a/app/services/user_service.py b/app/services/user_service.py
index c5b237e..2c749d5 100644
--- a/app/services/user_service.py
+++ b/app/services/user_service.py
@@ -161,3 +161,24 @@ class UserService:
except Exception as e:
db.session.rollback()
return False, f"更新失败: {str(e)}"
+
+ @staticmethod
+ def create_user(data):
+ """创建新用户"""
+ try:
+ new_user = User(
+ username=data['username'],
+ password=data['password'],
+ email=data['email'],
+ nickname=data.get('nickname') or data['username'],
+ phone=data.get('phone'),
+ role_id=data.get('role_id', 2), # 默认为普通用户
+ status=data.get('status', 1) # 默认为启用状态
+ )
+ db.session.add(new_user)
+ db.session.commit()
+ return True, '用户创建成功'
+ except Exception as e:
+ db.session.rollback()
+ logging.error(f"创建用户失败: {str(e)}")
+ return False, f'创建用户失败: {str(e)}'
\ No newline at end of file
diff --git a/app/static/covers/354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg b/app/static/covers/354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg
new file mode 100644
index 0000000..f0b63a4
Binary files /dev/null and b/app/static/covers/354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg differ
diff --git a/app/static/css/book-edit.css b/app/static/css/book-edit.css
new file mode 100644
index 0000000..f30fbfb
--- /dev/null
+++ b/app/static/css/book-edit.css
@@ -0,0 +1,424 @@
+/* ========== 优雅粉色主题 - 图书编辑系统 ========== */
+:root {
+ --primary-pink: #FF85A2;
+ --primary-pink-hover: #FF6D8E;
+ --secondary-pink: #FFC0D3;
+ --accent-pink: #FF4778;
+ --background-pink: #FFF5F7;
+ --border-pink: #FFD6E0;
+ --soft-lavender: #E2D1F9;
+ --mint-green: #D0F0C0;
+ --dark-text: #5D4E60;
+ --medium-text: #8A7B8F;
+ --light-text: #BFB5C6;
+ --white: #FFFFFF;
+ --shadow-sm: 0 4px 6px rgba(255, 133, 162, 0.1);
+ --shadow-md: 0 6px 12px rgba(255, 133, 162, 0.15);
+ --shadow-lg: 0 15px 25px rgba(255, 133, 162, 0.2);
+ --border-radius-sm: 8px;
+ --border-radius-md: 12px;
+ --border-radius-lg: 16px;
+ --transition-fast: 0.2s ease;
+ --transition-base: 0.3s ease;
+ --font-primary: 'Poppins', 'Helvetica Neue', sans-serif;
+ --font-secondary: 'Playfair Display', serif;
+}
+
+/* ========== 全局样式 ========== */
+body {
+ background-color: var(--background-pink);
+ color: var(--dark-text);
+ font-family: var(--font-primary);
+ line-height: 1.6;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: var(--font-secondary);
+ color: var(--dark-text);
+}
+
+a {
+ color: var(--accent-pink);
+ transition: color var(--transition-fast);
+}
+
+a:hover {
+ color: var(--primary-pink-hover);
+ text-decoration: none;
+}
+
+.btn {
+ border-radius: var(--border-radius-sm);
+ font-weight: 500;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-sm);
+ padding: 0.5rem 1.25rem;
+}
+
+.btn:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-primary {
+ background-color: var(--primary-pink);
+ border-color: var(--primary-pink);
+}
+
+.btn-primary:hover, .btn-primary:focus {
+ background-color: var(--primary-pink-hover);
+ border-color: var(--primary-pink-hover);
+}
+
+.btn-info {
+ background-color: var(--soft-lavender);
+ border-color: var(--soft-lavender);
+ color: var(--dark-text);
+}
+
+.btn-info:hover, .btn-info:focus {
+ background-color: #D4BFF0;
+ border-color: #D4BFF0;
+ color: var(--dark-text);
+}
+
+.btn-secondary {
+ background-color: var(--white);
+ border-color: var(--border-pink);
+ color: var(--medium-text);
+}
+
+.btn-secondary:hover, .btn-secondary:focus {
+ background-color: var(--border-pink);
+ border-color: var(--border-pink);
+ color: var(--dark-text);
+}
+
+.btn i {
+ margin-right: 8px;
+}
+
+/* ========== 表单容器 ========== */
+.book-form-container {
+ max-width: 1400px;
+ margin: 2rem auto;
+ padding: 2rem;
+ background-color: var(--white);
+ border-radius: var(--border-radius-lg);
+ box-shadow: var(--shadow-md);
+ position: relative;
+ overflow: hidden;
+}
+
+.book-form-container::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 8px;
+ background: linear-gradient(to right, var(--primary-pink), var(--accent-pink), var(--soft-lavender));
+}
+
+/* ========== 页面标题区域 ========== */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ padding-bottom: 1rem;
+ border-bottom: 2px solid var(--secondary-pink);
+}
+
+.page-header h1 {
+ font-size: 2.2rem;
+ font-weight: 700;
+ color: var(--primary-pink);
+ margin: 0;
+ position: relative;
+ font-family: var(--font-secondary);
+}
+
+.flower-icon {
+ color: var(--accent-pink);
+ margin-right: 8px;
+}
+
+.actions {
+ display: flex;
+ gap: 1rem;
+}
+
+/* ========== 表单元素 ========== */
+.form-row {
+ margin-bottom: 1.5rem;
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group label {
+ color: var(--dark-text);
+ font-weight: 500;
+ font-size: 0.95rem;
+ margin-bottom: 0.5rem;
+ display: block;
+}
+
+.form-control {
+ border: 2px solid var(--border-pink);
+ border-radius: var(--border-radius-sm);
+ padding: 0.75rem 1rem;
+ color: var(--dark-text);
+ transition: all var(--transition-fast);
+ font-size: 0.95rem;
+}
+
+.form-control:focus {
+ border-color: var(--primary-pink);
+ box-shadow: 0 0 0 0.2rem rgba(255, 133, 162, 0.25);
+}
+
+.form-control::placeholder {
+ color: var(--light-text);
+}
+
+.required {
+ color: var(--accent-pink);
+}
+
+select.form-control {
+height: 42px; / 确保高度一致,内容不截断 */
+line-height: 1.5;
+padding: 8px 12px;
+font-size: 0.95rem;
+color: var(--dark-text);
+background-color: var(--white);
+border: 1px solid var(--border-pink);
+border-radius: var(--border-radius-sm);
+appearance: none;
+-webkit-appearance: none;
+-moz-appearance: none;
+background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%235D4E60' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
+background-repeat: no-repeat;
+background-position: right 0.75rem center;
+background-size: 1rem;
+transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+select.form-control:focus {
+border-color: var(--primary-pink);
+outline: none;
+box-shadow: 0 0 0 0.2rem rgba(255, 133, 162, 0.2);
+}
+
+/* 状态选项 / 分类样式专属修复(可选项) */
+#status, #category_id {
+padding-top: 8px;
+padding-bottom: 8px;
+font-family: inherit;
+}
+
+/* iOS & Edge 下拉兼容优化 */
+select.form-control::-ms-expand {
+display: none;
+}
+
+/* 浏览器优雅过渡体验 */
+select.form-control:hover {
+border-color: var(--accent-pink);
+}
+
+select.form-control:disabled {
+background-color: var(--background-pink);
+color: var(--light-text);
+cursor: not-allowed;
+opacity: 0.7;
+}
+
+textarea.form-control {
+ min-height: 150px;
+ resize: vertical;
+}
+
+/* ========== 卡片样式 ========== */
+.card {
+ border: none;
+ border-radius: var(--border-radius-md);
+ box-shadow: var(--shadow-sm);
+ overflow: hidden;
+ transition: all var(--transition-base);
+ margin-bottom: 1.5rem;
+ background-color: var(--white);
+}
+
+.card:hover {
+ box-shadow: var(--shadow-md);
+}
+
+.card-header {
+ background-color: var(--secondary-pink);
+ border-bottom: none;
+ padding: 1rem 1.5rem;
+ font-family: var(--font-secondary);
+ font-weight: 600;
+ color: var(--dark-text);
+ font-size: 1.1rem;
+}
+
+.card-body {
+ padding: 1.5rem;
+ background-color: var(--white);
+}
+
+/* ========== 封面图片区域 ========== */
+.cover-preview-container {
+ padding: 1rem;
+ text-align: center;
+}
+
+.cover-preview {
+ min-height: 300px;
+ background-color: var(--background-pink);
+ border: 2px dashed var(--secondary-pink);
+ border-radius: var(--border-radius-sm);
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1rem;
+ position: relative;
+ transition: all var(--transition-fast);
+}
+
+.cover-preview:hover {
+ border-color: var(--primary-pink);
+}
+
+.cover-image {
+ max-width: 100%;
+ max-height: 300px;
+ border-radius: var(--border-radius-sm);
+ box-shadow: var(--shadow-sm);
+}
+
+.no-cover-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--light-text);
+ padding: 2rem;
+}
+
+.no-cover-placeholder i {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+}
+
+.upload-container {
+ margin-top: 1rem;
+}
+
+.btn-outline-primary {
+ color: var(--primary-pink);
+ border-color: var(--primary-pink);
+ background-color: transparent;
+ transition: all var(--transition-base);
+}
+
+.btn-outline-primary:hover, .btn-outline-primary:focus {
+ background-color: var(--primary-pink);
+ color: white;
+}
+
+/* ========== 提交按钮区域 ========== */
+.form-submit-container {
+ margin-top: 2rem;
+}
+
+.btn-lg {
+ padding: 1rem 1.5rem;
+ font-size: 1.1rem;
+}
+
+.btn-block {
+ width: 100%;
+}
+
+/* 输入组样式 */
+.input-group-prepend .input-group-text {
+ background-color: var(--secondary-pink);
+ border-color: var(--border-pink);
+ color: var(--dark-text);
+ border-radius: var(--border-radius-sm) 0 0 var(--border-radius-sm);
+}
+
+/* 聚焦效果 */
+.is-focused label {
+ color: var(--primary-pink);
+}
+
+/* ========== 动画效果 ========== */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.book-form-container {
+ animation: fadeIn 0.5s ease;
+}
+
+/* ========== 响应式样式 ========== */
+@media (max-width: 992px) {
+ .book-form-container {
+ padding: 1.5rem;
+ }
+
+ .page-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .actions {
+ margin-top: 1rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .book-form-container {
+ padding: 1rem;
+ }
+
+ .card-header, .card-body {
+ padding: 1rem;
+ }
+
+ .cover-preview {
+ min-height: 250px;
+ }
+
+ .col-md-8, .col-md-4 {
+ padding: 0 0.5rem;
+ }
+}
+
+.is-invalid {
+ border-color: #dc3545;
+}
+
+.is-valid {
+ border-color: #28a745;
+}
+
+.invalid-feedback {
+ display: none;
+ color: #dc3545;
+ font-size: 0.875rem;
+}
+
+.is-invalid ~ .invalid-feedback {
+ display: block;
+}
diff --git a/app/static/css/borrow_management.css b/app/static/css/borrow_management.css
new file mode 100644
index 0000000..45f3a51
--- /dev/null
+++ b/app/static/css/borrow_management.css
@@ -0,0 +1,520 @@
+/* borrow_management.css - Optimized for literary female audience */
+
+/* Main typography and colors */
+body {
+ font-family: 'Georgia', serif;
+ color: #4a3728;
+ background-color: #fcf8f3;
+}
+
+.page-title {
+ margin-bottom: 1.5rem;
+ color: #5d3511;
+ border-bottom: 2px solid #d9c7b8;
+ padding-bottom: 15px;
+ font-family: 'Playfair Display', Georgia, serif;
+ letter-spacing: 0.5px;
+ position: relative;
+}
+
+.page-title:after {
+ content: "❦";
+ position: absolute;
+ bottom: -12px;
+ left: 50%;
+ font-size: 18px;
+ color: #8d6e63;
+ background: #fcf8f3;
+ padding: 0 10px;
+ transform: translateX(-50%);
+}
+
+.container {
+ background-color: #fff9f5;
+ border-radius: 8px;
+ box-shadow: 0 3px 15px rgba(113, 66, 20, 0.1);
+ padding: 25px;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 1px solid #e8d9cb;
+}
+
+/* Tabs styling */
+.tabs {
+ display: flex;
+ border-bottom: 1px solid #d9c7b8;
+ margin-bottom: 25px;
+ position: relative;
+}
+
+.tabs:before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -3px;
+ height: 2px;
+ background: linear-gradient(to right, transparent, #8d6e63, transparent);
+}
+
+.tab {
+ padding: 12px 22px;
+ text-decoration: none;
+ color: #5d3511;
+ margin-right: 5px;
+ border-radius: 8px 8px 0 0;
+ position: relative;
+ transition: all 0.3s ease;
+ font-family: 'Georgia', serif;
+}
+
+.tab:hover {
+ background-color: #f1e6dd;
+ color: #704214;
+ text-decoration: none;
+}
+
+.tab.active {
+ background-color: #704214;
+ color: #f8f0e5;
+ font-weight: 500;
+}
+
+.tab.overdue-tab {
+ background-color: #f9e8e8;
+ color: #a15950;
+}
+
+.tab.overdue-tab:hover {
+ background-color: #f4d3d3;
+}
+
+/* 修改 count 样式,避免与 badge 冲突 */
+.count {
+ background-color: rgba(113, 66, 20, 0.15);
+ border-radius: 12px;
+ padding: 2px 10px;
+ font-size: 0.8em;
+ margin-left: 8px;
+ font-family: 'Arial', sans-serif;
+ display: inline-block;
+ position: static;
+ width: auto;
+ height: auto;
+}
+
+.tab.active .count {
+ background-color: rgba(255, 243, 224, 0.3);
+}
+
+.count.overdue-count {
+ background-color: rgba(161, 89, 80, 0.2);
+}
+
+/* Search and filters */
+.search-card {
+ margin-bottom: 25px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(113, 66, 20, 0.08);
+ border: 1px solid #e8d9cb;
+ background: linear-gradient(to bottom right, #fff, #fcf8f3);
+}
+
+.search-card .card-body {
+ padding: 20px;
+}
+
+.search-form {
+ margin-bottom: 0;
+}
+
+.form-control {
+ border: 1px solid #d9c7b8;
+ border-radius: 6px;
+ color: #5d3511;
+ background-color: #fff9f5;
+ transition: all 0.3s ease;
+ font-family: 'Georgia', serif;
+}
+
+.form-control:focus {
+ border-color: #704214;
+ box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.15);
+ background-color: #fff;
+}
+
+.btn-outline-secondary {
+ color: #704214;
+ border-color: #d9c7b8;
+ background-color: transparent;
+}
+
+.btn-outline-secondary:hover {
+ color: #fff;
+ background-color: #8d6e63;
+ border-color: #8d6e63;
+}
+
+.clear-filters {
+ display: block;
+ width: 100%;
+ text-align: center;
+ font-style: italic;
+}
+
+/* Table styling */
+.borrow-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ margin-bottom: 25px;
+ box-shadow: 0 2px 10px rgba(113, 66, 20, 0.05);
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid #e8d9cb;
+}
+
+.borrow-table th,
+.borrow-table td {
+ padding: 15px 18px;
+ text-align: left;
+ border-bottom: 1px solid #e8d9cb;
+ vertical-align: middle;
+}
+
+/* 调整借阅用户列向左偏移15px */
+.borrow-table th:nth-child(3),
+.borrow-table td:nth-child(3) {
+ padding-right: 3px;
+}
+
+.borrow-table th {
+ background-color: #f1e6dd;
+ color: #5d3511;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+/* 状态列调整 - 居中并确保内容显示 */
+.borrow-table th:nth-child(6) {
+ text-align: center;
+}
+
+.borrow-table td:nth-child(6) {
+ text-align: center;
+}
+
+.borrow-item:hover {
+ background-color: #f8f0e5;
+}
+
+.borrow-item:last-child td {
+ border-bottom: none;
+}
+
+.book-cover img {
+ width: 65px;
+ height: 90px;
+ object-fit: cover;
+ border-radius: 6px;
+ box-shadow: 0 3px 8px rgba(113, 66, 20, 0.15);
+ border: 2px solid #fff;
+ transition: transform 0.3s ease;
+}
+
+.book-cover img:hover {
+ transform: scale(1.05);
+}
+
+.book-title {
+ font-weight: 600;
+ font-family: 'Georgia', serif;
+}
+
+.book-title a {
+ color: #5d3511;
+ text-decoration: none;
+ transition: color 0.3s ease;
+}
+
+.book-title a:hover {
+ color: #a66321;
+ text-decoration: underline;
+}
+
+.book-author {
+ color: #8d6e63;
+ font-size: 0.9em;
+ margin-top: 5px;
+ font-style: italic;
+}
+
+/* 修改借阅用户显示方式 */
+.user-info {
+ text-align: center;
+ display: table-cell;
+ vertical-align: middle;
+ height: 100%;
+}
+
+.user-info a {
+ color: #5d3511;
+ text-decoration: none;
+ font-weight: 600;
+ transition: color 0.3s ease;
+ display: block;
+ margin-bottom: 8px;
+}
+
+.user-info a:hover {
+ color: #a66321;
+ text-decoration: underline;
+}
+
+.user-nickname {
+ color: #8d6e63;
+ font-size: 0.9em;
+ display: block;
+ margin-top: 0;
+}
+
+/* Badges and status indicators - 修复显示问题 */
+.badge {
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-weight: 500;
+ font-size: 0.85em;
+ letter-spacing: 0.5px;
+ display: inline-block;
+ margin-bottom: 5px;
+ position: static;
+ width: auto;
+ height: auto;
+ top: auto;
+ right: auto;
+}
+
+/* 给状态列的徽章额外的特异性 */
+.borrow-table td .badge {
+ position: static;
+ width: auto;
+ height: auto;
+ display: inline-block;
+ font-size: 0.85em;
+ border-radius: 20px;
+ padding: 5px 12px;
+}
+
+.badge-primary {
+ background-color: #704214;
+ color: white;
+}
+
+.badge-success {
+ background-color: #5b8a72;
+ color: white;
+}
+
+.badge-danger {
+ background-color: #a15950;
+ color: white;
+}
+
+.badge-info {
+ background-color: #6a8da9;
+ color: white;
+}
+
+.badge-warning {
+ background-color: #d4a76a;
+ color: #4a3728;
+}
+
+.return-date {
+ color: #8d6e63;
+ font-size: 0.9em;
+ margin-top: 5px;
+}
+
+/* 确保状态显示正确 */
+.borrow-item td:nth-child(6) span.badge {
+ min-width: 80px;
+}
+
+/* Buttons */
+.btn {
+ border-radius: 20px;
+ padding: 8px 16px;
+ transition: all 0.3s ease;
+ font-family: 'Georgia', serif;
+ letter-spacing: 0.5px;
+}
+
+.btn-primary {
+ background-color: #704214;
+ border-color: #704214;
+}
+
+.btn-primary:hover, .btn-primary:focus {
+ background-color: #5d3511;
+ border-color: #5d3511;
+ box-shadow: 0 0 0 0.2rem rgba(113, 66, 20, 0.25);
+}
+
+.btn-success {
+ background-color: #5b8a72;
+ border-color: #5b8a72;
+}
+
+.btn-success:hover, .btn-success:focus {
+ background-color: #4a7561;
+ border-color: #4a7561;
+}
+
+.btn-warning {
+ background-color: #d4a76a;
+ border-color: #d4a76a;
+ color: #4a3728;
+}
+
+.btn-warning:hover, .btn-warning:focus {
+ background-color: #c29355;
+ border-color: #c29355;
+ color: #4a3728;
+}
+
+.actions .btn {
+ margin-right: 5px;
+ margin-bottom: 6px;
+}
+
+.text-danger {
+ color: #a15950 !important;
+}
+
+.overdue {
+ background-color: rgba(161, 89, 80, 0.05);
+}
+
+/* Empty states */
+.no-records {
+ text-align: center;
+ padding: 60px 20px;
+ background-color: #f8f0e5;
+ border-radius: 8px;
+ margin: 25px 0;
+ border: 1px dashed #d9c7b8;
+}
+
+.empty-icon {
+ font-size: 4.5em;
+ color: #d9c7b8;
+ margin-bottom: 25px;
+}
+
+.empty-text {
+ color: #8d6e63;
+ margin-bottom: 25px;
+ font-style: italic;
+ font-size: 1.1em;
+}
+
+/* Pagination */
+.pagination-container {
+ display: flex;
+ justify-content: center;
+ margin-top: 25px;
+}
+
+.pagination .page-link {
+ color: #5d3511;
+ border-color: #e8d9cb;
+ margin: 0 3px;
+ border-radius: 4px;
+}
+
+.pagination .page-item.active .page-link {
+ background-color: #704214;
+ border-color: #704214;
+}
+
+.pagination .page-link:hover {
+ background-color: #f1e6dd;
+ color: #5d3511;
+}
+
+/* Modal customization */
+.modal-content {
+ border-radius: 8px;
+ border: 1px solid #e8d9cb;
+ box-shadow: 0 5px 20px rgba(113, 66, 20, 0.15);
+ background-color: #fff9f5;
+}
+
+.modal-header {
+ border-bottom: 1px solid #e8d9cb;
+ background-color: #f1e6dd;
+ border-radius: 8px 8px 0 0;
+}
+
+.modal-title {
+ color: #5d3511;
+ font-family: 'Georgia', serif;
+}
+
+.modal-footer {
+ border-top: 1px solid #e8d9cb;
+}
+
+/* Decorative elements */
+.container:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 150px;
+ height: 150px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cpath fill='%23d9c7b8' fill-opacity='0.2' d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/svg%3E");
+ opacity: 0.3;
+ pointer-events: none;
+ z-index: -1;
+}
+
+/* Responsive design */
+@media (max-width: 992px) {
+ .tabs {
+ flex-wrap: wrap;
+ }
+
+ .tab {
+ margin-bottom: 8px;
+ }
+}
+
+@media (max-width: 768px) {
+ .tabs {
+ flex-direction: column;
+ border-bottom: none;
+ }
+
+ .tab {
+ border-radius: 8px;
+ margin-right: 0;
+ margin-bottom: 8px;
+ border: 1px solid #d9c7b8;
+ }
+
+ .borrow-table {
+ display: block;
+ overflow-x: auto;
+ }
+
+ .book-cover img {
+ width: 50px;
+ height: 70px;
+ }
+
+ .search-card .row {
+ margin-bottom: 15px;
+ }
+}
diff --git a/app/static/css/browse.css b/app/static/css/browse.css
new file mode 100644
index 0000000..aa89546
--- /dev/null
+++ b/app/static/css/browse.css
@@ -0,0 +1,860 @@
+/* 图书浏览页面样式 */
+
+/* 全局容器 */
+.browse-container {
+ padding: 24px;
+ background-color: #f6f9fc;
+ min-height: calc(100vh - 60px);
+ position: relative;
+ overflow: hidden;
+}
+
+/* 装饰气泡 */
+.bubble {
+ position: absolute;
+ bottom: -50px;
+ background-color: rgba(221, 236, 255, 0.4);
+ border-radius: 50%;
+ z-index: 1;
+ animation: bubble 25s infinite ease-in;
+}
+
+@keyframes bubble {
+ 0% {
+ transform: translateY(100%) scale(0);
+ opacity: 0;
+ }
+ 50% {
+ opacity: 0.6;
+ }
+ 100% {
+ transform: translateY(-100vh) scale(1);
+ opacity: 0;
+ }
+}
+
+/* 为页面添加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 {
+ margin-bottom: 25px;
+ position: relative;
+ z-index: 2;
+ text-align: center;
+}
+
+.page-header h1 {
+ color: #3c4858;
+ font-size: 2.2rem;
+ font-weight: 700;
+ margin: 0;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.welcome-text {
+ margin-top: 10px;
+ color: #8492a6;
+ font-size: 1.1rem;
+}
+
+.welcome-text strong {
+ color: #764ba2;
+}
+
+/* 过滤和搜索部分 */
+.filter-section {
+ margin-bottom: 25px;
+ padding: 20px;
+ background-color: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
+ position: relative;
+ z-index: 2;
+}
+
+.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;
+ margin: 0 auto;
+}
+
+.search-group .form-control {
+ border: 1px solid #e4e7eb;
+ border-right: none;
+ border-radius: 25px 0 0 25px;
+ padding: 10px 20px;
+ height: 46px;
+ font-size: 1rem;
+ background-color: #f7fafc;
+ box-shadow: none;
+ transition: all 0.3s;
+ flex: 1;
+}
+
+.search-group .form-control:focus {
+ outline: none;
+ border-color: #a3bffa;
+ background-color: #ffffff;
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.25);
+}
+
+.search-group .btn {
+ border-radius: 0 25px 25px 0;
+ width: 46px;
+ height: 46px;
+ min-width: 46px;
+ padding: 0;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: -1px;
+ font-size: 1.1rem;
+ box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
+ transition: all 0.3s;
+ border: none;
+}
+
+.search-group .btn:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08);
+}
+
+.filter-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ width: 100%;
+ align-items: center;
+}
+
+.category-filters {
+ position: relative;
+ flex: 2;
+ min-width: 180px;
+}
+
+.category-filter-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 20px;
+ width: 100%;
+ height: 42px;
+ background: #f7fafc;
+ border: 1px solid #e4e7eb;
+ border-radius: 25px;
+ cursor: pointer;
+ font-size: 0.95rem;
+ color: #3c4858;
+ transition: all 0.3s;
+}
+
+.category-filter-toggle:hover {
+ background: #edf2f7;
+}
+
+.category-filter-toggle i.fa-chevron-down {
+ margin-left: 8px;
+ transition: transform 0.3s;
+}
+
+.category-filter-toggle.active i.fa-chevron-down {
+ transform: rotate(180deg);
+}
+
+.category-filter-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ margin-top: 8px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08);
+ padding: 10px;
+ z-index: 100;
+ display: none;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.category-filter-dropdown.show {
+ display: block;
+ animation: fadeIn 0.2s;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.category-item {
+ display: flex;
+ align-items: center;
+ padding: 10px 15px;
+ color: #3c4858;
+ border-radius: 6px;
+ text-decoration: none;
+ margin-bottom: 5px;
+ transition: all 0.2s;
+}
+
+.category-item:hover {
+ background: #f7fafc;
+ color: #667eea;
+}
+
+.category-item.active {
+ background: #ebf4ff;
+ color: #667eea;
+ font-weight: 500;
+}
+
+.category-item i {
+ margin-right: 10px;
+}
+
+.filter-group {
+ flex: 1;
+ min-width: 130px;
+}
+
+.filter-section .form-control {
+ border: 1px solid #e4e7eb;
+ border-radius: 25px;
+ height: 42px;
+ padding: 10px 20px;
+ background-color: #f7fafc;
+ 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='%23667eea' 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;
+ width: 100%;
+ transition: all 0.3s;
+}
+
+.filter-section .form-control:focus {
+ outline: none;
+ border-color: #a3bffa;
+ background-color: #ffffff;
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.25);
+}
+
+/* 图书统计显示 */
+.browse-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-bottom: 25px;
+ align-items: center;
+}
+
+.stat-item {
+ display: flex;
+ align-items: center;
+ background: white;
+ padding: 12px 20px;
+ border-radius: 12px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ flex: 1;
+ min-width: 160px;
+ max-width: 240px;
+}
+
+.stat-item i {
+ font-size: 24px;
+ color: #667eea;
+ margin-right: 15px;
+ background: #ebf4ff;
+ width: 45px;
+ height: 45px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+}
+
+.stat-content {
+ display: flex;
+ flex-direction: column;
+}
+
+.stat-value {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #3c4858;
+}
+
+.stat-label {
+ font-size: 0.875rem;
+ color: #8492a6;
+}
+
+.search-results {
+ flex: 2;
+ padding: 12px 20px;
+ background: #ebf4ff;
+ border-radius: 12px;
+ color: #667eea;
+ font-weight: 500;
+ text-align: center;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+}
+
+/* 图书网格布局 */
+.books-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 25px;
+ margin-bottom: 40px;
+ position: relative;
+ z-index: 2;
+}
+
+/* 图书卡片样式 */
+.book-card {
+ border-radius: 10px;
+ overflow: hidden;
+ background-color: white;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ opacity: 0;
+ transform: translateY(20px);
+ animation: fadeInUp 0.5s forwards;
+}
+
+@keyframes fadeInUp {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.books-grid .book-card:nth-child(1) { animation-delay: 0.1s; }
+.books-grid .book-card:nth-child(2) { animation-delay: 0.15s; }
+.books-grid .book-card:nth-child(3) { animation-delay: 0.2s; }
+.books-grid .book-card:nth-child(4) { animation-delay: 0.25s; }
+.books-grid .book-card:nth-child(5) { animation-delay: 0.3s; }
+.books-grid .book-card:nth-child(6) { animation-delay: 0.35s; }
+.books-grid .book-card:nth-child(7) { animation-delay: 0.4s; }
+.books-grid .book-card:nth-child(8) { animation-delay: 0.45s; }
+.books-grid .book-card:nth-child(9) { animation-delay: 0.5s; }
+.books-grid .book-card:nth-child(10) { animation-delay: 0.55s; }
+.books-grid .book-card:nth-child(11) { animation-delay: 0.6s; }
+.books-grid .book-card:nth-child(12) { animation-delay: 0.65s; }
+
+.book-card:hover {
+ transform: translateY(-8px);
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.12);
+}
+
+.book-cover {
+ height: 240px;
+ position: relative;
+ overflow: hidden;
+}
+
+.book-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.5s ease;
+}
+
+.book-card:hover .book-cover img {
+ transform: scale(1.08);
+}
+
+.cover-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(to bottom, rgba(0,0,0,0) 50%, rgba(0,0,0,0.5) 100%);
+ z-index: 1;
+}
+
+.book-ribbon {
+ position: absolute;
+ top: 10px;
+ right: -30px;
+ transform: rotate(45deg);
+ width: 120px;
+ text-align: center;
+ z-index: 2;
+}
+
+.book-ribbon span {
+ display: block;
+ width: 100%;
+ padding: 5px 0;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.book-ribbon .available {
+ background-color: #4caf50;
+ color: white;
+}
+
+.book-ribbon .unavailable {
+ background-color: #f44336;
+ color: white;
+}
+
+.no-cover {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ background: linear-gradient(135deg, #f6f9fc 0%, #e9ecef 100%);
+ color: #8492a6;
+}
+
+.no-cover i {
+ font-size: 40px;
+ margin-bottom: 10px;
+}
+
+.book-info {
+ padding: 20px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.book-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #3c4858;
+ margin: 0 0 8px;
+ 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: #8492a6;
+ margin-bottom: 15px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.book-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 20px;
+}
+
+.book-category {
+ padding: 5px 10px;
+ background-color: #ebf4ff;
+ color: #667eea;
+ border-radius: 20px;
+ font-size: 0.75rem;
+}
+
+.book-year {
+ padding: 5px 10px;
+ background-color: #f7fafc;
+ color: #8492a6;
+ border-radius: 20px;
+ font-size: 0.75rem;
+}
+
+.book-actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ margin-top: auto;
+}
+
+.book-actions a, .book-actions button {
+ padding: 10px 12px;
+ border-radius: 8px;
+ text-align: center;
+ text-decoration: none;
+ font-size: 0.9rem;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ transition: all 0.3s;
+}
+
+.btn-detail {
+ background-color: #e9ecef;
+ color: #3c4858;
+}
+
+.btn-detail:hover {
+ background-color: #dee2e6;
+ color: #2d3748;
+}
+
+.btn-borrow {
+ background-color: #667eea;
+ color: white;
+}
+
+.btn-borrow:hover {
+ background-color: #5a67d8;
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 10px rgba(102, 126, 234, 0.4);
+}
+
+.btn-borrow.disabled {
+ background-color: #cbd5e0;
+ color: #718096;
+ cursor: not-allowed;
+}
+
+/* 无图书状态 */
+.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;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.no-books-img {
+ max-width: 200px;
+ margin-bottom: 20px;
+}
+
+.no-books h3 {
+ font-size: 1.25rem;
+ color: #3c4858;
+ margin: 0 0 10px;
+}
+
+.no-books p {
+ font-size: 1rem;
+ color: #8492a6;
+ margin-bottom: 20px;
+}
+
+.btn-reset-search {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 20px;
+ background-color: #667eea;
+ color: white;
+ border-radius: 8px;
+ text-decoration: none;
+ font-weight: 500;
+ transition: all 0.3s;
+}
+
+.btn-reset-search:hover {
+ background-color: #5a67d8;
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 10px rgba(102, 126, 234, 0.4);
+}
+
+/* 分页容器 */
+.pagination-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 30px;
+ margin-bottom: 20px;
+ 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: #3c4858;
+ font-weight: 500;
+ transition: all 0.2s;
+ text-decoration: none;
+}
+
+.pagination .page-link:hover {
+ color: #667eea;
+ background-color: #f7fafc;
+}
+
+.pagination .page-item.active .page-link {
+ background-color: #667eea;
+ color: white;
+ box-shadow: none;
+}
+
+.pagination .page-item.disabled .page-link {
+ color: #cbd5e0;
+ background-color: #f7fafc;
+ cursor: not-allowed;
+}
+
+.pagination-info {
+ color: #8492a6;
+ font-size: 0.9rem;
+}
+
+/* 模态框样式优化 */
+.modal-content {
+ border-radius: 16px;
+ 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: #f7fafc;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.modal-title {
+ color: #3c4858;
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+.modal-body {
+ padding: 25px;
+}
+
+.modal-footer {
+ padding: 15px 25px;
+ border-top: 1px solid #e2e8f0;
+ background-color: #f7fafc;
+}
+
+.modal-info {
+ margin-top: 10px;
+ padding: 12px 16px;
+ background-color: #ebf8ff;
+ border-left: 4px solid #4299e1;
+ color: #2b6cb0;
+ font-size: 0.9rem;
+ border-radius: 4px;
+}
+
+.modal .close {
+ font-size: 1.5rem;
+ color: #a0aec0;
+ opacity: 0.8;
+ text-shadow: none;
+ transition: all 0.2s;
+}
+
+.modal .close:hover {
+ opacity: 1;
+ color: #667eea;
+}
+
+.modal .btn {
+ border-radius: 8px;
+ padding: 10px 20px;
+ font-weight: 500;
+ transition: all 0.3s;
+}
+
+.modal .btn-secondary {
+ background-color: #e2e8f0;
+ color: #4a5568;
+ border: none;
+}
+
+.modal .btn-secondary:hover {
+ background-color: #cbd5e0;
+ color: #2d3748;
+}
+
+.modal .btn-primary {
+ background-color: #667eea;
+ color: white;
+ border: none;
+}
+
+.modal .btn-primary:hover {
+ background-color: #5a67d8;
+ box-shadow: 0 5px 10px rgba(102, 126, 234, 0.4);
+}
+
+/* 响应式调整 */
+@media (max-width: 992px) {
+ .filter-row {
+ flex-wrap: wrap;
+ }
+
+ .category-filters {
+ flex: 1 0 100%;
+ margin-bottom: 10px;
+ }
+
+ .filter-group {
+ flex: 1 0 180px;
+ }
+}
+
+@media (max-width: 768px) {
+ .browse-container {
+ padding: 16px;
+ }
+
+ .page-header {
+ text-align: left;
+ }
+
+ .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: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 16px;
+ }
+
+ .stat-item {
+ min-width: 130px;
+ padding: 10px;
+ }
+
+ .stat-item i {
+ width: 35px;
+ height: 35px;
+ font-size: 18px;
+ }
+
+ .search-results {
+ padding: 10px;
+ font-size: 0.9rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .books-grid {
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ }
+
+ .book-cover {
+ height: 180px;
+ }
+
+ .book-info {
+ padding: 12px;
+ }
+
+ .book-title {
+ font-size: 0.9rem;
+ }
+
+ .book-author {
+ font-size: 0.8rem;
+ }
+
+ .book-actions {
+ grid-template-columns: 1fr;
+ gap: 8px;
+ }
+
+ .browse-stats {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .stat-item {
+ max-width: none;
+ }
+
+ .search-results {
+ width: 100%;
+ }
+}
diff --git a/app/static/css/inventory-adjust.css b/app/static/css/inventory-adjust.css
new file mode 100644
index 0000000..a5f2b40
--- /dev/null
+++ b/app/static/css/inventory-adjust.css
@@ -0,0 +1,461 @@
+/* 迪士尼主题库存管理页面样式 */
+
+/* 基础样式 */
+body {
+ background-color: #f9f7ff;
+ font-family: 'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif;
+ color: #3d4c65;
+}
+
+/* 迪士尼风格卡片 */
+.disney-inventory-card {
+ border: none;
+ border-radius: 20px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
+ background-color: #ffffff;
+ margin-bottom: 40px;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ padding: 2px;
+ border: 3px solid #f0e6fa;
+}
+
+.disney-inventory-card:hover {
+ box-shadow: 0 15px 30px rgba(110, 125, 249, 0.2);
+ transform: translateY(-5px);
+}
+
+/* 迪士尼装饰元素 */
+.disney-decoration {
+ position: absolute;
+ width: 60px;
+ height: 60px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ opacity: 0.8;
+ z-index: 1;
+}
+
+.top-left {
+ top: 10px;
+ left: 10px;
+ background-image: url('https://i.imgur.com/Vyo9IF4.png'); /* 替换为迪士尼星星图标URL */
+ transform: rotate(-15deg);
+}
+
+.top-right {
+ top: 10px;
+ right: 10px;
+ background-image: url('https://i.imgur.com/pLRUYhb.png'); /* 替换为迪士尼魔法棒图标URL */
+ transform: rotate(15deg);
+}
+
+.bottom-left {
+ bottom: 10px;
+ left: 10px;
+ background-image: url('https://i.imgur.com/KkMfwWv.png'); /* 替换为迪士尼城堡图标URL */
+ transform: rotate(-5deg);
+}
+
+.bottom-right {
+ bottom: 10px;
+ right: 10px;
+ background-image: url('https://i.imgur.com/TcA6PL2.png'); /* 替换为迪士尼皇冠图标URL */
+ transform: rotate(5deg);
+}
+
+/* 米奇耳朵标题装饰 */
+.card-header-disney {
+ background: linear-gradient(45deg, #e4c1f9, #d4a5ff);
+ color: #512b81;
+ padding: 1.8rem 1.5rem 1.5rem;
+ font-weight: 600;
+ border-radius: 18px 18px 0 0;
+ text-align: center;
+ position: relative;
+ z-index: 2;
+}
+
+.mickey-ears {
+ position: absolute;
+ top: -25px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 80px;
+ height: 40px;
+ background-image: url('https://i.imgur.com/pCPQoZx.png'); /* 替换为米奇耳朵图标URL */
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+/* 卡片内容 */
+.card-body-disney {
+ padding: 2.5rem;
+ background-color: #ffffff;
+ border-radius: 0 0 18px 18px;
+ position: relative;
+ z-index: 2;
+}
+
+/* 书籍封面 */
+.book-cover-container {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+}
+
+.book-cover {
+ max-height: 300px;
+ width: auto;
+ object-fit: contain;
+ border-radius: 12px;
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease;
+ position: relative;
+ z-index: 2;
+ border: 3px solid #f9f0ff;
+}
+
+.book-cover:hover {
+ transform: scale(1.03);
+}
+
+.disney-sparkles {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-image: url('https://i.imgur.com/8vZuwlG.png'); /* 替换为迪士尼闪光效果URL */
+ background-size: 200px;
+ background-repeat: no-repeat;
+ background-position: center;
+ opacity: 0;
+ transition: opacity 0.5s ease;
+ pointer-events: none;
+}
+
+.book-cover:hover + .disney-sparkles {
+ opacity: 0.7;
+}
+
+/* 书籍详情 */
+.book-details {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.book-title {
+ color: #5e35b1;
+ font-weight: 700;
+ margin-bottom: 1.8rem;
+ font-size: 1.8rem;
+ border-bottom: 3px dotted #e1bee7;
+ padding-bottom: 1rem;
+}
+
+.book-info {
+ font-size: 1.05rem;
+ color: #424242;
+}
+
+.book-info p {
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+}
+
+/* 迪士尼图标 */
+.disney-icon {
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ margin-right: 10px;
+ flex-shrink: 0;
+}
+
+.author-icon {
+ background-image: url('https://i.imgur.com/2K5qpgQ.png'); /* 替换为米妮图标URL */
+}
+
+.publisher-icon {
+ background-image: url('https://i.imgur.com/YKhKVT7.png'); /* 替换为唐老鸭图标URL */
+}
+
+.isbn-icon {
+ background-image: url('https://i.imgur.com/ioaQTBM.png'); /* 替换为高飞图标URL */
+}
+
+.inventory-icon {
+ background-image: url('https://i.imgur.com/D0jRTKX.png'); /* 替换为奇奇蒂蒂图标URL */
+}
+
+.type-icon {
+ background-image: url('https://i.imgur.com/xgQriQn.png'); /* 替换为米奇图标URL */
+}
+
+.amount-icon {
+ background-image: url('https://i.imgur.com/ioaQTBM.png'); /* 替换为高飞图标URL */
+}
+
+.remark-icon {
+ background-image: url('https://i.imgur.com/2K5qpgQ.png'); /* 替换为米妮图标URL */
+}
+
+/* 库存状态标签 */
+.stock-badge {
+ display: inline-block;
+ padding: 0.35em 0.9em;
+ border-radius: 50px;
+ font-weight: 600;
+ margin-left: 8px;
+ font-size: 0.9rem;
+}
+
+.high-stock {
+ background-color: #e0f7fa;
+ color: #0097a7;
+ border: 2px solid #80deea;
+}
+
+.low-stock {
+ background-color: #fff8e1;
+ color: #ff8f00;
+ border: 2px solid #ffe082;
+}
+
+.out-stock {
+ background-color: #ffebee;
+ color: #c62828;
+ border: 2px solid #ef9a9a;
+}
+
+/* 表单容器 */
+.form-container {
+ background-color: #f8f4ff;
+ padding: 2rem;
+ border-radius: 15px;
+ margin-top: 2rem;
+ border: 2px dashed #d1c4e9;
+ position: relative;
+}
+
+.form-group {
+ position: relative;
+}
+
+/* 表单标签 */
+.disney-label {
+ color: #5e35b1;
+ font-weight: 600;
+ margin-bottom: 0.8rem;
+ display: flex;
+ align-items: center;
+ font-size: 1.1rem;
+}
+
+/* 自定义表单控件 */
+.disney-select,
+.disney-input,
+.disney-textarea {
+ display: block;
+ width: 100%;
+ padding: 0.8rem 1rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #495057;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 2px solid #d1c4e9;
+ border-radius: 12px;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.disney-select:focus,
+.disney-input:focus,
+.disney-textarea:focus {
+ border-color: #9575cd;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(149, 117, 205, 0.25);
+}
+
+/* 确保下拉菜单选项可见 */
+.disney-select option {
+ background-color: #fff;
+ color: #495057;
+ padding: 8px;
+}
+
+/* 库存提示 */
+.stock-hint {
+ color: #757575;
+ font-size: 0.95rem;
+ margin-top: 0.6rem;
+ font-weight: 500;
+}
+
+.stock-hint.warning {
+ color: #ff8f00;
+ font-weight: bold;
+}
+
+.stock-hint.danger {
+ color: #c62828;
+ font-weight: bold;
+}
+
+/* 按钮样式 */
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15px;
+ margin-top: 2rem;
+}
+
+.btn {
+ padding: 0.7rem 2rem;
+ border-radius: 50px;
+ font-weight: 600;
+ font-size: 1rem;
+ letter-spacing: 0.5px;
+ display: inline-block;
+ text-align: center;
+ vertical-align: middle;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.disney-cancel-btn {
+ background-color: #f3e5f5;
+ color: #6a1b9a;
+ border: 2px solid #ce93d8;
+}
+
+.disney-cancel-btn:hover {
+ background-color: #e1bee7;
+ color: #4a148c;
+ transform: translateY(-3px);
+}
+
+.disney-confirm-btn {
+ background: linear-gradient(45deg, #7e57c2, #5e35b1);
+ color: white;
+ border: none;
+}
+
+.disney-confirm-btn:hover {
+ background: linear-gradient(45deg, #673ab7, #4527a0);
+ transform: translateY(-3px);
+ box-shadow: 0 7px 15px rgba(103, 58, 183, 0.3);
+}
+
+.disney-confirm-btn:before {
+ content: "";
+ position: absolute;
+ top: -10px;
+ left: -20px;
+ width: 40px;
+ height: 40px;
+ background-image: url('https://i.imgur.com/8vZuwlG.png'); /* 替换为迪士尼魔法效果URL */
+ background-size: contain;
+ background-repeat: no-repeat;
+ opacity: 0;
+ transition: all 0.5s ease;
+ transform: scale(0.5);
+}
+
+.disney-confirm-btn:hover:before {
+ opacity: 0.8;
+ transform: scale(1) rotate(45deg);
+ top: -5px;
+ left: 10px;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+ .book-cover-container {
+ margin-bottom: 30px;
+ }
+
+ .book-cover {
+ max-height: 250px;
+ }
+
+ .book-title {
+ text-align: center;
+ font-size: 1.5rem;
+ }
+
+ .disney-decoration {
+ width: 40px;
+ height: 40px;
+ }
+
+ .button-group {
+ flex-direction: column;
+ }
+
+ .btn {
+ width: 100%;
+ margin-bottom: 10px;
+ }
+
+ .card-header-disney,
+ .card-body-disney {
+ padding: 1.5rem;
+ }
+}
+
+/* 表单元素聚焦效果 */
+.form-group.focused {
+ transform: translateY(-3px);
+}
+
+.form-group.focused .disney-label {
+ color: #7e57c2;
+}
+
+/* 提交动画 */
+.disney-inventory-card.submitting {
+ animation: submitPulse 1s ease;
+}
+
+@keyframes submitPulse {
+ 0% { transform: scale(1); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); }
+ 50% { transform: scale(1.02); box-shadow: 0 15px 35px rgba(126, 87, 194, 0.3); }
+ 100% { transform: scale(1); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); }
+}
+
+/* 确认按钮动画 */
+.disney-confirm-btn.active {
+ animation: btnPulse 0.3s ease;
+}
+
+@keyframes btnPulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+ 100% { transform: scale(1); }
+}
+
+/* 表单过渡效果 */
+.form-group {
+ transition: transform 0.3s ease;
+}
+
+.disney-select,
+.disney-input,
+.disney-textarea {
+ transition: all 0.3s ease;
+}
+
+/* 闪光效果持续时间 */
+.disney-sparkles {
+ transition: opacity 0.8s ease;
+}
diff --git a/app/static/css/inventory-book-logs.css b/app/static/css/inventory-book-logs.css
new file mode 100644
index 0000000..e04a806
--- /dev/null
+++ b/app/static/css/inventory-book-logs.css
@@ -0,0 +1,715 @@
+/* 冰雪奇缘主题库存日志页面样式 */
+
+/* 基础背景与字体 */
+body {
+ font-family: 'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif;
+ background-color: #e6f2ff;
+ color: #2c3e50;
+}
+
+/* 冰雪背景 */
+.frozen-background {
+ position: relative;
+ min-height: 100vh;
+ padding: 30px 0 50px;
+ background: linear-gradient(135deg, #e4f1fe, #d4e6fb, #c9e0ff);
+ overflow: hidden;
+}
+
+/* 雪花效果 */
+.snowflakes {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 1;
+}
+
+.snowflake {
+ position: absolute;
+ color: #fff;
+ font-size: 1.5em;
+ opacity: 0.8;
+ top: -20px;
+ animation: snowfall linear infinite;
+}
+
+.snowflake:nth-child(1) { left: 10%; animation-duration: 15s; animation-delay: 0s; }
+.snowflake:nth-child(2) { left: 20%; animation-duration: 12s; animation-delay: 1s; }
+.snowflake:nth-child(3) { left: 30%; animation-duration: 13s; animation-delay: 2s; }
+.snowflake:nth-child(4) { left: 40%; animation-duration: 10s; animation-delay: 0s; }
+.snowflake:nth-child(5) { left: 50%; animation-duration: 16s; animation-delay: 3s; }
+.snowflake:nth-child(6) { left: 60%; animation-duration: 14s; animation-delay: 1s; }
+.snowflake:nth-child(7) { left: 70%; animation-duration: 12s; animation-delay: 0s; }
+.snowflake:nth-child(8) { left: 80%; animation-duration: 15s; animation-delay: 2s; }
+.snowflake:nth-child(9) { left: 90%; animation-duration: 13s; animation-delay: 1s; }
+.snowflake:nth-child(10) { left: 95%; animation-duration: 14s; animation-delay: 3s; }
+
+@keyframes snowfall {
+ 0% {
+ transform: translateY(0) rotate(0deg);
+ }
+ 100% {
+ transform: translateY(100vh) rotate(360deg);
+ }
+}
+
+/* 冰雪主题卡片 */
+.frozen-card {
+ position: relative;
+ background-color: rgba(255, 255, 255, 0.85);
+ border-radius: 20px;
+ box-shadow: 0 10px 30px rgba(79, 149, 255, 0.2);
+ backdrop-filter: blur(10px);
+ border: 2px solid #e1f0ff;
+ margin-bottom: 40px;
+ overflow: hidden;
+ z-index: 2;
+}
+
+/* 城堡装饰 */
+.castle-decoration {
+ position: absolute;
+ top: -40px;
+ right: 30px;
+ width: 120px;
+ height: 120px;
+ background-image: url('https://i.imgur.com/KkMfwWv.png');
+ background-size: contain;
+ background-repeat: no-repeat;
+ opacity: 0.6;
+ z-index: 1;
+ transform: rotate(10deg);
+ filter: hue-rotate(190deg);
+}
+
+/* 卡片标题栏 */
+.card-header-frozen {
+ background: linear-gradient(45deg, #7AB6FF, #94C5FF);
+ color: #fff;
+ padding: 1.5rem;
+ border-radius: 18px 18px 0 0;
+ text-align: center;
+ position: relative;
+ text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.card-header-frozen h4 {
+ font-weight: 700;
+ margin: 0;
+ font-size: 1.6rem;
+ z-index: 1;
+}
+
+.card-header-frozen i {
+ margin-right: 10px;
+}
+
+/* 冰晶装饰 */
+.ice-crystal {
+ position: absolute;
+ width: 50px;
+ height: 50px;
+ background-image: url('https://i.imgur.com/8vZuwlG.png');
+ background-size: contain;
+ background-repeat: no-repeat;
+ filter: brightness(1.2) hue-rotate(190deg);
+}
+
+.ice-crystal.left {
+ left: 20px;
+ transform: rotate(-30deg) scale(0.8);
+}
+
+.ice-crystal.right {
+ right: 20px;
+ transform: rotate(30deg) scale(0.8);
+}
+
+/* 卡片内容区 */
+.card-body-frozen {
+ padding: 2.5rem;
+ position: relative;
+ z-index: 2;
+}
+
+/* 书籍基本信息区域 */
+.book-info-row {
+ background: linear-gradient(to right, rgba(232, 244, 255, 0.7), rgba(216, 234, 255, 0.4));
+ border-radius: 15px;
+ padding: 20px;
+ margin-bottom: 30px !important;
+ box-shadow: 0 5px 15px rgba(79, 149, 255, 0.1);
+ position: relative;
+ overflow: hidden;
+}
+
+/* 书籍封面 */
+.book-cover-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.book-frame {
+ position: relative;
+ padding: 10px;
+ background-color: white;
+ border-radius: 10px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+ transform: rotate(-3deg);
+ transition: transform 0.5s ease;
+ z-index: 1;
+}
+
+.book-frame:hover {
+ transform: rotate(0deg) scale(1.05);
+}
+
+.book-cover {
+ max-height: 250px;
+ width: auto;
+ object-fit: contain;
+ border-radius: 5px;
+ transform: rotate(3deg);
+ transition: transform 0.5s ease;
+}
+
+.book-frame:hover .book-cover {
+ transform: rotate(0deg);
+}
+
+.book-glow {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle at 50% 50%, rgba(173, 216, 230, 0.4), rgba(173, 216, 230, 0) 70%);
+ opacity: 0;
+ transition: opacity 0.5s ease;
+ pointer-events: none;
+}
+
+.book-frame:hover .book-glow {
+ opacity: 1;
+}
+
+/* 书籍详情 */
+.book-details {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.book-title {
+ color: #4169e1;
+ font-weight: 700;
+ margin-bottom: 20px;
+ font-size: 1.8rem;
+ position: relative;
+ display: inline-block;
+}
+
+.book-title::after {
+ content: "";
+ position: absolute;
+ bottom: -10px;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background: linear-gradient(to right, #7AB6FF, transparent);
+ border-radius: 3px;
+}
+
+.book-info {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+}
+
+.info-item {
+ margin: 0;
+ display: flex;
+ align-items: center;
+ font-size: 1.1rem;
+ color: #34495e;
+}
+
+.info-item i {
+ color: #7AB6FF;
+ margin-right: 10px;
+ font-size: 1.2rem;
+ width: 24px;
+ text-align: center;
+}
+
+/* 库存标签 */
+.frozen-badge {
+ display: inline-block;
+ padding: 0.35em 0.9em;
+ border-radius: 50px;
+ font-weight: 600;
+ margin-left: 8px;
+ font-size: 0.95rem;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.high-stock {
+ background: linear-gradient(45deg, #e0f7fa, #b3e5fc);
+ color: #0277bd;
+ border: 1px solid #81d4fa;
+}
+
+.low-stock {
+ background: linear-gradient(45deg, #fff8e1, #ffecb3);
+ color: #ff8f00;
+ border: 1px solid #ffe082;
+}
+
+.out-stock {
+ background: linear-gradient(45deg, #ffebee, #ffcdd2);
+ color: #c62828;
+ border: 1px solid #ef9a9a;
+}
+
+/* 历史记录区域 */
+.history-section {
+ position: relative;
+ margin-top: 40px;
+}
+
+.section-title {
+ color: #4169e1;
+ font-weight: 700;
+ font-size: 1.4rem;
+ margin-bottom: 25px;
+ position: relative;
+ display: inline-block;
+}
+
+.section-title i {
+ margin-right: 10px;
+ color: #7AB6FF;
+}
+
+.magic-underline {
+ position: absolute;
+ bottom: -8px;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background: linear-gradient(to right, #7AB6FF, transparent);
+ animation: sparkle 2s infinite;
+}
+
+@keyframes sparkle {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 1; }
+}
+
+/* 自定义表格 */
+.table-container {
+ position: relative;
+ margin-bottom: 30px;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 5px 15px rgba(79, 149, 255, 0.1);
+}
+
+.table-frozen {
+ width: 100%;
+ background-color: white;
+ border-collapse: collapse;
+}
+
+.table-header-row {
+ display: grid;
+ grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 2fr 1.5fr;
+ background: linear-gradient(45deg, #5e81ac, #81a1c1);
+ color: white;
+ font-weight: 600;
+}
+
+.th-frozen {
+ padding: 15px;
+ text-align: center;
+ position: relative;
+}
+
+.th-frozen:not(:last-child)::after {
+ content: "";
+ position: absolute;
+ right: 0;
+ top: 20%;
+ height: 60%;
+ width: 1px;
+ background-color: rgba(255, 255, 255, 0.3);
+}
+
+.table-body {
+ max-height: 500px;
+ overflow-y: auto;
+}
+
+.table-row {
+ display: grid;
+ grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 2fr 1.5fr;
+ border-bottom: 1px solid #ecf0f1;
+ transition: all 0.3s ease;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+}
+
+.table-row:hover {
+ background-color: #f0f8ff;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 10px rgba(79, 149, 255, 0.1);
+}
+
+.table-row::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 4px;
+ background: linear-gradient(to bottom, #7AB6FF, #5e81ac);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.table-row:hover::before {
+ opacity: 1;
+}
+
+.td-frozen {
+ padding: 15px;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.remark-cell {
+ text-align: left;
+ justify-content: flex-start;
+ font-style: italic;
+ color: #7f8c8d;
+}
+
+/* 表格中的徽章 */
+.operation-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 5px 12px;
+ border-radius: 50px;
+ font-weight: 600;
+ font-size: 0.9rem;
+}
+
+.operation-badge i {
+ margin-left: 5px;
+}
+
+.in-badge {
+ background: linear-gradient(45deg, #e0f7fa, #b3e5fc);
+ color: #0277bd;
+ border: 1px solid #81d4fa;
+}
+
+.out-badge {
+ background: linear-gradient(45deg, #fff8e1, #ffecb3);
+ color: #ff8f00;
+ border: 1px solid #ffe082;
+}
+
+/* 奥拉夫空状态 */
+.empty-log {
+ grid-template-columns: 1fr !important;
+ height: 250px;
+}
+
+.empty-message {
+ grid-column: span 7;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+}
+
+.olaf-empty {
+ text-align: center;
+}
+
+.olaf-image {
+ width: 120px;
+ height: 150px;
+ background-image: url('https://i.imgur.com/lM0cLxb.png');
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ margin: 0 auto 15px;
+ animation: olaf-wave 3s infinite;
+}
+
+@keyframes olaf-wave {
+ 0%, 100% { transform: rotate(-5deg); }
+ 50% { transform: rotate(5deg); }
+}
+
+.olaf-empty p {
+ font-size: 1.2rem;
+ color: #7f8c8d;
+ margin: 0;
+}
+
+/* 特殊的行样式 */
+.log-entry[data-type="in"] {
+ background-color: rgba(224, 247, 250, 0.2);
+}
+
+.log-entry[data-type="out"] {
+ background-color: rgba(255, 248, 225, 0.2);
+}
+
+/* 分页容器 */
+.pagination-container {
+ margin-top: 30px;
+ margin-bottom: 10px;
+}
+
+.frozen-pagination {
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+ justify-content: center;
+ gap: 5px;
+}
+
+.frozen-pagination .page-item {
+ margin: 0 2px;
+}
+
+.frozen-pagination .page-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ color: #4169e1;
+ background-color: white;
+ border: 1px solid #e1f0ff;
+ border-radius: 50px;
+ text-decoration: none;
+ transition: all 0.3s ease;
+ min-width: 40px;
+}
+
+.frozen-pagination .page-link:hover {
+ background-color: #e1f0ff;
+ color: #2c3e50;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 10px rgba(79, 149, 255, 0.1);
+}
+
+.frozen-pagination .page-item.active .page-link {
+ background: linear-gradient(45deg, #7AB6FF, #5e81ac);
+ color: white;
+ border-color: #5e81ac;
+}
+
+.frozen-pagination .page-item.disabled .page-link {
+ color: #95a5a6;
+ background-color: #f8f9fa;
+ cursor: not-allowed;
+}
+
+/* 页脚 */
+.card-footer-frozen {
+ background: linear-gradient(45deg, #ecf5ff, #d8e6ff);
+ padding: 1.5rem;
+ border-radius: 0 0 18px 18px;
+ position: relative;
+}
+
+.footer-actions {
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ z-index: 2;
+}
+
+.footer-decoration {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 15px;
+ background-image: url('https://i.imgur.com/KkMfwWv.png');
+ background-size: 50px;
+ background-repeat: repeat-x;
+ opacity: 0.2;
+ filter: hue-rotate(190deg);
+}
+
+/* 冰雪风格按钮 */
+.frozen-btn {
+ padding: 10px 20px;
+ border-radius: 50px;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+ border: none;
+ color: white;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+}
+
+.frozen-btn i {
+ margin-right: 8px;
+}
+
+.return-btn {
+ background: linear-gradient(45deg, #81a1c1, #5e81ac);
+}
+
+.return-btn:hover {
+ background: linear-gradient(45deg, #5e81ac, #4c6f94);
+ transform: translateY(-3px);
+ box-shadow: 0 8px 15px rgba(94, 129, 172, 0.3);
+ color: white;
+}
+
+.adjust-btn {
+ background: linear-gradient(45deg, #7AB6FF, #5d91e5);
+}
+
+.adjust-btn:hover {
+ background: linear-gradient(45deg, #5d91e5, #4169e1);
+ transform: translateY(-3px);
+ box-shadow: 0 8px 15px rgba(65, 105, 225, 0.3);
+ color: white;
+}
+
+.frozen-btn::after {
+ content: "";
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: rgba(255, 255, 255, 0.1);
+ transform: rotate(45deg);
+ transition: all 0.3s ease;
+ opacity: 0;
+}
+
+.frozen-btn:hover::after {
+ opacity: 1;
+ transform: rotate(45deg) translateY(-50%);
+}
+
+/* 动画类 */
+.fade-in {
+ animation: fadeIn 0.5s ease forwards;
+ opacity: 0;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.selected-row {
+ background-color: #e3f2fd !important;
+ position: relative;
+ z-index: 1;
+}
+
+.selected-row::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(to right, rgba(122, 182, 255, 0.1), transparent);
+ pointer-events: none;
+}
+
+/* 响应式调整 */
+@media (max-width: 992px) {
+ .table-header-row,
+ .table-row {
+ grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 1.2fr 1.2fr;
+ }
+
+ .book-info {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .book-cover-container {
+ margin-bottom: 30px;
+ }
+
+ .book-frame {
+ transform: rotate(0);
+ max-width: 180px;
+ }
+
+ .book-cover {
+ transform: rotate(0);
+ max-height: 200px;
+ }
+
+ .book-title {
+ text-align: center;
+ font-size: 1.5rem;
+ }
+
+ .table-header-row,
+ .table-row {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .th-frozen:after {
+ display: none;
+ }
+
+ .th-frozen {
+ text-align: left;
+ padding: 10px 15px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+ }
+
+ .td-frozen {
+ justify-content: flex-start;
+ padding: 10px 15px;
+ border-bottom: 1px solid #ecf0f1;
+ }
+
+ .td-frozen:before {
+ content: attr(data-label);
+ font-weight: 600;
+ margin-right: 10px;
+ color: #7f8c8d;
+ }
+
+ .footer-actions {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .frozen-btn {
+ width: 100%;
+ }
+}
diff --git a/app/static/css/inventory-list.css b/app/static/css/inventory-list.css
new file mode 100644
index 0000000..fc4d452
--- /dev/null
+++ b/app/static/css/inventory-list.css
@@ -0,0 +1,417 @@
+/* 全局变量设置 */
+:root {
+ --primary-color: #f2a3b3;
+ --primary-light: #ffd6e0;
+ --primary-dark: #e57f9a;
+ --secondary-color: #a9d1f7;
+ --text-color: #4a4a4a;
+ --light-text: #6e6e6e;
+ --success-color: #77dd77;
+ --warning-color: #fdfd96;
+ --danger-color: #ff9e9e;
+ --background-color: #fff9fb;
+ --card-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
+ --transition: all 0.3s ease;
+ --border-radius: 12px;
+ --card-padding: 20px;
+}
+
+/* 基础样式 */
+body {
+ background-color: var(--background-color);
+ color: var(--text-color);
+ font-family: 'Helvetica Neue', Arial, sans-serif;
+ line-height: 1.6;
+}
+
+.inventory-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+/* 页面标题 */
+.page-header {
+ background: linear-gradient(135deg, var(--primary-light), var(--secondary-color));
+ border-radius: var(--border-radius);
+ margin-bottom: 30px;
+ padding: 40px 30px;
+ text-align: center;
+ box-shadow: var(--card-shadow);
+ position: relative;
+ overflow: hidden;
+}
+
+.page-header::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml;utf8,') repeat;
+ background-size: 80px 80px;
+ opacity: 0.4;
+}
+
+.header-content {
+ position: relative;
+ z-index: 2;
+}
+
+.page-header h1 {
+ color: #fff;
+ margin: 0;
+ font-size: 2.5rem;
+ font-weight: 300;
+ letter-spacing: 1px;
+ text-shadow: 1px 1px 3px rgba(0,0,0,0.1);
+}
+
+.header-icon {
+ margin-right: 15px;
+ color: #fff;
+}
+
+.subtitle {
+ color: #fff;
+ margin-top: 10px;
+ font-size: 1.1rem;
+ font-weight: 300;
+ opacity: 0.9;
+}
+
+/* 搜索框样式 */
+.search-card {
+ background: #fff;
+ border-radius: var(--border-radius);
+ padding: var(--card-padding);
+ margin-bottom: 30px;
+ box-shadow: var(--card-shadow);
+ border-top: 4px solid var(--primary-color);
+}
+
+.search-form {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 15px;
+}
+
+.search-input-group {
+ display: flex;
+ flex: 1;
+ min-width: 300px;
+}
+
+.search-input-container {
+ position: relative;
+ flex: 1;
+}
+
+.search-icon {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--light-text);
+}
+
+.search-input {
+ width: 100%;
+ padding: 12px 15px 12px 40px;
+ border: 1px solid #e3e3e3;
+ border-radius: var(--border-radius) 0 0 var(--border-radius);
+ font-size: 1rem;
+ transition: var(--transition);
+ outline: none;
+}
+
+.search-input:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px var(--primary-light);
+}
+
+.search-button {
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ padding: 12px 25px;
+ font-size: 1rem;
+ border-radius: 0 var(--border-radius) var(--border-radius) 0;
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+.search-button:hover {
+ background-color: var(--primary-dark);
+}
+
+.log-button {
+ background-color: #fff;
+ color: var(--primary-color);
+ border: 1px solid var(--primary-color);
+ padding: 11px 20px;
+ border-radius: var(--border-radius);
+ text-decoration: none;
+ font-size: 0.95rem;
+ transition: var(--transition);
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.log-button:hover {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+}
+
+/* 表格样式 */
+.table-container {
+ background: #fff;
+ border-radius: var(--border-radius);
+ padding: var(--card-padding);
+ margin-bottom: 30px;
+ box-shadow: var(--card-shadow);
+ overflow: hidden;
+}
+
+.inventory-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.95rem;
+}
+
+.inventory-table th {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ padding: 15px;
+ text-align: left;
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 0.85rem;
+ letter-spacing: 0.5px;
+}
+
+.inventory-table tr {
+ border-bottom: 1px solid #f3f3f3;
+ transition: var(--transition);
+}
+
+.inventory-table tr:last-child {
+ border-bottom: none;
+}
+
+.inventory-table tr:hover {
+ background-color: #f9f9f9;
+}
+
+.inventory-table td {
+ padding: 15px;
+ vertical-align: middle;
+}
+
+.book-title {
+ font-weight: 500;
+ color: var(--text-color);
+}
+
+.book-author {
+ color: var(--light-text);
+ font-style: italic;
+}
+
+/* 库存和状态标签样式 */
+.stock-badge, .status-badge {
+ display: inline-block;
+ padding: 6px 12px;
+ border-radius: 50px;
+ font-size: 0.85rem;
+ font-weight: 500;
+ text-align: center;
+ min-width: 60px;
+}
+
+.stock-high {
+ background-color: var(--success-color);
+ color: #fff;
+}
+
+.stock-medium {
+ background-color: var(--warning-color);
+ color: #8a7800;
+}
+
+.stock-low {
+ background-color: var(--danger-color);
+ color: #fff;
+}
+
+.status-active {
+ background-color: #d9f5e6;
+ color: #2a9d5c;
+}
+
+.status-inactive {
+ background-color: #ffe8e8;
+ color: #e35555;
+}
+
+/* 操作按钮 */
+.action-buttons {
+ display: flex;
+ gap: 8px;
+}
+
+.btn-adjust, .btn-view {
+ padding: 8px 12px;
+ border-radius: var(--border-radius);
+ text-decoration: none;
+ font-size: 0.85rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ transition: var(--transition);
+}
+
+.btn-adjust {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ border: 1px solid var(--primary-color);
+}
+
+.btn-adjust:hover {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+.btn-view {
+ background-color: var(--secondary-color);
+ color: #3573b5;
+ border: 1px solid #8ab9e3;
+}
+
+.btn-view:hover {
+ background-color: #8ab9e3;
+ color: white;
+}
+
+/* 分页样式 */
+.pagination-wrapper {
+ display: flex;
+ justify-content: center;
+ margin-top: 30px;
+}
+
+.pagination {
+ display: flex;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ gap: 5px;
+}
+
+.page-item {
+ display: inline-block;
+}
+
+.page-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 40px;
+ height: 40px;
+ padding: 0 15px;
+ border-radius: var(--border-radius);
+ background-color: #fff;
+ color: var(--text-color);
+ text-decoration: none;
+ transition: var(--transition);
+ border: 1px solid #e3e3e3;
+}
+
+.page-item.active .page-link {
+ background-color: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+}
+
+.page-item:not(.active) .page-link:hover {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ border-color: var(--primary-light);
+}
+
+.page-item.disabled .page-link {
+ background-color: #f5f5f5;
+ color: #aaa;
+ cursor: not-allowed;
+}
+
+/* 响应式调整 */
+@media (max-width: 992px) {
+ .inventory-container {
+ padding: 15px;
+ }
+
+ .page-header {
+ padding: 30px 20px;
+ }
+
+ .page-header h1 {
+ font-size: 2rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .search-form {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .log-button {
+ text-align: center;
+ }
+
+ .page-header h1 {
+ font-size: 1.8rem;
+ }
+
+ .table-container {
+ overflow-x: auto;
+ }
+
+ .inventory-table {
+ min-width: 800px;
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ }
+
+ .btn-adjust, .btn-view {
+ text-align: center;
+ }
+}
+
+@media (max-width: 576px) {
+ .page-header {
+ padding: 25px 15px;
+ }
+
+ .page-header h1 {
+ font-size: 1.5rem;
+ }
+
+ .subtitle {
+ font-size: 1rem;
+ }
+
+ .pagination .page-link {
+ min-width: 35px;
+ height: 35px;
+ padding: 0 10px;
+ font-size: 0.9rem;
+ }
+}
diff --git a/app/static/css/inventory-logs.css b/app/static/css/inventory-logs.css
new file mode 100644
index 0000000..41f6e1d
--- /dev/null
+++ b/app/static/css/inventory-logs.css
@@ -0,0 +1,710 @@
+/* 冰雪奇缘主题库存日志页面样式 */
+@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400;700&family=Nunito:wght@300;400;600;700&display=swap');
+
+:root {
+ --primary-blue: #6fa8dc;
+ --light-blue: #cfe2f3;
+ --dark-blue: #1a5190;
+ --accent-pink: #f4b8c4;
+ --accent-purple: #b19cd9;
+ --subtle-gold: #ffd966;
+ --ice-white: #f3f9ff;
+ --snow-white: #ffffff;
+ --text-dark: #2c3e50;
+ --text-light: #ecf0f1;
+ --shadow-color: rgba(0, 53, 102, 0.15);
+ --frost-blue: #a2d5f2;
+ --elsa-blue: #85c1e9;
+ --anna-purple: #c39bd3;
+ --olaf-white: #f9fcff;
+}
+
+/* 全局样式重置 */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Nunito', sans-serif;
+ background: #f5f9ff url('/static/images/disney-bg.jpg') no-repeat center center fixed;
+ background-size: cover;
+ color: var(--text-dark);
+ line-height: 1.6;
+ position: relative;
+ overflow-x: hidden;
+ min-height: 100vh;
+}
+
+/* 容器样式 */
+.disney-container {
+ max-width: 95%;
+ margin: 2rem auto;
+ padding: 0 15px;
+ position: relative;
+ z-index: 1;
+}
+
+/* 魔法粒子效果层 */
+#magic-particles {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ pointer-events: none;
+}
+
+/* 主卡片样式 */
+.disney-card {
+ background: linear-gradient(135deg, var(--ice-white) 0%, var(--snow-white) 100%);
+ border-radius: 20px;
+ box-shadow: 0 10px 30px var(--shadow-color),
+ 0 0 50px rgba(137, 196, 244, 0.3),
+ inset 0 0 15px rgba(255, 255, 255, 0.8);
+ overflow: hidden;
+ position: relative;
+ margin-bottom: 2rem;
+ border: 1px solid rgba(200, 223, 255, 0.8);
+ animation: card-glow 3s infinite alternate;
+}
+
+/* 卡片发光动画 */
+@keyframes card-glow {
+ from {
+ box-shadow: 0 10px 30px var(--shadow-color),
+ 0 0 50px rgba(137, 196, 244, 0.3),
+ inset 0 0 15px rgba(255, 255, 255, 0.8);
+ }
+ to {
+ box-shadow: 0 10px 30px var(--shadow-color),
+ 0 0 70px rgba(137, 196, 244, 0.5),
+ inset 0 0 20px rgba(255, 255, 255, 0.9);
+ }
+}
+
+/* 装饰元素 */
+.disney-decoration {
+ position: absolute;
+ background-size: contain;
+ background-repeat: no-repeat;
+ opacity: 0.7;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.book-icon {
+ top: 20px;
+ right: 30px;
+ width: 60px;
+ height: 60px;
+ background-image: url('https://api.iconify.design/ph:books-duotone.svg?color=%236fa8dc');
+ transform: rotate(10deg);
+ animation: float 6s ease-in-out infinite;
+}
+
+.crown-icon {
+ bottom: 40px;
+ left: 20px;
+ width: 50px;
+ height: 50px;
+ background-image: url('https://api.iconify.design/fa6-solid:crown.svg?color=%23ffd966');
+ animation: float 5s ease-in-out infinite 1s;
+}
+
+.wand-icon {
+ top: 60px;
+ left: 40px;
+ width: 40px;
+ height: 40px;
+ background-image: url('https://api.iconify.design/fa-solid:magic.svg?color=%23b19cd9');
+ animation: float 7s ease-in-out infinite 0.5s;
+}
+
+.snowflake-icon {
+ bottom: 70px;
+ right: 50px;
+ width: 45px;
+ height: 45px;
+ background-image: url('https://api.iconify.design/fa-regular:snowflake.svg?color=%23a2d5f2');
+ animation: float 4s ease-in-out infinite 1.5s, spin 15s linear infinite;
+}
+
+@keyframes float {
+ 0% { transform: translateY(0) rotate(0deg); }
+ 50% { transform: translateY(-15px) rotate(5deg); }
+ 100% { transform: translateY(0) rotate(0deg); }
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* 卡片头部 */
+.card-header-disney {
+ background: linear-gradient(45deg, var(--elsa-blue), var(--frost-blue));
+ color: var(--text-light);
+ padding: 1.5rem;
+ text-align: center;
+ position: relative;
+ border-bottom: 3px solid rgba(255, 255, 255, 0.5);
+}
+
+.card-header-disney h4 {
+ font-size: 1.8rem;
+ font-weight: 700;
+ margin: 0;
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
+ position: relative;
+ z-index: 2;
+ font-family: 'Dancing Script', cursive;
+ letter-spacing: 1px;
+}
+
+.card-header-disney i {
+ margin-right: 10px;
+ color: var(--subtle-gold);
+ animation: pulse 2s infinite;
+}
+
+.princess-crown {
+ position: absolute;
+ top: -20px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 60px;
+ height: 30px;
+ background-image: url('https://api.iconify.design/fa6-solid:crown.svg?color=%23ffd966');
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ filter: drop-shadow(0 0 5px rgba(255, 217, 102, 0.7));
+}
+
+@keyframes pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
+}
+
+/* 卡片内容 */
+.card-body-disney {
+ padding: 2rem;
+ position: relative;
+ z-index: 2;
+}
+
+/* 图书信息部分 */
+.book-details-container {
+ display: flex;
+ background: linear-gradient(to right, rgba(162, 213, 242, 0.1), rgba(177, 156, 217, 0.1));
+ border-radius: 15px;
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
+ border: 1px solid rgba(162, 213, 242, 0.3);
+ position: relative;
+ overflow: hidden;
+}
+
+.book-cover-wrapper {
+ flex: 0 0 150px;
+ margin-right: 2rem;
+ position: relative;
+}
+
+.disney-book-cover {
+ width: 100%;
+ height: auto;
+ border-radius: 8px;
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ border: 4px solid white;
+ object-fit: cover;
+ z-index: 2;
+ position: relative;
+}
+
+.disney-book-cover:hover {
+ transform: translateY(-5px) scale(1.03);
+ box-shadow: 0 15px 25px rgba(0, 0, 0, 0.15);
+}
+
+.book-cover-glow {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle, rgba(162, 213, 242, 0.6) 0%, rgba(255, 255, 255, 0) 70%);
+ z-index: 1;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ pointer-events: none;
+}
+
+.book-cover-wrapper:hover .book-cover-glow {
+ opacity: 1;
+ animation: glow-pulse 2s infinite;
+}
+
+@keyframes glow-pulse {
+ 0% { opacity: 0.3; }
+ 50% { opacity: 0.7; }
+ 100% { opacity: 0.3; }
+}
+
+.book-info {
+ flex: 1;
+}
+
+.book-title {
+ font-family: 'Dancing Script', cursive;
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ color: var(--dark-blue);
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
+ position: relative;
+ display: inline-block;
+}
+
+.book-title::after {
+ content: '';
+ position: absolute;
+ bottom: -5px;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background: linear-gradient(to right, var(--elsa-blue), var(--anna-purple));
+ border-radius: 2px;
+}
+
+.info-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.8rem;
+ font-size: 1rem;
+}
+
+.disney-icon {
+ width: 24px;
+ height: 24px;
+ margin-right: 10px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.author-icon {
+ background-image: url('https://api.iconify.design/fa-solid:user-edit.svg?color=%236fa8dc');
+}
+
+.publisher-icon {
+ background-image: url('https://api.iconify.design/fa-solid:building.svg?color=%236fa8dc');
+}
+
+.isbn-icon {
+ background-image: url('https://api.iconify.design/fa-solid:barcode.svg?color=%236fa8dc');
+}
+
+.stock-icon {
+ background-image: url('https://api.iconify.design/fa-solid:warehouse.svg?color=%236fa8dc');
+}
+
+.stock-badge {
+ display: inline-block;
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-weight: bold;
+ font-size: 0.9rem;
+ color: white;
+ margin-left: 5px;
+}
+
+.high-stock {
+ background-color: #2ecc71;
+ animation: badge-pulse 2s infinite;
+}
+
+.low-stock {
+ background-color: #f39c12;
+}
+
+.out-stock {
+ background-color: #e74c3c;
+}
+
+@keyframes badge-pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
+}
+
+/* 日志部分 */
+.logs-section {
+ background-color: var(--ice-white);
+ border-radius: 15px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
+ padding: 1.5rem;
+ position: relative;
+ overflow: hidden;
+ border: 1px solid rgba(162, 213, 242, 0.3);
+}
+
+.logs-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: url('https://api.iconify.design/ph:snowflake-thin.svg?color=%23a2d5f2');
+ background-size: 20px;
+ opacity: 0.05;
+ pointer-events: none;
+ animation: snow-bg 60s linear infinite;
+}
+
+@keyframes snow-bg {
+ from { background-position: 0 0; }
+ to { background-position: 100% 100%; }
+}
+
+.logs-title {
+ text-align: center;
+ font-size: 1.5rem;
+ margin-bottom: 1.5rem;
+ color: var(--dark-blue);
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: 'Dancing Script', cursive;
+}
+
+.title-decoration {
+ width: 100px;
+ height: 2px;
+ background: linear-gradient(to right, transparent, var(--elsa-blue), transparent);
+ margin: 0 15px;
+}
+
+.title-decoration.right {
+ transform: scaleX(-1);
+}
+
+.table-container {
+ overflow-x: auto;
+ border-radius: 10px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
+ margin-bottom: 1.5rem;
+}
+
+.disney-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ background-color: white;
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.disney-table thead {
+ background: linear-gradient(45deg, var(--elsa-blue), var(--frost-blue));
+ color: white;
+}
+
+.disney-table th {
+ padding: 1rem 0.8rem;
+ text-align: left;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ position: relative;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.disney-table th:first-child {
+ border-top-left-radius: 10px;
+}
+
+.disney-table th:last-child {
+ border-top-right-radius: 10px;
+}
+
+.disney-table td {
+ padding: 0.8rem;
+ border-bottom: 1px solid rgba(162, 213, 242, 0.2);
+ vertical-align: middle;
+}
+
+.log-row {
+ transition: all 0.3s ease;
+}
+
+.log-row:hover {
+ background-color: rgba(162, 213, 242, 0.1);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
+}
+
+.operation-badge {
+ padding: 4px 10px;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-align: center;
+ display: inline-block;
+ min-width: 80px;
+}
+
+.in-badge {
+ background-color: rgba(46, 204, 113, 0.15);
+ color: #27ae60;
+ border: 1px solid rgba(46, 204, 113, 0.3);
+}
+
+.out-badge {
+ background-color: rgba(231, 76, 60, 0.15);
+ color: #c0392b;
+ border: 1px solid rgba(231, 76, 60, 0.3);
+}
+
+.remark-cell {
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.empty-logs {
+ text-align: center;
+ padding: 3rem 0 !important;
+}
+
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: #95a5a6;
+}
+
+.empty-icon {
+ width: 80px;
+ height: 80px;
+ background-image: url('https://api.iconify.design/ph:book-open-duotone.svg?color=%2395a5a6');
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ margin-bottom: 1rem;
+ opacity: 0.7;
+}
+
+.empty-state p {
+ font-size: 1.1rem;
+}
+
+/* 分页样式 */
+.disney-pagination {
+ margin-top: 1.5rem;
+ display: flex;
+ justify-content: center;
+}
+
+.pagination-list {
+ display: flex;
+ list-style: none;
+ gap: 5px;
+ align-items: center;
+}
+
+.page-item {
+ margin: 0 2px;
+}
+
+.page-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 12px;
+ border-radius: 20px;
+ color: var(--dark-blue);
+ background-color: white;
+ text-decoration: none;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ min-width: 40px;
+ border: 1px solid rgba(111, 168, 220, 0.3);
+}
+
+.page-link:hover:not(.disabled .page-link) {
+ background-color: var(--light-blue);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+}
+
+.page-item.active .page-link {
+ background: linear-gradient(45deg, var(--elsa-blue), var(--frost-blue));
+ color: white;
+ box-shadow: 0 4px 8px rgba(111, 168, 220, 0.3);
+}
+
+.page-item.dots .page-link {
+ border: none;
+ background: none;
+ pointer-events: none;
+}
+
+.page-item.disabled .page-link {
+ color: #b2bec3;
+ pointer-events: none;
+ background-color: rgba(236, 240, 241, 0.5);
+ border: 1px solid rgba(189, 195, 199, 0.3);
+}
+
+/* 卡片底部 */
+.card-footer-disney {
+ padding: 1.5rem;
+ background: linear-gradient(45deg, rgba(162, 213, 242, 0.2), rgba(177, 156, 217, 0.2));
+ border-top: 1px solid rgba(162, 213, 242, 0.3);
+}
+
+.button-container {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.disney-button {
+ padding: 10px 20px;
+ border-radius: 30px;
+ font-weight: 600;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+}
+
+.disney-button::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transition: all 0.6s ease;
+ z-index: -1;
+}
+
+.disney-button:hover::before {
+ left: 100%;
+}
+
+.disney-button:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
+}
+
+.button-icon {
+ margin-right: 10px;
+}
+
+.return-btn {
+ background: linear-gradient(45deg, #3498db, #2980b9);
+ color: white;
+}
+
+.adjust-btn {
+ background: linear-gradient(45deg, #9b59b6, #8e44ad);
+ color: white;
+}
+
+/* 响应式设计 */
+@media (max-width: 992px) {
+ .book-details-container {
+ flex-direction: column;
+ }
+
+ .book-cover-wrapper {
+ margin-right: 0;
+ margin-bottom: 1.5rem;
+ text-align: center;
+ width: 180px;
+ margin: 0 auto 1.5rem;
+ }
+
+ .logs-title {
+ font-size: 1.3rem;
+ }
+
+ .title-decoration {
+ width: 50px;
+ }
+}
+
+@media (max-width: 768px) {
+ .disney-container {
+ margin: 1rem auto;
+ }
+
+ .card-header-disney h4 {
+ font-size: 1.5rem;
+ }
+
+ .card-body-disney {
+ padding: 1.5rem 1rem;
+ }
+
+ .book-title {
+ font-size: 1.7rem;
+ }
+
+ .disney-button {
+ padding: 8px 15px;
+ font-size: 0.9rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .button-container {
+ justify-content: center;
+ gap: 1rem;
+ }
+
+ .book-title {
+ font-size: 1.5rem;
+ }
+
+ .logs-title {
+ font-size: 1.2rem;
+ }
+
+ .title-decoration {
+ width: 30px;
+ }
+}
+
+/* 飘落的雪花 */
+.snowflake {
+ position: fixed;
+ top: -50px;
+ animation: fall linear infinite;
+ z-index: 0;
+ pointer-events: none;
+ color: rgba(255, 255, 255, 0.8);
+ text-shadow: 0 0 5px rgba(162, 213, 242, 0.5);
+}
+
+@keyframes fall {
+ to {
+ transform: translateY(100vh) rotate(360deg);
+ }
+}
diff --git a/app/static/css/my_borrows.css b/app/static/css/my_borrows.css
new file mode 100644
index 0000000..a7bdb63
--- /dev/null
+++ b/app/static/css/my_borrows.css
@@ -0,0 +1,474 @@
+/* my_borrows.css - 少女粉色风格图书管理系统 */
+:root {
+ --primary-color: #e686a5; /* 主要粉色 */
+ --primary-light: #ffedf2; /* 浅粉色 */
+ --primary-dark: #d26a8c; /* 深粉色 */
+ --accent-color: #9a83c9; /* 紫色点缀 */
+ --text-primary: #4a4a4a; /* 主要文字颜色 */
+ --text-secondary: #848484; /* 次要文字颜色 */
+ --border-color: #f4d7e1; /* 边框颜色 */
+ --success-color: #7ac9a1; /* 成功色 */
+ --danger-color: #ff8f9e; /* 危险色 */
+ --white: #ffffff;
+}
+
+body {
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ color: var(--text-primary);
+ background-color: #fdf6f8;
+}
+
+/* 容器 */
+.container {
+ width: 95% !important;
+ max-width: 1200px !important;
+ margin: 1.5rem auto;
+ padding: 1.5rem;
+ background-color: var(--white);
+ box-shadow: 0 3px 15px rgba(230, 134, 165, 0.15);
+ border-radius: 20px;
+ box-sizing: border-box;
+ position: relative;
+}
+
+/* 页面标题 */
+.page-title {
+ margin-bottom: 1.8rem;
+ color: var(--primary-dark);
+ border-bottom: 2px solid var(--border-color);
+ padding-bottom: 12px;
+ font-size: 1.8rem;
+ font-weight: 600;
+ position: relative;
+ text-align: center;
+}
+
+.page-title:after {
+ content: "";
+ position: absolute;
+ width: 80px;
+ height: 3px;
+ background-color: var(--primary-color);
+ bottom: -2px;
+ left: 50%;
+ transform: translateX(-50%);
+ border-radius: 3px;
+}
+
+/* 标签页样式 */
+.tabs {
+ display: flex;
+ width: 100%;
+ margin-bottom: 25px;
+ border: none;
+ background-color: var(--primary-light);
+ border-radius: 25px;
+ padding: 5px;
+ box-shadow: 0 3px 10px rgba(230, 134, 165, 0.1);
+}
+
+/* tab 项 */
+.tab {
+ flex: 1;
+ padding: 10px 20px;
+ text-decoration: none;
+ color: var(--text-primary);
+ margin-right: 2px;
+ border-radius: 20px;
+ transition: all 0.3s ease;
+ font-size: 0.95rem;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.tab:hover {
+ background-color: rgba(230, 134, 165, 0.1);
+ color: var(--primary-dark);
+ text-decoration: none;
+}
+
+.tab.active {
+ background-color: var(--primary-color);
+ color: white;
+ box-shadow: 0 3px 8px rgba(230, 134, 165, 0.3);
+}
+
+.count {
+ background-color: rgba(255, 255, 255, 0.3);
+ border-radius: 20px;
+ padding: 2px 8px;
+ font-size: 0.75em;
+ display: inline-block;
+ margin-left: 5px;
+ font-weight: 600;
+}
+
+.tab.active .count {
+ background-color: rgba(255, 255, 255, 0.4);
+}
+
+/* 借阅列表与表格 */
+.borrow-list {
+ margin-top: 20px;
+ margin-bottom: 2rem;
+ width: 100%;
+ overflow-x: auto;
+}
+
+.borrow-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ margin-bottom: 25px;
+ border-radius: 15px;
+ overflow: hidden;
+ box-shadow: 0 5px 20px rgba(230, 134, 165, 0.08);
+}
+
+/* 调整列宽 - 解决状态列和操作列问题 */
+.borrow-table th:nth-child(1),
+.borrow-table td:nth-child(1) { width: 90px; }
+.borrow-table th:nth-child(2),
+.borrow-table td:nth-child(2) { width: 20%; }
+.borrow-table th:nth-child(3),
+.borrow-table td:nth-child(3),
+.borrow-table th:nth-child(4),
+.borrow-table td:nth-child(4) { width: 15%; }
+
+/* 状态列 */
+.borrow-table th:nth-child(5),
+.borrow-table td:nth-child(5) {
+ width: 15%;
+ min-width: 120px;
+ position: relative;
+ overflow: visible;
+ padding: 14px 25px;
+ vertical-align: middle;
+}
+
+/* 状态表头文字微调 - 向右移动2px */
+.borrow-table th:nth-child(5) {
+ padding-left: 28px; /* 增加左内边距,使文字看起来稍微向右移动 */
+}
+
+/* 操作列 */
+.borrow-table th:nth-child(6),
+.borrow-table td:nth-child(6) {
+ width: 18%;
+ min-width: 140px;
+ padding: 14px 18px;
+ vertical-align: middle;
+ text-align: left;
+ padding: 14px 0 14px 15px; /* 减少右内边距,增加左内边距 */
+}
+
+.borrow-table th,
+.borrow-table td {
+ padding: 14px 18px;
+ text-align: left;
+ vertical-align: middle;
+}
+
+.borrow-table th {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ font-weight: 600;
+ font-size: 0.9rem;
+ letter-spacing: 0.3px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.borrow-table tr {
+ border-bottom: 1px solid var(--border-color);
+ transition: all 0.2s ease;
+}
+
+.borrow-table tbody tr:last-child {
+ border-bottom: none;
+}
+
+.borrow-item {
+ background-color: var(--white);
+}
+
+.borrow-item:hover {
+ background-color: rgba(230, 134, 165, 0.03);
+}
+
+.borrow-item.overdue {
+ background-color: rgba(255, 143, 158, 0.08);
+}
+
+/* 图书封面 */
+.book-cover img {
+ width: 65px;
+ height: 90px;
+ object-fit: cover;
+ border-radius: 8px;
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease;
+ border: 3px solid var(--white);
+}
+
+.book-cover img:hover {
+ transform: scale(1.05);
+ box-shadow: 0 5px 15px rgba(230, 134, 165, 0.3);
+}
+
+/* 书名与作者 */
+.book-title {
+ font-weight: 600;
+ font-size: 1rem;
+}
+
+.book-title a {
+ color: var(--primary-dark);
+ text-decoration: none;
+ transition: color 0.3s ease;
+}
+
+.book-title a:hover {
+ color: var(--primary-color);
+}
+
+.book-author {
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ margin-top: 5px;
+ display: flex;
+ align-items: center;
+}
+
+.book-author:before {
+ content: "🖋";
+ margin-right: 5px;
+ font-size: 0.9em;
+}
+
+/* 徽章 - 修复状态显示问题 */
+.borrow-table .badge,
+.book-status .badge {
+ padding: 4px 10px;
+ border-radius: 20px;
+ font-weight: 500;
+ font-size: 0.75rem;
+ display: inline-block;
+ margin-bottom: 4px;
+ letter-spacing: 0.3px;
+ white-space: nowrap;
+ text-align: center;
+ min-width: 60px;
+}
+
+.borrow-table .badge {
+ position: static;
+ top: auto;
+ right: auto;
+}
+
+.badge-primary { background-color: var(--primary-color); color: white; }
+.badge-success { background-color: var(--success-color); color: white; }
+.badge-danger { background-color: var(--danger-color); color: white; }
+.badge-info { background-color: var(--accent-color); color: white; }
+
+.return-date {
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ margin-top: 5px;
+ display: flex;
+ align-items: center;
+}
+
+.return-date:before {
+ content: "📅";
+ margin-right: 5px;
+}
+
+.text-danger {
+ color: var(--danger-color) !important;
+ font-weight: 600;
+}
+
+/* 操作按钮 - 简化样式 */
+.actions {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ align-items: center;
+ padding-left: 15px; /* 整体左移5px */
+ margin-top: 17px;
+ margin-right: 30px;
+}
+
+.actions .btn {
+ min-width: 60px;
+ padding: 8px 15px;
+ font-size: 0.85rem;
+ font-weight: 500;
+ border-radius: 20px;
+ border: none;
+ text-align: center;
+ white-space: nowrap;
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+}
+
+.btn-success {
+ background-color: var(--success-color);
+ color: white;
+}
+
+.btn-success:hover {
+ background-color: #65b088;
+ transform: translateY(-2px);
+ box-shadow: 0 5px 12px rgba(122, 201, 161, 0.3);
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: var(--primary-dark);
+ transform: translateY(-2px);
+ box-shadow: 0 5px 12px rgba(230, 134, 165, 0.3);
+}
+
+.btn-secondary {
+ background-color: #a0a0a0;
+ color: white;
+}
+
+/* 无记录状态 */
+.no-records {
+ text-align: center;
+ padding: 60px 30px;
+ background-color: var(--primary-light);
+ border-radius: 15px;
+ margin: 30px 0;
+ box-shadow: inset 0 0 15px rgba(230, 134, 165, 0.1);
+}
+
+.empty-icon {
+ font-size: 4em;
+ color: var(--primary-color);
+ margin-bottom: 20px;
+ opacity: 0.7;
+}
+
+.empty-text {
+ color: var(--text-primary);
+ margin-bottom: 25px;
+ font-size: 1.1rem;
+ max-width: 450px;
+ margin: 0 auto;
+ line-height: 1.6;
+}
+
+/* 分页 */
+.pagination-container {
+ display: flex;
+ justify-content: center;
+ margin-top: 25px;
+}
+
+.pagination {
+ display: flex;
+ list-style: none;
+ padding: 0;
+ gap: 5px;
+}
+
+.page-item { margin: 0 2px; }
+
+.page-link {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ background-color: white;
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ transition: all 0.3s ease;
+ font-size: 0.9rem;
+}
+
+.page-item.active .page-link,
+.page-link:hover {
+ background-color: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+ box-shadow: 0 3px 8px rgba(230, 134, 165, 0.3);
+}
+
+/* 模态框 */
+.modal-dialog {
+ max-width: 95%;
+ width: 500px;
+ margin: 1.75rem auto;
+}
+
+.modal-content {
+ border-radius: 15px;
+ border: none;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.modal-header {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+ border-bottom: 1px solid var(--border-color);
+ padding: 15px 20px;
+}
+
+.modal-body {
+ padding: 25px 20px;
+ font-size: 1.1rem;
+ text-align: center;
+}
+
+.modal-footer {
+ border-top: 1px solid var(--border-color);
+ padding: 15px 20px;
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+}
+
+/* 响应式 */
+@media (max-width: 992px) {
+ .container {
+ width: 98% !important;
+ padding: 1rem;
+ margin: 0.5rem auto;
+ }
+}
+
+@media (max-width: 768px) {
+ .tabs {
+ flex-direction: column;
+ background: none;
+ padding: 0;
+ }
+
+ .tab {
+ border-radius: 15px;
+ margin-bottom: 8px;
+ margin-right: 0;
+ padding: 12px 15px;
+ background-color: var(--primary-light);
+ }
+
+ .borrow-table {
+ min-width: 700px; /* 确保在小屏幕上可以滚动 */
+ }
+
+ .book-cover img {
+ width: 45px;
+ height: 65px;
+ }
+}
diff --git a/app/static/css/overdue.css b/app/static/css/overdue.css
new file mode 100644
index 0000000..3c421d4
--- /dev/null
+++ b/app/static/css/overdue.css
@@ -0,0 +1,396 @@
+/* overdue.css - 适合文艺少女的深棕色调设计 */
+
+body {
+ font-family: 'Georgia', 'Times New Roman', serif;
+ color: #4a3728;
+ background-color: #fcf8f3;
+}
+
+.container {
+ background-color: #fff9f5;
+ border-radius: 8px;
+ box-shadow: 0 3px 15px rgba(113, 66, 20, 0.1);
+ padding: 25px;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 1px solid #e8d9cb;
+ position: relative;
+}
+
+.page-title {
+ margin-bottom: 0;
+ color: #5d3511;
+ font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+.d-flex {
+ position: relative;
+}
+
+.d-flex:after {
+ content: "";
+ display: block;
+ height: 2px;
+ width: 100%;
+ background: linear-gradient(to right, #d9c7b8, #8d6e63, #d9c7b8);
+ margin-top: 15px;
+ margin-bottom: 20px;
+}
+
+.alert-warning {
+ background-color: #f9e8d0;
+ border: 1px solid #ebd6ba;
+ color: #8a6d3b;
+ border-radius: 8px;
+ padding: 15px;
+ margin-bottom: 25px;
+ box-shadow: 0 2px 5px rgba(138, 109, 59, 0.1);
+ position: relative;
+ overflow: hidden;
+}
+
+.alert-warning:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(to right, #d4a76a, transparent);
+}
+
+/* 表格样式 */
+.overdue-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ margin-bottom: 25px;
+ box-shadow: 0 2px 10px rgba(113, 66, 20, 0.05);
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid #e8d9cb;
+}
+
+.overdue-table th,
+.overdue-table td {
+ padding: 15px 18px;
+ text-align: left;
+ border-bottom: 1px solid #e8d9cb;
+}
+
+.overdue-table th {
+ background-color: #f1e6dd;
+ color: #5d3511;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+.overdue-item:hover {
+ background-color: #f8f0e5;
+}
+
+.overdue-item:last-child td {
+ border-bottom: none;
+}
+
+.book-cover img {
+ width: 65px;
+ height: 90px;
+ object-fit: cover;
+ border-radius: 6px;
+ box-shadow: 0 3px 8px rgba(113, 66, 20, 0.15);
+ border: 2px solid #fff;
+ transition: transform 0.3s ease;
+}
+
+.book-cover img:hover {
+ transform: scale(1.05);
+}
+
+.book-title {
+ font-weight: 600;
+ font-family: 'Georgia', 'Times New Roman', serif;
+}
+
+.book-title a {
+ color: #5d3511;
+ text-decoration: none;
+ transition: color 0.3s ease;
+}
+
+.book-title a:hover {
+ color: #a66321;
+ text-decoration: underline;
+}
+
+.book-author {
+ color: #8d6e63;
+ font-size: 0.9em;
+ margin-top: 5px;
+ font-style: italic;
+}
+
+.user-info a {
+ color: #5d3511;
+ text-decoration: none;
+ font-weight: 600;
+ transition: color 0.3s ease;
+}
+
+.user-info a:hover {
+ color: #a66321;
+ text-decoration: underline;
+}
+
+.user-nickname {
+ color: #8d6e63;
+ font-size: 0.9em;
+ margin-top: 3px;
+}
+
+.user-contact {
+ margin-top: 8px;
+}
+
+.user-contact a {
+ color: #8d6e63;
+ margin-right: 10px;
+ text-decoration: none;
+}
+
+.user-contact a:hover {
+ color: #704214;
+}
+
+.email-link, .phone-link {
+ display: inline-block;
+ padding: 4px 10px;
+ font-size: 0.85em;
+ background-color: #f1e6dd;
+ border-radius: 15px;
+ border: 1px solid #e8d9cb;
+ transition: all 0.3s ease;
+}
+
+.email-link:hover, .phone-link:hover {
+ background-color: #e8d9cb;
+}
+
+.text-danger {
+ color: #a15950 !important;
+}
+
+.overdue-days {
+ font-weight: 600;
+}
+
+/* 徽章 */
+.badge {
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-weight: 500;
+ font-size: 0.85em;
+ letter-spacing: 0.5px;
+}
+
+.badge-danger {
+ background-color: #a15950;
+ color: white;
+}
+
+.badge-warning {
+ background-color: #d4a76a;
+ color: #4a3728;
+}
+
+.badge-info {
+ background-color: #6a8da9;
+ color: white;
+}
+
+/* 按钮 */
+.btn {
+ border-radius: 20px;
+ padding: 8px 16px;
+ transition: all 0.3s ease;
+ letter-spacing: 0.3px;
+}
+
+.btn-outline-secondary {
+ color: #704214;
+ border-color: #d9c7b8;
+ background-color: transparent;
+}
+
+.btn-outline-secondary:hover {
+ color: #fff;
+ background-color: #8d6e63;
+ border-color: #8d6e63;
+}
+
+.btn-success {
+ background-color: #5b8a72;
+ border-color: #5b8a72;
+}
+
+.btn-success:hover, .btn-success:focus {
+ background-color: #4a7561;
+ border-color: #4a7561;
+}
+
+.btn-warning {
+ background-color: #d4a76a;
+ border-color: #d4a76a;
+ color: #4a3728;
+}
+
+.btn-warning:hover, .btn-warning:focus {
+ background-color: #c29355;
+ border-color: #c29355;
+ color: #4a3728;
+}
+
+.btn-primary {
+ background-color: #704214;
+ border-color: #704214;
+}
+
+.btn-primary:hover, .btn-primary:focus {
+ background-color: #5d3511;
+ border-color: #5d3511;
+}
+
+.actions .btn {
+ margin-right: 5px;
+ margin-bottom: 6px;
+}
+
+/* 空状态 */
+.no-records {
+ text-align: center;
+ padding: 60px 20px;
+ background-color: #f8f0e5;
+ border-radius: 8px;
+ margin: 25px 0;
+ border: 1px dashed #d9c7b8;
+ position: relative;
+}
+
+.no-records:before, .no-records:after {
+ content: "❦";
+ position: absolute;
+ color: #d9c7b8;
+ font-size: 24px;
+}
+
+.no-records:before {
+ top: 20px;
+ left: 20px;
+}
+
+.no-records:after {
+ bottom: 20px;
+ right: 20px;
+}
+
+.empty-icon {
+ font-size: 4.5em;
+ color: #5b8a72;
+ margin-bottom: 25px;
+}
+
+.empty-text {
+ color: #5b8a72;
+ margin-bottom: 25px;
+ font-style: italic;
+ font-size: 1.1em;
+}
+
+/* 分页 */
+.pagination-container {
+ display: flex;
+ justify-content: center;
+ margin-top: 25px;
+}
+
+.pagination .page-link {
+ color: #5d3511;
+ border-color: #e8d9cb;
+ margin: 0 3px;
+ border-radius: 4px;
+}
+
+.pagination .page-item.active .page-link {
+ background-color: #704214;
+ border-color: #704214;
+}
+
+.pagination .page-link:hover {
+ background-color: #f1e6dd;
+ color: #5d3511;
+}
+
+/* 模态框定制 */
+.modal-content {
+ border-radius: 8px;
+ border: 1px solid #e8d9cb;
+ box-shadow: 0 5px 20px rgba(113, 66, 20, 0.15);
+ background-color: #fff9f5;
+}
+
+.modal-header {
+ border-bottom: 1px solid #e8d9cb;
+ background-color: #f1e6dd;
+ border-radius: 8px 8px 0 0;
+}
+
+.modal-title {
+ color: #5d3511;
+ font-family: 'Georgia', 'Times New Roman', serif;
+ font-weight: 600;
+}
+
+.modal-footer {
+ border-top: 1px solid #e8d9cb;
+}
+
+/* 装饰元素 */
+.container:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 150px;
+ height: 150px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cpath fill='%23d9c7b8' fill-opacity='0.2' d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/svg%3E");
+ opacity: 0.3;
+ pointer-events: none;
+ z-index: -1;
+}
+
+/* 响应式设计 */
+@media (max-width: 992px) {
+ .actions .btn {
+ display: block;
+ width: 100%;
+ margin-bottom: 8px;
+ }
+
+ .no-records:before, .no-records:after {
+ display: none;
+ }
+}
+
+@media (max-width: 768px) {
+ .overdue-table {
+ display: block;
+ overflow-x: auto;
+ }
+
+ .book-cover img {
+ width: 50px;
+ height: 70px;
+ }
+}
diff --git a/app/static/css/user-form.css b/app/static/css/user-form.css
new file mode 100644
index 0000000..f9d9696
--- /dev/null
+++ b/app/static/css/user-form.css
@@ -0,0 +1,636 @@
+/* 用户表单样式 - 甜美风格 */
+.user-form-container {
+ max-width: 850px;
+ margin: 25px auto;
+ padding: 0 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 25px;
+ padding-bottom: 15px;
+ border-bottom: 2px solid #f8e6e8;
+ animation: slideInDown 0.6s ease-out;
+}
+
+.page-header h1 {
+ margin: 0;
+ font-size: 28px;
+ color: #e75480; /* 粉红色调 */
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+.form-card {
+ background-color: #fff;
+ border-radius: 12px;
+ box-shadow: 0 5px 20px rgba(231, 84, 128, 0.08);
+ padding: 30px;
+ border: 1px solid #f8e6e8;
+ position: relative;
+ overflow: visible;
+ animation: fadeIn 0.7s ease-out;
+}
+
+.form-group {
+ margin-bottom: 22px;
+ animation: slideInRight 0.4s ease-out;
+ animation-fill-mode: both;
+}
+
+/* 为每个表单组添加延迟,创造波浪效果 */
+.form-group:nth-child(1) { animation-delay: 0.1s; }
+.form-group:nth-child(2) { animation-delay: 0.2s; }
+.form-group:nth-child(3) { animation-delay: 0.3s; }
+.form-group:nth-child(4) { animation-delay: 0.4s; }
+.form-group:nth-child(5) { animation-delay: 0.5s; }
+.form-group:nth-child(6) { animation-delay: 0.6s; }
+.form-group:nth-child(7) { animation-delay: 0.7s; }
+.form-group:nth-child(8) { animation-delay: 0.8s; }
+.form-group:nth-child(9) { animation-delay: 0.9s; }
+.form-group:nth-child(10) { animation-delay: 1.0s; }
+
+.form-group.required label:after {
+ content: " *";
+ color: #ff6b8b;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: #5d5d5d;
+ font-size: 15px;
+ transition: all 0.3s ease;
+}
+
+.form-group:hover label {
+ color: #e75480;
+ transform: translateX(3px);
+}
+
+.form-control {
+ display: block;
+ width: 100%;
+ padding: 12px 15px;
+ font-size: 15px;
+ line-height: 1.5;
+ color: #555;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1.5px solid #ffd1dc; /* 淡粉色边框 */
+ border-radius: 8px;
+ transition: all 0.3s ease;
+}
+
+.form-control:focus {
+ border-color: #ff8da1;
+ outline: 0;
+ box-shadow: 0 0 0 3px rgba(255, 141, 161, 0.25);
+ transform: translateY(-2px);
+}
+
+.form-control::placeholder {
+ color: #bbb;
+ font-style: italic;
+}
+
+.password-field {
+ position: relative;
+}
+
+.toggle-password {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #ff8da1;
+ transition: all 0.3s ease;
+ z-index: 2;
+}
+
+.toggle-password:hover {
+ color: #e75480;
+ transform: translateY(-50%) scale(1.2);
+}
+
+.input-with-button {
+ display: flex;
+ gap: 12px;
+}
+
+.input-with-button .form-control {
+ flex: 1;
+}
+
+.input-with-button .btn {
+ white-space: nowrap;
+}
+
+.form-text {
+ display: block;
+ margin-top: 6px;
+ font-size: 13.5px;
+ color: #888;
+ font-style: italic;
+ transition: all 0.3s ease;
+}
+
+.form-text.text-danger {
+ color: #ff5c77;
+ font-style: normal;
+ animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
+}
+
+.form-text.text-success {
+ color: #7ac98f;
+ font-style: normal;
+ animation: pulse 0.5s ease;
+}
+
+.form-actions {
+ display: flex;
+ gap: 15px;
+ margin-top: 35px;
+ justify-content: center;
+ animation: fadeInUp 0.8s ease-out;
+ animation-delay: 1.2s;
+ animation-fill-mode: both;
+}
+
+.btn {
+ display: inline-block;
+ font-weight: 500;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ user-select: none;
+ border: 1.5px solid transparent;
+ padding: 10px 22px;
+ font-size: 15px;
+ line-height: 1.5;
+ border-radius: 25px; /* 圆润按钮 */
+ transition: all 0.3s ease;
+ cursor: pointer;
+ letter-spacing: 0.3px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+/* 按钮波纹效果 */
+.btn:after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 5px;
+ height: 5px;
+ background: rgba(255, 255, 255, 0.5);
+ opacity: 0;
+ border-radius: 100%;
+ transform: scale(1, 1) translate(-50%);
+ transform-origin: 50% 50%;
+}
+
+.btn:focus:not(:active)::after {
+ animation: ripple 1s ease-out;
+}
+
+@keyframes ripple {
+ 0% {
+ transform: scale(0, 0);
+ opacity: 0.5;
+ }
+ 20% {
+ transform: scale(25, 25);
+ opacity: 0.3;
+ }
+ 100% {
+ transform: scale(50, 50);
+ opacity: 0;
+ }
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #ff8da1;
+ border-color: #ff8da1;
+}
+
+.btn-primary:hover {
+ color: #fff;
+ background-color: #ff7389;
+ border-color: #ff7389;
+ box-shadow: 0 4px 8px rgba(255, 141, 161, 0.3);
+ transform: translateY(-3px);
+}
+
+.btn-primary:active {
+ transform: translateY(-1px);
+}
+
+.btn-secondary {
+ color: #777;
+ background-color: #f8f9fa;
+ border-color: #e6e6e6;
+}
+
+.btn-secondary:hover {
+ color: #555;
+ background-color: #f1f1f1;
+ border-color: #d9d9d9;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
+ transform: translateY(-3px);
+}
+
+.btn-outline-primary {
+ color: #ff8da1;
+ background-color: transparent;
+ border-color: #ff8da1;
+}
+
+.btn-outline-primary:hover {
+ color: #fff;
+ background-color: #ff8da1;
+ border-color: #ff8da1;
+ box-shadow: 0 4px 8px rgba(255, 141, 161, 0.2);
+ transform: translateY(-2px);
+}
+
+.btn i {
+ margin-right: 6px;
+ transition: transform 0.3s ease;
+}
+
+.btn:hover i {
+ transform: translateX(-3px);
+}
+
+/* 禁用状态 */
+.btn:disabled,
+.btn.disabled {
+ opacity: 0.65;
+ cursor: not-allowed;
+ transform: none !important;
+ box-shadow: none !important;
+}
+
+/* 提示信息 */
+.alert {
+ position: relative;
+ padding: 14px 20px;
+ margin-bottom: 25px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
+}
+
+.alert-danger {
+ color: #ff5c77;
+ background-color: #fff0f3;
+ border-color: #ffe0e5;
+}
+
+/* 装饰元素 */
+.form-card::before {
+ content: "";
+ position: absolute;
+ top: -15px;
+ right: 30px;
+ width: 40px;
+ height: 40px;
+ background-color: #ffeaef;
+ border-radius: 50%;
+ z-index: -1;
+ opacity: 0.8;
+ animation: float 6s ease-in-out infinite;
+}
+
+.form-card::after {
+ content: "";
+ position: absolute;
+ bottom: -20px;
+ left: 50px;
+ width: 60px;
+ height: 60px;
+ background-color: #ffeaef;
+ border-radius: 50%;
+ z-index: -1;
+ opacity: 0.6;
+ animation: float 7s ease-in-out infinite reverse;
+}
+
+/* 修复选择框问题 */
+s/* 专门修复下拉框文字显示问题 */
+select.form-control {
+ /* 保持一致的外观 */
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23ff8da1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 12px center;
+ background-size: 16px;
+
+ /* 修正文字显示问题 */
+ padding: 12px 40px 12px 15px; /* 增加右侧内边距,确保文字不被箭头遮挡 */
+ text-overflow: ellipsis; /* 如果文字太长会显示省略号 */
+ white-space: nowrap; /* 防止文本换行 */
+ color: #555 !important; /* 强制文本颜色 */
+ font-weight: normal;
+ line-height: 1.5;
+ position: relative;
+ z-index: 1;
+}
+
+/* 确保选定的选项能被完整显示 */
+select.form-control option {
+ padding: 10px 15px;
+ color: #555;
+ background-color: #fff;
+ font-size: 15px;
+ line-height: 1.5;
+}
+
+/* 针对特定浏览器的修复 */
+@-moz-document url-prefix() {
+ select.form-control {
+ color: #555;
+ text-indent: 0;
+ text-overflow: clip;
+ }
+}
+
+/* 针对Safari的修复 */
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+ select.form-control {
+ text-indent: 1px;
+ text-overflow: clip;
+ }
+}
+
+/* 设置选中文本的样式 */
+select.form-control:focus option:checked {
+ background: #ffeaef;
+ color: #555;
+}
+
+/* 修复IE特定问题 */
+select::-ms-expand {
+ display: none;
+}
+
+/* 确保选项在下拉框中正确展示 */
+select.form-control option {
+ font-weight: normal;
+}
+
+/* 解决Chrome中的问题 */
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+ select.form-control {
+ border-radius: 8px;
+ }
+}
+
+/* 更明确地设置选择状态的样式 */
+select.form-control {
+ border: 1.5px solid #ffd1dc;
+ background-color: #fff;
+}
+
+select.form-control:focus {
+ border-color: #ff8da1;
+ outline: 0;
+ box-shadow: 0 0 0 3px rgba(255, 141, 161, 0.25);
+}
+
+/* 尝试不同的方式设置下拉箭头 */
+.select-wrapper {
+ position: relative;
+ display: block;
+ width: 100%;
+}
+
+.select-wrapper::after {
+ content: '⌄';
+ font-size: 24px;
+ color: #ff8da1;
+ position: absolute;
+ right: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+}
+
+/* 移除自定义背景图,改用伪元素作为箭头 */
+select.form-control {
+ background-image: none;
+}
+
+/* 美化表单分组 */
+.form-card {
+ position: relative;
+ overflow: hidden;
+}
+
+.form-group {
+ position: relative;
+ z-index: 1;
+ transition: transform 0.3s ease;
+}
+
+.form-group:hover {
+ transform: translateX(5px);
+}
+
+/* 甜美风格的表单组分隔线 */
+.form-group:not(:last-child):after {
+ content: "";
+ display: block;
+ height: 1px;
+ width: 0;
+ background: linear-gradient(to right, transparent, #ffe0e8, transparent);
+ margin-top: 22px;
+ transition: width 0.5s ease;
+}
+
+.form-group:not(:last-child):hover:after {
+ width: 100%;
+}
+
+/* 必填项标记美化 */
+.form-group.required label {
+ position: relative;
+}
+
+.form-group.required label:after {
+ content: " *";
+ color: #ff6b8b;
+ font-size: 18px;
+ line-height: 0;
+ position: relative;
+ top: 5px;
+ transition: all 0.3s ease;
+}
+
+.form-group.required:hover label:after {
+ color: #ff3958;
+ transform: scale(1.2);
+}
+
+/* 美化滚动条 */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #fff;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #ffc0cb;
+ border-radius: 20px;
+ border: 2px solid #fff;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: #ff8da1;
+}
+
+/* 添加动画 */
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideInRight {
+ from { opacity: 0; transform: translateX(20px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes slideInDown {
+ from { opacity: 0; transform: translateY(-20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes fadeInUp {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes float {
+ 0% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-15px);
+ }
+ 100% {
+ transform: translateY(0px);
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes shake {
+ 10%, 90% {
+ transform: translateX(-1px);
+ }
+ 20%, 80% {
+ transform: translateX(2px);
+ }
+ 30%, 50%, 70% {
+ transform: translateX(-3px);
+ }
+ 40%, 60% {
+ transform: translateX(3px);
+ }
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .form-actions {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .input-with-button {
+ flex-direction: column;
+ }
+
+ .page-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .page-header .actions {
+ margin-top: 12px;
+ }
+
+ .btn {
+ width: 100%;
+ }
+}
+
+/* 表单光影效果 */
+.form-card {
+ position: relative;
+ overflow: hidden;
+}
+
+.form-card:before, .form-card:after {
+ content: "";
+ position: absolute;
+ z-index: -1;
+}
+
+/* 移入表单时添加光晕效果 */
+.form-card:hover:before {
+ content: "";
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: radial-gradient(circle, rgba(255,232,238,0.3) 0%, rgba(255,255,255,0) 70%);
+ animation: glowEffect 2s infinite linear;
+}
+
+@keyframes glowEffect {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+/* 输入焦点时的动画 */
+.form-control:focus {
+ animation: focusPulse 1s infinite alternate;
+}
+
+@keyframes focusPulse {
+ from {
+ box-shadow: 0 0 0 3px rgba(255, 141, 161, 0.25);
+ }
+ to {
+ box-shadow: 0 0 0 5px rgba(255, 141, 161, 0.15);
+ }
+}
diff --git a/app/static/js/book-add.js b/app/static/js/book-add.js
index 4918f24..c25b94a 100644
--- a/app/static/js/book-add.js
+++ b/app/static/js/book-add.js
@@ -127,7 +127,7 @@ $(document).ready(function() {
}
// 初始化表单验证
- ffunction initFormValidation() {
+ function initFormValidation() {
$('#bookForm').on('submit', function(e) {
// 如果表单正在提交中,阻止重复提交
if (isSubmitting) {
diff --git a/app/static/js/book-edit.js b/app/static/js/book-edit.js
new file mode 100644
index 0000000..456988b
--- /dev/null
+++ b/app/static/js/book-edit.js
@@ -0,0 +1,176 @@
+// 用于调试
+console.log("ISBN验证脚本已加载 v2.0");
+
+$(document).ready(function() {
+ console.log("DOM已加载,开始设置ISBN验证");
+
+ // 获取ISBN输入框
+ const isbnInput = $("#isbn");
+
+ if (isbnInput.length === 0) {
+ console.error("找不到ISBN输入字段!");
+ return;
+ }
+
+ console.log("找到ISBN输入框:", isbnInput.val());
+
+ // 添加ISBN帮助文本和错误提示
+ isbnInput.after('请输入有效的10位或13位ISBN号码
');
+ isbnInput.after('请输入有效的ISBN-10或ISBN-13格式');
+
+ // 验证ISBN函数
+ function validateISBN(isbn) {
+ // 空值视为有效
+ if (!isbn || isbn.trim() === '') return true;
+
+ // 移除所有连字符和空格
+ isbn = isbn.replace(/[-\s]/g, '');
+
+ console.log("验证ISBN:", isbn, "长度:", isbn.length);
+
+ // 长度检查
+ if (isbn.length !== 10 && isbn.length !== 13) {
+ console.log("ISBN长度无效");
+ return false;
+ }
+
+ // ISBN-10验证
+ if (isbn.length === 10) {
+ // 检查前9位是否为数字,最后一位可以是X
+ if (!/^\d{9}[\dXx]$/.test(isbn)) {
+ console.log("ISBN-10格式错误");
+ return false;
+ }
+
+ let sum = 0;
+ for (let i = 0; i < 9; i++) {
+ sum += parseInt(isbn[i]) * (10 - i);
+ }
+
+ // 处理校验位
+ let checkDigit = isbn[9].toUpperCase() === 'X' ? 10 : parseInt(isbn[9]);
+ sum += checkDigit;
+
+ let valid = (sum % 11 === 0);
+ console.log("ISBN-10校验结果:", valid);
+ return valid;
+ }
+
+ // ISBN-13验证
+ if (isbn.length === 13) {
+ // 检查是否全是数字
+ if (!/^\d{13}$/.test(isbn)) {
+ console.log("ISBN-13必须全是数字");
+ return false;
+ }
+
+ let sum = 0;
+ for (let i = 0; i < 12; i++) {
+ sum += parseInt(isbn[i]) * (i % 2 === 0 ? 1 : 3);
+ }
+
+ let checkDigit = (10 - (sum % 10)) % 10;
+ let valid = (checkDigit === parseInt(isbn[12]));
+
+ console.log("ISBN-13校验结果:", valid);
+ return valid;
+ }
+
+ return false;
+ }
+
+ // 添加输入事件处理
+ isbnInput.on('input', function() {
+ const value = $(this).val().trim();
+
+ if (value === '') {
+ // 不验证空值
+ $(this).removeClass('is-invalid is-valid');
+ } else if (validateISBN(value)) {
+ $(this).removeClass('is-invalid').addClass('is-valid');
+ } else {
+ $(this).removeClass('is-valid').addClass('is-invalid');
+ }
+ });
+
+ // 表单提交验证
+ const form = $(".book-form");
+ form.on('submit', function(event) {
+ const isbnValue = isbnInput.val().trim();
+
+ console.log("表单提交,ISBN值:", isbnValue);
+
+ // 仅当有ISBN且无效时才阻止提交
+ if (isbnValue !== '' && !validateISBN(isbnValue)) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ isbnInput.addClass('is-invalid');
+ isbnInput.focus();
+
+ alert('请输入有效的ISBN号码,或将此字段留空');
+ console.log("表单提交被阻止 - ISBN无效");
+
+ return false;
+ }
+
+ console.log("表单提交继续 - ISBN有效或为空");
+ });
+
+ // 处理封面图片预览
+ $('#cover').change(function() {
+ const file = this.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ $('#coverPreview').html(`
`);
+ };
+ reader.readAsDataURL(file);
+ } else {
+ // 默认回退
+ if (typeof bookCoverUrl !== 'undefined' && bookCoverUrl) {
+ $('#coverPreview').html(`
`);
+ } else {
+ $('#coverPreview').html(`
+
+
+ 暂无封面
+
+ `);
+ }
+ }
+ });
+
+ // 在页面加载时初始验证现有ISBN
+ if (isbnInput.val()) {
+ isbnInput.trigger('input');
+ }
+
+ // 添加帮助样式
+ $("