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