book_version_1_onlylist
This commit is contained in:
		
							parent
							
								
									805a648119
								
							
						
					
					
						commit
						423730c50a
					
				@ -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
 | 
			
		||||
    # 模板过滤器
 | 
			
		||||
    @app.template_filter('nl2br')
 | 
			
		||||
    def nl2br_filter(s):
 | 
			
		||||
        if not s:
 | 
			
		||||
            return s
 | 
			
		||||
        return s.replace('\n', '<br>')
 | 
			
		||||
 | 
			
		||||
    return app
 | 
			
		||||
 | 
			
		||||
@ -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/<int:book_id>')
 | 
			
		||||
@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/<int:book_id>', 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/<int:book_id>', 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/<int:category_id>', 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/<int:category_id>', 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('<br>'.join(errors[:10]) + (f'<br>...等共{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}'))
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
        # ... 其余代码 ...
 | 
			
		||||
@ -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'<Category {self.name}>'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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'<Book {self.title}>'
 | 
			
		||||
@ -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'<BorrowRecord {self.id}>'
 | 
			
		||||
@ -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'<InventoryLog {self.id}>'
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/static/covers/bainiangudu.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/static/covers/bainiangudu.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 994 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/static/covers/zhongguotongshi.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/static/covers/zhongguotongshi.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 759 KiB  | 
							
								
								
									
										215
									
								
								app/static/css/book-detail.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								app/static/css/book-detail.css
									
									
									
									
									
										Normal file
									
								
							@ -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%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								app/static/css/book-form.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								app/static/css/book-form.css
									
									
									
									
									
										Normal file
									
								
							@ -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%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								app/static/css/book-import.css  
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								app/static/css/book-import.css  
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										723
									
								
								app/static/css/book.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										723
									
								
								app/static/css/book.css
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								app/static/css/categories.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								app/static/css/categories.css
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/static/images/book-placeholder.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/static/images/book-placeholder.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 939 KiB  | 
							
								
								
									
										302
									
								
								app/static/js/book-list.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								app/static/js/book-list.js
									
									
									
									
									
										Normal file
									
								
							@ -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 = `
 | 
			
		||||
      <div class="notification-alert ${alertClass}">
 | 
			
		||||
        <div class="notification-icon">
 | 
			
		||||
          <i class="fas ${iconClass}"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="notification-message">${message}</div>
 | 
			
		||||
        <button class="notification-close">
 | 
			
		||||
          <i class="fas fa-times"></i>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    $('body').append(notification);
 | 
			
		||||
 | 
			
		||||
    // 显示通知
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      $('.notification-alert').addClass('show');
 | 
			
		||||
    }, 10);
 | 
			
		||||
 | 
			
		||||
    // 通知自动关闭
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      $('.notification-alert').removeClass('show');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        $('.notification-alert').remove();
 | 
			
		||||
      }, 300);
 | 
			
		||||
    }, 4000);
 | 
			
		||||
 | 
			
		||||
    // 点击关闭按钮
 | 
			
		||||
    $('.notification-close').click(function() {
 | 
			
		||||
      $(this).closest('.notification-alert').removeClass('show');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        $(this).closest('.notification-alert').remove();
 | 
			
		||||
      }, 300);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 添加通知样式
 | 
			
		||||
  const notificationCSS = `
 | 
			
		||||
    .notification-alert {
 | 
			
		||||
      position: fixed;
 | 
			
		||||
      top: 20px;
 | 
			
		||||
      right: 20px;
 | 
			
		||||
      min-width: 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);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
 | 
			
		||||
  // 将通知样式添加到头部
 | 
			
		||||
  $('<style>').text(notificationCSS).appendTo('head');
 | 
			
		||||
 | 
			
		||||
  // 修复图书卡片布局的高度问题
 | 
			
		||||
  function adjustCardHeights() {
 | 
			
		||||
    // 重置所有卡片高度
 | 
			
		||||
    $('.book-card').css('height', 'auto');
 | 
			
		||||
 | 
			
		||||
    // 在大屏幕上应用等高布局
 | 
			
		||||
    if (window.innerWidth >= 768) {
 | 
			
		||||
      // 分组按行
 | 
			
		||||
      const rows = {};
 | 
			
		||||
      $('.book-card').each(function() {
 | 
			
		||||
        const offsetTop = $(this).offset().top;
 | 
			
		||||
        if (!rows[offsetTop]) {
 | 
			
		||||
          rows[offsetTop] = [];
 | 
			
		||||
        }
 | 
			
		||||
        rows[offsetTop].push($(this));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // 为每行设置相同高度
 | 
			
		||||
      Object.keys(rows).forEach(offsetTop => {
 | 
			
		||||
        const cards = rows[offsetTop];
 | 
			
		||||
        let maxHeight = 0;
 | 
			
		||||
 | 
			
		||||
        // 找出最大高度
 | 
			
		||||
        cards.forEach(card => {
 | 
			
		||||
          const height = card.outerHeight();
 | 
			
		||||
          if (height > maxHeight) {
 | 
			
		||||
            maxHeight = height;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 应用最大高度
 | 
			
		||||
        cards.forEach(card => {
 | 
			
		||||
          card.css('height', maxHeight + 'px');
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 初始调整高度
 | 
			
		||||
  $(window).on('load', adjustCardHeights);
 | 
			
		||||
 | 
			
		||||
  // 窗口大小变化时调整高度
 | 
			
		||||
  $(window).on('resize', adjustCardHeights);
 | 
			
		||||
 | 
			
		||||
  // 为封面图片添加加载错误处理
 | 
			
		||||
  $('.book-cover').on('error', function() {
 | 
			
		||||
    const $this = $(this);
 | 
			
		||||
    const title = $this.attr('alt') || '图书';
 | 
			
		||||
 | 
			
		||||
    // 替换为默认封面
 | 
			
		||||
    $this.replaceWith(`
 | 
			
		||||
      <div class="default-cover">
 | 
			
		||||
        <i class="fas fa-book"></i>
 | 
			
		||||
        <span class="default-cover-text">${title.charAt(0)}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    `);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // 添加初始动画效果
 | 
			
		||||
  $('.book-card').each(function(index) {
 | 
			
		||||
    $(this).css({
 | 
			
		||||
      'opacity': '0',
 | 
			
		||||
      'transform': 'translateY(20px)'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      $(this).css({
 | 
			
		||||
        'opacity': '1',
 | 
			
		||||
        'transform': 'translateY(0)',
 | 
			
		||||
        'transition': 'opacity 0.5s ease, transform 0.5s ease'
 | 
			
		||||
      });
 | 
			
		||||
    }, 50 * index);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,119 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>{% block title %}图书管理系统{% endblock %}</title>
 | 
			
		||||
    <!-- 通用CSS -->
 | 
			
		||||
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
 | 
			
		||||
    <!-- 页面特定CSS -->
 | 
			
		||||
    {% block head %}{% endblock %}
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="app-container">
 | 
			
		||||
        <!-- 侧边导航栏 -->
 | 
			
		||||
        <nav class="sidebar">
 | 
			
		||||
            <div class="logo-container">
 | 
			
		||||
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo">
 | 
			
		||||
                <h2>图书管理系统</h2>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ul class="nav-links">
 | 
			
		||||
                <li class="{% if request.path == '/' %}active{% endif %}">
 | 
			
		||||
                    <a href="{{ url_for('index') }}"><i class="fas fa-home"></i> 首页</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/book/list' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="{{ url_for('book.book_list') }}"><i class="fas fa-book"></i> 图书浏览</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/borrow' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/announcement' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-bell"></i> 通知公告</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                {% if current_user.role_id == 1 %}
 | 
			
		||||
                <li class="nav-category">管理功能</li>
 | 
			
		||||
                <li class="{% if '/user/manage' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-users"></i> 用户管理</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/book/list' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="{{ url_for('book.book_list') }}"><i class="fas fa-layer-group"></i> 图书管理</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/borrow/manage' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/inventory' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-warehouse"></i> 库存管理</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/statistics' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="{% if '/log' in request.path %}active{% endif %}">
 | 
			
		||||
                    <a href="#"><i class="fas fa-history"></i> 日志管理</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </nav>
 | 
			
		||||
 | 
			
		||||
        <!-- 主内容区 -->
 | 
			
		||||
        <main class="main-content">
 | 
			
		||||
            <!-- 顶部导航 -->
 | 
			
		||||
            <header class="top-bar">
 | 
			
		||||
                <div class="search-container">
 | 
			
		||||
                    <i class="fas fa-search search-icon"></i>
 | 
			
		||||
                    <input type="text" placeholder="搜索图书..." class="search-input">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="user-menu">
 | 
			
		||||
                    <div class="notifications">
 | 
			
		||||
                        <i class="fas fa-bell"></i>
 | 
			
		||||
                        <span class="badge">3</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="user-info">
 | 
			
		||||
                        <div class="user-avatar">
 | 
			
		||||
                            {{ current_user.username[0] }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="user-details">
 | 
			
		||||
                            <span class="user-name">{{ current_user.username }}</span>
 | 
			
		||||
                            <span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="dropdown-menu">
 | 
			
		||||
                            <a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
 | 
			
		||||
                            <a href="#"><i class="fas fa-cog"></i> 设置</a>
 | 
			
		||||
                            <a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </header>
 | 
			
		||||
 | 
			
		||||
            <!-- 内容区 - 这里是核心变化 -->
 | 
			
		||||
            <div class="content-wrapper">
 | 
			
		||||
                {% block content %}
 | 
			
		||||
                <!-- 子模板将在这里添加内容 -->
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 通用JavaScript -->
 | 
			
		||||
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
            // 用户菜单下拉
 | 
			
		||||
            const userInfo = document.querySelector('.user-info');
 | 
			
		||||
            userInfo.addEventListener('click', function(e) {
 | 
			
		||||
                userInfo.classList.toggle('active');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // 点击其他区域关闭下拉菜单
 | 
			
		||||
            document.addEventListener('click', function(e) {
 | 
			
		||||
                if (!userInfo.contains(e.target)) {
 | 
			
		||||
                    userInfo.classList.remove('active');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
 | 
			
		||||
    <!-- 页面特定JavaScript -->
 | 
			
		||||
    {% block scripts %}{% endblock %}
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										155
									
								
								app/templates/book/add.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								app/templates/book/add.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,155 @@
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}添加图书 - 图书管理系统{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-form.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="book-form-container">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
        <h1>添加新图书</h1>
 | 
			
		||||
        <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
 | 
			
		||||
            <i class="fas fa-arrow-left"></i> 返回列表
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <form method="POST" enctype="multipart/form-data" class="book-form">
 | 
			
		||||
        <div class="form-row">
 | 
			
		||||
            <div class="col-md-8">
 | 
			
		||||
                <div class="card">
 | 
			
		||||
                    <div class="card-header">基本信息</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-12">
 | 
			
		||||
                                <label for="title">书名 <span class="required">*</span></label>
 | 
			
		||||
                                <input type="text" class="form-control" id="title" name="title" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="author">作者 <span class="required">*</span></label>
 | 
			
		||||
                                <input type="text" class="form-control" id="author" name="author" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="publisher">出版社</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="publisher" name="publisher">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="isbn">ISBN</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="isbn" name="isbn">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="publish_year">出版年份</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="publish_year" name="publish_year">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="category_id">分类</label>
 | 
			
		||||
                                <select class="form-control" id="category_id" name="category_id">
 | 
			
		||||
                                    <option value="">未分类</option>
 | 
			
		||||
                                    {% for category in categories %}
 | 
			
		||||
                                    <option value="{{ category.id }}">{{ category.name }}</option>
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="tags">标签</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="tags" name="tags" placeholder="多个标签用逗号分隔">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="card mt-4">
 | 
			
		||||
                    <div class="card-header">图书简介</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介"></textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="col-md-4">
 | 
			
		||||
                <div class="card">
 | 
			
		||||
                    <div class="card-header">封面图片</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="cover-preview-container">
 | 
			
		||||
                            <div class="cover-preview" id="coverPreview">
 | 
			
		||||
                                <div class="no-cover-placeholder">
 | 
			
		||||
                                    <i class="fas fa-image"></i>
 | 
			
		||||
                                    <span>暂无封面</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="upload-container">
 | 
			
		||||
                                <label for="cover" class="btn btn-outline-primary btn-block">
 | 
			
		||||
                                    <i class="fas fa-upload"></i> 上传封面
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="card mt-4">
 | 
			
		||||
                    <div class="card-header">库存和价格</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="stock">库存数量</label>
 | 
			
		||||
                            <input type="number" class="form-control" id="stock" name="stock" min="0" value="0">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="price">价格</label>
 | 
			
		||||
                            <div class="input-group">
 | 
			
		||||
                                <div class="input-group-prepend">
 | 
			
		||||
                                    <span class="input-group-text">¥</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <input type="number" class="form-control" id="price" name="price" step="0.01" min="0">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-submit-container">
 | 
			
		||||
                    <button type="submit" class="btn btn-primary btn-lg btn-block">
 | 
			
		||||
                        <i class="fas fa-save"></i> 保存图书
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $(document).ready(function() {
 | 
			
		||||
        // 封面预览
 | 
			
		||||
        $('#cover').change(function() {
 | 
			
		||||
            const file = this.files[0];
 | 
			
		||||
            if (file) {
 | 
			
		||||
                const reader = new FileReader();
 | 
			
		||||
                reader.onload = function(e) {
 | 
			
		||||
                    $('#coverPreview').html(`<img src="${e.target.result}" class="cover-image">`);
 | 
			
		||||
                }
 | 
			
		||||
                reader.readAsDataURL(file);
 | 
			
		||||
            } else {
 | 
			
		||||
                $('#coverPreview').html(`
 | 
			
		||||
                    <div class="no-cover-placeholder">
 | 
			
		||||
                        <i class="fas fa-image"></i>
 | 
			
		||||
                        <span>暂无封面</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                `);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										313
									
								
								app/templates/book/categories.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								app/templates/book/categories.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,313 @@
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}图书分类管理 - 图书管理系统{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/categories.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="categories-container">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
        <h1>图书分类管理</h1>
 | 
			
		||||
        <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
 | 
			
		||||
            <i class="fas fa-arrow-left"></i> 返回图书列表
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-4">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    添加新分类
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form id="categoryForm">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="categoryName">分类名称</label>
 | 
			
		||||
                            <input type="text" class="form-control" id="categoryName" name="name" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="parentCategory">父级分类</label>
 | 
			
		||||
                            <select class="form-control" id="parentCategory" name="parent_id">
 | 
			
		||||
                                <option value="">无 (顶级分类)</option>
 | 
			
		||||
                                {% for category in categories %}
 | 
			
		||||
                                <option value="{{ category.id }}">{{ category.name }}</option>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="categorySort">排序</label>
 | 
			
		||||
                            <input type="number" class="form-control" id="categorySort" name="sort" value="0" min="0">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <button type="submit" class="btn btn-primary btn-block">
 | 
			
		||||
                            <i class="fas fa-plus"></i> 添加分类
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="col-md-8">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    分类列表
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    {% if categories %}
 | 
			
		||||
                    <table class="table table-hover category-table">
 | 
			
		||||
                        <thead>
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <th width="5%">ID</th>
 | 
			
		||||
                                <th width="40%">分类名称</th>
 | 
			
		||||
                                <th width="20%">父级分类</th>
 | 
			
		||||
                                <th width="10%">排序</th>
 | 
			
		||||
                                <th width="25%">操作</th>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        </thead>
 | 
			
		||||
                        <tbody>
 | 
			
		||||
                            {% for category in categories %}
 | 
			
		||||
                            <tr data-id="{{ category.id }}">
 | 
			
		||||
                                <td>{{ category.id }}</td>
 | 
			
		||||
                                <td>{{ category.name }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    {% if category.parent %}
 | 
			
		||||
                                    {{ category.parent.name }}
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                    <span class="text-muted">无</span>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>{{ category.sort }}</td>
 | 
			
		||||
                                                                <td>
 | 
			
		||||
                                    <button class="btn btn-sm btn-primary edit-category" data-id="{{ category.id }}"
 | 
			
		||||
                                            data-name="{{ category.name }}"
 | 
			
		||||
                                            data-parent="{{ category.parent_id or '' }}"
 | 
			
		||||
                                            data-sort="{{ category.sort }}">
 | 
			
		||||
                                        <i class="fas fa-edit"></i> 编辑
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <button class="btn btn-sm btn-danger delete-category" data-id="{{ category.id }}" data-name="{{ category.name }}">
 | 
			
		||||
                                        <i class="fas fa-trash"></i> 删除
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="no-categories">
 | 
			
		||||
                        <i class="fas fa-exclamation-circle"></i>
 | 
			
		||||
                        <p>暂无分类数据</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- 编辑分类模态框 -->
 | 
			
		||||
<div class="modal fade" id="editCategoryModal" tabindex="-1" role="dialog" aria-labelledby="editCategoryModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog" role="document">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="editCategoryModalLabel">编辑分类</h5>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form id="editCategoryForm">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <input type="hidden" id="editCategoryId">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="editCategoryName">分类名称</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="editCategoryName" name="name" required>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="editParentCategory">父级分类</label>
 | 
			
		||||
                        <select class="form-control" id="editParentCategory" name="parent_id">
 | 
			
		||||
                            <option value="">无 (顶级分类)</option>
 | 
			
		||||
                            {% for category in categories %}
 | 
			
		||||
                            <option value="{{ category.id }}">{{ category.name }}</option>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="editCategorySort">排序</label>
 | 
			
		||||
                        <input type="number" class="form-control" id="editCategorySort" name="sort" value="0" min="0">
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-primary">保存修改</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- 删除确认模态框 -->
 | 
			
		||||
<div class="modal fade" id="deleteCategoryModal" tabindex="-1" role="dialog" aria-labelledby="deleteCategoryModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog" role="document">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="deleteCategoryModalLabel">确认删除</h5>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-body">
 | 
			
		||||
                <p>您确定要删除分类 "<span id="deleteCategoryName"></span>" 吗?</p>
 | 
			
		||||
                <p class="text-danger">注意: 如果该分类下有图书或子分类,将无法删除!</p>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="modal-footer">
 | 
			
		||||
                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
 | 
			
		||||
                <button type="button" class="btn btn-danger" id="confirmDeleteCategory">确认删除</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $(document).ready(function() {
 | 
			
		||||
        // 添加分类
 | 
			
		||||
        $('#categoryForm').submit(function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
 | 
			
		||||
            const formData = {
 | 
			
		||||
                name: $('#categoryName').val(),
 | 
			
		||||
                parent_id: $('#parentCategory').val(),
 | 
			
		||||
                sort: $('#categorySort').val()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                url: '{{ url_for("book.add_category") }}',
 | 
			
		||||
                type: 'POST',
 | 
			
		||||
                data: formData,
 | 
			
		||||
                success: function(response) {
 | 
			
		||||
                    if (response.success) {
 | 
			
		||||
                        showNotification(response.message, 'success');
 | 
			
		||||
                        setTimeout(function() {
 | 
			
		||||
                            location.reload();
 | 
			
		||||
                        }, 1000);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        showNotification(response.message, 'error');
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                error: function() {
 | 
			
		||||
                    showNotification('操作失败,请稍后重试', 'error');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 打开编辑模态框
 | 
			
		||||
        $('.edit-category').click(function() {
 | 
			
		||||
            const id = $(this).data('id');
 | 
			
		||||
            const name = $(this).data('name');
 | 
			
		||||
            const parentId = $(this).data('parent');
 | 
			
		||||
            const sort = $(this).data('sort');
 | 
			
		||||
 | 
			
		||||
            $('#editCategoryId').val(id);
 | 
			
		||||
            $('#editCategoryName').val(name);
 | 
			
		||||
            $('#editParentCategory').val(parentId);
 | 
			
		||||
            $('#editCategorySort').val(sort);
 | 
			
		||||
 | 
			
		||||
            // 禁用选择自己作为父级
 | 
			
		||||
            $('#editParentCategory option').removeAttr('disabled');
 | 
			
		||||
            $(`#editParentCategory option[value="${id}"]`).attr('disabled', 'disabled');
 | 
			
		||||
 | 
			
		||||
            $('#editCategoryModal').modal('show');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 提交编辑表单
 | 
			
		||||
        $('#editCategoryForm').submit(function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
 | 
			
		||||
            const id = $('#editCategoryId').val();
 | 
			
		||||
            const formData = {
 | 
			
		||||
                name: $('#editCategoryName').val(),
 | 
			
		||||
                parent_id: $('#editParentCategory').val(),
 | 
			
		||||
                sort: $('#editCategorySort').val()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                url: `/book/categories/edit/${id}`,
 | 
			
		||||
                type: 'POST',
 | 
			
		||||
                data: formData,
 | 
			
		||||
                success: function(response) {
 | 
			
		||||
                    if (response.success) {
 | 
			
		||||
                        $('#editCategoryModal').modal('hide');
 | 
			
		||||
                        showNotification(response.message, 'success');
 | 
			
		||||
                        setTimeout(function() {
 | 
			
		||||
                            location.reload();
 | 
			
		||||
                        }, 1000);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        showNotification(response.message, 'error');
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                error: function() {
 | 
			
		||||
                    showNotification('操作失败,请稍后重试', 'error');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 打开删除确认框
 | 
			
		||||
        $('.delete-category').click(function() {
 | 
			
		||||
            const id = $(this).data('id');
 | 
			
		||||
            const name = $(this).data('name');
 | 
			
		||||
 | 
			
		||||
            $('#deleteCategoryName').text(name);
 | 
			
		||||
            $('#confirmDeleteCategory').data('id', id);
 | 
			
		||||
 | 
			
		||||
            $('#deleteCategoryModal').modal('show');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 确认删除
 | 
			
		||||
        $('#confirmDeleteCategory').click(function() {
 | 
			
		||||
            const id = $(this).data('id');
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                url: `/book/categories/delete/${id}`,
 | 
			
		||||
                type: 'POST',
 | 
			
		||||
                success: function(response) {
 | 
			
		||||
                    $('#deleteCategoryModal').modal('hide');
 | 
			
		||||
 | 
			
		||||
                    if (response.success) {
 | 
			
		||||
                        showNotification(response.message, 'success');
 | 
			
		||||
                        setTimeout(function() {
 | 
			
		||||
                            location.reload();
 | 
			
		||||
                        }, 1000);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        showNotification(response.message, 'error');
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                error: function() {
 | 
			
		||||
                    showNotification('操作失败,请稍后重试', 'error');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 显示通知
 | 
			
		||||
        function showNotification(message, type) {
 | 
			
		||||
            const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
 | 
			
		||||
            const alert = `
 | 
			
		||||
                <div class="alert ${alertClass} alert-dismissible fade show notification-alert" role="alert">
 | 
			
		||||
                    ${message}
 | 
			
		||||
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
 | 
			
		||||
                        <span aria-hidden="true">×</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            `;
 | 
			
		||||
 | 
			
		||||
            $('body').append(alert);
 | 
			
		||||
 | 
			
		||||
            // 5秒后自动关闭
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                $('.notification-alert').alert('close');
 | 
			
		||||
            }, 5000);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										197
									
								
								app/templates/book/detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								app/templates/book/detail.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,197 @@
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ book.title }} - 图书详情{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-detail.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="book-detail-container">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
        <h1>图书详情</h1>
 | 
			
		||||
        <div class="actions">
 | 
			
		||||
            <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
 | 
			
		||||
                <i class="fas fa-arrow-left"></i> 返回列表
 | 
			
		||||
            </a>
 | 
			
		||||
            {% if current_user.role_id == 1 %}
 | 
			
		||||
            <a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary">
 | 
			
		||||
                <i class="fas fa-edit"></i> 编辑图书
 | 
			
		||||
            </a>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if book.stock > 0 %}
 | 
			
		||||
            <a href="#" class="btn btn-success" id="borrowBtn">
 | 
			
		||||
                <i class="fas fa-hand-holding"></i> 借阅此书
 | 
			
		||||
            </a>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="book-content">
 | 
			
		||||
        <div class="book-header">
 | 
			
		||||
            <div class="book-cover-large">
 | 
			
		||||
                {% if book.cover_url %}
 | 
			
		||||
                <img src="{{ book.cover_url }}" alt="{{ book.title }}">
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <div class="no-cover-large">
 | 
			
		||||
                    <i class="fas fa-book"></i>
 | 
			
		||||
                    <span>无封面</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="book-main-info">
 | 
			
		||||
                <h2 class="book-title">{{ book.title }}</h2>
 | 
			
		||||
                <p class="book-author"><i class="fas fa-user-edit"></i> 作者: {{ book.author }}</p>
 | 
			
		||||
 | 
			
		||||
                <div class="book-meta-info">
 | 
			
		||||
                    <div class="meta-item">
 | 
			
		||||
                        <i class="fas fa-building"></i>
 | 
			
		||||
                        <span>出版社: </span>
 | 
			
		||||
                        <span class="meta-value">{{ book.publisher or '未知' }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="meta-item">
 | 
			
		||||
                        <i class="fas fa-calendar-alt"></i>
 | 
			
		||||
                        <span>出版年份: </span>
 | 
			
		||||
                        <span class="meta-value">{{ book.publish_year or '未知' }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="meta-item">
 | 
			
		||||
                        <i class="fas fa-barcode"></i>
 | 
			
		||||
                        <span>ISBN: </span>
 | 
			
		||||
                        <span class="meta-value">{{ book.isbn or '未知' }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="meta-item">
 | 
			
		||||
                        <i class="fas fa-layer-group"></i>
 | 
			
		||||
                        <span>分类: </span>
 | 
			
		||||
                        <span class="meta-value">{{ book.category.name if book.category else '未分类' }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% if book.tags %}
 | 
			
		||||
                    <div class="meta-item">
 | 
			
		||||
                        <i class="fas fa-tags"></i>
 | 
			
		||||
                        <span>标签: </span>
 | 
			
		||||
                        <span class="meta-value">
 | 
			
		||||
                            {% for tag in book.tags.split(',') %}
 | 
			
		||||
                            <span class="tag">{{ tag.strip() }}</span>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <div class="meta-item">
 | 
			
		||||
                        <i class="fas fa-yuan-sign"></i>
 | 
			
		||||
                        <span>价格: </span>
 | 
			
		||||
                        <span class="meta-value">{{ book.price or '未知' }}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="book-status-info">
 | 
			
		||||
                    <div class="status-badge {{ 'available' if book.stock > 0 else 'unavailable' }}">
 | 
			
		||||
                        {{ '可借阅' if book.stock > 0 else '无库存' }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="stock-info">
 | 
			
		||||
                        <i class="fas fa-cubes"></i> 库存: {{ book.stock }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="book-details-section">
 | 
			
		||||
            <h3>图书简介</h3>
 | 
			
		||||
            <div class="book-description">
 | 
			
		||||
                {% if book.description %}
 | 
			
		||||
                <p>{{ book.description|nl2br }}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="no-description">暂无图书简介</p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- 借阅历史 (仅管理员可见) -->
 | 
			
		||||
        {% if current_user.role_id == 1 %}
 | 
			
		||||
        <div class="book-borrow-history">
 | 
			
		||||
            <h3>借阅历史</h3>
 | 
			
		||||
            {% set borrow_records = book.borrow_records.order_by(BorrowRecord.borrow_date.desc()).limit(10).all() %}
 | 
			
		||||
 | 
			
		||||
            {% if borrow_records %}
 | 
			
		||||
            <table class="table borrow-table">
 | 
			
		||||
                <thead>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th>借阅用户</th>
 | 
			
		||||
                        <th>借阅日期</th>
 | 
			
		||||
                        <th>应还日期</th>
 | 
			
		||||
                        <th>实际归还</th>
 | 
			
		||||
                        <th>状态</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    {% for record in borrow_records %}
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td>{{ record.user.username }}</td>
 | 
			
		||||
                        <td>{{ record.borrow_date.strftime('%Y-%m-%d') }}</td>
 | 
			
		||||
                        <td>{{ record.due_date.strftime('%Y-%m-%d') }}</td>
 | 
			
		||||
                        <td>{{ record.return_date.strftime('%Y-%m-%d') if record.return_date else '-' }}</td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            {% if record.status == 1 and record.due_date < now %}
 | 
			
		||||
                            <span class="badge badge-danger">已逾期</span>
 | 
			
		||||
                            {% elif record.status == 1 %}
 | 
			
		||||
                            <span class="badge badge-warning">借阅中</span>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <span class="badge badge-success">已归还</span>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <p class="no-records">暂无借阅记录</p>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- 借阅确认模态框 -->
 | 
			
		||||
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog" role="document">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header">
 | 
			
		||||
                <h5 class="modal-title" id="borrowModalLabel">借阅确认</h5>
 | 
			
		||||
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | 
			
		||||
                    <span aria-hidden="true">×</span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form id="borrowForm" action="{{ url_for('borrow.borrow_book') }}" method="POST">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <p>您确定要借阅《{{ book.title }}》吗?</p>
 | 
			
		||||
                    <input type="hidden" name="book_id" value="{{ book.id }}">
 | 
			
		||||
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="borrow_days">借阅天数</label>
 | 
			
		||||
                        <select class="form-control" id="borrow_days" name="borrow_days">
 | 
			
		||||
                            <option value="7">7天</option>
 | 
			
		||||
                            <option value="14" selected>14天</option>
 | 
			
		||||
                            <option value="30">30天</option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-success">确认借阅</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $(document).ready(function() {
 | 
			
		||||
        // 借阅按钮点击事件
 | 
			
		||||
        $('#borrowBtn').click(function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            $('#borrowModal').modal('show');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										177
									
								
								app/templates/book/edit.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								app/templates/book/edit.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,177 @@
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}编辑图书 - {{ book.title }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-form.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="book-form-container">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
        <h1>编辑图书</h1>
 | 
			
		||||
        <div class="actions">
 | 
			
		||||
            <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info">
 | 
			
		||||
                <i class="fas fa-eye"></i> 查看详情
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
 | 
			
		||||
                <i class="fas fa-arrow-left"></i> 返回列表
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <form method="POST" enctype="multipart/form-data" class="book-form">
 | 
			
		||||
        <div class="form-row">
 | 
			
		||||
            <div class="col-md-8">
 | 
			
		||||
                <div class="card">
 | 
			
		||||
                    <div class="card-header">基本信息</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-12">
 | 
			
		||||
                                <label for="title">书名 <span class="required">*</span></label>
 | 
			
		||||
                                <input type="text" class="form-control" id="title" name="title" value="{{ book.title }}" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="author">作者 <span class="required">*</span></label>
 | 
			
		||||
                                <input type="text" class="form-control" id="author" name="author" value="{{ book.author }}" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="publisher">出版社</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="publisher" name="publisher" value="{{ book.publisher or '' }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="isbn">ISBN</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="isbn" name="isbn" value="{{ book.isbn or '' }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="publish_year">出版年份</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="publish_year" name="publish_year" value="{{ book.publish_year or '' }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="form-row">
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="category_id">分类</label>
 | 
			
		||||
                                <select class="form-control" id="category_id" name="category_id">
 | 
			
		||||
                                    <option value="">未分类</option>
 | 
			
		||||
                                    {% for category in categories %}
 | 
			
		||||
                                    <option value="{{ category.id }}" {% if book.category_id == category.id %}selected{% endif %}>
 | 
			
		||||
                                        {{ category.name }}
 | 
			
		||||
                                    </option>
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="form-group col-md-6">
 | 
			
		||||
                                <label for="tags">标签</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="tags" name="tags" value="{{ book.tags or '' }}" placeholder="多个标签用逗号分隔">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="card mt-4">
 | 
			
		||||
                    <div class="card-header">图书简介</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <textarea class="form-control" id="description" name="description" rows="8" placeholder="请输入图书简介">{{ book.description or '' }}</textarea>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="col-md-4">
 | 
			
		||||
                <div class="card">
 | 
			
		||||
                    <div class="card-header">封面图片</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="cover-preview-container">
 | 
			
		||||
                            <div class="cover-preview" id="coverPreview">
 | 
			
		||||
                                {% if book.cover_url %}
 | 
			
		||||
                                <img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
 | 
			
		||||
                                {% else %}
 | 
			
		||||
                                <div class="no-cover-placeholder">
 | 
			
		||||
                                    <i class="fas fa-image"></i>
 | 
			
		||||
                                    <span>暂无封面</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="upload-container">
 | 
			
		||||
                                <label for="cover" class="btn btn-outline-primary btn-block">
 | 
			
		||||
                                    <i class="fas fa-upload"></i> 更换封面
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="card mt-4">
 | 
			
		||||
                    <div class="card-header">库存和价格</div>
 | 
			
		||||
                    <div class="card-body">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="stock">库存数量</label>
 | 
			
		||||
                            <input type="number" class="form-control" id="stock" name="stock" min="0" value="{{ book.stock }}">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="price">价格</label>
 | 
			
		||||
                            <div class="input-group">
 | 
			
		||||
                                <div class="input-group-prepend">
 | 
			
		||||
                                    <span class="input-group-text">¥</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <input type="number" class="form-control" id="price" name="price" step="0.01" min="0" value="{{ book.price or '' }}">
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="status">状态</label>
 | 
			
		||||
                            <select class="form-control" id="status" name="status">
 | 
			
		||||
                                <option value="1" {% if book.status == 1 %}selected{% endif %}>上架</option>
 | 
			
		||||
                                <option value="0" {% if book.status == 0 %}selected{% endif %}>下架</option>
 | 
			
		||||
                            </select>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="form-submit-container">
 | 
			
		||||
                    <button type="submit" class="btn btn-primary btn-lg btn-block">
 | 
			
		||||
                        <i class="fas fa-save"></i> 保存修改
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $(document).ready(function() {
 | 
			
		||||
        // 封面预览
 | 
			
		||||
        $('#cover').change(function() {
 | 
			
		||||
            const file = this.files[0];
 | 
			
		||||
            if (file) {
 | 
			
		||||
                const reader = new FileReader();
 | 
			
		||||
                reader.onload = function(e) {
 | 
			
		||||
                    $('#coverPreview').html(`<img src="${e.target.result}" class="cover-image">`);
 | 
			
		||||
                }
 | 
			
		||||
                reader.readAsDataURL(file);
 | 
			
		||||
            } else {
 | 
			
		||||
                $('#coverPreview').html(`
 | 
			
		||||
                    {% if book.cover_url %}
 | 
			
		||||
                    <img src="{{ book.cover_url }}" class="cover-image" alt="{{ book.title }}">
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <div class="no-cover-placeholder">
 | 
			
		||||
                        <i class="fas fa-image"></i>
 | 
			
		||||
                        <span>暂无封面</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                `);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										83
									
								
								app/templates/book/import.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/templates/book/import.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}批量导入图书 - 图书管理系统{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/book-import.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="import-container">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
        <h1>批量导入图书</h1>
 | 
			
		||||
        <a href="{{ url_for('book.book_list') }}" class="btn btn-secondary">
 | 
			
		||||
            <i class="fas fa-arrow-left"></i> 返回图书列表
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-8 offset-md-2">
 | 
			
		||||
            <div class="card">
 | 
			
		||||
                <div class="card-header">
 | 
			
		||||
                    <h4>Excel文件导入</h4>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <form method="POST" enctype="multipart/form-data">
 | 
			
		||||
                        <div class="form-group">
 | 
			
		||||
                            <label for="file">选择Excel文件</label>
 | 
			
		||||
                            <div class="custom-file">
 | 
			
		||||
                                <input type="file" class="custom-file-input" id="file" name="file" accept=".xlsx, .xls" required>
 | 
			
		||||
                                <label class="custom-file-label" for="file">选择文件...</label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <small class="form-text text-muted">支持的文件格式: .xlsx, .xls</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <button type="submit" class="btn btn-primary btn-lg btn-block">
 | 
			
		||||
                            <i class="fas fa-upload"></i> 开始导入
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </form>
 | 
			
		||||
 | 
			
		||||
                    <hr>
 | 
			
		||||
 | 
			
		||||
                    <div class="import-instructions">
 | 
			
		||||
                        <h5>导入说明:</h5>
 | 
			
		||||
                        <ul>
 | 
			
		||||
                            <li>Excel文件须包含以下列 (标题行必须与下列完全一致):</li>
 | 
			
		||||
                            <li class="required-field">title - 图书标题 (必填)</li>
 | 
			
		||||
                            <li class="required-field">author - 作者名称 (必填)</li>
 | 
			
		||||
                            <li>publisher - 出版社</li>
 | 
			
		||||
                            <li>category_id - 分类ID (对应系统中的分类ID)</li>
 | 
			
		||||
                            <li>tags - 标签 (多个标签用逗号分隔)</li>
 | 
			
		||||
                            <li>isbn - ISBN编号 (建议唯一)</li>
 | 
			
		||||
                            <li>publish_year - 出版年份</li>
 | 
			
		||||
                            <li>description - 图书简介</li>
 | 
			
		||||
                            <li>cover_url - 封面图片URL</li>
 | 
			
		||||
                            <li>stock - 库存数量</li>
 | 
			
		||||
                            <li>price - 价格</li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
 | 
			
		||||
                        <div class="template-download">
 | 
			
		||||
                            <p>下载导入模板:</p>
 | 
			
		||||
                            <a href="{{ url_for('static', filename='templates/book_import_template.xlsx') }}" class="btn btn-outline-primary">
 | 
			
		||||
                                <i class="fas fa-download"></i> 下载Excel模板
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $(document).ready(function() {
 | 
			
		||||
        // 显示选择的文件名
 | 
			
		||||
        $('.custom-file-input').on('change', function() {
 | 
			
		||||
            const fileName = $(this).val().split('\\').pop();
 | 
			
		||||
            $(this).next('.custom-file-label').html(fileName);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										188
									
								
								app/templates/book/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								app/templates/book/list.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,188 @@
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
{% block title %}图书列表 - 图书管理系统{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/book.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="book-list-container">
 | 
			
		||||
    <!-- 添加泡泡动画元素 -->
 | 
			
		||||
    <!-- 这些泡泡会通过 JS 动态创建 -->
 | 
			
		||||
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
        <h1>图书管理</h1>
 | 
			
		||||
 | 
			
		||||
        {% if current_user.role_id == 1 %}
 | 
			
		||||
        <div class="action-buttons">
 | 
			
		||||
            <a href="{{ url_for('book.add_book') }}" class="btn btn-primary">
 | 
			
		||||
                <i class="fas fa-plus"></i> 添加图书
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="{{ url_for('book.import_books') }}" class="btn btn-success">
 | 
			
		||||
                <i class="fas fa-file-upload"></i> 批量导入
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="{{ url_for('book.export_books') }}" class="btn btn-info">
 | 
			
		||||
                <i class="fas fa-file-download"></i> 导出图书
 | 
			
		||||
            </a>
 | 
			
		||||
            <a href="{{ url_for('book.category_list') }}" class="btn btn-secondary">
 | 
			
		||||
                <i class="fas fa-tags"></i> 分类管理
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="filter-section">
 | 
			
		||||
        <form method="GET" action="{{ url_for('book.book_list') }}" class="search-form">
 | 
			
		||||
            <div class="search-row">
 | 
			
		||||
                <div class="form-group search-group">
 | 
			
		||||
                    <input type="text" name="search" class="form-control" placeholder="搜索书名/作者/ISBN" value="{{ search }}">
 | 
			
		||||
                    <button type="submit" class="btn btn-primary">
 | 
			
		||||
                        <i class="fas fa-search"></i>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="filter-row">
 | 
			
		||||
                <div class="form-group filter-group">
 | 
			
		||||
                    <select name="category_id" class="form-control" onchange="this.form.submit()">
 | 
			
		||||
                        <option value="">全部分类</option>
 | 
			
		||||
                        {% for category in categories %}
 | 
			
		||||
                        <option value="{{ category.id }}" {% if category_id == category.id %}selected{% endif %}>
 | 
			
		||||
                            {{ category.name }}
 | 
			
		||||
                        </option>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group filter-group">
 | 
			
		||||
                    <select name="sort" class="form-control" onchange="this.form.submit()">
 | 
			
		||||
                        <option value="id" {% if sort == 'id' %}selected{% endif %}>默认排序</option>
 | 
			
		||||
                        <option value="created_at" {% if sort == 'created_at' %}selected{% endif %}>入库时间</option>
 | 
			
		||||
                        <option value="title" {% if sort == 'title' %}selected{% endif %}>书名</option>
 | 
			
		||||
                        <option value="stock" {% if sort == 'stock' %}selected{% endif %}>库存</option>
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-group filter-group">
 | 
			
		||||
                    <select name="order" class="form-control" onchange="this.form.submit()">
 | 
			
		||||
                        <option value="desc" {% if order == 'desc' %}selected{% endif %}>降序</option>
 | 
			
		||||
                        <option value="asc" {% if order == 'asc' %}selected{% endif %}>升序</option>
 | 
			
		||||
                    </select>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="books-grid">
 | 
			
		||||
        {% for book in books %}
 | 
			
		||||
        <div class="book-card">
 | 
			
		||||
            <div class="book-cover">
 | 
			
		||||
                {% if book.cover_url %}
 | 
			
		||||
                <img src="{{ book.cover_url }}" alt="{{ book.title }}">
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <div class="no-cover">
 | 
			
		||||
                    <i class="fas fa-book"></i>
 | 
			
		||||
                    <span>无封面</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                <!-- 添加书名覆盖层 -->
 | 
			
		||||
                <div class="cover-title-bar">{{ book.title }}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="book-info">
 | 
			
		||||
                <h3 class="book-title">{{ book.title }}</h3>
 | 
			
		||||
                <p class="book-author">{{ book.author }}</p>
 | 
			
		||||
 | 
			
		||||
                <div class="book-meta">
 | 
			
		||||
                    {% if book.category %}
 | 
			
		||||
                    <span class="book-category">{{ book.category.name }}</span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <span class="book-status {{ 'available' if book.stock > 0 else 'unavailable' }}">
 | 
			
		||||
                        {{ '可借阅' if book.stock > 0 else '无库存' }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="book-details">
 | 
			
		||||
                    <p><strong>ISBN:</strong> <span>{{ book.isbn or '无' }}</span></p>
 | 
			
		||||
                    <p><strong>出版社:</strong> <span>{{ book.publisher or '无' }}</span></p>
 | 
			
		||||
                    <p><strong>库存:</strong> <span>{{ book.stock }}</span></p>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="book-actions">
 | 
			
		||||
                    <a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="btn btn-info btn-sm">
 | 
			
		||||
                        <i class="fas fa-info-circle"></i> 详情
 | 
			
		||||
                    </a>
 | 
			
		||||
                    {% if current_user.role_id == 1 %}
 | 
			
		||||
                    <a href="{{ url_for('book.edit_book', book_id=book.id) }}" class="btn btn-primary btn-sm">
 | 
			
		||||
                        <i class="fas fa-edit"></i> 编辑
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <button class="btn btn-danger btn-sm delete-book" data-id="{{ book.id }}" data-title="{{ book.title }}">
 | 
			
		||||
                        <i class="fas fa-trash"></i> 删除
 | 
			
		||||
                    </button>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if book.stock > 0 %}
 | 
			
		||||
                    <a href="#" class="btn btn-success btn-sm borrow-book" data-id="{{ book.id }}">
 | 
			
		||||
                        <i class="fas fa-hand-holding"></i> 借阅
 | 
			
		||||
                    </a>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <div class="no-books">
 | 
			
		||||
            <i class="fas fa-exclamation-circle"></i>
 | 
			
		||||
            <p>没有找到符合条件的图书</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 分页 -->
 | 
			
		||||
    {% if pagination.pages > 1 %}
 | 
			
		||||
    <div class="pagination-container">
 | 
			
		||||
        <ul class="pagination">
 | 
			
		||||
            {% if pagination.has_prev %}
 | 
			
		||||
            <li class="page-item">
 | 
			
		||||
                <a class="page-link" href="{{ url_for('book.book_list', page=pagination.prev_num, search=search, category_id=category_id, sort=sort, order=order) }}">
 | 
			
		||||
                    <i class="fas fa-chevron-left"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            {% for p in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
 | 
			
		||||
                {% if p %}
 | 
			
		||||
                    {% if p == pagination.page %}
 | 
			
		||||
                    <li class="page-item active">
 | 
			
		||||
                        <span class="page-link">{{ p }}</span>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <li class="page-item">
 | 
			
		||||
                        <a class="page-link" href="{{ url_for('book.book_list', page=p, search=search, category_id=category_id, sort=sort, order=order) }}">{{ p }}</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <li class="page-item disabled">
 | 
			
		||||
                    <span class="page-link">...</span>
 | 
			
		||||
                </li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
 | 
			
		||||
            {% if pagination.has_next %}
 | 
			
		||||
            <li class="page-item">
 | 
			
		||||
                <a class="page-link" href="{{ url_for('book.book_list', page=pagination.next_num, search=search, category_id=category_id, sort=sort, order=order) }}">
 | 
			
		||||
                    <i class="fas fa-chevron-right"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </ul>
 | 
			
		||||
        <div class="pagination-info">
 | 
			
		||||
            显示 {{ pagination.total }} 条结果中的第 {{ (pagination.page - 1) * pagination.per_page + 1 }}
 | 
			
		||||
            到 {{ min(pagination.page * pagination.per_page, pagination.total) }} 条
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script src="{{ url_for('static', filename='js/book-list.js') }}"></script>
 | 
			
		||||
{{ super() }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -1,214 +1,145 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>首页 - 图书管理系统</title>
 | 
			
		||||
    <!-- 只引用index页面的专用样式 -->
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
 | 
			
		||||
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="app-container">
 | 
			
		||||
        <!-- 侧边导航栏 -->
 | 
			
		||||
        <nav class="sidebar">
 | 
			
		||||
            <div class="logo-container">
 | 
			
		||||
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo">
 | 
			
		||||
                <h2>图书管理系统</h2>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ul class="nav-links">
 | 
			
		||||
                <li class="active"><a href="#"><i class="fas fa-home"></i> 首页</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-book"></i> 图书浏览</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-bell"></i> 通知公告</a></li>
 | 
			
		||||
                {% if current_user.role_id == 1 %}
 | 
			
		||||
                <li class="nav-category">管理功能</li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-users"></i> 用户管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-layer-group"></i> 图书管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-warehouse"></i> 库存管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-history"></i> 日志管理</a></li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </nav>
 | 
			
		||||
{% extends 'base.html' %}
 | 
			
		||||
 | 
			
		||||
        <!-- 主内容区 -->
 | 
			
		||||
        <main class="main-content">
 | 
			
		||||
            <!-- 顶部导航 -->
 | 
			
		||||
            <header class="top-bar">
 | 
			
		||||
                <div class="search-container">
 | 
			
		||||
                    <i class="fas fa-search search-icon"></i>
 | 
			
		||||
                    <input type="text" placeholder="搜索图书..." class="search-input">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="user-menu">
 | 
			
		||||
                    <div class="notifications">
 | 
			
		||||
                        <i class="fas fa-bell"></i>
 | 
			
		||||
                        <span class="badge">3</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="user-info">
 | 
			
		||||
                        <div class="user-avatar">
 | 
			
		||||
                            {{ current_user.username[0] }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="user-details">
 | 
			
		||||
                            <span class="user-name">{{ current_user.username }}</span>
 | 
			
		||||
                            <span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="dropdown-menu">
 | 
			
		||||
                            <a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
 | 
			
		||||
                            <a href="#"><i class="fas fa-cog"></i> 设置</a>
 | 
			
		||||
                            <a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </header>
 | 
			
		||||
{% block title %}首页 - 图书管理系统{% endblock %}
 | 
			
		||||
 | 
			
		||||
            <!-- 欢迎区域 -->
 | 
			
		||||
            <div class="welcome-section">
 | 
			
		||||
                <h1>欢迎回来,{{ current_user.username }}!</h1>
 | 
			
		||||
                <p>今天是 <span id="current-date"></span>,祝您使用愉快。</p>
 | 
			
		||||
            </div>
 | 
			
		||||
{% block head %}
 | 
			
		||||
<!-- 只引用index页面的专用样式 -->
 | 
			
		||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
            <!-- 快速统计 -->
 | 
			
		||||
            <div class="stats-container">
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-book stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>馆藏总量</h3>
 | 
			
		||||
                        <p class="stat-number">8,567</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- 欢迎区域 -->
 | 
			
		||||
<div class="welcome-section">
 | 
			
		||||
    <h1>欢迎回来,{{ current_user.username }}!</h1>
 | 
			
		||||
    <p>今天是 <span id="current-date"></span>,祝您使用愉快。</p>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- 快速统计 -->
 | 
			
		||||
<div class="stats-container">
 | 
			
		||||
    <div class="stat-card">
 | 
			
		||||
        <i class="fas fa-book stat-icon"></i>
 | 
			
		||||
        <div class="stat-info">
 | 
			
		||||
            <h3>馆藏总量</h3>
 | 
			
		||||
            <p class="stat-number">8,567</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="stat-card">
 | 
			
		||||
        <i class="fas fa-users stat-icon"></i>
 | 
			
		||||
        <div class="stat-info">
 | 
			
		||||
            <h3>注册用户</h3>
 | 
			
		||||
            <p class="stat-number">1,245</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="stat-card">
 | 
			
		||||
        <i class="fas fa-exchange-alt stat-icon"></i>
 | 
			
		||||
        <div class="stat-info">
 | 
			
		||||
            <h3>当前借阅</h3>
 | 
			
		||||
            <p class="stat-number">352</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="stat-card">
 | 
			
		||||
        <i class="fas fa-clock stat-icon"></i>
 | 
			
		||||
        <div class="stat-info">
 | 
			
		||||
            <h3>待还图书</h3>
 | 
			
		||||
            <p class="stat-number">{{ 5 }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- 主要内容区 -->
 | 
			
		||||
<div class="main-sections">
 | 
			
		||||
    <!-- 最新图书 -->
 | 
			
		||||
    <div class="content-section book-section">
 | 
			
		||||
        <div class="section-header">
 | 
			
		||||
            <h2>最新图书</h2>
 | 
			
		||||
            <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="book-grid">
 | 
			
		||||
            {% for i in range(4) %}
 | 
			
		||||
            <div class="book-card">
 | 
			
		||||
                <div class="book-cover">
 | 
			
		||||
                    <img src="https://via.placeholder.com/150x210?text=No+Cover" alt="Book Cover">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-users stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>注册用户</h3>
 | 
			
		||||
                        <p class="stat-number">1,245</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-exchange-alt stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>当前借阅</h3>
 | 
			
		||||
                        <p class="stat-number">352</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-clock stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>待还图书</h3>
 | 
			
		||||
                        <p class="stat-number">{{ 5 }}</p>
 | 
			
		||||
                <div class="book-info">
 | 
			
		||||
                    <h3 class="book-title">示例图书标题</h3>
 | 
			
		||||
                    <p class="book-author">作者名</p>
 | 
			
		||||
                    <div class="book-meta">
 | 
			
		||||
                        <span class="book-category">计算机</span>
 | 
			
		||||
                        <span class="book-status available">可借阅</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <button class="borrow-btn">借阅</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 主要内容区 -->
 | 
			
		||||
            <div class="main-sections">
 | 
			
		||||
                <!-- 最新图书 -->
 | 
			
		||||
                <div class="content-section book-section">
 | 
			
		||||
                    <div class="section-header">
 | 
			
		||||
                        <h2>最新图书</h2>
 | 
			
		||||
                        <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="book-grid">
 | 
			
		||||
                        {% for i in range(4) %}
 | 
			
		||||
                        <div class="book-card">
 | 
			
		||||
                            <div class="book-cover">
 | 
			
		||||
                                <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="Book Cover" onerror="this.src='https://via.placeholder.com/150x210?text=No+Cover'">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="book-info">
 | 
			
		||||
                                <h3 class="book-title">示例图书标题</h3>
 | 
			
		||||
                                <p class="book-author">作者名</p>
 | 
			
		||||
                                <div class="book-meta">
 | 
			
		||||
                                    <span class="book-category">计算机</span>
 | 
			
		||||
                                    <span class="book-status available">可借阅</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <button class="borrow-btn">借阅</button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- 通知公告 -->
 | 
			
		||||
                <div class="content-section notice-section">
 | 
			
		||||
                    <div class="section-header">
 | 
			
		||||
                        <h2>通知公告</h2>
 | 
			
		||||
                        <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="notice-list">
 | 
			
		||||
                        <div class="notice-item">
 | 
			
		||||
                            <div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
 | 
			
		||||
                            <div class="notice-content">
 | 
			
		||||
                                <h3>关于五一假期图书馆开放时间调整的通知</h3>
 | 
			
		||||
                                <p>五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。</p>
 | 
			
		||||
                                <div class="notice-meta">
 | 
			
		||||
                                    <span class="notice-time">2023-04-28</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="notice-item">
 | 
			
		||||
                            <div class="notice-icon"><i class="fas fa-bell"></i></div>
 | 
			
		||||
                            <div class="notice-content">
 | 
			
		||||
                                <h3>您有2本图书即将到期</h3>
 | 
			
		||||
                                <p>《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。</p>
 | 
			
		||||
                                <div class="notice-meta">
 | 
			
		||||
                                    <span class="notice-time">2023-04-27</span>
 | 
			
		||||
                                    <button class="renew-btn">一键续借</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 热门图书区域 -->
 | 
			
		||||
            <div class="content-section popular-section">
 | 
			
		||||
                <div class="section-header">
 | 
			
		||||
                    <h2>热门图书</h2>
 | 
			
		||||
                    <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="popular-books">
 | 
			
		||||
                    {% for i in range(5) %}
 | 
			
		||||
                    <div class="popular-book-item">
 | 
			
		||||
                        <div class="rank-badge">{{ i+1 }}</div>
 | 
			
		||||
                        <div class="book-cover small">
 | 
			
		||||
                            <img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="book-details">
 | 
			
		||||
                            <h3 class="book-title">热门图书标题示例</h3>
 | 
			
		||||
                            <p class="book-author">知名作者</p>
 | 
			
		||||
                            <div class="book-stats">
 | 
			
		||||
                                <span><i class="fas fa-eye"></i> 1024 次浏览</span>
 | 
			
		||||
                                <span><i class="fas fa-bookmark"></i> 89 次借阅</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
            // 设置当前日期
 | 
			
		||||
            const now = new Date();
 | 
			
		||||
            const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
 | 
			
		||||
            document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', options);
 | 
			
		||||
    <!-- 通知公告 -->
 | 
			
		||||
    <div class="content-section notice-section">
 | 
			
		||||
        <div class="section-header">
 | 
			
		||||
            <h2>通知公告</h2>
 | 
			
		||||
            <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="notice-list">
 | 
			
		||||
            <div class="notice-item">
 | 
			
		||||
                <div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
 | 
			
		||||
                <div class="notice-content">
 | 
			
		||||
                    <h3>关于五一假期图书馆开放时间调整的通知</h3>
 | 
			
		||||
                    <p>五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。</p>
 | 
			
		||||
                    <div class="notice-meta">
 | 
			
		||||
                        <span class="notice-time">2023-04-28</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="notice-item">
 | 
			
		||||
                <div class="notice-icon"><i class="fas fa-bell"></i></div>
 | 
			
		||||
                <div class="notice-content">
 | 
			
		||||
                    <h3>您有2本图书即将到期</h3>
 | 
			
		||||
                    <p>《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。</p>
 | 
			
		||||
                    <div class="notice-meta">
 | 
			
		||||
                        <span class="notice-time">2023-04-27</span>
 | 
			
		||||
                        <button class="renew-btn">一键续借</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
            // 用户菜单下拉
 | 
			
		||||
            const userInfo = document.querySelector('.user-info');
 | 
			
		||||
            userInfo.addEventListener('click', function(e) {
 | 
			
		||||
                userInfo.classList.toggle('active');
 | 
			
		||||
            });
 | 
			
		||||
<!-- 热门图书区域 -->
 | 
			
		||||
<div class="content-section popular-section">
 | 
			
		||||
    <div class="section-header">
 | 
			
		||||
        <h2>热门图书</h2>
 | 
			
		||||
        <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="popular-books">
 | 
			
		||||
        {% for i in range(5) %}
 | 
			
		||||
        <div class="popular-book-item">
 | 
			
		||||
            <div class="rank-badge">{{ i+1 }}</div>
 | 
			
		||||
            <div class="book-cover small">
 | 
			
		||||
                <img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="book-details">
 | 
			
		||||
                <h3 class="book-title">热门图书标题示例</h3>
 | 
			
		||||
                <p class="book-author">知名作者</p>
 | 
			
		||||
                <div class="book-stats">
 | 
			
		||||
                    <span><i class="fas fa-eye"></i> 1024 次浏览</span>
 | 
			
		||||
                    <span><i class="fas fa-bookmark"></i> 89 次借阅</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
            // 点击其他区域关闭下拉菜单
 | 
			
		||||
            document.addEventListener('click', function(e) {
 | 
			
		||||
                if (!userInfo.contains(e.target)) {
 | 
			
		||||
                    userInfo.classList.remove('active');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
        // 设置当前日期
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
 | 
			
		||||
        document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', options);
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from flask import g, redirect, url_for, flash, request
 | 
			
		||||
 | 
			
		||||
def login_required(f):
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def decorated_function(*args, **kwargs):
 | 
			
		||||
        if g.user is None:
 | 
			
		||||
            flash('请先登录', 'warning')
 | 
			
		||||
            return redirect(url_for('user.login', next=request.url))
 | 
			
		||||
        return f(*args, **kwargs)
 | 
			
		||||
    return decorated_function
 | 
			
		||||
 | 
			
		||||
def admin_required(f):
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def decorated_function(*args, **kwargs):
 | 
			
		||||
        if g.user is None:
 | 
			
		||||
            flash('请先登录', 'warning')
 | 
			
		||||
            return redirect(url_for('user.login', next=request.url))
 | 
			
		||||
        if g.user.role_id != 1:  # 假设role_id=1是管理员
 | 
			
		||||
            flash('权限不足', 'danger')
 | 
			
		||||
            return redirect(url_for('index'))
 | 
			
		||||
        return f(*args, **kwargs)
 | 
			
		||||
    return decorated_function
 | 
			
		||||
							
								
								
									
										4135
									
								
								code_collection.txt
									
									
									
									
									
								
							
							
						
						
									
										4135
									
								
								code_collection.txt
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,6 +1,11 @@
 | 
			
		||||
Flask==2.3.3
 | 
			
		||||
Flask-SQLAlchemy==3.1.1
 | 
			
		||||
pymysql==1.1.0
 | 
			
		||||
Werkzeug==2.3.7
 | 
			
		||||
email-validator==2.1.0.post1
 | 
			
		||||
cryptography
 | 
			
		||||
flask==2.2.3
 | 
			
		||||
werkzeug==2.2.3
 | 
			
		||||
flask-sqlalchemy==3.0.3
 | 
			
		||||
sqlalchemy==2.0.7
 | 
			
		||||
pymysql==1.0.3
 | 
			
		||||
python-dotenv==1.0.0
 | 
			
		||||
pandas==2.0.0
 | 
			
		||||
openpyxl==3.1.2
 | 
			
		||||
xlrd==2.0.1
 | 
			
		||||
email-validator==2.0.0
 | 
			
		||||
pillow==9.5.0
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user