book_version_1_onlylist

This commit is contained in:
superlishunqin 2025-04-30 16:23:05 +08:00
parent 805a648119
commit 423730c50a
27 changed files with 7315 additions and 1130 deletions

View File

@ -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

View File

@ -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}'))

View File

@ -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()
# ... 其余代码 ...

View File

@ -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}>'

View File

@ -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}>'

View File

@ -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}>'

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

View 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%;
}
}

View 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%;
}
}

View 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
View 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;
}
}

View 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;
}
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

302
app/static/js/book-list.js Normal file
View 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);
});
});

View File

@ -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
View 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 %}

View 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">&times;</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">&times;</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">&times;</span>
</button>
</div>
`;
$('body').append(alert);
// 5秒后自动关闭
setTimeout(() => {
$('.notification-alert').alert('close');
}, 5000);
}
});
</script>
{% endblock %}

View 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">&times;</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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -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 %}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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