This commit is contained in:
superlishunqin 2025-05-06 12:01:11 +08:00
parent 5f46d87528
commit 29914a4178
44 changed files with 20245 additions and 443 deletions

View File

@ -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()}

View File

@ -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():
<p>是否管理员: {'' if current_user.role_id == 1 else ''}</p>
<p><a href="/book/admin/list">尝试访问管理页面</a></p>
"""
# 添加到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,)

View File

@ -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/<int:borrow_id>', 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/<int:borrow_id>', 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/<int:borrow_id>', 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)}'
})

View File

@ -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/<int:book_id>', 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/<int:book_id>/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)

View File

@ -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)

View File

@ -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'<Book {self.title}>'

View File

@ -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):

View File

@ -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)}'

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -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;
}

View File

@ -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;
}
}

860
app/static/css/browse.css Normal file
View File

@ -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%;
}
}

View File

@ -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;
}

View File

@ -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%;
}
}

View File

@ -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,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M95,50A45,45,0,1,1,50,5,45,45,0,0,1,95,50Z" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="10"/></svg>') 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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

396
app/static/css/overdue.css Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -127,7 +127,7 @@ $(document).ready(function() {
}
// 初始化表单验证
ffunction initFormValidation() {
function initFormValidation() {
$('#bookForm').on('submit', function(e) {
// 如果表单正在提交中,阻止重复提交
if (isSubmitting) {

View File

@ -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('<div class="invalid-feedback" id="isbn-error">请输入有效的10位或13位ISBN号码</div>');
isbnInput.after('<small class="form-text text-muted">请输入有效的ISBN-10或ISBN-13格式</small>');
// 验证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(`<img src="${e.target.result}" class="cover-image" alt="新封面预览">`);
};
reader.readAsDataURL(file);
} else {
// 默认回退
if (typeof bookCoverUrl !== 'undefined' && bookCoverUrl) {
$('#coverPreview').html(`<img src="${bookCoverUrl}" class="cover-image" alt="${bookTitle || '图书封面'}">`);
} else {
$('#coverPreview').html(`
<div class="no-cover-placeholder">
<i class="fas fa-book"></i>
<span>暂无封面</span>
</div>
`);
}
}
});
// 在页面加载时初始验证现有ISBN
if (isbnInput.val()) {
isbnInput.trigger('input');
}
// 添加帮助样式
$("<style>")
.prop("type", "text/css")
.html(`
.is-invalid {
border-color: #dc3545 !important;
background-color: #fff8f8 !important;
}
.is-valid {
border-color: #28a745 !important;
background-color: #f8fff8 !important;
}
.invalid-feedback {
display: none;
color: #dc3545;
font-size: 80%;
margin-top: 0.25rem;
}
.is-invalid + .invalid-feedback,
.is-invalid ~ .invalid-feedback {
display: block !important;
}
`)
.appendTo("head");
});

View File

@ -0,0 +1,244 @@
// borrow_management.js
document.addEventListener('DOMContentLoaded', function() {
// 归还图书功能
const returnButtons = document.querySelectorAll('.return-btn');
const returnModal = document.getElementById('returnModal');
const returnBookTitle = document.getElementById('returnBookTitle');
const confirmReturnButton = document.getElementById('confirmReturn');
let currentBorrowId = null;
returnButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
returnBookTitle.textContent = bookTitle;
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#returnModal').modal('show');
});
});
confirmReturnButton.addEventListener('click', function() {
if (!currentBorrowId) return;
// 发送归还请求
fetch(`/borrow/return/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#returnModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 重新加载页面以更新借阅状态
setTimeout(() => window.location.reload(), 1500);
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#returnModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 续借图书功能
const renewButtons = document.querySelectorAll('.renew-btn');
const renewModal = document.getElementById('renewModal');
const renewBookTitle = document.getElementById('renewBookTitle');
const confirmRenewButton = document.getElementById('confirmRenew');
renewButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
renewBookTitle.textContent = bookTitle;
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#renewModal').modal('show');
});
});
confirmRenewButton.addEventListener('click', function() {
if (!currentBorrowId) return;
// 发送续借请求
fetch(`/borrow/renew/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#renewModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 重新加载页面以更新借阅状态
setTimeout(() => window.location.reload(), 1500);
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#renewModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 逾期通知功能
const notifyButtons = document.querySelectorAll('.notify-btn');
const notifyModal = document.getElementById('notifyModal');
const notifyBookTitle = document.getElementById('notifyBookTitle');
const confirmNotifyButton = document.getElementById('confirmNotify');
notifyButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
notifyBookTitle.textContent = bookTitle;
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#notifyModal').modal('show');
});
});
confirmNotifyButton.addEventListener('click', function() {
if (!currentBorrowId) return;
// 发送通知请求
fetch(`/borrow/overdue/notify/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#notifyModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 禁用已点击的通知按钮
document.querySelector(`.notify-btn[data-id="${currentBorrowId}"]`).disabled = true;
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#notifyModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 图书搜索功能
const bookSearchInput = document.getElementById('bookSearch');
const searchBookBtn = document.getElementById('searchBookBtn');
const bookSelect = document.getElementById('bookSelect');
function searchBooks() {
const searchTerm = bookSearchInput.value.trim();
if (searchTerm.length < 2) {
showAlert('warning', '请输入至少2个字符进行搜索');
return;
}
// 清空当前选项
bookSelect.innerHTML = '<option value="">正在搜索...</option>';
// 发送搜索请求
fetch(`/book/api/search?q=${encodeURIComponent(searchTerm)}`)
.then(response => response.json())
.then(data => {
bookSelect.innerHTML = '';
if (data.success && data.books.length > 0) {
// 添加找到的图书
data.books.forEach(book => {
const option = document.createElement('option');
option.value = book.id;
option.textContent = `${book.title} - ${book.author} (库存: ${book.stock})`;
// 如果库存为0禁用该选项
if (book.stock <= 0) {
option.disabled = true;
option.textContent += ' [无库存]';
}
bookSelect.appendChild(option);
});
} else {
// 未找到图书
const option = document.createElement('option');
option.value = '';
option.textContent = '未找到相关图书';
bookSelect.appendChild(option);
}
})
.catch(error => {
console.error('搜索图书时出错:', error);
bookSelect.innerHTML = '<option value="">搜索失败,请重试</option>';
});
}
// 绑定搜索按钮点击事件
searchBookBtn.addEventListener('click', searchBooks);
// 绑定回车键搜索
bookSearchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
searchBooks();
}
});
// 显示提示消息
function showAlert(type, message) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show fixed-top mx-auto mt-3`;
alertDiv.style.maxWidth = '500px';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动消失
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
});

292
app/static/js/browse.js Normal file
View File

@ -0,0 +1,292 @@
// 图书浏览页面脚本
$(document).ready(function() {
// 分类筛选下拉菜单
$('.category-filter-toggle').click(function() {
$(this).toggleClass('active');
$('.category-filter-dropdown').toggleClass('show');
});
// 点击外部关闭下拉菜单
$(document).click(function(e) {
if (!$(e.target).closest('.category-filters').length) {
$('.category-filter-dropdown').removeClass('show');
$('.category-filter-toggle').removeClass('active');
}
});
// 处理借阅图书
let bookIdToBorrow = null;
let bookTitleToBorrow = '';
$('.borrow-book').click(function(e) {
e.preventDefault();
bookIdToBorrow = $(this).data('id');
// 获取图书标题
const bookCard = $(this).closest('.book-card');
bookTitleToBorrow = bookCard.find('.book-title').text();
$('#borrowBookTitle').text(bookTitleToBorrow);
$('#borrowModal').modal('show');
});
$('#confirmBorrow').click(function() {
if (!bookIdToBorrow) return;
// 禁用按钮防止重复提交
$(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 处理中...');
$.ajax({
url: `/borrow/add/${bookIdToBorrow}`,
type: 'POST',
success: function(response) {
$('#borrowModal').modal('hide');
if (response.success) {
showNotification(response.message, 'success');
// 更新UI显示
const bookCard = $(`.book-card[data-id="${bookIdToBorrow}"]`);
// 更改可借状态
bookCard.find('.book-ribbon span').removeClass('available').addClass('unavailable').text('已借出');
// 更改借阅按钮
bookCard.find('.btn-borrow').replaceWith(`
<button class="btn-borrow disabled" disabled>
<i class="fas fa-check-circle"></i>
</button>
`);
// 创建借阅成功动画
const successOverlay = $('<div class="borrow-success-overlay"><i class="fas fa-check-circle"></i><span>借阅成功</span></div>');
bookCard.append(successOverlay);
setTimeout(() => {
successOverlay.fadeOut(500, function() {
$(this).remove();
});
}, 2000);
} else {
showNotification(response.message, 'error');
}
// 恢复按钮状态
$('#confirmBorrow').prop('disabled', false).html('确认借阅');
},
error: function() {
$('#borrowModal').modal('hide');
showNotification('借阅操作失败,请稍后重试', 'error');
$('#confirmBorrow').prop('disabled', false).html('确认借阅');
}
});
});
// 清除模态框数据
$('#borrowModal').on('hidden.bs.modal', function() {
bookIdToBorrow = null;
bookTitleToBorrow = '';
$('#borrowBookTitle').text('');
});
// 动态添加动画CSS
const animationCSS = `
.borrow-success-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(102, 126, 234, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
border-radius: 10px;
z-index: 10;
animation: fadeIn 0.3s;
}
.borrow-success-overlay i {
font-size: 40px;
margin-bottom: 10px;
animation: scaleIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { transform: scale(0); }
to { transform: scale(1); }
}
`;
$('<style>').text(animationCSS).appendTo('head');
// 显示通知
function showNotification(message, type) {
// 移除可能存在的旧通知
$('.notification-alert').remove();
const alertClass = type === 'success' ? 'notification-success' : 'notification-error';
const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
const notification = `
<div class="notification-alert ${alertClass}">
<div class="notification-icon">
<i class="fas ${iconClass}"></i>
</div>
<div class="notification-message">${message}</div>
<button class="notification-close">
<i class="fas fa-times"></i>
</button>
</div>
`;
$('body').append(notification);
// 显示通知
setTimeout(() => {
$('.notification-alert').addClass('show');
}, 10);
// 通知自动关闭
setTimeout(() => {
$('.notification-alert').removeClass('show');
setTimeout(() => {
$('.notification-alert').remove();
}, 300);
}, 4000);
// 点击关闭按钮
$('.notification-close').click(function() {
$(this).closest('.notification-alert').removeClass('show');
setTimeout(() => {
$(this).closest('.notification-alert').remove();
}, 300);
});
}
// 添加通知样式
const notificationCSS = `
.notification-alert {
position: fixed;
top: 20px;
right: 20px;
min-width: 300px;
max-width: 400px;
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
padding: 15px;
transform: translateX(calc(100% + 20px));
transition: transform 0.3s ease;
z-index: 9999;
}
.notification-alert.show {
transform: translateX(0);
}
.notification-success {
border-left: 4px solid #4caf50;
}
.notification-error {
border-left: 4px solid #f44336;
}
.notification-icon {
margin-right: 15px;
font-size: 24px;
}
.notification-success .notification-icon {
color: #4caf50;
}
.notification-error .notification-icon {
color: #f44336;
}
.notification-message {
flex: 1;
font-size: 0.95rem;
color: #3c4858;
}
.notification-close {
background: none;
border: none;
color: #a0aec0;
cursor: pointer;
padding: 5px;
margin-left: 10px;
font-size: 0.8rem;
}
.notification-close:hover {
color: #4a5568;
}
@media (max-width: 576px) {
.notification-alert {
top: auto;
bottom: 20px;
left: 20px;
right: 20px;
min-width: auto;
max-width: none;
transform: translateY(calc(100% + 20px));
}
.notification-alert.show {
transform: translateY(0);
}
}
`;
$('<style>').text(notificationCSS).appendTo('head');
// 卡片淡入效果
$('.book-card').each(function(index) {
$(this).css('animation-delay', `${0.05 * index}s`);
});
// 鼠标悬停时添加卡片提示信息
$('.book-card').each(function() {
const title = $(this).find('.book-title').text();
const author = $(this).find('.book-author').text();
$(this).attr('title', `${title} - ${author}`);
});
// 懒加载图片处理(可选)
if ('IntersectionObserver' in window) {
const imgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imgObserver.observe(img);
});
} else {
// 回退机制,直接加载所有图片
document.querySelectorAll('img.lazy').forEach(img => {
img.src = img.dataset.src;
});
}
});

View File

@ -0,0 +1,103 @@
// 库存调整页面的JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
const changeTypeSelect = document.getElementById('change_type');
const changeAmountInput = document.getElementById('change_amount');
const stockHint = document.getElementById('stock-hint');
// 当前库存数量通过HTML中的全局变量CURRENT_STOCK获取
const currentStock = CURRENT_STOCK;
// 当改变调整类型时,更新提示信息
changeTypeSelect.addEventListener('change', updateStockHint);
changeAmountInput.addEventListener('input', updateStockHint);
function updateStockHint() {
const changeType = changeTypeSelect.value;
const changeAmount = parseInt(changeAmountInput.value) || 0;
if (changeType === 'out') {
// 出库检查
if (changeAmount > currentStock) {
stockHint.textContent = `警告: 出库数量(${changeAmount})超过当前库存(${currentStock})!`;
stockHint.className = 'form-text stock-hint danger';
changeAmountInput.setCustomValidity('出库数量不能超过当前库存');
} else {
const newStock = currentStock - changeAmount;
stockHint.textContent = `出库后库存将变为: ${newStock}`;
stockHint.className = 'form-text stock-hint';
changeAmountInput.setCustomValidity('');
if (newStock <= 5 && newStock > 0) {
stockHint.classList.add('warning');
} else if (newStock <= 0) {
stockHint.classList.add('danger');
}
}
} else {
// 入库提示
const newStock = currentStock + changeAmount;
stockHint.textContent = `入库后库存将变为: ${newStock}`;
stockHint.className = 'form-text stock-hint';
changeAmountInput.setCustomValidity('');
}
// 添加一些迪士尼风格的交互效果
addDisneyInteractions();
}
// 添加迪士尼风格的交互效果
function addDisneyInteractions() {
// 闪光效果
const sparkleEffect = document.querySelector('.disney-sparkles');
if (sparkleEffect) {
sparkleEffect.style.opacity = '0.7';
setTimeout(() => { sparkleEffect.style.opacity = '0'; }, 800);
}
// 按钮动画效果
const confirmBtn = document.querySelector('.disney-confirm-btn');
if (confirmBtn) {
confirmBtn.classList.add('active');
setTimeout(() => { confirmBtn.classList.remove('active'); }, 300);
}
}
// 初始化提示
updateStockHint();
// 表单提交前验证
document.querySelector('form').addEventListener('submit', function(event) {
const changeType = changeTypeSelect.value;
const changeAmount = parseInt(changeAmountInput.value) || 0;
if (changeType === 'out' && changeAmount > currentStock) {
event.preventDefault();
alert('出库数量不能超过当前库存!');
return false;
}
if (changeAmount <= 0) {
event.preventDefault();
alert('调整数量必须大于0!');
return false;
}
// 添加提交成功的动画效果
const card = document.querySelector('.disney-inventory-card');
if (card) {
card.classList.add('submitting');
}
});
// 为表单元素添加迪士尼风格的交互效果
const formElements = document.querySelectorAll('.disney-select, .disney-input, .disney-textarea');
formElements.forEach(element => {
element.addEventListener('focus', function() {
this.parentNode.classList.add('focused');
});
element.addEventListener('blur', function() {
this.parentNode.classList.remove('focused');
});
});
});

View File

@ -0,0 +1,263 @@
// 冰雪奇缘风格的图书库存日志页面JavaScript
document.addEventListener('DOMContentLoaded', function() {
// 雪花动画效果
createSnowflakes();
// 表格行动画效果
animateTableRows();
// 为表格行添加互动效果
addTableRowInteractions();
// 添加书籍封面魔法效果
addBookCoverMagic();
// 添加按钮魔法效果
addButtonMagic();
// 响应式表格标签
makeTableResponsive();
});
// 创建额外的雪花
function createSnowflakes() {
const snowflakesContainer = document.querySelector('.snowflakes');
if (!snowflakesContainer) return;
// 添加更多雪花共30个
for (let i = 0; i < 20; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'snowflake';
snowflake.textContent = ['❄', '❅', '❆'][Math.floor(Math.random() * 3)];
// 随机位置和动画
const left = Math.random() * 100;
const animDuration = 8 + Math.random() * 10;
const animDelay = Math.random() * 5;
const fontSize = 0.8 + Math.random() * 1.2;
snowflake.style.left = `${left}%`;
snowflake.style.animationDuration = `${animDuration}s`;
snowflake.style.animationDelay = `${animDelay}s`;
snowflake.style.fontSize = `${fontSize}em`;
snowflakesContainer.appendChild(snowflake);
}
}
// 表格行动画效果
function animateTableRows() {
const tableRows = document.querySelectorAll('.table-row');
tableRows.forEach((row, index) => {
// 设置动画延迟,创建瀑布效果
row.style.animationDelay = `${index * 0.08}s`;
row.classList.add('fade-in');
// 为入库和出库行添加不同的动画效果
if (row.dataset.type === 'in') {
row.classList.add('in-animation');
} else if (row.dataset.type === 'out') {
row.classList.add('out-animation');
}
});
}
// 为表格行添加互动效果
function addTableRowInteractions() {
const tableRows = document.querySelectorAll('.table-row');
tableRows.forEach(row => {
// 点击高亮效果
row.addEventListener('click', function() {
// 移除其他行的选中状态
document.querySelectorAll('.table-row').forEach(r => {
r.classList.remove('selected-row');
});
// 添加当前行的选中状态
this.classList.add('selected-row');
// 添加闪光效果
const sparkle = document.createElement('div');
sparkle.className = 'row-sparkle';
this.appendChild(sparkle);
// 移除闪光效果
setTimeout(() => {
sparkle.remove();
}, 800);
});
// 鼠标悬停效果
row.addEventListener('mouseenter', function() {
// 添加冰晶效果
const iceEffect = document.createElement('div');
iceEffect.className = 'ice-effect';
this.appendChild(iceEffect);
// 移除冰晶效果
this.addEventListener('mouseleave', function() {
iceEffect.remove();
}, { once: true });
});
});
}
// 添加书籍封面魔法效果
function addBookCoverMagic() {
const bookCover = document.querySelector('.book-cover');
const bookFrame = document.querySelector('.book-frame');
if (bookCover && bookFrame) {
// 鼠标移动时添加3D效果
bookFrame.addEventListener('mousemove', function(e) {
const rect = this.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const deltaX = (x - centerX) / 20;
const deltaY = (y - centerY) / 20;
bookCover.style.transform = `rotate(0deg) perspective(800px) rotateX(${-deltaY}deg) rotateY(${deltaX}deg)`;
});
// 鼠标离开时恢复
bookFrame.addEventListener('mouseleave', function() {
bookCover.style.transform = 'rotate(3deg)';
});
// 点击时添加闪光效果
bookFrame.addEventListener('click', function() {
const glow = document.querySelector('.book-glow');
glow.style.opacity = '1';
glow.style.background = 'radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.8), rgba(173, 216, 230, 0) 70%)';
setTimeout(() => {
glow.style.opacity = '0';
}, 500);
});
}
}
// 添加按钮魔法效果
function addButtonMagic() {
const buttons = document.querySelectorAll('.frozen-btn');
buttons.forEach(button => {
button.addEventListener('mouseenter', function() {
// 创建冰晶效果
const sparkles = document.createElement('div');
sparkles.className = 'btn-sparkles';
this.appendChild(sparkles);
// 冰晶动画
for (let i = 0; i < 5; i++) {
const sparkle = document.createElement('div');
sparkle.className = 'btn-sparkle';
sparkle.style.left = `${Math.random() * 100}%`;
sparkle.style.top = `${Math.random() * 100}%`;
sparkle.style.animationDelay = `${Math.random() * 0.5}s`;
sparkles.appendChild(sparkle);
}
});
button.addEventListener('mouseleave', function() {
const sparkles = this.querySelector('.btn-sparkles');
if (sparkles) {
sparkles.remove();
}
});
});
}
// 响应式表格
function makeTableResponsive() {
if (window.innerWidth <= 768) {
const headerCells = document.querySelectorAll('.th-frozen');
const headers = Array.from(headerCells).map(cell => cell.textContent);
const dataCells = document.querySelectorAll('.td-frozen');
dataCells.forEach((cell, index) => {
const headerIndex = index % headers.length;
cell.setAttribute('data-label', headers[headerIndex]);
});
}
}
// 创建额外CSS样式
function addExtraStyles() {
const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerHTML = `
.row-sparkle {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
pointer-events: none;
animation: rowSparkle 0.8s ease forwards;
}
@keyframes rowSparkle {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.ice-effect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0), rgba(173, 216, 230, 0.1), rgba(255, 255, 255, 0));
pointer-events: none;
backdrop-filter: brightness(1.03);
}
.btn-sparkles {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.btn-sparkle {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.8);
pointer-events: none;
animation: btnSparkle 1s ease infinite;
}
@keyframes btnSparkle {
0%, 100% { transform: scale(0); opacity: 0; }
50% { transform: scale(1); opacity: 1; }
}
.in-animation {
border-left: 3px solid rgba(3, 169, 244, 0.5);
}
.out-animation {
border-left: 3px solid rgba(255, 152, 0, 0.5);
}
`;
document.head.appendChild(styleSheet);
}
// 初始化额外样式
addExtraStyles();
// 窗口大小变化时重新调整
window.addEventListener('resize', makeTableResponsive);

View File

@ -0,0 +1,30 @@
// 库存管理页面的JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
// 库存显示颜色标记
const stockElements = document.querySelectorAll('.book-stock');
stockElements.forEach(element => {
const stockValue = parseInt(element.textContent.trim());
if (stockValue <= 0) {
element.classList.add('book-stock-critical');
} else if (stockValue <= 5) {
element.classList.add('book-stock-warning');
} else {
element.classList.add('book-stock-normal');
}
});
// 表格排序功能
const tableHeaders = document.querySelectorAll('th[data-sort]');
tableHeaders.forEach(header => {
header.addEventListener('click', function() {
const sort = this.dataset.sort;
const currentOrder = new URLSearchParams(window.location.search).get('order') || 'asc';
const newOrder = currentOrder === 'asc' ? 'desc' : 'asc';
const url = new URL(window.location);
url.searchParams.set('sort', sort);
url.searchParams.set('order', newOrder);
window.location.href = url.toString();
});
});
});

View File

@ -0,0 +1,219 @@
// 库存日志页面的JavaScript功能
document.addEventListener('DOMContentLoaded', function() {
// 日期选择器联动
const dateFrom = document.getElementById('date_from');
const dateTo = document.getElementById('date_to');
// 确保结束日期不早于开始日期
if (dateFrom && dateTo) {
dateFrom.addEventListener('change', function() {
if (dateTo.value && dateFrom.value > dateTo.value) {
dateTo.value = dateFrom.value;
}
});
dateTo.addEventListener('change', function() {
if (dateFrom.value && dateFrom.value > dateTo.value) {
dateFrom.value = dateTo.value;
}
});
}
// 重置筛选按钮
const resetButton = document.getElementById('reset-filters');
if (resetButton) {
resetButton.addEventListener('click', function() {
document.getElementById('book_id').value = '';
document.getElementById('change_type').value = '';
document.getElementById('date_from').value = '';
document.getElementById('date_to').value = '';
// 提交表单以应用重置的筛选条件
document.querySelector('form').submit();
});
}
// 备注信息悬停显示完整内容
const remarkCells = document.querySelectorAll('.remark-cell');
remarkCells.forEach(cell => {
if (cell.offsetWidth < cell.scrollWidth) {
cell.title = cell.textContent;
}
});
// 创建雪花效果
createSnowflakes();
// 初始化particles.js特效
initParticles();
// 给表格行添加动画延迟
animateTableRows();
});
// 创建雪花效果
function createSnowflakes() {
const snowflakesCount = 50; // 雪花数量
const container = document.body;
const snowflakeChars = ['❄', '❅', '❆', '✱', '*'];
for (let i = 0; i < snowflakesCount; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'snowflake';
snowflake.textContent = snowflakeChars[Math.floor(Math.random() * snowflakeChars.length)];
// 随机样式
snowflake.style.left = `${Math.random() * 100}%`;
snowflake.style.opacity = Math.random();
snowflake.style.fontSize = `${Math.random() * 15 + 10}px`;
// 动画
const duration = Math.random() * 30 + 20;
snowflake.style.animationDuration = `${duration}s`;
snowflake.style.animationDelay = `${Math.random() * 5}s`;
container.appendChild(snowflake);
}
}
// 初始化particles.js
function initParticles() {
if (typeof particlesJS !== 'undefined') {
particlesJS('magic-particles', {
"particles": {
"number": {
"value": 80,
"density": {
"enable": true,
"value_area": 800
}
},
"color": {
"value": ["#a2d5f2", "#6fa8dc", "#cfe2f3", "#b19cd9"]
},
"shape": {
"type": ["circle", "star"],
"stroke": {
"width": 0,
"color": "#000000"
},
"polygon": {
"nb_sides": 5
}
},
"opacity": {
"value": 0.3,
"random": true,
"anim": {
"enable": true,
"speed": 1,
"opacity_min": 0.1,
"sync": false
}
},
"size": {
"value": 5,
"random": true,
"anim": {
"enable": false,
"speed": 40,
"size_min": 0.1,
"sync": false
}
},
"line_linked": {
"enable": true,
"distance": 150,
"color": "#a2d5f2",
"opacity": 0.2,
"width": 1
},
"move": {
"enable": true,
"speed": 2,
"direction": "none",
"random": true,
"straight": false,
"out_mode": "out",
"bounce": false,
"attract": {
"enable": true,
"rotateX": 600,
"rotateY": 1200
}
}
},
"interactivity": {
"detect_on": "canvas",
"events": {
"onhover": {
"enable": true,
"mode": "grab"
},
"onclick": {
"enable": true,
"mode": "push"
},
"resize": true
},
"modes": {
"grab": {
"distance": 140,
"line_linked": {
"opacity": 0.5
}
},
"bubble": {
"distance": 400,
"size": 4,
"duration": 2,
"opacity": 1,
"speed": 3
},
"repulse": {
"distance": 200,
"duration": 0.4
},
"push": {
"particles_nb": 4
},
"remove": {
"particles_nb": 2
}
}
},
"retina_detect": true
});
}
}
// 给表格行添加动画延迟
function animateTableRows() {
const rows = document.querySelectorAll('.log-row');
rows.forEach((row, index) => {
row.style.animationDelay = `${index * 0.05}s`;
row.style.animationDuration = '0.5s';
row.style.animationName = 'fadeInUp';
row.style.animationFillMode = 'both';
});
}
// 添加淡入向上动画
const fadeInUpKeyframes = `
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}`;
// 添加动画样式到文档
document.addEventListener('DOMContentLoaded', function() {
const style = document.createElement('style');
style.textContent = fadeInUpKeyframes;
document.head.appendChild(style);
});

132
app/static/js/my_borrows.js Normal file
View File

@ -0,0 +1,132 @@
// my_borrows.js
document.addEventListener('DOMContentLoaded', function() {
// 归还图书功能
const returnButtons = document.querySelectorAll('.return-btn');
const returnModal = document.getElementById('returnModal');
const returnBookTitle = document.getElementById('returnBookTitle');
const confirmReturnButton = document.getElementById('confirmReturn');
let currentBorrowId = null;
returnButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
returnBookTitle.textContent = bookTitle;
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#returnModal').modal('show');
});
});
confirmReturnButton.addEventListener('click', function() {
if (!currentBorrowId) return;
// 发送归还请求
fetch(`/borrow/return/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#returnModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 重新加载页面以更新借阅状态
setTimeout(() => window.location.reload(), 1500);
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#returnModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 续借图书功能
const renewButtons = document.querySelectorAll('.renew-btn');
const renewModal = document.getElementById('renewModal');
const renewBookTitle = document.getElementById('renewBookTitle');
const confirmRenewButton = document.getElementById('confirmRenew');
renewButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
renewBookTitle.textContent = bookTitle;
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#renewModal').modal('show');
});
});
confirmRenewButton.addEventListener('click', function() {
if (!currentBorrowId) return;
// 发送续借请求
fetch(`/borrow/renew/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#renewModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 重新加载页面以更新借阅状态
setTimeout(() => window.location.reload(), 1500);
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#renewModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 显示提示消息
function showAlert(type, message) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show fixed-top mx-auto mt-3`;
alertDiv.style.maxWidth = '500px';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动消失
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
});

138
app/static/js/overdue.js Normal file
View File

@ -0,0 +1,138 @@
// overdue.js
document.addEventListener('DOMContentLoaded', function() {
// 归还图书功能
const returnButtons = document.querySelectorAll('.return-btn');
const returnModal = document.getElementById('returnModal');
const returnBookTitle = document.getElementById('returnBookTitle');
const overdueRemark = document.getElementById('overdueRemark');
const confirmReturnButton = document.getElementById('confirmReturn');
let currentBorrowId = null;
returnButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
returnBookTitle.textContent = bookTitle;
overdueRemark.value = '';
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#returnModal').modal('show');
});
});
confirmReturnButton.addEventListener('click', function() {
if (!currentBorrowId) return;
const remark = overdueRemark.value.trim();
// 发送归还请求
fetch(`/borrow/return/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
remark: remark
})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#returnModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 重新加载页面以更新借阅状态
setTimeout(() => window.location.reload(), 1500);
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#returnModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 逾期通知功能
const notifyButtons = document.querySelectorAll('.notify-btn');
const notifyModal = document.getElementById('notifyModal');
const notifyBookTitle = document.getElementById('notifyBookTitle');
const confirmNotifyButton = document.getElementById('confirmNotify');
notifyButtons.forEach(button => {
button.addEventListener('click', function() {
const borrowId = this.getAttribute('data-id');
const bookTitle = this.getAttribute('data-title');
currentBorrowId = borrowId;
notifyBookTitle.textContent = bookTitle;
// 使用 Bootstrap 的 jQuery 方法显示模态框
$('#notifyModal').modal('show');
});
});
confirmNotifyButton.addEventListener('click', function() {
if (!currentBorrowId) return;
// 发送通知请求
fetch(`/borrow/overdue/notify/${currentBorrowId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
// 隐藏模态框
$('#notifyModal').modal('hide');
if (data.success) {
// 显示成功消息
showAlert('success', data.message);
// 禁用已点击的通知按钮
document.querySelector(`.notify-btn[data-id="${currentBorrowId}"]`).disabled = true;
} else {
// 显示错误消息
showAlert('danger', data.message);
}
})
.catch(error => {
$('#notifyModal').modal('hide');
showAlert('danger', '操作失败,请稍后重试');
console.error('Error:', error);
});
});
// 显示提示消息
function showAlert(type, message) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show fixed-top mx-auto mt-3`;
alertDiv.style.maxWidth = '500px';
alertDiv.style.zIndex = '9999';
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
`;
document.body.appendChild(alertDiv);
// 3秒后自动消失
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
});

140
app/static/js/user-add.js Normal file
View File

@ -0,0 +1,140 @@
document.addEventListener('DOMContentLoaded', function() {
// 密码显示/隐藏切换
const togglePasswordButtons = document.querySelectorAll('.toggle-password');
togglePasswordButtons.forEach(button => {
button.addEventListener('click', function() {
const passwordField = this.previousElementSibling;
const type = passwordField.getAttribute('type') === 'password' ? 'text' : 'password';
passwordField.setAttribute('type', type);
// 更改图标
const icon = this.querySelector('i');
if (type === 'text') {
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
});
});
// 密码一致性检查
const passwordInput = document.getElementById('password');
const confirmPasswordInput = document.getElementById('confirm_password');
const passwordMatchMessage = document.getElementById('password-match-message');
function checkPasswordMatch() {
if (confirmPasswordInput.value === '') {
passwordMatchMessage.textContent = '';
passwordMatchMessage.className = 'form-text';
return;
}
if (passwordInput.value === confirmPasswordInput.value) {
passwordMatchMessage.textContent = '密码匹配';
passwordMatchMessage.className = 'form-text text-success';
} else {
passwordMatchMessage.textContent = '密码不匹配';
passwordMatchMessage.className = 'form-text text-danger';
}
}
passwordInput.addEventListener('input', checkPasswordMatch);
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
// 发送邮箱验证码
const sendVerificationCodeButton = document.getElementById('sendVerificationCode');
const emailInput = document.getElementById('email');
sendVerificationCodeButton.addEventListener('click', function() {
const email = emailInput.value.trim();
// 验证邮箱格式
if (!email) {
alert('请输入邮箱地址');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('请输入有效的邮箱地址');
return;
}
// 禁用按钮,防止重复点击
this.disabled = true;
this.textContent = '发送中...';
// 发送AJAX请求
fetch('/user/send_verification_code', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 成功发送,开始倒计时
startCountdown(this);
alert(data.message);
} else {
// 发送失败,恢复按钮状态
this.disabled = false;
this.textContent = '发送验证码';
alert(data.message || '发送失败,请稍后重试');
}
})
.catch(error => {
console.error('Error:', error);
this.disabled = false;
this.textContent = '发送验证码';
alert('发送失败,请稍后重试');
});
});
// 验证码倒计时(60秒)
function startCountdown(button) {
let seconds = 60;
const originalText = '发送验证码';
const countdownInterval = setInterval(() => {
seconds--;
button.textContent = `${seconds}秒后重发`;
if (seconds <= 0) {
clearInterval(countdownInterval);
button.textContent = originalText;
button.disabled = false;
}
}, 1000);
}
// 表单提交前验证
const addUserForm = document.getElementById('addUserForm');
addUserForm.addEventListener('submit', function(event) {
// 检查密码是否匹配
if (passwordInput.value !== confirmPasswordInput.value) {
event.preventDefault();
alert('两次输入的密码不匹配,请重新输入');
return;
}
// 如果还有其他前端验证,可以继续添加
});
// 自动填充用户名为昵称的默认值
const usernameInput = document.getElementById('username');
const nicknameInput = document.getElementById('nickname');
usernameInput.addEventListener('change', function() {
// 只有当昵称字段为空时才自动填充
if (!nicknameInput.value) {
nicknameInput.value = this.value;
}
});
});

View File

@ -24,10 +24,10 @@
<a href="{{ url_for('index') }}"><i class="fas fa-home"></i> 首页</a>
</li>
<li class="{% if '/book/list' in request.path %}active{% endif %}">
<a href="{{ url_for('book.book_list') }}"><i class="fas fa-book"></i> 图书浏览</a>
<a href="{{ url_for('book.browse_books') }}"><i class="fas fa-book"></i> 图书浏览</a>
</li>
<li class="{% if '/borrow' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a>
<a href="{{ url_for('borrow.my_borrows') }}"><i class="fas fa-bookmark"></i> 我的借阅</a>
</li>
<li class="{% if '/announcement' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-bell"></i> 通知公告</a>
@ -44,10 +44,12 @@
<a href="{{ url_for('book.admin_book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
</li>
<li class="{% if '/borrow/manage' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a>
{% if current_user.role_id == 1 %}
<a href="{{ url_for('borrow.manage_borrows') }}"><i class="fas fa-exchange-alt"></i> 借阅管理</a>
{% endif %}
</li>
<li class="{% if '/inventory' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-warehouse"></i> 库存管理</a>
<a href="{{ url_for('inventory.inventory_list') }}"><i class="fas fa-warehouse"></i> 库存管理</a>
</li>
<li class="{% if '/statistics' in request.path %}active{% endif %}">
<a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a>

View File

@ -0,0 +1,224 @@
{% extends 'base.html' %}
{% block title %}图书浏览 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/browse.css') }}">
{% endblock %}
{% block content %}
<div class="browse-container">
<!-- 装饰气泡 -->
{% for i in range(15) %}
<div class="bubble"></div>
{% endfor %}
<div class="page-header">
<h1>图书浏览</h1>
<p class="welcome-text">欢迎 <strong>{{ current_user.nickname or current_user.username }}</strong>,今天想读点什么?</p>
</div>
<div class="filter-section">
<form method="GET" action="{{ url_for('book.browse_books') }}" class="search-form">
<div class="search-row">
<div class="form-group search-group">
<input type="text" name="search" class="form-control" placeholder="搜索书名/作者/ISBN" value="{{ search }}">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="filter-row">
<div class="category-filters">
<button type="button" class="category-filter-toggle">
<i class="fas fa-tags"></i> 分类筛选
<i class="fas fa-chevron-down"></i>
</button>
<div class="category-filter-dropdown">
<a href="{{ url_for('book.browse_books', search=search, sort=sort, order=order) }}"
class="category-item {% if not category_id %}active{% endif %}">
<i class="fas fa-globe"></i> 全部
</a>
{% for category in categories %}
<a href="{{ url_for('book.browse_books', search=search, category_id=category.id, sort=sort, order=order) }}"
class="category-item {% if category_id == category.id %}active{% endif %}">
<i class="fas fa-tag"></i> {{ category.name }}
</a>
{% endfor %}
</div>
</div>
<div class="form-group filter-group">
<select name="sort" class="form-control" onchange="this.form.submit()">
<option value="id" {% if sort == 'id' %}selected{% endif %}>默认排序</option>
<option value="created_at" {% if sort == 'created_at' %}selected{% endif %}>入库时间</option>
<option value="title" {% if sort == 'title' %}selected{% endif %}>书名</option>
<option value="stock" {% if sort == 'stock' %}selected{% endif %}>库存</option>
</select>
</div>
<div class="form-group filter-group">
<select name="order" class="form-control" onchange="this.form.submit()">
<option value="desc" {% if order == 'desc' %}selected{% endif %}>降序</option>
<option value="asc" {% if order == 'asc' %}selected{% endif %}>升序</option>
</select>
</div>
</div>
</form>
</div>
<!-- 图书数据统计 -->
<div class="browse-stats">
<div class="stat-item">
<i class="fas fa-book"></i>
<div class="stat-content">
<span class="stat-value">{{ pagination.total }}</span>
<span class="stat-label">可借图书</span>
</div>
</div>
<div class="stat-item">
<i class="fas fa-tag"></i>
<div class="stat-content">
<span class="stat-value">{{ categories|length }}</span>
<span class="stat-label">图书分类</span>
</div>
</div>
{% if search %}
<div class="search-results">
搜索 "{{ search }}" 找到 {{ pagination.total }} 本图书
</div>
{% endif %}
</div>
<div class="books-grid">
{% for book in books %}
<div class="book-card" data-id="{{ book.id }}">
<div class="book-cover">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}">
{% else %}
<div class="no-cover">
<i class="fas fa-book"></i>
<span>无封面</span>
</div>
{% endif %}
<div class="cover-overlay"></div>
<div class="book-ribbon">
{% if book.stock > 0 %}
<span class="available">可借阅</span>
{% else %}
<span class="unavailable">无库存</span>
{% endif %}
</div>
</div>
<div class="book-info">
<h3 class="book-title" title="{{ book.title }}">{{ book.title }}</h3>
<div class="book-author">{{ book.author }}</div>
<div class="book-meta">
{% if book.category %}
<span class="book-category">{{ book.category.name }}</span>
{% endif %}
<span class="book-year">{{ book.publish_year or '未知年份' }}</span>
</div>
<div class="book-actions">
<a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn-detail">
<i class="fas fa-info-circle"></i> 详情
</a>
{% if book.stock > 0 %}
<a href="#" class="btn-borrow borrow-book" data-id="{{ book.id }}">
<i class="fas fa-hand-holding"></i> 借阅
</a>
{% else %}
<button class="btn-borrow disabled" disabled>
<i class="fas fa-hand-holding"></i> 暂无库存
</button>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="no-books">
<img src="{{ url_for('static', filename='images/no-books.svg') }}" alt="没有找到书籍" class="no-books-img">
<h3>没有找到符合条件的图书</h3>
<p>尝试调整搜索条件或浏览其他分类</p>
<a href="{{ url_for('book.browse_books') }}" class="btn-reset-search">
<i class="fas fa-sync"></i> 重置搜索
</a>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if pagination.pages > 1 %}
<div class="pagination-container">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('book.browse_books', page=pagination.prev_num, search=search, category_id=category_id, sort=sort, order=order) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for p in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if p %}
{% if p == pagination.page %}
<li class="page-item active">
<span class="page-link">{{ p }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('book.browse_books', page=p, search=search, category_id=category_id, sort=sort, order=order) }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('book.browse_books', page=pagination.next_num, search=search, category_id=category_id, sort=sort, order=order) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
<div class="pagination-info">
显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }}
到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
</div>
</div>
{% endif %}
</div>
<!-- 借阅确认模态框 -->
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="borrowModalLabel">确认借阅</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>确定要借阅《<span id="borrowBookTitle"></span>》吗?</p>
<p class="modal-info">借阅期限为30天请在到期前归还。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmBorrow">确认借阅</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/browse.js') }}"></script>
{{ super() }}
{% endblock %}

View File

@ -11,18 +11,33 @@
<div class="page-header">
<h1>图书详情</h1>
<div class="actions">
<!-- 根据来源返回不同页面 -->
{% if request.referrer and 'browse' in request.referrer %}
<a href="{{ url_for('book.browse_books') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回浏览
</a>
{% else %}
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
{% if current_user.role_id == 1 %}
{% endif %}
<!-- 编辑按钮只对管理员显示 -->
{% if current_user.is_authenticated and current_user.role_id == 1 %}
<a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> 编辑图书
</a>
{% endif %}
<!-- 借阅按钮对所有用户显示,但需要有库存 -->
{% if book.stock > 0 %}
<a href="#" class="btn btn-success" id="borrowBtn">
<i class="fas fa-hand-holding"></i> 借阅此书
</a>
{% else %}
<button class="btn btn-secondary" disabled>
<i class="fas fa-ban"></i> 暂无库存
</button>
{% endif %}
</div>
</div>
@ -106,7 +121,7 @@
</div>
<!-- 借阅历史 (仅管理员可见) -->
{% if current_user.role_id == 1 %}
{% if current_user.is_authenticated and current_user.role_id == 1 %}
<div class="book-borrow-history">
<h3>借阅历史</h3>
{% if borrow_records %}

View File

@ -3,175 +3,176 @@
{% block title %}编辑图书 - {{ book.title }}{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-form.css') }}">
<!-- 使用我们为成熟御姐风新建的 CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-edit.css') }}">
<!-- 字体 -->
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="book-form-container">
<div class="page-header">
<h1>编辑图书</h1>
<div class="actions">
<a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info">
<i class="fas fa-eye"></i> 查看详情
</a>
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
<!-- 显示Flash消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="page-header">
<h1>编辑图书</h1>
<div class="actions">
<a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-secondary">
<i class="fas fa-eye"></i> 查看详情
</a>
<a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
</div>
</div>
<form method="POST" enctype="multipart/form-data" class="book-form">
<div class="form-row">
<div class="col-md-8">
<div class="card">
<div class="card-header">基本信息</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-12">
<label for="title">书名 <span class="required">*</span></label>
<input type="text" class="form-control" id="title" name="title" value="{{ book.title }}" required>
</div>
</div>
<form method="POST" enctype="multipart/form-data" class="book-form">
<div class="form-row">
<div class="col-md-8">
<div class="card">
<div class="card-header">基本信息</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-6">
<label for="author">作者 <span class="required">*</span></label>
<input type="text" class="form-control" id="author" name="author" value="{{ book.author }}" required>
</div>
<div class="form-group col-md-6">
<label for="publisher">出版社</label>
<input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="isbn">ISBN</label>
<input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn or '' }}">
</div>
<div class="form-group col-md-6">
<label for="publish_year">出版年份</label>
<input type="text" class="form-control" id="publish_year" name="publish_year" value="{{ book.publish_year or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="category_id">分类</label>
<select class="form-control" id="category_id" name="category_id">
<option value="">未分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if book.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group col-md-6">
<label for="tags">标签</label>
<input type="text" class="form-control" id="tags" name="tags" value="{{ book.tags or '' }}" placeholder="多个标签用逗号分隔">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">图书简介</div>
<div class="card-body">
<div class="form-group">
<textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介">{{ book.description or '' }}</textarea>
</div>
</div>
</div>
<div class="form-group">
<label for="title">书名 <span class="required">*</span></label>
<input type="text" class="form-control" id="title" name="title" value="{{ book.title }}" required>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">封面图片</div>
<div class="card-body">
<div class="cover-preview-container">
<div class="cover-preview" id="coverPreview">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
{% else %}
<div class="no-cover-placeholder">
<i class="fas fa-image"></i>
<span>暂无封面</span>
</div>
{% endif %}
</div>
<div class="upload-container">
<label for="cover" class="btn btn-outline-primary btn-block">
<i class="fas fa-upload"></i> 更换封面
</label>
<input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">库存和价格</div>
<div class="card-body">
<div class="form-group">
<label for="stock">库存数量</label>
<input type="number" class="form-control" id="stock" name="stock" min="0" value="{{ book.stock }}">
</div>
<div class="form-group">
<label for="price">价格</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">¥</span>
</div>
<input type="number" class="form-control" id="price" name="price" step="0.01" min="0" value="{{ book.price or '' }}">
</div>
</div>
<div class="form-group">
<label for="status">状态</label>
<select class="form-control" id="status" name="status">
<option value="1" {% if book.status == 1 %}selected{% endif %}>上架</option>
<option value="0" {% if book.status == 0 %}selected{% endif %}>下架</option>
</select>
</div>
</div>
</div>
<div class="form-submit-container">
<button type="submit" class="btn btn-primary btn-lg btn-block">
<i class="fas fa-save"></i> 保存修改
</button>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="author">作者 <span class="required">*</span></label>
<input type="text" class="form-control" id="author" name="author" value="{{ book.author }}" required>
</div>
<div class="form-group col-md-6">
<label for="publisher">出版社</label>
<input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="isbn">ISBN</label>
<input type="text" class="form-control {% if isbn_error %}is-invalid{% endif %}"
id="isbn" name="isbn" value="{{ book.isbn or '' }}">
{% if isbn_error %}
<div class="invalid-feedback">
{{ isbn_error }}
</div>
{% endif %}
<small class="form-text text-muted">ISBN必须是有效的10位或13位格式</small>
</div>
<div class="form-group col-md-6">
<label for="publish_year">出版年份</label>
<input type="text" class="form-control" id="publish_year" name="publish_year" value="{{ book.publish_year or '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="category_id">分类</label>
<select class="form-control" id="category_id" name="category_id">
<option value="">请选择分类</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if book.category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group col-md-6">
<label for="tags">标签</label>
<input type="text" class="form-control" id="tags" name="tags" value="{{ book.tags or '' }}" placeholder="多个标签用逗号分隔">
</div>
</div>
</div>
</div>
</form>
<div class="card">
<div class="card-header">图书简介</div>
<div class="card-body">
<div class="form-group">
<textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介...">{{ book.description or '' }}</textarea>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">封面图片</div>
<div class="card-body">
<div class="cover-preview" id="coverPreview">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
{% else %}
<div class="no-cover-placeholder">
<i class="fas fa-book"></i>
<span>暂无封面</span>
</div>
{% endif %}
</div>
<div class="upload-container" style="margin-top: 1rem;">
<label for="cover" class="btn btn-secondary btn-block">
更换封面
</label>
<input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
</div>
</div>
</div>
<div class="card">
<div class="card-header">库存与价格</div>
<div class="card-body">
<div class="form-group">
<label for="stock">库存数量</label>
<input type="number" class="form-control" id="stock" name="stock" min="0" value="{{ book.stock }}">
</div>
<div class="form-group">
<label for="price">价格(¥)</label>
<input type="number" class="form-control" id="price" name="price" step="0.01" min="0" value="{{ book.price or '' }}">
</div>
<div class="form-group">
<label for="status">状态</label>
<select class="form-control" id="status" name="status">
<option value="1" {% if book.status == 1 %}selected{% endif %}>上架</option>
<option value="0" {% if book.status == 0 %}selected{% endif %}>下架</option>
</select>
</div>
</div>
</div>
<div class="form-submit-container">
<button type="submit" class="btn btn-primary btn-lg btn-block">
保存修改
</button>
</div>
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 封面预览
$('#cover').change(function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
$('#coverPreview').html(`<img src="${e.target.result}" class="cover-image">`);
}
reader.readAsDataURL(file);
} else {
$('#coverPreview').html(`
{% if book.cover_url %}
<img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
{% else %}
<div class="no-cover-placeholder">
<i class="fas fa-image"></i>
<span>暂无封面</span>
</div>
{% endif %}
`);
}
});
});
// 安全地传递变量给JS
const bookCoverUrl = {{ book.cover_url|default('', true)|tojson|safe }};
const bookTitle = {{ book.title|default('', true)|tojson|safe }};
const bookId = {{ book.id|default(0)|tojson|safe }};
</script>
<!-- 确保jQuery已加载 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- 加载验证脚本 -->
<script src="{{ url_for('static', filename='js/book-edit.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,317 @@
{% extends 'base.html' %}
{% block title %}借阅管理 - 图书管理系统{% endblock %}
{% block head %}
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/borrow_management.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<div class="decorative-corner"></div>
<h1 class="page-title">借阅管理</h1>
<div class="row">
<div class="col-md-8">
<div class="tabs">
<a href="{{ url_for('borrow.manage_borrows', status=1, search=search, user_id=user_id, book_id=book_id) }}" class="tab {% if status == 1 %}active{% endif %}">
当前借阅 <span class="count">{{ current_borrows_count }}</span>
</a>
<a href="{{ url_for('borrow.manage_borrows', status=0, search=search, user_id=user_id, book_id=book_id) }}" class="tab {% if status == 0 %}active{% endif %}">
已归还 <span class="count">{{ history_borrows_count }}</span>
</a>
<a href="{{ url_for('borrow.manage_borrows', search=search, user_id=user_id, book_id=book_id) }}" class="tab {% if status is none %}active{% endif %}">
全部借阅 <span class="count">{{ current_borrows_count + history_borrows_count }}</span>
</a>
<a href="{{ url_for('borrow.overdue_borrows') }}" class="tab overdue-tab">
逾期管理 <span class="count overdue-count">{{ overdue_count }}</span>
</a>
</div>
</div>
<div class="col-md-4">
<div class="float-right">
<button class="btn btn-primary" data-toggle="modal" data-target="#addBorrowModal">
<i class="fas fa-plus"></i> 添加借阅
</button>
</div>
</div>
</div>
<div class="card search-card">
<div class="card-body">
<form action="{{ url_for('borrow.manage_borrows') }}" method="GET" class="search-form">
{% if status is not none %}<input type="hidden" name="status" value="{{ status }}">{% endif %}
<div class="row">
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control" name="search" value="{{ search }}" placeholder="搜索用户名或图书标题...">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
<div class="col-md-3">
<select class="form-control" name="user_id" onchange="this.form.submit()">
<option value="">选择用户</option>
{% for user in users %}
<option value="{{ user.id }}" {% if user_id == user.id %}selected{% endif %}>
{{ user.username }}
</option>
{% endfor %}
</select>
</div>
{% if search or user_id or book_id %}
<div class="col-md-3">
<a href="{{ url_for('borrow.manage_borrows', status=status) }}" class="btn btn-outline-secondary clear-filters">
<i class="fas fa-times"></i> 清除筛选
</a>
</div>
{% endif %}
</div>
</form>
</div>
</div>
<div class="borrow-list">
{% if pagination.items %}
<table class="borrow-table">
<thead>
<tr>
<th width="10%">图书封面</th>
<th width="20%">书名</th>
<th width="15%">借阅用户</th>
<th width="12%">借阅日期</th>
<th width="12%">应还日期</th>
<th width="15%">状态</th>
<th width="16%">操作</th>
</tr>
</thead>
<tbody>
{% for borrow in pagination.items %}
<tr class="borrow-item {% if borrow.status == 1 and borrow.due_date < now %}overdue{% endif %}">
<td class="book-cover">
{% if borrow.book.cover_url %}
<img src="{{ borrow.book.cover_url }}" alt="{{ borrow.book.title }}">
{% else %}
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="{{ borrow.book.title }}">
{% endif %}
</td>
<td class="book-title">
<a href="{{ url_for('book.book_detail', book_id=borrow.book_id) }}">{{ borrow.book.title }}</a>
<div class="book-author">{{ borrow.book.author }}</div>
</td>
<td class="user-info">
<a href="{{ url_for('user.user_edit', user_id=borrow.user_id) }}">{{ borrow.user.username }}</a>
{% if borrow.user.nickname %}
<div class="user-nickname">{{ borrow.user.nickname }}</div>
{% endif %}
</td>
<td>{{ borrow.borrow_date.strftime('%Y-%m-%d') }}</td>
<td class="due-date {% if borrow.status == 1 and borrow.due_date < now %}text-danger{% endif %}">
{{ borrow.due_date.strftime('%Y-%m-%d') }}
{% if borrow.status == 1 and borrow.due_date < now %}
<span class="badge badge-danger">已逾期</span>
{% endif %}
</td>
<td>
{% if borrow.status == 1 %}
<span class="badge badge-primary">借阅中</span>
{% if borrow.renew_count > 0 %}
<span class="badge badge-info">已续借{{ borrow.renew_count }}次</span>
{% endif %}
{% else %}
<span class="badge badge-success">已归还</span>
<div class="return-date">{{ borrow.return_date.strftime('%Y-%m-%d') }}</div>
{% endif %}
</td>
<td class="actions">
{% if borrow.status == 1 %}
<button class="btn btn-sm btn-success return-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">归还</button>
{% if borrow.renew_count < 2 and borrow.due_date >= now %}
<button class="btn btn-sm btn-primary renew-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">续借</button>
{% endif %}
{% if borrow.due_date < now %}
<button class="btn btn-sm btn-warning notify-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">逾期通知</button>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 分页 -->
<div class="pagination-container">
{% if pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('borrow.manage_borrows', page=pagination.prev_num, status=status, search=search, user_id=user_id, book_id=book_id) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('borrow.manage_borrows', page=page_num, status=status, search=search, user_id=user_id, book_id=book_id) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('borrow.manage_borrows', page=pagination.next_num, status=status, search=search, user_id=user_id, book_id=book_id) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% else %}
<div class="no-records">
<i class="fas fa-book-reader empty-icon"></i>
<p class="empty-text">
{% if status == 1 %}
没有进行中的借阅记录。
{% elif status == 0 %}
没有已归还的借阅记录。
{% else %}
没有任何借阅记录。
{% endif %}
</p>
</div>
{% endif %}
</div>
</div>
<!-- 添加借阅模态框 -->
<div class="modal fade" id="addBorrowModal" tabindex="-1" role="dialog" aria-labelledby="addBorrowModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addBorrowModalLabel">添加借阅记录</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form action="{{ url_for('borrow.admin_add_borrow') }}" method="POST">
<div class="modal-body">
<div class="form-group">
<label for="userSelect">借阅用户</label>
<select class="form-control" id="userSelect" name="user_id" required>
<option value="">请选择用户</option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.username }}{% if user.nickname %} ({{ user.nickname }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="bookSelect">借阅图书</label>
<div class="input-group">
<input type="text" class="form-control" id="bookSearch" placeholder="搜索图书...">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="searchBookBtn">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<select class="form-control mt-2" id="bookSelect" name="book_id" required>
<option value="">请先搜索图书</option>
</select>
</div>
<div class="form-group">
<label for="borrowDays">借阅天数</label>
<input type="number" class="form-control" id="borrowDays" name="borrow_days" value="14" min="1" max="60">
<small class="form-text text-muted">默认借阅时间为14天</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">添加借阅</button>
</div>
</form>
</div>
</div>
</div>
<!-- 归还确认模态框 -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="returnModalLabel">归还确认</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
您确定要归还《<span id="returnBookTitle"></span>》吗?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-success" id="confirmReturn">确认归还</button>
</div>
</div>
</div>
</div>
<!-- 续借确认模态框 -->
<div class="modal fade" id="renewModal" tabindex="-1" role="dialog" aria-labelledby="renewModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renewModalLabel">续借确认</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
您确定要续借《<span id="renewBookTitle"></span>》吗续借后将延长14天的借阅期限。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmRenew">确认续借</button>
</div>
</div>
</div>
</div>
<!-- 通知确认模态框 -->
<div class="modal fade" id="notifyModal" tabindex="-1" role="dialog" aria-labelledby="notifyModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notifyModalLabel">发送逾期通知</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
您确定要发送《<span id="notifyBookTitle"></span>》的逾期通知吗?
此操作将向借阅用户发送逾期提醒消息。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="confirmNotify">发送通知</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/borrow_management.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,186 @@
{% extends 'base.html' %}
{% block title %}我的梦幻书架 - 图书管理系统{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/my_borrows.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="container">
<h1 class="page-title">✨ 我的梦幻书架 ✨</h1>
<div class="tabs">
<a href="{{ url_for('borrow.my_borrows', status=1) }}" class="tab {% if status == 1 %}active{% endif %}">
<i class="fas fa-book-open"></i> 当前借阅 <span class="count">{{ current_borrows_count }}</span>
</a>
<a href="{{ url_for('borrow.my_borrows', status=0) }}" class="tab {% if status == 0 %}active{% endif %}">
<i class="fas fa-history"></i> 历史借阅 <span class="count">{{ history_borrows_count }}</span>
</a>
<a href="{{ url_for('borrow.my_borrows') }}" class="tab {% if status is none %}active{% endif %}">
<i class="fas fa-heart"></i> 全部借阅 <span class="count">{{ current_borrows_count + history_borrows_count }}</span>
</a>
</div>
<div class="borrow-list">
{% if pagination.items %}
<table class="borrow-table">
<thead>
<tr>
<th>图书封面</th>
<th>书名</th>
<th>借阅日期</th>
<th>应还日期</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for borrow in pagination.items %}
<tr class="borrow-item {% if borrow.status == 1 and borrow.due_date < now %}overdue{% endif %}">
<td class="book-cover">
{% if borrow.book.cover_url %}
{% if borrow.book.cover_url.startswith('/') %}
<img src="{{ borrow.book.cover_url }}" alt="{{ borrow.book.title }}">
{% endif %}
{% else %}
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="{{ borrow.book.title }}">
{% endif %}
</td>
<td class="book-title">
<a href="{{ url_for('book.book_detail', book_id=borrow.book_id) }}">{{ borrow.book.title }}</a>
<div class="book-author">{{ borrow.book.author }}</div>
</td>
<td>{{ borrow.borrow_date.strftime('%Y-%m-%d') }}</td>
<td class="due-date {% if borrow.status == 1 and borrow.due_date < now %}text-danger{% endif %}">
{{ borrow.due_date.strftime('%Y-%m-%d') }}
{% if borrow.status == 1 and borrow.due_date < now %}
<span class="badge badge-danger">已逾期</span>
{% endif %}
</td>
<td>
{% if borrow.status == 1 %}
<span class="badge badge-primary">借阅中</span>
{% if borrow.renew_count > 0 %}
<span class="badge badge-info">已续借{{ borrow.renew_count }}次</span>
{% endif %}
{% else %}
<span class="badge badge-success">已归还</span>
<div class="return-date">{{ borrow.return_date.strftime('%Y-%m-%d') }}</div>
{% endif %}
</td>
<td class="actions">
{% if borrow.status == 1 %}
<button class="btn btn-sm btn-success return-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">归还</button>
{% if borrow.renew_count < 2 and borrow.due_date >= now %}
<button class="btn btn-sm btn-primary renew-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">续借</button>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 分页 -->
<div class="pagination-container">
{% if pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('borrow.my_borrows', page=pagination.prev_num, status=status) }}" aria-label="Previous">
<span aria-hidden="true"><i class="fas fa-chevron-left"></i></span>
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('borrow.my_borrows', page=page_num, status=status) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('borrow.my_borrows', page=pagination.next_num, status=status) }}" aria-label="Next">
<span aria-hidden="true"><i class="fas fa-chevron-right"></i></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% else %}
<div class="no-records">
<i class="fas fa-book-reader empty-icon"></i>
<p class="empty-text">
{% if status == 1 %}
哎呀~你还没有借阅任何图书呢!快去探索那些等待与你相遇的故事吧~
{% elif status == 0 %}
亲爱的,你还没有归还过任何图书呢~一起开启阅读的奇妙旅程吧!
{% else %}
你的书架空空如也~赶快挑选几本心动的书籍,开启你的阅读冒险吧!
{% endif %}
</p>
<a href="{{ url_for('book.book_list') }}" class="btn btn-primary">
<i class="fas fa-heart"></i> 探索好书
</a>
</div>
{% endif %}
</div>
</div>
<!-- 归还确认模态框 -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="returnModalLabel"><i class="fas fa-heart"></i> 归还确认</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
亲爱的读者,你确定要归还《<span id="returnBookTitle"></span>》这本书吗?希望它带给你美好的阅读体验~
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">再想想</button>
<button type="button" class="btn btn-success" id="confirmReturn"><i class="fas fa-check"></i> 确认归还</button>
</div>
</div>
</div>
</div>
<!-- 续借确认模态框 -->
<div class="modal fade" id="renewModal" tabindex="-1" role="dialog" aria-labelledby="renewModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renewModalLabel"><i class="fas fa-magic"></i> 续借确认</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
亲爱的读者,想要与《<span id="renewBookTitle"></span>》多相处一段时间吗续借后将延长14天的阅读时光哦
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">再想想</button>
<button type="button" class="btn btn-primary" id="confirmRenew"><i class="fas fa-check"></i> 确认续借</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/my_borrows.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,179 @@
{% extends 'base.html' %}
{% block title %}逾期管理 - 图书管理系统{% endblock %}
{% block head %}
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/overdue.css') }}">
{% endblock %}
{% block content %}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="page-title">逾期管理</h1>
<a href="{{ url_for('borrow.manage_borrows') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> 返回借阅管理
</a>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
当前共有 <strong>{{ overdue_count }}</strong> 条逾期未归还的借阅记录,请及时处理。
</div>
<div class="overdue-list">
{% if pagination.items %}
<table class="overdue-table">
<thead>
<tr>
<th width="10%">图书封面</th>
<th width="20%">书名</th>
<th width="15%">借阅用户</th>
<th width="12%">借阅日期</th>
<th width="12%">应还日期</th>
<th width="15%">逾期天数</th>
<th width="16%">操作</th>
</tr>
</thead>
<tbody>
{% for borrow in pagination.items %}
<tr class="overdue-item">
<td class="book-cover">
<img src="{{ url_for('static', filename='covers/' + borrow.book.cover_url) if borrow.book.cover_url else url_for('static', filename='images/book-placeholder.jpg') }}" alt="{{ borrow.book.title }}">
</td>
<td class="book-title">
<a href="{{ url_for('book.book_detail', book_id=borrow.book_id) }}">{{ borrow.book.title }}</a>
<div class="book-author">{{ borrow.book.author }}</div>
</td>
<td class="user-info">
<a href="{{ url_for('user.user_edit', user_id=borrow.user_id) }}">{{ borrow.user.username }}</a>
{% if borrow.user.nickname %}
<div class="user-nickname">{{ borrow.user.nickname }}</div>
{% endif %}
<div class="user-contact">
{% if borrow.user.email %}
<a href="mailto:{{ borrow.user.email }}" class="email-link">
<i class="fas fa-envelope"></i>
</a>
{% endif %}
{% if borrow.user.phone %}
<a href="tel:{{ borrow.user.phone }}" class="phone-link">
<i class="fas fa-phone"></i>
</a>
{% endif %}
</div>
</td>
<td>{{ borrow.borrow_date.strftime('%Y-%m-%d') }}</td>
<td class="due-date text-danger">
{{ borrow.due_date.strftime('%Y-%m-%d') }}
</td>
<td class="overdue-days">
{% set days_overdue = ((now - borrow.due_date).days) %}
<span class="badge {% if days_overdue > 30 %}badge-danger{% elif days_overdue > 14 %}badge-warning{% else %}badge-info{% endif %}">
{{ days_overdue }} 天
</span>
</td>
<td class="actions">
<button class="btn btn-sm btn-success return-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">归还处理</button>
<button class="btn btn-sm btn-warning notify-btn" data-id="{{ borrow.id }}" data-title="{{ borrow.book.title }}">发送通知</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 分页 -->
<div class="pagination-container">
{% if pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('borrow.overdue_borrows', page=pagination.prev_num) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('borrow.overdue_borrows', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('borrow.overdue_borrows', page=pagination.next_num) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% else %}
<div class="no-records">
<i class="fas fa-check-circle empty-icon"></i>
<p class="empty-text">目前没有逾期的借阅记录,继续保持!</p>
<a href="{{ url_for('borrow.manage_borrows') }}" class="btn btn-primary">返回借阅管理</a>
</div>
{% endif %}
</div>
</div>
<!-- 归还确认模态框 -->
<div class="modal fade" id="returnModal" tabindex="-1" role="dialog" aria-labelledby="returnModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="returnModalLabel">逾期归还处理</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>您正在处理《<span id="returnBookTitle"></span>》的逾期归还:</p>
<div class="form-group">
<label for="overdueRemark">备注信息(可选)</label>
<textarea class="form-control" id="overdueRemark" rows="3" placeholder="可以输入处理结果、是否收取逾期费用等信息..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-success" id="confirmReturn">确认归还</button>
</div>
</div>
</div>
</div>
<!-- 通知确认模态框 -->
<div class="modal fade" id="notifyModal" tabindex="-1" role="dialog" aria-labelledby="notifyModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notifyModalLabel">发送逾期通知</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
您确定要发送《<span id="notifyBookTitle"></span>》的逾期通知吗?
此操作将向借阅用户发送逾期提醒消息。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="confirmNotify">发送通知</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/overdue.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,96 @@
{% extends 'base.html' %}
{% block title %}调整库存 - {{ book.title }}{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-adjust.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="disney-inventory-card">
<div class="disney-decoration top-left"></div>
<div class="disney-decoration top-right"></div>
<div class="disney-decoration bottom-left"></div>
<div class="disney-decoration bottom-right"></div>
<div class="card-header-disney">
<div class="mickey-ears"></div>
<h4>调整图书库存</h4>
</div>
<div class="card-body-disney">
<div class="row mb-4">
<div class="col-md-4 book-cover-container">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}" class="img-fluid book-cover">
{% else %}
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="默认封面" class="img-fluid book-cover">
{% endif %}
<div class="disney-sparkles"></div>
</div>
<div class="col-md-8 book-details">
<h3 class="book-title">{{ book.title }}</h3>
<div class="book-info">
<p><span class="disney-icon author-icon"></span> <strong>作者:</strong> {{ book.author }}</p>
<p><span class="disney-icon publisher-icon"></span> <strong>出版社:</strong> {{ book.publisher }}</p>
<p><span class="disney-icon isbn-icon"></span> <strong>ISBN:</strong> {{ book.isbn }}</p>
<p><span class="disney-icon inventory-icon"></span> <strong>当前库存:</strong>
<span class="stock-badge {{ 'high-stock' if book.stock > 5 else 'low-stock' if book.stock > 0 else 'out-stock' }}">
{{ book.stock }}
</span>
</p>
</div>
</div>
</div>
<div class="form-container">
<form method="POST" action="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}">
<div class="mb-4 form-group">
<label for="change_type" class="form-label disney-label">
<span class="disney-icon type-icon"></span> 调整类型
</label>
<select class="form-select disney-select" id="change_type" name="change_type" required>
<option value="in">入库(增加库存)</option>
<option value="out">出库(减少库存)</option>
</select>
</div>
<div class="mb-4 form-group">
<label for="change_amount" class="form-label disney-label">
<span class="disney-icon amount-icon"></span> 调整数量
</label>
<input type="number" class="form-control disney-input" id="change_amount" name="change_amount" min="1" value="1" required>
<div class="form-text stock-hint" id="stock-hint">当前库存: {{ book.stock }}</div>
</div>
<div class="mb-4 form-group">
<label for="remark" class="form-label disney-label">
<span class="disney-icon remark-icon"></span> 备注
</label>
<textarea class="form-control disney-textarea" id="remark" name="remark" rows="3" placeholder="填写库存调整原因,如:新书入库、丢失、损坏等"></textarea>
</div>
<div class="button-group">
<a href="{{ url_for('inventory.inventory_list') }}" class="btn disney-cancel-btn">
取消
</a>
<button type="submit" class="btn disney-confirm-btn">
确认调整
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 将当前库存数量传递给JavaScript
const CURRENT_STOCK = {{ book.stock }};
</script>
<script src="{{ url_for('static', filename='js/inventory-adjust.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,186 @@
{% extends 'base.html' %}
{% block title %}《{{ book.title }}》库存日志{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-book-logs.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% endblock %}
{% block content %}
<div class="frozen-background">
<div class="snowflakes" aria-hidden="true">
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
<div class="snowflake"></div>
</div>
<div class="container mt-5">
<div class="frozen-card">
<div class="castle-decoration"></div>
<div class="card-header-frozen">
<div class="ice-crystal left"></div>
<h4><i class="fas fa-book-open"></i> 《{{ book.title }}》库存变动日志</h4>
<div class="ice-crystal right"></div>
</div>
<div class="card-body-frozen">
<div class="row mb-4 book-info-row">
<div class="col-md-3 book-cover-container">
<div class="book-frame">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}" class="img-fluid book-cover">
{% else %}
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="默认封面" class="img-fluid book-cover">
{% endif %}
<div class="book-glow"></div>
</div>
</div>
<div class="col-md-9 book-details">
<h3 class="book-title">{{ book.title }}</h3>
<div class="book-info">
<p class="info-item"><i class="fas fa-feather-alt"></i> <strong>作者:</strong> {{ book.author }}</p>
<p class="info-item"><i class="fas fa-building"></i> <strong>出版社:</strong> {{ book.publisher }}</p>
<p class="info-item"><i class="fas fa-barcode"></i> <strong>ISBN:</strong> {{ book.isbn }}</p>
<p class="info-item">
<i class="fas fa-cubes"></i> <strong>当前库存:</strong>
<span class="frozen-badge {{ 'high-stock' if book.stock > 5 else 'low-stock' if book.stock > 0 else 'out-stock' }}">
{{ book.stock }}
</span>
</p>
</div>
</div>
</div>
<div class="history-section">
<h5 class="section-title">
<i class="fas fa-history"></i> 库存变动历史记录
<div class="magic-underline"></div>
</h5>
<div class="table-container">
<div class="table-frozen">
<div class="table-header-row">
<div class="th-frozen">ID</div>
<div class="th-frozen">操作类型</div>
<div class="th-frozen">变动数量</div>
<div class="th-frozen">变动后库存</div>
<div class="th-frozen">操作人</div>
<div class="th-frozen">备注</div>
<div class="th-frozen">操作时间</div>
</div>
<div class="table-body">
{% for log in logs %}
<div class="table-row log-entry" data-type="{{ log.change_type }}">
<div class="td-frozen">{{ log.id }}</div>
<div class="td-frozen">
<span class="operation-badge {{ 'in-badge' if log.change_type == 'in' else 'out-badge' }}">
{{ '入库' if log.change_type == 'in' else '出库' }}
<i class="fas {{ 'fa-arrow-circle-down' if log.change_type == 'in' else 'fa-arrow-circle-up' }}"></i>
</span>
</div>
<div class="td-frozen">{{ log.change_amount }}</div>
<div class="td-frozen">{{ log.after_stock }}</div>
<div class="td-frozen">{{ log.operator.username if log.operator else '系统' }}</div>
<div class="td-frozen remark-cell">{{ log.remark or '-' }}</div>
<div class="td-frozen">{{ log.changed_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
{% endfor %}
{% if not logs %}
<div class="table-row empty-log">
<div class="td-frozen empty-message" colspan="7">
<div class="olaf-empty">
<div class="olaf-image"></div>
<p>暂无库存变动记录,要不要堆个雪人?</p>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container">
<nav aria-label="Page navigation">
<ul class="frozen-pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.book_inventory_logs', book_id=book.id, page=pagination.prev_num) }}">
<i class="fas fa-chevron-left"></i> 上一页
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">
<i class="fas fa-chevron-left"></i> 上一页
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
{% if page_num == pagination.page %}
<li class="page-item active">
<a class="page-link" href="#">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.book_inventory_logs', book_id=book.id, page=page_num) }}">{{ page_num }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.book_inventory_logs', book_id=book.id, page=pagination.next_num) }}">
下一页 <i class="fas fa-chevron-right"></i>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">
下一页 <i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
<div class="card-footer-frozen">
<div class="footer-actions">
<a href="{{ url_for('inventory.inventory_list') }}" class="btn frozen-btn return-btn">
<i class="fas fa-arrow-left"></i> 返回库存管理
</a>
<a href="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}" class="btn frozen-btn adjust-btn">
<i class="fas fa-sliders-h"></i> 调整库存
</a>
</div>
<div class="footer-decoration"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/inventory-book-logs.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,135 @@
{% extends 'base.html' %}
{% block title %}图书库存管理{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-list.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
{% endblock %}
{% block content %}
<div class="inventory-container">
<div class="page-header">
<div class="header-content">
<h1><i class="fas fa-book-open header-icon"></i>图书库存管理</h1>
<p class="subtitle">优雅管理您的书籍资源</p>
</div>
</div>
<div class="search-card">
<form method="GET" action="{{ url_for('inventory.inventory_list') }}" class="search-form">
<div class="search-input-group">
<div class="search-input-container">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input" name="search" placeholder="搜索书名、作者或ISBN" value="{{ search }}">
</div>
<button class="search-button" type="submit">搜索</button>
</div>
<a href="{{ url_for('inventory.inventory_logs') }}" class="log-button">
<i class="fas fa-history"></i> 查看库存日志
</a>
</form>
</div>
<div class="table-container">
<table class="inventory-table">
<thead>
<tr>
<th>ID</th>
<th>书名</th>
<th>作者</th>
<th>ISBN</th>
<th>当前库存</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for book in books %}
<tr>
<td>{{ book.id }}</td>
<td class="book-title">{{ book.title }}</td>
<td class="book-author">{{ book.author }}</td>
<td>{{ book.isbn }}</td>
<td>
<span class="stock-badge {{ 'stock-high' if book.stock > 5 else 'stock-medium' if book.stock > 0 else 'stock-low' }}">
{{ book.stock }}
</span>
</td>
<td>
<span class="status-badge {{ 'status-active' if book.status == 1 else 'status-inactive' }}">
{{ '正常' if book.status == 1 else '已下架' }}
</span>
</td>
<td class="action-buttons">
<a href="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}" class="btn-adjust">
<i class="fas fa-edit"></i> 调整
</a>
<a href="{{ url_for('inventory.book_inventory_logs', book_id=book.id) }}" class="btn-view">
<i class="fas fa-list-alt"></i> 日志
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<nav aria-label="Page navigation">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.inventory_list', page=pagination.prev_num, search=search, sort=sort, order=order) }}">
<i class="fas fa-chevron-left"></i> 上一页
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">
<i class="fas fa-chevron-left"></i> 上一页
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
{% if page_num == pagination.page %}
<li class="page-item active">
<a class="page-link" href="#">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.inventory_list', page=page_num, search=search, sort=sort, order=order) }}">{{ page_num }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.inventory_list', page=pagination.next_num, search=search, sort=sort, order=order) }}">
下一页 <i class="fas fa-chevron-right"></i>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">
下一页 <i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/inventory-list.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,210 @@
{% extends 'base.html' %}
{% block title %}《{{ book.title }}》库存日志{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/inventory-logs.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
{% endblock %}
{% block content %}
<div class="disney-container"
<!-- 雪花/魔法效果层 -->
<div id="magic-particles"></div>
<!-- 主内容区 -->
<div class="disney-card">
<!-- 装饰元素 -->
<div class="disney-decoration book-icon"></div>
<div class="disney-decoration crown-icon"></div>
<div class="disney-decoration wand-icon"></div>
<div class="disney-decoration snowflake-icon"></div>
<!-- 卡片头部 -->
<div class="card-header-disney">
<div class="princess-crown"></div>
<h4><i class="fas fa-book-open"></i>
{% if book %}
《{{ book.title }}》库存变动日志
{% else %}
全部库存变动日志
{% endif %}
</h4>
</div>
<!-- 卡片内容 -->
<div class="card-body-disney">
<!-- 图书信息部分 -->
{% if book %}
<div class="book-details-container">
<div class="book-cover-wrapper">
{% if book.cover_url %}
<img src="{{ book.cover_url }}" alt="{{ book.title }}" class="disney-book-cover">
{% else %}
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="默认封面" class="disney-book-cover">
{% endif %}
<div class="book-cover-glow"></div>
</div>
<div class="book-info">
<h3 class="book-title">{{ book.title }}</h3>
<div class="info-row">
<div class="disney-icon author-icon"></div>
<div><strong>作者:</strong> {{ book.author }}</div>
</div>
<div class="info-row">
<div class="disney-icon publisher-icon"></div>
<div><strong>出版社:</strong> {{ book.publisher }}</div>
</div>
<div class="info-row">
<div class="disney-icon isbn-icon"></div>
<div><strong>ISBN:</strong> {{ book.isbn }}</div>
</div>
<div class="info-row">
<div class="disney-icon stock-icon"></div>
<div>
<strong>当前库存:</strong>
<span class="stock-badge {{ 'high-stock' if book.stock > 5 else 'low-stock' if book.stock > 0 else 'out-stock' }}">
{{ book.stock }}
</span>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 日志表格部分 -->
<div class="logs-section">
<h5 class="logs-title">
<div class="title-decoration left"></div>
库存变动历史记录
<div class="title-decoration right"></div>
</h5>
<div class="table-container">
<table class="disney-table">
<thead>
<tr>
<th>ID</th>
{% if not book %}<th>图书</th>{% endif %}
<th>操作类型</th>
<th>变动数量</th>
<th>变动后库存</th>
<th>操作人</th>
<th>备注</th>
<th>操作时间</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr class="log-row">
<td>{{ log.id }}</td>
{% if not book %}
<td>
<a href="{{ url_for('inventory.book_inventory_logs', book_id=log.book_id) }}">
{{ log.book.title if log.book else '未知图书' }}
</a>
</td>
{% endif %}
<td>
<span class="operation-badge {{ 'in-badge' if log.change_type == 'in' or log.change_amount > 0 else 'out-badge' }}">
{{ '入库' if log.change_type == 'in' or log.change_amount > 0 else '出库' }}
</span>
</td>
<td>{{ log.change_amount }}</td>
<td>{{ log.after_stock }}</td>
<td>{{ log.operator.username if log.operator else '系统' }}</td>
<td class="remark-cell" title="{{ log.remark or '-' }}">{{ log.remark or '-' }}</td>
<td>{{ log.changed_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endfor %}
{% if not logs %}
<tr>
<td colspan="{{ 8 if not book else 7 }}" class="empty-logs">
<div class="empty-state">
<div class="empty-icon"></div>
<p>暂无库存变动记录</p>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="disney-pagination">
<nav aria-label="Page navigation">
<ul class="pagination-list">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.inventory_logs' if not book else 'inventory.book_inventory_logs', book_id=book.id if book else None, page=pagination.prev_num) }}">
<i class="fas fa-chevron-left"></i> 上一页
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="fas fa-chevron-left"></i> 上一页</span>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
{% if page_num == pagination.page %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.inventory_logs' if not book else 'inventory.book_inventory_logs', book_id=book.id if book else None, page=page_num) }}">{{ page_num }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item dots">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('inventory.inventory_logs' if not book else 'inventory.book_inventory_logs', book_id=book.id if book else None, page=pagination.next_num) }}">
下一页 <i class="fas fa-chevron-right"></i>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">下一页 <i class="fas fa-chevron-right"></i></span>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
</div>
<!-- 卡片底部 -->
<div class="card-footer-disney">
<div class="button-container">
<a href="{{ url_for('inventory.inventory_list') }}" class="disney-button return-btn">
<span class="button-icon"><i class="fas fa-arrow-left"></i></span>
<span class="button-text">返回库存管理</span>
</a>
{% if book %}
<a href="{{ url_for('inventory.adjust_inventory', book_id=book.id) }}" class="disney-button adjust-btn">
<span class="button-icon"><i class="fas fa-edit"></i></span>
<span class="button-text">调整库存</span>
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js"></script>
<script src="{{ url_for('static', filename='js/inventory-book-logs.js') }}"></script>
{% endblock %}

120
app/templates/user/add.html Normal file
View File

@ -0,0 +1,120 @@
{% extends "base.html" %}
{% block title %}添加用户 - 图书管理系统{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user-form.css') }}">
{% endblock %}
{% block content %}
<div class="user-form-container">
<div class="page-header">
<h1>添加用户</h1>
<div class="actions">
<a href="{{ url_for('user.user_list') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回用户列表
</a>
</div>
</div>
<div class="form-card">
<form id="addUserForm" method="POST" action="{{ url_for('user.add_user') }}">
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<div class="form-group required">
<label for="username">用户名</label>
<input type="text" id="username" name="username" class="form-control" required
placeholder="请输入用户名3-20个字符" minlength="3" maxlength="20">
<small class="form-text text-muted">用户名将用于登录,不可重复</small>
</div>
<div class="form-group required">
<label for="password">密码</label>
<div class="password-field">
<input type="password" id="password" name="password" class="form-control" required
placeholder="请输入密码至少6位" minlength="6">
<button type="button" class="toggle-password">
<i class="fas fa-eye"></i>
</button>
</div>
<small class="form-text text-muted">密码至少包含6个字符</small>
</div>
<div class="form-group required">
<label for="confirm_password">确认密码</label>
<div class="password-field">
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required
placeholder="请再次输入密码" minlength="6">
<button type="button" class="toggle-password">
<i class="fas fa-eye"></i>
</button>
</div>
<small id="password-match-message" class="form-text"></small>
</div>
<div class="form-group required">
<label for="email">电子邮箱</label>
<div class="input-with-button">
<input type="email" id="email" name="email" class="form-control" required
placeholder="请输入有效的电子邮箱">
<button type="button" id="sendVerificationCode" class="btn btn-outline-primary">
发送验证码
</button>
</div>
</div>
<div class="form-group required">
<label for="verification_code">验证码</label>
<input type="text" id="verification_code" name="verification_code" class="form-control" required
placeholder="请输入邮箱验证码" maxlength="6">
<small class="form-text text-muted">请输入发送到邮箱的6位验证码</small>
</div>
<div class="form-group">
<label for="nickname">昵称</label>
<input type="text" id="nickname" name="nickname" class="form-control"
placeholder="请输入昵称(选填)" maxlength="64">
</div>
<div class="form-group">
<label for="phone">手机号码</label>
<input type="tel" id="phone" name="phone" class="form-control"
placeholder="请输入手机号码(选填)" maxlength="20">
</div>
<div class="form-group required">
<label for="role_id">用户角色</label>
<select id="role_id" name="role_id" class="form-control" required>
{% for role in roles %}
<option value="{{ role.id }}">{{ role.role_name }}</option>
{% endfor %}
</select>
<small class="form-text text-muted">默认为普通用户,请根据需要选择合适的角色</small>
</div>
<div class="form-group required">
<label for="status">账号状态</label>
<select id="status" name="status" class="form-control" required>
<option value="1" selected>正常</option>
<option value="0">禁用</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 保存用户
</button>
<button type="reset" class="btn btn-secondary">
<i class="fas fa-undo"></i> 重置表单
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/user-add.js') }}"></script>
{% endblock %}

View File

@ -12,7 +12,7 @@
<div class="page-header">
<h1>用户管理</h1>
<div class="actions">
<a href="{{ url_for('user.register') }}" class="btn btn-primary">
<a href="{{ url_for('user.add_user') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i> 添加用户
</a>
</div>

File diff suppressed because it is too large Load Diff