diff --git a/app/__init__.py b/app/__init__.py index 41e38d3..2f67fa5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,7 @@ from flask import Flask, render_template, session, g from app.models.user import db, User from app.controllers.user import user_bp +from app.controllers.book import book_bp # 引入图书蓝图 import os @@ -32,11 +33,27 @@ def create_app(): # 注册蓝图 app.register_blueprint(user_bp, url_prefix='/user') + app.register_blueprint(book_bp, url_prefix='/book') # 注册图书蓝图 # 创建数据库表 with app.app_context(): + # 先导入基础模型 + from app.models.user import User, Role + from app.models.book import Book, Category + + # 创建表 db.create_all() + # 再导入依赖模型 + from app.models.borrow import BorrowRecord + from app.models.inventory import InventoryLog + + # 现在添加反向关系 + # 这样可以确保所有类都已经定义好 + Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic') + Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic') + Category.books = db.relationship('Book', backref='category', lazy='dynamic') + # 创建默认角色 from app.models.user import Role if not Role.query.filter_by(id=1).first(): @@ -58,6 +75,21 @@ def create_app(): ) db.session.add(admin) + # 创建基础分类 + from app.models.book import Category + if not Category.query.first(): + categories = [ + Category(name='文学', sort=1), + Category(name='计算机', sort=2), + Category(name='历史', sort=3), + Category(name='科学', sort=4), + Category(name='艺术', sort=5), + Category(name='经济', sort=6), + Category(name='哲学', sort=7), + Category(name='教育', sort=8) + ] + db.session.add_all(categories) + db.session.commit() # 请求前处理 @@ -82,4 +114,11 @@ def create_app(): def page_not_found(e): return render_template('404.html'), 404 - return app \ No newline at end of file + # 模板过滤器 + @app.template_filter('nl2br') + def nl2br_filter(s): + if not s: + return s + return s.replace('\n', '
') + + return app diff --git a/app/controllers/book.py b/app/controllers/book.py index e69de29..7733cc6 100644 --- a/app/controllers/book.py +++ b/app/controllers/book.py @@ -0,0 +1,484 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, g, jsonify +from app.models.book import Book, Category +from app.models.user import db +from app.utils.auth import login_required, admin_required +import os +from werkzeug.utils import secure_filename +import datetime +import pandas as pd +import uuid + +book_bp = Blueprint('book', __name__) + + +# 图书列表页面 +@book_bp.route('/list') +@login_required +def book_list(): + print("访问图书列表页面") # 调试输出 + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + + query = Book.query + + # 搜索功能 + 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/list.html', + books=books, + pagination=pagination, + search=search, + categories=categories, + category_id=category_id, + sort=sort, + order=order, + current_user=g.user) + + +# 图书详情页面 +@book_bp.route('/detail/') +@login_required +def book_detail(book_id): + book = Book.query.get_or_404(book_id) + return render_template('book/detail.html', book=book, current_user=g.user) + + +# 添加图书页面 +@book_bp.route('/add', methods=['GET', 'POST']) +@login_required +@admin_required +def add_book(): + if request.method == 'POST': + title = request.form.get('title') + author = request.form.get('author') + publisher = request.form.get('publisher') + category_id = request.form.get('category_id') + tags = request.form.get('tags') + isbn = request.form.get('isbn') + publish_year = request.form.get('publish_year') + description = request.form.get('description') + stock = request.form.get('stock', type=int) + price = request.form.get('price') + + if not title or not author: + flash('书名和作者不能为空', 'danger') + categories = Category.query.all() + return render_template('book/add.html', categories=categories, current_user=g.user) + + # 处理封面图片上传 + cover_url = None + if 'cover' in request.files: + cover_file = request.files['cover'] + if cover_file and cover_file.filename != '': + filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}") + upload_folder = os.path.join(current_app.static_folder, 'uploads/covers') + + # 确保上传目录存在 + if not os.path.exists(upload_folder): + os.makedirs(upload_folder) + + file_path = os.path.join(upload_folder, filename) + cover_file.save(file_path) + cover_url = f'/static/covers/{filename}' + + # 创建新图书 + book = Book( + title=title, + author=author, + publisher=publisher, + category_id=category_id, + tags=tags, + isbn=isbn, + publish_year=publish_year, + description=description, + cover_url=cover_url, + stock=stock, + price=price, + status=1, + created_at=datetime.datetime.now(), + updated_at=datetime.datetime.now() + ) + + db.session.add(book) + + # 记录库存日志 + if stock and int(stock) > 0: + from app.models.inventory import InventoryLog + inventory_log = InventoryLog( + book_id=book.id, + change_type='入库', + change_amount=stock, + after_stock=stock, + operator_id=g.user.id, + remark='新书入库', + changed_at=datetime.datetime.now() + ) + db.session.add(inventory_log) + + db.session.commit() + + flash('图书添加成功', 'success') + return redirect(url_for('book.book_list')) + + categories = Category.query.all() + return render_template('book/add.html', categories=categories, current_user=g.user) + + +# 编辑图书 +@book_bp.route('/edit/', methods=['GET', 'POST']) +@login_required +@admin_required +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') + category_id = request.form.get('category_id') + tags = request.form.get('tags') + isbn = request.form.get('isbn') + publish_year = request.form.get('publish_year') + description = request.form.get('description') + 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) + + # 处理库存变更 + new_stock = request.form.get('stock', type=int) + if new_stock != book.stock: + from app.models.inventory import InventoryLog + change_amount = new_stock - book.stock + change_type = '入库' if change_amount > 0 else '出库' + + inventory_log = InventoryLog( + book_id=book.id, + change_type=change_type, + change_amount=abs(change_amount), + after_stock=new_stock, + operator_id=g.user.id, + remark=f'管理员编辑图书库存 - {book.title}', + changed_at=datetime.datetime.now() + ) + db.session.add(inventory_log) + book.stock = new_stock + + # 处理封面图片上传 + if 'cover' in request.files: + cover_file = request.files['cover'] + if cover_file and cover_file.filename != '': + filename = secure_filename(f"{uuid.uuid4()}_{cover_file.filename}") + upload_folder = os.path.join(current_app.static_folder, 'uploads/covers') + + # 确保上传目录存在 + if not os.path.exists(upload_folder): + os.makedirs(upload_folder) + + file_path = os.path.join(upload_folder, filename) + cover_file.save(file_path) + book.cover_url = f'/static/covers/{filename}' + + # 更新图书信息 + book.title = title + book.author = author + book.publisher = publisher + book.category_id = category_id + book.tags = tags + book.isbn = isbn + book.publish_year = publish_year + book.description = description + book.price = price + book.status = status + book.updated_at = datetime.datetime.now() + + db.session.commit() + + flash('图书信息更新成功', 'success') + return redirect(url_for('book.book_list')) + + categories = Category.query.all() + return render_template('book/edit.html', book=book, categories=categories, current_user=g.user) + + +# 删除图书 +@book_bp.route('/delete/', methods=['POST']) +@login_required +@admin_required +def delete_book(book_id): + book = Book.query.get_or_404(book_id) + + # 检查该书是否有借阅记录 + from app.models.borrow import BorrowRecord + active_borrows = BorrowRecord.query.filter_by(book_id=book_id, status=1).count() + + if active_borrows > 0: + return jsonify({'success': False, 'message': '该图书有未归还的借阅记录,无法删除'}) + + # 考虑软删除而不是物理删除 + book.status = 0 # 0表示已删除/下架 + book.updated_at = datetime.datetime.now() + db.session.commit() + + return jsonify({'success': True, 'message': '图书已成功下架'}) + + +# 图书分类管理 +@book_bp.route('/categories', methods=['GET']) +@login_required +@admin_required +def category_list(): + categories = Category.query.all() + return render_template('book/categories.html', categories=categories, current_user=g.user) + + +# 添加分类 +@book_bp.route('/categories/add', methods=['POST']) +@login_required +@admin_required +def add_category(): + name = request.form.get('name') + parent_id = request.form.get('parent_id') or None + sort = request.form.get('sort', 0, type=int) + + if not name: + return jsonify({'success': False, 'message': '分类名称不能为空'}) + + category = Category(name=name, parent_id=parent_id, sort=sort) + db.session.add(category) + db.session.commit() + + return jsonify({'success': True, 'message': '分类添加成功', 'id': category.id, 'name': category.name}) + + +# 编辑分类 +@book_bp.route('/categories/edit/', methods=['POST']) +@login_required +@admin_required +def edit_category(category_id): + category = Category.query.get_or_404(category_id) + + name = request.form.get('name') + parent_id = request.form.get('parent_id') or None + sort = request.form.get('sort', 0, type=int) + + if not name: + return jsonify({'success': False, 'message': '分类名称不能为空'}) + + category.name = name + category.parent_id = parent_id + category.sort = sort + db.session.commit() + + return jsonify({'success': True, 'message': '分类更新成功'}) + + +# 删除分类 +@book_bp.route('/categories/delete/', methods=['POST']) +@login_required +@admin_required +def delete_category(category_id): + category = Category.query.get_or_404(category_id) + + # 检查是否有书籍使用此分类 + books_count = Book.query.filter_by(category_id=category_id).count() + if books_count > 0: + return jsonify({'success': False, 'message': f'该分类下有{books_count}本图书,无法删除'}) + + # 检查是否有子分类 + children_count = Category.query.filter_by(parent_id=category_id).count() + if children_count > 0: + return jsonify({'success': False, 'message': f'该分类下有{children_count}个子分类,无法删除'}) + + db.session.delete(category) + db.session.commit() + + return jsonify({'success': True, 'message': '分类删除成功'}) + + +# 批量导入图书 +@book_bp.route('/import', methods=['GET', 'POST']) +@login_required +@admin_required +def import_books(): + if request.method == 'POST': + if 'file' not in request.files: + flash('未选择文件', 'danger') + return redirect(request.url) + + file = request.files['file'] + if file.filename == '': + flash('未选择文件', 'danger') + return redirect(request.url) + + if file and file.filename.endswith(('.xlsx', '.xls')): + try: + # 读取Excel文件 + df = pd.read_excel(file) + success_count = 0 + error_count = 0 + errors = [] + + # 处理每一行数据 + for index, row in df.iterrows(): + try: + # 检查必填字段 + if pd.isna(row.get('title')) or pd.isna(row.get('author')): + errors.append(f'第{index + 2}行: 书名或作者为空') + error_count += 1 + continue + + # 检查ISBN是否已存在 + isbn = row.get('isbn') + if isbn and not pd.isna(isbn) and Book.query.filter_by(isbn=str(isbn)).first(): + errors.append(f'第{index + 2}行: ISBN {isbn} 已存在') + error_count += 1 + continue + + # 创建新书籍记录 + book = Book( + title=row.get('title'), + author=row.get('author'), + publisher=row.get('publisher') if not pd.isna(row.get('publisher')) else None, + category_id=row.get('category_id') if not pd.isna(row.get('category_id')) else None, + tags=row.get('tags') if not pd.isna(row.get('tags')) else None, + isbn=str(row.get('isbn')) if not pd.isna(row.get('isbn')) else None, + publish_year=str(row.get('publish_year')) if not pd.isna(row.get('publish_year')) else None, + description=row.get('description') if not pd.isna(row.get('description')) else None, + cover_url=row.get('cover_url') if not pd.isna(row.get('cover_url')) else None, + stock=int(row.get('stock')) if not pd.isna(row.get('stock')) else 0, + price=float(row.get('price')) if not pd.isna(row.get('price')) else None, + status=1, + created_at=datetime.datetime.now(), + updated_at=datetime.datetime.now() + ) + + db.session.add(book) + # 提交以获取book的id + db.session.flush() + + # 创建库存日志 + if book.stock > 0: + from app.models.inventory import InventoryLog + inventory_log = InventoryLog( + book_id=book.id, + change_type='入库', + change_amount=book.stock, + after_stock=book.stock, + operator_id=g.user.id, + remark='批量导入图书', + changed_at=datetime.datetime.now() + ) + db.session.add(inventory_log) + + success_count += 1 + except Exception as e: + errors.append(f'第{index + 2}行: {str(e)}') + error_count += 1 + + db.session.commit() + flash(f'导入完成: 成功{success_count}条,失败{error_count}条', 'info') + if errors: + flash('
'.join(errors[:10]) + (f'
...等共{len(errors)}个错误' if len(errors) > 10 else ''), + 'warning') + + return redirect(url_for('book.book_list')) + + except Exception as e: + flash(f'导入失败: {str(e)}', 'danger') + return redirect(request.url) + else: + flash('只支持Excel文件(.xlsx, .xls)', 'danger') + return redirect(request.url) + + return render_template('book/import.html', current_user=g.user) + + +# 导出图书 +@book_bp.route('/export') +@login_required +@admin_required +def export_books(): + # 获取查询参数 + search = request.args.get('search', '') + category_id = request.args.get('category_id', type=int) + + query = Book.query + + if search: + query = query.filter( + (Book.title.contains(search)) | + (Book.author.contains(search)) | + (Book.isbn.contains(search)) + ) + + if category_id: + query = query.filter_by(category_id=category_id) + + books = query.all() + + # 创建DataFrame + data = [] + for book in books: + category_name = book.category.name if book.category else "" + data.append({ + 'id': book.id, + 'title': book.title, + 'author': book.author, + 'publisher': book.publisher, + 'category': category_name, + 'tags': book.tags, + 'isbn': book.isbn, + 'publish_year': book.publish_year, + 'description': book.description, + 'stock': book.stock, + 'price': book.price, + 'status': '上架' if book.status == 1 else '下架', + 'created_at': book.created_at.strftime('%Y-%m-%d %H:%M:%S') if book.created_at else '', + 'updated_at': book.updated_at.strftime('%Y-%m-%d %H:%M:%S') if book.updated_at else '' + }) + + df = pd.DataFrame(data) + + # 创建临时文件 + timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + filename = f'books_export_{timestamp}.xlsx' + filepath = os.path.join(current_app.static_folder, 'temp', filename) + + # 确保目录存在 + os.makedirs(os.path.dirname(filepath), exist_ok=True) + + # 写入Excel + df.to_excel(filepath, index=False) + + # 提供下载链接 + return redirect(url_for('static', filename=f'temp/{filename}')) diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29..f72ae56 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1,19 @@ +def create_app(): + app = Flask(__name__) + + # ... 配置代码 ... + + # 初始化数据库 + db.init_app(app) + + # 导入模型,确保所有模型在创建表之前被加载 + from app.models.user import User, Role + from app.models.book import Book, Category + from app.models.borrow import BorrowRecord + from app.models.inventory import InventoryLog + + # 创建数据库表 + with app.app_context(): + db.create_all() + + # ... 其余代码 ... diff --git a/app/models/book.py b/app/models/book.py index e69de29..d09d37f 100644 --- a/app/models/book.py +++ b/app/models/book.py @@ -0,0 +1,42 @@ +from app.models.user import db +from datetime import datetime + + +class Category(db.Model): + __tablename__ = 'categories' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), nullable=False) + parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) + sort = db.Column(db.Integer, default=0) + + # 关系 - 只保留与自身的关系 + parent = db.relationship('Category', remote_side=[id], backref='children') + + def __repr__(self): + return f'' + + +class Book(db.Model): + __tablename__ = 'books' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=False) + author = db.Column(db.String(128), nullable=False) + publisher = db.Column(db.String(128), nullable=True) + category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True) + tags = db.Column(db.String(255), nullable=True) + isbn = db.Column(db.String(32), unique=True, nullable=True) + publish_year = db.Column(db.String(16), nullable=True) + description = db.Column(db.Text, nullable=True) + cover_url = db.Column(db.String(255), nullable=True) + stock = db.Column(db.Integer, default=0) + price = db.Column(db.Numeric(10, 2), nullable=True) + status = db.Column(db.Integer, default=1) # 1:可用, 0:不可用 + created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) + updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now) + + # 移除所有关系引用 + + def __repr__(self): + return f'' diff --git a/app/models/borrow.py b/app/models/borrow.py index e69de29..4a486b2 100644 --- a/app/models/borrow.py +++ b/app/models/borrow.py @@ -0,0 +1,26 @@ +from app.models.user import db +from datetime import datetime + + +class BorrowRecord(db.Model): + __tablename__ = 'borrow_records' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False) + borrow_date = db.Column(db.DateTime, nullable=False, default=datetime.now) + due_date = db.Column(db.DateTime, nullable=False) + return_date = db.Column(db.DateTime, nullable=True) + renew_count = db.Column(db.Integer, default=0) + status = db.Column(db.Integer, default=1) # 1: 借出, 0: 已归还 + remark = db.Column(db.String(255), nullable=True) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.now) + updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now) + + # 添加反向关系引用 + user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic')) + + # book 关系会在后面步骤添加 + + def __repr__(self): + return f'' diff --git a/app/models/inventory.py b/app/models/inventory.py index e69de29..5a228a2 100644 --- a/app/models/inventory.py +++ b/app/models/inventory.py @@ -0,0 +1,23 @@ +from app.models.user import db +from datetime import datetime + + +class InventoryLog(db.Model): + __tablename__ = 'inventory_logs' + + id = db.Column(db.Integer, primary_key=True) + book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False) + change_type = db.Column(db.String(32), nullable=False) # 'in' 入库, 'out' 出库 + change_amount = db.Column(db.Integer, nullable=False) + after_stock = db.Column(db.Integer, nullable=False) + operator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + remark = db.Column(db.String(255), nullable=True) + changed_at = db.Column(db.DateTime, nullable=False, default=datetime.now) + + # 添加反向关系引用 + operator = db.relationship('User', backref=db.backref('inventory_logs', lazy='dynamic')) + + # book 关系会在后面步骤添加 + + def __repr__(self): + return f'' diff --git a/app/static/covers/bainiangudu.jpg b/app/static/covers/bainiangudu.jpg new file mode 100644 index 0000000..e6bcd0d Binary files /dev/null and b/app/static/covers/bainiangudu.jpg differ diff --git a/app/static/covers/zhongguotongshi.jpg b/app/static/covers/zhongguotongshi.jpg new file mode 100644 index 0000000..4b57d84 Binary files /dev/null and b/app/static/covers/zhongguotongshi.jpg differ diff --git a/app/static/css/book-detail.css b/app/static/css/book-detail.css new file mode 100644 index 0000000..cf9b1f5 --- /dev/null +++ b/app/static/css/book-detail.css @@ -0,0 +1,215 @@ +/* 图书详情页样式 */ +.book-detail-container { + padding: 20px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.actions { + display: flex; + gap: 10px; +} + +.book-content { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + overflow: hidden; +} + +.book-header { + display: flex; + padding: 25px; + border-bottom: 1px solid #f0f0f0; + background-color: #f9f9f9; +} + +.book-cover-large { + flex: 0 0 200px; + height: 300px; + background-color: #f0f0f0; + border-radius: 5px; + overflow: hidden; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + margin-right: 30px; +} + +.book-cover-large img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.no-cover-large { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #aaa; +} + +.no-cover-large i { + font-size: 48px; + margin-bottom: 10px; +} + +.book-main-info { + flex: 1; +} + +.book-title { + font-size: 1.8rem; + font-weight: 600; + margin-bottom: 15px; + color: #333; +} + +.book-author { + font-size: 1.1rem; + color: #555; + margin-bottom: 20px; +} + +.book-meta-info { + margin-bottom: 25px; +} + +.meta-item { + display: flex; + align-items: center; + margin-bottom: 12px; + color: #666; +} + +.meta-item i { + width: 20px; + margin-right: 10px; + text-align: center; + color: #555; +} + +.meta-value { + font-weight: 500; + color: #444; +} + +.tag { + display: inline-block; + background-color: #e9ecef; + color: #495057; + padding: 2px 8px; + border-radius: 3px; + margin-right: 5px; + margin-bottom: 5px; + font-size: 0.85rem; +} + +.book-status-info { + display: flex; + align-items: center; + gap: 20px; + margin-top: 20px; +} + +.status-badge { + display: inline-block; + padding: 8px 16px; + border-radius: 4px; + font-weight: 600; + font-size: 0.9rem; +} + +.status-badge.available { + background-color: #d4edda; + color: #155724; +} + +.status-badge.unavailable { + background-color: #f8d7da; + color: #721c24; +} + +.stock-info { + font-size: 0.95rem; + color: #555; +} + +.book-details-section { + padding: 25px; +} + +.book-details-section h3 { + font-size: 1.3rem; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + color: #444; +} + +.book-description { + color: #555; + line-height: 1.6; +} + +.no-description { + color: #888; + font-style: italic; +} + +.book-borrow-history { + padding: 0 25px 25px; +} + +.book-borrow-history h3 { + font-size: 1.3rem; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; + color: #444; +} + +.borrow-table { + border: 1px solid #eee; +} + +.no-records { + color: #888; + font-style: italic; + text-align: center; + padding: 20px; + background-color: #f9f9f9; + border-radius: 4px; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .book-header { + flex-direction: column; + } + + .book-cover-large { + margin-right: 0; + margin-bottom: 20px; + max-width: 200px; + align-self: center; + } + + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .actions { + width: 100%; + } +} diff --git a/app/static/css/book-form.css b/app/static/css/book-form.css new file mode 100644 index 0000000..6315be4 --- /dev/null +++ b/app/static/css/book-form.css @@ -0,0 +1,108 @@ +/* 图书表单页面样式 */ +.book-form-container { + padding: 20px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.actions { + display: flex; + gap: 10px; +} + +.book-form { + margin-bottom: 30px; +} + +.card { + margin-bottom: 20px; + border: 1px solid rgba(0,0,0,0.125); + border-radius: 0.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + background-color: rgba(0,0,0,0.03); + border-bottom: 1px solid rgba(0,0,0,0.125); + font-weight: 600; +} + +.card-body { + padding: 1.25rem; +} + +/* 必填项标记 */ +.required { + color: #dc3545; + margin-left: 2px; +} + +/* 封面预览区域 */ +.cover-preview-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.cover-preview { + width: 100%; + max-width: 200px; + height: 280px; + border: 1px dashed #ccc; + border-radius: 4px; + overflow: hidden; + background-color: #f8f9fa; + margin-bottom: 10px; +} + +.cover-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.no-cover-placeholder { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #aaa; +} + +.no-cover-placeholder i { + font-size: 48px; + margin-bottom: 10px; +} + +.upload-container { + width: 100%; + max-width: 200px; +} + +/* 提交按钮容器 */ +.form-submit-container { + margin-top: 30px; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .actions { + width: 100%; + } +} diff --git a/app/static/css/book-import.css b/app/static/css/book-import.css new file mode 100644 index 0000000..f9e9c3d --- /dev/null +++ b/app/static/css/book-import.css @@ -0,0 +1,70 @@ +/* 图书批量导入页面样式 */ +.import-container { + padding: 20px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.card { + margin-bottom: 20px; + border: 1px solid rgba(0,0,0,0.125); + border-radius: 0.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + background-color: rgba(0,0,0,0.03); + border-bottom: 1px solid rgba(0,0,0,0.125); +} + +.card-body { + padding: 1.25rem; +} + +.import-instructions { + margin-top: 20px; +} + +.import-instructions h5 { + margin-bottom: 15px; + color: #555; +} + +.import-instructions ul { + margin-bottom: 20px; + padding-left: 20px; +} + +.import-instructions li { + margin-bottom: 8px; + color: #666; +} + +.required-field { + color: #dc3545; + font-weight: bold; +} + +.template-download { + margin-top: 20px; + text-align: center; + padding: 15px; + background-color: #f8f9fa; + border-radius: 5px; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } +} diff --git a/app/static/css/book.css b/app/static/css/book.css new file mode 100644 index 0000000..bab522e --- /dev/null +++ b/app/static/css/book.css @@ -0,0 +1,723 @@ +/* 图书列表页面样式 - 女性友好版 */ + +/* 背景和泡泡动画 */ +.book-list-container { + padding: 24px; + background-color: #ffeef2; /* 淡粉色背景 */ + min-height: calc(100vh - 60px); + position: relative; + overflow: hidden; +} + +/* 泡泡动画 */ +.book-list-container::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 0; +} + +@keyframes bubble { + 0% { + transform: translateY(100%) scale(0); + opacity: 0; + } + 50% { + opacity: 0.6; + } + 100% { + transform: translateY(-100vh) scale(1); + opacity: 0; + } +} + +.bubble { + position: absolute; + bottom: -50px; + background-color: rgba(255, 255, 255, 0.5); + border-radius: 50%; + z-index: 1; + animation: bubble 15s infinite ease-in; +} + +/* 为页面添加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 { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(233, 152, 174, 0.3); + position: relative; + z-index: 2; +} + +.page-header h1 { + color: #d23f6e; + font-size: 1.9rem; + font-weight: 600; + margin: 0; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8); +} + +/* 更漂亮的顶部按钮 */ +.action-buttons { + display: flex; + gap: 12px; + position: relative; + z-index: 2; +} + +.action-buttons .btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 50px; + font-weight: 500; + padding: 9px 18px; + transition: all 0.3s ease; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06); + border: none; + font-size: 0.95rem; + position: relative; + overflow: hidden; +} + +.action-buttons .btn::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), transparent); + pointer-events: none; +} + +.action-buttons .btn:hover { + transform: translateY(-3px); + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08); +} + +.action-buttons .btn:active { + transform: translateY(1px); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +/* 按钮颜色 */ +.btn-primary { + background: linear-gradient(135deg, #5c88da, #4a73c7); + color: white; +} + +.btn-success { + background: linear-gradient(135deg, #56c596, #41b384); + color: white; +} + +.btn-info { + background: linear-gradient(135deg, #5bc0de, #46b8da); + color: white; +} + +.btn-secondary { + background: linear-gradient(135deg, #f0ad4e, #ec971f); + color: white; +} + +.btn-danger { + background: linear-gradient(135deg, #ff7676, #ff5252); + color: white; +} + +/* 过滤和搜索部分 */ +.filter-section { + margin-bottom: 25px; + padding: 18px; + background-color: rgba(255, 255, 255, 0.8); + border-radius: 16px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); + position: relative; + z-index: 2; + backdrop-filter: blur(5px); +} + +.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; +} + +.search-group .form-control { + border: 1px solid #f9c0d0; + border-right: none; + border-radius: 25px 0 0 25px; + padding: 10px 20px; + height: 42px; + font-size: 0.95rem; + background-color: rgba(255, 255, 255, 0.9); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); + transition: all 0.3s; + flex: 1; +} + +.search-group .form-control:focus { + outline: none; + border-color: #e67e9f; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2); +} + +.search-group .btn { + border-radius: 50%; + width: 42px; + height: 42px; + min-width: 42px; + padding: 0; + background: linear-gradient(135deg, #e67e9f 60%, #ffd3e1 100%); + color: white; + display: flex; + align-items: center; + justify-content: center; + margin-left: -1px; /* 防止和输入框间有缝隙 */ + font-size: 1.1rem; + box-shadow: 0 2px 6px rgba(230, 126, 159, 0.10); + transition: background 0.2s, box-shadow 0.2s; +} + +.search-group .btn:hover { + background: linear-gradient(135deg, #d23f6e 80%, #efb6c6 100%); + color: #fff; + box-shadow: 0 4px 12px rgba(230, 126, 159, 0.14); +} + +.filter-row { + display: flex; + flex-wrap: wrap; + gap: 15px; + width: 100%; +} + +.filter-group { + flex: 1; + min-width: 130px; +} + +.filter-section .form-control { + border: 1px solid #f9c0d0; + border-radius: 25px; + height: 42px; + padding: 10px 20px; + background-color: rgba(255, 255, 255, 0.9); + 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='%23e67e9f' 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; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); + width: 100%; +} + +.filter-section .form-control:focus { + outline: none; + border-color: #e67e9f; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2); +} + +/* 图书网格布局 */ +.books-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 24px; + margin-bottom: 30px; + position: relative; + z-index: 2; +} + +/* 图书卡片样式 */ +.book-card { + display: flex; + flex-direction: column; + border-radius: 16px; + overflow: hidden; + background-color: white; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.06); + transition: all 0.3s ease; + height: 100%; + position: relative; + border: 1px solid rgba(233, 152, 174, 0.2); +} + +.book-card:hover { + transform: translateY(-8px); + box-shadow: 0 12px 25px rgba(0, 0, 0, 0.1); +} + +.book-cover { + width: 100%; + height: 180px; + background-color: #faf3f5; + overflow: hidden; + position: relative; +} + +.book-cover::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to bottom, transparent 60%, rgba(249, 219, 227, 0.4)); + pointer-events: none; +} + +.book-cover img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.book-card:hover .book-cover img { + transform: scale(1.05); +} + +.no-cover { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, #ffeef2 0%, #ffd9e2 100%); + color: #e67e9f; + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 1; + pointer-events: none; +} + +.no-cover i { + font-size: 36px; + margin-bottom: 10px; +} + +.book-info { + padding: 20px; + display: flex; + flex-direction: column; + flex: 1; +} + +.book-title { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 10px; + color: #d23f6e; + 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: #888; + margin-bottom: 15px; +} + +.book-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 15px; +} + +.book-category { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.8rem; + background-color: #ffebf0; + color: #e67e9f; + font-weight: 500; +} + +.book-status { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; +} + +.book-status.available { + background-color: #dffff6; + color: #26a69a; +} + +.book-status.unavailable { + background-color: #ffeeee; + color: #e57373; +} + +.book-details { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; + font-size: 0.9rem; + color: #777; +} + +.book-details p { + margin: 0; + display: flex; +} + +.book-details strong { + min-width: 65px; + color: #999; + font-weight: 600; +} + +/* 按钮组样式 */ +.book-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-top: auto; +} + +.book-actions .btn { + padding: 8px 0; + font-size: 0.9rem; + text-align: center; + border-radius: 25px; + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + border: none; + font-weight: 500; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.book-actions .btn:hover { + transform: translateY(-3px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12); +} + +.book-actions .btn i { + font-size: 0.85rem; +} + +/* 具体按钮颜色 */ +.book-actions .btn-primary { + background: linear-gradient(135deg, #5c88da, #4a73c7); +} + +.book-actions .btn-info { + background: linear-gradient(135deg, #5bc0de, #46b8da); +} + +.book-actions .btn-success { + background: linear-gradient(135deg, #56c596, #41b384); +} + +.book-actions .btn-danger { + background: linear-gradient(135deg, #ff7676, #ff5252); +} + +/* 无图书状态 */ +.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; +} + +.no-books i { + font-size: 60px; + color: #f9c0d0; + margin-bottom: 20px; +} + +.no-books p { + font-size: 1.1rem; + color: #e67e9f; + font-weight: 500; +} + +/* 分页容器 */ +.pagination-container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 30px; + 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: #777; + font-weight: 500; + transition: all 0.2s; + position: relative; +} + +.pagination .page-link:hover { + color: #e67e9f; + background-color: #fff9fb; +} + +.pagination .page-item.active .page-link { + background-color: #e67e9f; + color: white; + box-shadow: none; +} + +.pagination .page-item.disabled .page-link { + color: #bbb; + background-color: #f9f9f9; +} + +.pagination-info { + color: #999; + font-size: 0.9rem; +} + +/* 优化模态框样式 */ +.modal-content { + border-radius: 20px; + 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: #ffeef2; + border-bottom: 1px solid #ffe0e9; +} + +.modal-title { + color: #d23f6e; + font-size: 1.2rem; + font-weight: 600; +} + +.modal-body { + padding: 25px; +} + +.modal-footer { + padding: 15px 25px; + border-top: 1px solid #ffe0e9; + background-color: #ffeef2; +} + +.modal-body p { + color: #666; + font-size: 1rem; + line-height: 1.6; +} + +.modal-body p.text-danger { + color: #ff5252 !important; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; +} + +.modal-body p.text-danger::before { + content: "\f06a"; + font-family: "Font Awesome 5 Free"; + font-weight: 900; +} + +.modal .close { + font-size: 1.5rem; + color: #e67e9f; + opacity: 0.8; + text-shadow: none; + transition: all 0.2s; +} + +.modal .close:hover { + opacity: 1; + color: #d23f6e; +} + +.modal .btn { + border-radius: 25px; + padding: 8px 20px; + font-weight: 500; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); + border: none; +} + +.modal .btn-secondary { + background: linear-gradient(135deg, #a0a0a0, #808080); + color: white; +} + +.modal .btn-danger { + background: linear-gradient(135deg, #ff7676, #ff5252); + color: white; +} + +/* 封面标题栏 */ +.cover-title-bar { + position: absolute; + left: 0; right: 0; bottom: 0; + background: linear-gradient(0deg, rgba(233,152,174,0.92) 0%, rgba(255,255,255,0.08) 90%); + color: #fff; + font-size: 1rem; + font-weight: bold; + padding: 10px 14px 7px 14px; + text-shadow: 0 2px 6px rgba(180,0,80,0.14); + line-height: 1.3; + width: 100%; + box-sizing: border-box; + display: flex; + align-items: flex-end; + min-height: 38px; + z-index: 2; +} + +.book-card:hover .cover-title-bar { + background: linear-gradient(0deg, #d23f6e 0%, rgba(255,255,255,0.1) 100%); + font-size: 1.07rem; + letter-spacing: .5px; +} + +/* 响应式调整 */ +@media (max-width: 992px) { + .filter-row { + flex-wrap: wrap; + } + + .filter-group { + flex: 1 0 180px; + } +} + +@media (max-width: 768px) { + .book-list-container { + padding: 16px; + } + + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .action-buttons { + width: 100%; + overflow-x: auto; + padding-bottom: 8px; + flex-wrap: nowrap; + justify-content: flex-start; + } + + .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: 1fr; + } + + .book-actions { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 600px) { + .cover-title-bar { + font-size: 0.95rem; + min-height: 27px; + padding: 8px 8px 5px 10px; + } + + .book-actions { + grid-template-columns: 1fr; + } +} diff --git a/app/static/css/categories.css b/app/static/css/categories.css new file mode 100644 index 0000000..ed4d563 --- /dev/null +++ b/app/static/css/categories.css @@ -0,0 +1,68 @@ +/* 分类管理页面样式 */ +.categories-container { + padding: 20px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.card { + margin-bottom: 20px; + border: 1px solid rgba(0,0,0,0.125); + border-radius: 0.25rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + background-color: rgba(0,0,0,0.03); + border-bottom: 1px solid rgba(0,0,0,0.125); + font-weight: 600; +} + +.card-body { + padding: 1.25rem; +} + +.category-table { + border: 1px solid #eee; +} + +.category-table th { + background-color: #f8f9fa; +} + +.no-categories { + text-align: center; + padding: 30px; + color: #888; +} + +.no-categories i { + font-size: 48px; + color: #ddd; + margin-bottom: 10px; +} + +/* 通知弹窗 */ +.notification-alert { + position: fixed; + top: 20px; + right: 20px; + min-width: 300px; + z-index: 1050; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } +} diff --git a/app/static/css/main.css b/app/static/css/main.css index 79fdbfa..fe94da4 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -1,469 +1,261 @@ -/* 主样式文件 - 从登录页面复制过来的样式 */ -/* 从您提供的登录页CSS复制,但省略了不需要的部分 */ +/* 基础样式 */ * { margin: 0; padding: 0; box-sizing: border-box; - font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; -} - -:root { - --primary-color: #4a89dc; - --primary-hover: #3b78c4; - --secondary-color: #5cb85c; - --text-color: #333; - --light-text: #666; - --bg-color: #f5f7fa; - --card-bg: #ffffff; - --border-color: #ddd; - --error-color: #e74c3c; - --success-color: #2ecc71; -} - -body.dark-mode { - --primary-color: #5a9aed; - --primary-hover: #4a89dc; - --secondary-color: #6bc76b; - --text-color: #f1f1f1; - --light-text: #aaa; - --bg-color: #1a1a1a; - --card-bg: #2c2c2c; - --border-color: #444; } body { - background-color: var(--bg-color); - background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); - background-size: cover; - background-position: center; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f0f2f5; + color: #333; +} + +.app-container { display: flex; - flex-direction: column; min-height: 100vh; - color: var(--text-color); - transition: all 0.3s ease; } -.theme-toggle { - position: absolute; - top: 20px; - right: 20px; - z-index: 10; - cursor: pointer; - padding: 8px; - border-radius: 50%; - background-color: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(5px); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.overlay { - background-color: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(5px); +/* 侧边栏样式 */ +.sidebar { + width: 250px; + background-color: #2c3e50; + color: white; + box-shadow: 2px 0 5px rgba(0,0,0,0.1); position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: -1; + height: 100vh; + overflow-y: auto; + z-index: 1000; } -.main-container { +.logo-container { display: flex; - justify-content: center; align-items: center; - flex: 1; - padding: 20px; -} - -.login-container { - background-color: var(--card-bg); - border-radius: 12px; - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); - width: 450px; - padding: 35px; - position: relative; - overflow: hidden; - animation: fadeIn 0.5s ease; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } + padding: 20px 15px; + border-bottom: 1px solid rgba(255,255,255,0.1); } .logo { - text-align: center; - margin-bottom: 25px; - position: relative; + width: 40px; + height: 40px; + margin-right: 10px; } -.logo img { - width: 90px; - height: 90px; - border-radius: 12px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - padding: 5px; - background-color: #fff; - transition: transform 0.3s ease; -} - -h1 { - text-align: center; - color: var(--text-color); - margin-bottom: 10px; +.logo-container h2 { + font-size: 1.2rem; font-weight: 600; - font-size: 28px; } -.subtitle { +.nav-links { + list-style: none; + padding: 15px 0; +} + +.nav-category { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 1px; + padding: 15px 20px 5px; + color: #adb5bd; +} + +.nav-links li { + position: relative; +} + +.nav-links li.active { + background-color: rgba(255,255,255,0.1); +} + +.nav-links li a { + display: flex; + align-items: center; + padding: 12px 20px; + color: #ecf0f1; + text-decoration: none; + transition: all 0.3s; +} + +.nav-links li a:hover { + background-color: rgba(255,255,255,0.05); +} + +.nav-links li a i { + margin-right: 10px; + width: 20px; text-align: center; - color: var(--light-text); - margin-bottom: 30px; - font-size: 14px; } -.form-group { - margin-bottom: 22px; - position: relative; +/* 主内容区样式 */ +.main-content { + flex: 1; + margin-left: 250px; + display: flex; + flex-direction: column; + min-height: 100vh; } -.form-group label { - display: block; - margin-bottom: 8px; - color: var(--text-color); - font-weight: 500; - font-size: 14px; -} - -.input-with-icon { - position: relative; -} - -.input-icon { - position: absolute; - left: 15px; - top: 50%; - transform: translateY(-50%); - color: var(--light-text); -} - -.form-control { - width: 100%; - height: 48px; - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 0 15px 0 45px; - font-size: 15px; - transition: all 0.3s ease; - background-color: var(--card-bg); - color: var(--text-color); -} - -.form-control:focus { - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2); - outline: none; -} - -.password-toggle { - position: absolute; - right: 15px; - top: 50%; - transform: translateY(-50%); - cursor: pointer; - color: var(--light-text); -} - -.validation-message { - margin-top: 6px; - font-size: 12px; - color: var(--error-color); - display: none; -} - -.validation-message.show { - display: block; - animation: shake 0.5s ease; -} - -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } - 20%, 40%, 60%, 80% { transform: translateX(5px); } -} - -.remember-forgot { +.top-bar { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 25px; -} - -.custom-checkbox { - position: relative; - padding-left: 30px; - cursor: pointer; - font-size: 14px; - user-select: none; - color: var(--light-text); -} - -.custom-checkbox input { - position: absolute; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; -} - -.checkmark { - position: absolute; + padding: 15px 25px; + background-color: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + position: sticky; top: 0; - left: 0; - height: 18px; - width: 18px; - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 3px; - transition: all 0.2s ease; + z-index: 900; } -.custom-checkbox:hover input ~ .checkmark { - border-color: var(--primary-color); -} - -.custom-checkbox input:checked ~ .checkmark { - background-color: var(--primary-color); - border-color: var(--primary-color); -} - -.checkmark:after { - content: ""; - position: absolute; - display: none; -} - -.custom-checkbox input:checked ~ .checkmark:after { - display: block; -} - -.custom-checkbox .checkmark:after { - left: 6px; - top: 2px; - width: 4px; - height: 9px; - border: solid white; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -.forgot-password a { - color: var(--primary-color); - text-decoration: none; - font-size: 14px; - transition: color 0.3s ease; -} - -.forgot-password a:hover { - color: var(--primary-hover); - text-decoration: underline; -} - -.btn-login { - width: 100%; - height: 48px; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 6px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; +.search-container { position: relative; - overflow: hidden; + width: 350px; } -.btn-login:hover { - background-color: var(--primary-hover); -} - -.btn-login:active { - transform: scale(0.98); -} - -.btn-login .loading { - display: none; +.search-icon { position: absolute; + left: 10px; top: 50%; - left: 50%; - transform: translate(-50%, -50%); + transform: translateY(-50%); + color: #adb5bd; } -.btn-login.loading-state { - color: transparent; +.search-input { + width: 100%; + padding: 10px 10px 10px 35px; + border: 1px solid #dee2e6; + border-radius: 20px; + font-size: 0.9rem; } -.btn-login.loading-state .loading { - display: block; +.search-input:focus { + outline: none; + border-color: #4a6cf7; } -.signup { - text-align: center; - margin-top: 25px; - font-size: 14px; - color: var(--light-text); -} - -.signup a { - color: var(--primary-color); - text-decoration: none; - font-weight: 600; - transition: color 0.3s ease; -} - -.signup a:hover { - color: var(--primary-hover); - text-decoration: underline; -} - -.features { +.user-menu { display: flex; - justify-content: center; - margin-top: 25px; - gap: 30px; -} - -.feature-item { - text-align: center; - font-size: 12px; - color: var(--light-text); - display: flex; - flex-direction: column; align-items: center; } -.feature-icon { - margin-bottom: 5px; - font-size: 18px; +.notifications { + position: relative; + margin-right: 20px; + cursor: pointer; } -footer { - text-align: center; - padding: 20px; - color: rgba(255, 255, 255, 0.7); - font-size: 12px; +.badge { + position: absolute; + top: -5px; + right: -5px; + background-color: #e74c3c; + color: white; + font-size: 0.7rem; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; } -footer a { - color: rgba(255, 255, 255, 0.9); +.user-info { + display: flex; + align-items: center; + cursor: pointer; + position: relative; +} + +.user-avatar { + width: 40px; + height: 40px; + background-color: #4a6cf7; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + margin-right: 10px; +} + +.user-details { + display: flex; + flex-direction: column; +} + +.user-name { + font-weight: 600; + font-size: 0.9rem; +} + +.user-role { + font-size: 0.8rem; + color: #6c757d; +} + +.dropdown-menu { + position: absolute; + top: 100%; + right: 0; + background-color: white; + box-shadow: 0 3px 10px rgba(0,0,0,0.1); + border-radius: 5px; + width: 200px; + padding: 10px 0; + display: none; + z-index: 1000; +} + +.user-info.active .dropdown-menu { + display: block; +} + +.dropdown-menu a { + display: block; + padding: 8px 15px; + color: #333; text-decoration: none; + transition: background-color 0.3s; } -.alert { - padding: 10px; - margin-bottom: 15px; - border-radius: 4px; - color: #721c24; - background-color: #f8d7da; - border: 1px solid #f5c6cb; +.dropdown-menu a:hover { + background-color: #f8f9fa; } -.verification-code-container { - display: flex; - gap: 10px; +.dropdown-menu a i { + width: 20px; + margin-right: 10px; + text-align: center; } -.verification-input { +/* 内容区域 */ +.content-wrapper { flex: 1; - height: 48px; - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 0 15px; - font-size: 15px; + padding: 20px; + background-color: #f0f2f5; } -.send-code-btn { - padding: 0 15px; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - white-space: nowrap; -} - -.register-container { - width: 500px; -} - -@media (max-width: 576px) { - .login-container { - width: 100%; - padding: 25px; - border-radius: 0; - } - - .theme-toggle { - top: 10px; - } - - .logo img { +/* 响应式适配 */ +@media (max-width: 768px) { + .sidebar { width: 70px; - height: 70px; + overflow: visible; } - h1 { - font-size: 22px; + .logo-container h2 { + display: none; } - .main-container { - padding: 0; + .nav-links li a span { + display: none; } - .verification-code-container { - flex-direction: column; + .main-content { + margin-left: 70px; } - .register-container { - width: 100%; + .user-details { + display: none; } } - -.verification-code-container { - display: flex; - gap: 10px; -} - -.verification-input { - flex: 1; - height: 48px; - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 0 15px; - font-size: 15px; - transition: all 0.3s ease; - background-color: var(--card-bg); - color: var(--text-color); -} - -.send-code-btn { - padding: 0 15px; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 14px; - white-space: nowrap; - transition: all 0.3s ease; -} - -.send-code-btn:hover { - background-color: var(--primary-hover); -} - -.send-code-btn:disabled { - background-color: #ccc; - cursor: not-allowed; -} diff --git a/app/static/images/book-placeholder.jpg b/app/static/images/book-placeholder.jpg new file mode 100644 index 0000000..d1e40dc Binary files /dev/null and b/app/static/images/book-placeholder.jpg differ diff --git a/app/static/js/book-list.js b/app/static/js/book-list.js new file mode 100644 index 0000000..ddc013d --- /dev/null +++ b/app/static/js/book-list.js @@ -0,0 +1,302 @@ +// 图书列表页面脚本 +$(document).ready(function() { + // 处理分类筛选 + function setFilter(button, categoryId) { + // 移除所有按钮的活跃状态 + $('.filter-btn').removeClass('active'); + // 为当前点击的按钮添加活跃状态 + $(button).addClass('active'); + // 设置隐藏的分类ID输入值 + $('#category_id').val(categoryId); + // 提交表单 + $(button).closest('form').submit(); + } + + // 处理排序方向切换 + function toggleSortDirection(button) { + const $button = $(button); + const isAsc = $button.hasClass('asc'); + + // 切换方向类 + $button.toggleClass('asc desc'); + + // 更新图标 + if (isAsc) { + $button.find('i').removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down'); + $('#sort_order').val('desc'); + } else { + $button.find('i').removeClass('fa-sort-amount-down').addClass('fa-sort-amount-up'); + $('#sort_order').val('asc'); + } + + // 提交表单 + $button.closest('form').submit(); + } + + // 将函数暴露到全局作用域 + window.setFilter = setFilter; + window.toggleSortDirection = toggleSortDirection; + + // 处理删除图书 + let bookIdToDelete = null; + + $('.delete-btn').click(function(e) { + e.preventDefault(); + bookIdToDelete = $(this).data('id'); + const bookTitle = $(this).data('title'); + $('#deleteBookTitle').text(bookTitle); + $('#deleteModal').modal('show'); + }); + + $('#confirmDelete').click(function() { + if (!bookIdToDelete) return; + + $.ajax({ + url: `/book/delete/${bookIdToDelete}`, + type: 'POST', + success: function(response) { + if (response.success) { + $('#deleteModal').modal('hide'); + // 显示成功消息 + showNotification(response.message, 'success'); + // 移除图书卡片 + setTimeout(() => { + location.reload(); + }, 800); + } else { + showNotification(response.message, 'error'); + } + }, + error: function() { + showNotification('删除操作失败,请稍后重试', 'error'); + } + }); + }); + + // 处理借阅图书 + $('.borrow-btn').click(function(e) { + e.preventDefault(); + const bookId = $(this).data('id'); + + $.ajax({ + url: `/borrow/add/${bookId}`, + type: 'POST', + success: function(response) { + if (response.success) { + showNotification(response.message, 'success'); + // 可以更新UI显示,比如更新库存或禁用借阅按钮 + setTimeout(() => { + location.reload(); + }, 800); + } else { + showNotification(response.message, 'error'); + } + }, + error: function() { + showNotification('借阅操作失败,请稍后重试', 'error'); + } + }); + }); + + // 显示通知 + function showNotification(message, type) { + // 移除可能存在的旧通知 + $('.notification-alert').remove(); + + const alertClass = type === 'success' ? 'notification-success' : 'notification-error'; + const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'; + + const notification = ` +
+
+ +
+
${message}
+ +
+ `; + + $('body').append(notification); + + // 显示通知 + setTimeout(() => { + $('.notification-alert').addClass('show'); + }, 10); + + // 通知自动关闭 + setTimeout(() => { + $('.notification-alert').removeClass('show'); + setTimeout(() => { + $('.notification-alert').remove(); + }, 300); + }, 4000); + + // 点击关闭按钮 + $('.notification-close').click(function() { + $(this).closest('.notification-alert').removeClass('show'); + setTimeout(() => { + $(this).closest('.notification-alert').remove(); + }, 300); + }); + } + + // 添加通知样式 + const notificationCSS = ` + .notification-alert { + position: fixed; + top: 20px; + right: 20px; + min-width: 280px; + max-width: 350px; + background-color: white; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); + display: flex; + align-items: center; + padding: 15px; + transform: translateX(calc(100% + 20px)); + transition: transform 0.3s ease; + z-index: 9999; + } + + .notification-alert.show { + transform: translateX(0); + } + + .notification-success { + border-left: 4px solid var(--success-color); + } + + .notification-error { + border-left: 4px solid var(--danger-color); + } + + .notification-icon { + margin-right: 15px; + font-size: 24px; + } + + .notification-success .notification-icon { + color: var(--success-color); + } + + .notification-error .notification-icon { + color: var(--danger-color); + } + + .notification-message { + flex: 1; + font-size: 0.95rem; + color: var(--text-color); + } + + .notification-close { + background: none; + border: none; + color: var(--text-lighter); + cursor: pointer; + padding: 5px; + margin-left: 10px; + font-size: 0.8rem; + } + + .notification-close:hover { + color: var(--text-color); + } + + @media (max-width: 576px) { + .notification-alert { + top: auto; + bottom: 20px; + left: 20px; + right: 20px; + min-width: auto; + max-width: none; + transform: translateY(calc(100% + 20px)); + } + + .notification-alert.show { + transform: translateY(0); + } + } + `; + + // 将通知样式添加到头部 + $('