From 29914a41784d60c5d412b3e6f89aacc54da60308 Mon Sep 17 00:00:00 2001 From: superlishunqin <852326703@qq.com> Date: Tue, 6 May 2025 12:01:11 +0800 Subject: [PATCH] 0506 --- app/__init__.py | 6 + app/controllers/book.py | 132 +- app/controllers/borrow.py | 397 +- app/controllers/inventory.py | 161 + app/controllers/user.py | 67 + app/models/book.py | 3 +- app/models/user.py | 3 +- app/services/user_service.py | 21 + .../354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg | Bin 0 -> 65431 bytes app/static/css/book-edit.css | 424 + app/static/css/borrow_management.css | 520 + app/static/css/browse.css | 860 ++ app/static/css/inventory-adjust.css | 461 + app/static/css/inventory-book-logs.css | 715 ++ app/static/css/inventory-list.css | 417 + app/static/css/inventory-logs.css | 710 ++ app/static/css/my_borrows.css | 474 + app/static/css/overdue.css | 396 + app/static/css/user-form.css | 636 + app/static/js/book-add.js | 2 +- app/static/js/book-edit.js | 176 + app/static/js/borrow_management.js | 244 + app/static/js/browse.js | 292 + app/static/js/inventory-adjust.js | 103 + app/static/js/inventory-book-logs.js | 263 + app/static/js/inventory-list.js | 30 + app/static/js/inventory-logs.js | 219 + app/static/js/my_borrows.js | 132 + app/static/js/overdue.js | 138 + app/static/js/user-add.js | 140 + app/templates/base.html | 10 +- app/templates/book/browse.html | 224 + app/templates/book/detail.html | 19 +- app/templates/book/edit.html | 307 +- app/templates/borrow/borrow_management.html | 317 + app/templates/borrow/my_borrows.html | 186 + app/templates/borrow/overdue.html | 179 + app/templates/inventory/adjust.html | 96 + app/templates/inventory/book_logs.html | 186 + app/templates/inventory/list.html | 135 + app/templates/inventory/logs.html | 210 + app/templates/user/add.html | 120 + app/templates/user/list.html | 2 +- code_collection.txt | 10555 +++++++++++++++- 44 files changed, 20245 insertions(+), 443 deletions(-) create mode 100644 app/static/covers/354fed85-17f3-4626-b1a9-f061b7ac94f9.jpg create mode 100644 app/static/css/book-edit.css create mode 100644 app/static/css/borrow_management.css create mode 100644 app/static/css/browse.css create mode 100644 app/static/css/inventory-adjust.css create mode 100644 app/static/css/inventory-book-logs.css create mode 100644 app/static/css/inventory-list.css create mode 100644 app/static/css/inventory-logs.css create mode 100644 app/static/css/my_borrows.css create mode 100644 app/static/css/overdue.css create mode 100644 app/static/css/user-form.css create mode 100644 app/static/js/book-edit.js create mode 100644 app/static/js/borrow_management.js create mode 100644 app/static/js/browse.js create mode 100644 app/static/js/inventory-adjust.js create mode 100644 app/static/js/inventory-book-logs.js create mode 100644 app/static/js/inventory-list.js create mode 100644 app/static/js/inventory-logs.js create mode 100644 app/static/js/my_borrows.js create mode 100644 app/static/js/overdue.js create mode 100644 app/static/js/user-add.js create mode 100644 app/templates/book/browse.html create mode 100644 app/templates/borrow/borrow_management.html create mode 100644 app/templates/borrow/my_borrows.html create mode 100644 app/templates/borrow/overdue.html create mode 100644 app/templates/inventory/adjust.html create mode 100644 app/templates/inventory/book_logs.html create mode 100644 app/templates/inventory/list.html create mode 100644 app/templates/inventory/logs.html create mode 100644 app/templates/user/add.html 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 0000000000000000000000000000000000000000..f0b63a4c0418907eadcba957ec30d40a72ee54ca GIT binary patch literal 65431 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 literal 0 HcmV?d00001 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'); + } + + // 添加帮助样式 + $("