diff --git a/app/__init__.py b/app/__init__.py index c84812c..0ea45e4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ from app.models.user import db, User from app.controllers.user import user_bp from app.controllers.book import book_bp from app.controllers.borrow import borrow_bp +from app.controllers.inventory import inventory_bp from flask_login import LoginManager, current_user import os @@ -48,6 +49,7 @@ def create_app(config=None): app.register_blueprint(user_bp, url_prefix='/user') app.register_blueprint(book_bp, url_prefix='/book') app.register_blueprint(borrow_bp, url_prefix='/borrow') + app.register_blueprint(inventory_bp) # 创建数据库表 with app.app_context(): @@ -132,3 +134,7 @@ def create_app(config=None): return s return app + + @app.context_processor + def inject_now(): + return {'now': datetime.datetime.now()} diff --git a/app/controllers/book.py b/app/controllers/book.py index 19ec822..0c75f7d 100644 --- a/app/controllers/book.py +++ b/app/controllers/book.py @@ -116,7 +116,8 @@ def book_detail(book_id): # 如果用户是管理员,预先查询并排序借阅记录 borrow_records = [] - if g.user.role_id == 1: # 假设 role_id 1 为管理员 + # 防御性编程:确保 g.user 存在且有 role_id 属性 + if hasattr(g, 'user') and g.user is not None and hasattr(g.user, 'role_id') and g.user.role_id == 1: from app.models.borrow import BorrowRecord borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit( 10).all() @@ -124,12 +125,13 @@ def book_detail(book_id): return render_template( 'book/detail.html', book=book, - current_user=g.user, + current_user=current_user, # 使用 flask_login 的 current_user 而不是 g.user borrow_records=borrow_records, now=now ) + # 添加图书页面 @book_bp.route('/add', methods=['GET', 'POST']) @login_required @@ -283,6 +285,7 @@ def edit_book(book_id): book = Book.query.get_or_404(book_id) if request.method == 'POST': + # 获取表单数据 title = request.form.get('title') author = request.form.get('author') publisher = request.form.get('publisher') @@ -294,13 +297,72 @@ def edit_book(book_id): price = request.form.get('price') status = request.form.get('status', type=int) + # 基本验证 if not title or not author: flash('书名和作者不能为空', 'danger') categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + # ISBN验证 + if isbn and isbn.strip(): # 确保ISBN不是空字符串 + # 移除连字符和空格 + clean_isbn = isbn.replace('-', '').replace(' ', '') + + # 长度检查 + if len(clean_isbn) != 10 and len(clean_isbn) != 13: + flash('ISBN必须是10位或13位', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + + # ISBN-10验证 + if len(clean_isbn) == 10: + # 检查前9位是否为数字 + if not clean_isbn[:9].isdigit(): + flash('ISBN-10的前9位必须是数字', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + + # 检查最后一位是否为数字或'X' + if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'): + flash('ISBN-10的最后一位必须是数字或X', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + + # 校验和验证 + sum = 0 + for i in range(9): + sum += int(clean_isbn[i]) * (10 - i) + + check_digit = 10 if clean_isbn[9].upper() == 'X' else int(clean_isbn[9]) + sum += check_digit + + if sum % 11 != 0: + flash('ISBN-10校验和无效', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + + # ISBN-13验证 + if len(clean_isbn) == 13: + # 检查是否全是数字 + if not clean_isbn.isdigit(): + flash('ISBN-13必须全是数字', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + + # 校验和验证 + sum = 0 + for i in range(12): + sum += int(clean_isbn[i]) * (1 if i % 2 == 0 else 3) + + check_digit = (10 - (sum % 10)) % 10 + + if check_digit != int(clean_isbn[12]): + flash('ISBN-13校验和无效', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + # 处理库存变更 - new_stock = request.form.get('stock', type=int) + new_stock = request.form.get('stock', type=int) or 0 # 默认为0而非None if new_stock != book.stock: from app.models.inventory import InventoryLog change_amount = new_stock - book.stock @@ -346,11 +408,17 @@ def edit_book(book_id): book.status = status book.updated_at = datetime.datetime.now() - db.session.commit() - - flash('图书信息更新成功', 'success') - return redirect(url_for('book.book_list')) + try: + db.session.commit() + flash('图书信息更新成功', 'success') + return redirect(url_for('book.book_list')) + except Exception as e: + db.session.rollback() + flash(f'保存失败: {str(e)}', 'danger') + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + # GET 请求 categories = Category.query.all() return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) @@ -622,3 +690,53 @@ def test_permissions():

是否管理员: {'是' if current_user.role_id == 1 else '否'}

尝试访问管理页面

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