0506
This commit is contained in:
parent
5f46d87528
commit
29914a4178
@ -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()}
|
||||
|
||||
@ -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,)
|
||||
|
||||
@ -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)}'
|
||||
})
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
||||
@ -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}>'
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)}'
|
||||
BIN
app/static/covers/354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg
Normal file
BIN
app/static/covers/354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
424
app/static/css/book-edit.css
Normal file
424
app/static/css/book-edit.css
Normal 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;
|
||||
}
|
||||
520
app/static/css/borrow_management.css
Normal file
520
app/static/css/borrow_management.css
Normal 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
860
app/static/css/browse.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
461
app/static/css/inventory-adjust.css
Normal file
461
app/static/css/inventory-adjust.css
Normal 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;
|
||||
}
|
||||
715
app/static/css/inventory-book-logs.css
Normal file
715
app/static/css/inventory-book-logs.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
417
app/static/css/inventory-list.css
Normal file
417
app/static/css/inventory-list.css
Normal 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;
|
||||
}
|
||||
}
|
||||
710
app/static/css/inventory-logs.css
Normal file
710
app/static/css/inventory-logs.css
Normal 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);
|
||||
}
|
||||
}
|
||||
474
app/static/css/my_borrows.css
Normal file
474
app/static/css/my_borrows.css
Normal 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
396
app/static/css/overdue.css
Normal 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;
|
||||
}
|
||||
}
|
||||
636
app/static/css/user-form.css
Normal file
636
app/static/css/user-form.css
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -127,7 +127,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
// 初始化表单验证
|
||||
ffunction initFormValidation() {
|
||||
function initFormValidation() {
|
||||
$('#bookForm').on('submit', function(e) {
|
||||
// 如果表单正在提交中,阻止重复提交
|
||||
if (isSubmitting) {
|
||||
|
||||
176
app/static/js/book-edit.js
Normal file
176
app/static/js/book-edit.js
Normal 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");
|
||||
});
|
||||
244
app/static/js/borrow_management.js
Normal file
244
app/static/js/borrow_management.js
Normal 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">×</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
292
app/static/js/browse.js
Normal file
292
app/static/js/browse.js
Normal 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;
|
||||
});
|
||||
}
|
||||
});
|
||||
103
app/static/js/inventory-adjust.js
Normal file
103
app/static/js/inventory-adjust.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
263
app/static/js/inventory-book-logs.js
Normal file
263
app/static/js/inventory-book-logs.js
Normal 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);
|
||||
30
app/static/js/inventory-list.js
Normal file
30
app/static/js/inventory-list.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
219
app/static/js/inventory-logs.js
Normal file
219
app/static/js/inventory-logs.js
Normal 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
132
app/static/js/my_borrows.js
Normal 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">×</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
138
app/static/js/overdue.js
Normal file
138
app/static/js/overdue.js
Normal 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">×</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
140
app/static/js/user-add.js
Normal file
140
app/static/js/user-add.js
Normal 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
|
||||
224
app/templates/book/browse.html
Normal file
224
app/templates/book/browse.html
Normal 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">×</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 %}
|
||||
@ -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 %}
|
||||
|
||||
@ -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">×</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 %}
|
||||
|
||||
317
app/templates/borrow/borrow_management.html
Normal file
317
app/templates/borrow/borrow_management.html
Normal 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">«</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">»</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">×</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">×</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">×</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">×</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 %}
|
||||
186
app/templates/borrow/my_borrows.html
Normal file
186
app/templates/borrow/my_borrows.html
Normal 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">×</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">×</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 %}
|
||||
179
app/templates/borrow/overdue.html
Normal file
179
app/templates/borrow/overdue.html
Normal 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">«</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">»</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">×</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">×</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 %}
|
||||
96
app/templates/inventory/adjust.html
Normal file
96
app/templates/inventory/adjust.html
Normal 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 %}
|
||||
186
app/templates/inventory/book_logs.html
Normal file
186
app/templates/inventory/book_logs.html
Normal 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 %}
|
||||
135
app/templates/inventory/list.html
Normal file
135
app/templates/inventory/list.html
Normal 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 %}
|
||||
210
app/templates/inventory/logs.html
Normal file
210
app/templates/inventory/logs.html
Normal 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
120
app/templates/user/add.html
Normal 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 %}
|
||||
@ -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>
|
||||
|
||||
10555
code_collection.txt
10555
code_collection.txt
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user