This commit is contained in:
superlishunqin 2025-04-30 23:28:51 +08:00
parent 389e8f0bf8
commit 0c1d1b0d19
15 changed files with 4128 additions and 1306 deletions

View File

@ -1,7 +1,8 @@
from flask import Flask, render_template, session, g
from flask import Flask, render_template, session, g, Markup
from app.models.user import db, User
from app.controllers.user import user_bp
from app.controllers.book import book_bp # 引入图书蓝图
from app.controllers.book import book_bp
from app.controllers.borrow import borrow_bp
import os
@ -20,7 +21,7 @@ def create_app():
EMAIL_PORT=587,
EMAIL_ENCRYPTION='starttls',
EMAIL_USERNAME='3399560459@qq.com',
EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码不是邮箱密码
EMAIL_PASSWORD='fzwhyirhbqdzcjgf',
EMAIL_FROM='3399560459@qq.com',
EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
)
@ -33,7 +34,8 @@ def create_app():
# 注册蓝图
app.register_blueprint(user_bp, url_prefix='/user')
app.register_blueprint(book_bp, url_prefix='/book') # 注册图书蓝图
app.register_blueprint(book_bp, url_prefix='/book')
app.register_blueprint(borrow_bp, url_prefix='/borrow')
# 创建数据库表
with app.app_context():
@ -44,15 +46,14 @@ def create_app():
# 创建表
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')
# 移除这些重复的关系定义
# 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
@ -92,7 +93,7 @@ def create_app():
db.session.commit()
# 请求前处理
# 其余代码保持不变...
@app.before_request
def load_logged_in_user():
user_id = session.get('user_id')
@ -102,23 +103,20 @@ def create_app():
else:
g.user = User.query.get(user_id)
# 首页路由
@app.route('/')
def index():
if not g.user:
return render_template('login.html')
return render_template('index.html', current_user=g.user)
# 错误处理
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
# 模板过滤器
@app.template_filter('nl2br')
def nl2br_filter(s):
if not s:
return s
return s.replace('\n', '<br>')
if s:
return Markup(s.replace('\n', '<br>'))
return s
return app

View File

@ -66,9 +66,27 @@ def book_list():
@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)
# 添加当前时间用于判断借阅是否逾期
now = datetime.datetime.now()
# 如果用户是管理员,预先查询并排序借阅记录
borrow_records = []
if g.user.role_id == 1: # 假设 role_id 1 为管理员
from app.models.borrow import BorrowRecord
borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit(
10).all()
return render_template(
'book/detail.html',
book=book,
current_user=g.user,
borrow_records=borrow_records,
now=now
)
# 添加图书页面
# 添加图书页面
@book_bp.route('/add', methods=['GET', 'POST'])
@login_required
@ -83,73 +101,137 @@ def add_book():
isbn = request.form.get('isbn')
publish_year = request.form.get('publish_year')
description = request.form.get('description')
stock = request.form.get('stock', type=int)
stock = request.form.get('stock', type=int, default=0)
price = request.form.get('price')
if not title or not author:
flash('书名和作者不能为空', 'danger')
# 表单验证
errors = []
if not title:
errors.append('书名不能为空')
if not author:
errors.append('作者不能为空')
# 检查ISBN是否已存在(如果提供了ISBN)
if isbn:
existing_book = Book.query.filter_by(isbn=isbn).first()
if existing_book:
errors.append(f'ISBN "{isbn}" 已存在请检查ISBN或查找现有图书')
if errors:
for error in errors:
flash(error, 'danger')
categories = Category.query.all()
return render_template('book/add.html', categories=categories, current_user=g.user)
# 保留已填写的表单数据
book_data = {
'title': title,
'author': author,
'publisher': publisher,
'category_id': category_id,
'tags': tags,
'isbn': isbn,
'publish_year': publish_year,
'description': description,
'stock': stock,
'price': price
}
return render_template('book/add.html', categories=categories,
current_user=g.user, book=book_data)
# 处理封面图片上传
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')
try:
# 更清晰的文件命名
original_filename = secure_filename(cover_file.filename)
# 保留原始文件扩展名
_, ext = os.path.splitext(original_filename)
if not ext:
ext = '.jpg' # 默认扩展名
# 确保上传目录存在
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
filename = f"{uuid.uuid4()}{ext}"
upload_folder = os.path.join(current_app.static_folder, 'uploads', 'covers')
file_path = os.path.join(upload_folder, filename)
cover_file.save(file_path)
cover_url = f'/static/covers/{filename}'
# 确保上传目录存在
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
# 创建新图书
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()
)
file_path = os.path.join(upload_folder, filename)
cover_file.save(file_path)
cover_url = f'/static/uploads/covers/{filename}'
except Exception as e:
current_app.logger.error(f"封面上传失败: {str(e)}")
flash(f"封面上传失败: {str(e)}", 'warning')
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()
try:
# 创建新图书
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(inventory_log)
db.session.commit()
db.session.add(book)
# 先提交以获取book的id
db.session.commit()
flash('图书添加成功', 'success')
return redirect(url_for('book.book_list'))
# 记录库存日志 - 在获取 book.id 后
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(f'{title}》添加成功', 'success')
return redirect(url_for('book.book_list'))
except Exception as e:
db.session.rollback()
error_msg = str(e)
# 记录详细错误日志
current_app.logger.error(f"添加图书失败: {error_msg}")
flash(f'添加图书失败: {error_msg}', 'danger')
categories = Category.query.all()
# 保留已填写的表单数据
book_data = {
'title': title,
'author': author,
'publisher': publisher,
'category_id': category_id,
'tags': tags,
'isbn': isbn,
'publish_year': publish_year,
'description': description,
'stock': stock,
'price': price
}
return render_template('book/add.html', categories=categories,
current_user=g.user, book=book_data)
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

View File

@ -0,0 +1,82 @@
from flask import Blueprint, request, redirect, url_for, flash, g
from app.models.book import Book
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
from app.models.user import db # 修正:从 user 模型导入 db
from app.utils.auth import login_required
import datetime
# 创建借阅蓝图
borrow_bp = Blueprint('borrow', __name__, url_prefix='/borrow')
@borrow_bp.route('/book', methods=['POST'])
@login_required
def borrow_book():
book_id = request.form.get('book_id', type=int)
borrow_days = request.form.get('borrow_days', type=int, default=14)
if not book_id:
flash('请选择要借阅的图书', 'danger')
return redirect(url_for('book.book_list'))
book = Book.query.get_or_404(book_id)
# 检查库存
if book.stock <= 0:
flash(f'{book.title}》当前无库存,无法借阅', 'danger')
return redirect(url_for('book.book_detail', book_id=book_id))
# 检查当前用户是否已借阅此书
existing_borrow = BorrowRecord.query.filter_by(
user_id=g.user.id,
book_id=book_id,
status=1 # 1表示借阅中
).first()
if existing_borrow:
flash(f'您已借阅《{book.title}》,请勿重复借阅', 'warning')
return redirect(url_for('book.book_detail', book_id=book_id))
try:
# 创建借阅记录
now = datetime.datetime.now()
due_date = now + datetime.timedelta(days=borrow_days)
borrow_record = BorrowRecord(
user_id=g.user.id,
book_id=book_id,
borrow_date=now,
due_date=due_date,
status=1, # 1表示借阅中
created_at=now,
updated_at=now
)
# 更新图书库存
book.stock -= 1
book.updated_at = now
db.session.add(borrow_record)
db.session.commit()
# 添加库存变更日志
inventory_log = InventoryLog(
book_id=book_id,
change_type='借出',
change_amount=-1,
after_stock=book.stock,
operator_id=g.user.id,
remark='用户借书',
changed_at=now
)
db.session.add(inventory_log)
db.session.commit()
flash(f'成功借阅《{book.title}》,请在 {due_date.strftime("%Y-%m-%d")} 前归还', 'success')
except Exception as e:
db.session.rollback()
flash(f'借阅失败: {str(e)}', 'danger')
return redirect(url_for('book.book_detail', book_id=book_id))

View File

@ -19,6 +19,7 @@ class BorrowRecord(db.Model):
# 添加反向关系引用
user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic'))
book = db.relationship('Book', backref=db.backref('borrow_records', lazy='dynamic'))
# book 关系会在后面步骤添加

File diff suppressed because it is too large Load Diff

644
app/static/js/book-add.js Normal file
View File

@ -0,0 +1,644 @@
/**
* 图书添加页面脚本
* 处理图书表单的交互验证和预览功能
*/
let isSubmitting = false;
$(document).ready(function() {
// 全局变量
let cropper;
let coverBlob;
let tags = [];
const coverPreview = $('#coverPreview');
const coverInput = $('#cover');
const tagInput = $('#tagInput');
const tagsContainer = $('#tagsContainer');
const tagsHiddenInput = $('#tags');
// 初始化函数
function initialize() {
initSelect2();
initFormProgress();
initTagsFromInput();
initCoverHandlers();
initNumberControls();
initPriceSlider();
initCharCounter();
initFormValidation();
attachEventListeners();
}
// ========== 组件初始化 ==========
// 初始化Select2
function initSelect2() {
$('.select2').select2({
placeholder: "选择分类...",
allowClear: true,
theme: "classic",
width: '100%'
});
}
// 初始化表单进度条
function initFormProgress() {
updateFormProgress();
$('input, textarea, select').on('change keyup', function() {
updateFormProgress();
});
}
// 初始化标签(从隐藏输入字段)
function initTagsFromInput() {
const tagsValue = $('#tags').val();
if (tagsValue) {
tags = tagsValue.split(',');
renderTags();
}
}
// 初始化封面处理
function initCoverHandlers() {
// 拖放上传功能
coverPreview.on('dragover', function(e) {
e.preventDefault();
$(this).addClass('dragover');
}).on('dragleave drop', function(e) {
e.preventDefault();
$(this).removeClass('dragover');
}).on('drop', function(e) {
e.preventDefault();
const file = e.originalEvent.dataTransfer.files[0];
if (file && file.type.match('image.*')) {
coverInput[0].files = e.originalEvent.dataTransfer.files;
coverInput.trigger('change');
}
}).on('click', function() {
if (!$(this).find('img').length) {
coverInput.click();
}
});
// 重置页面加载完后的字符计数
if ($('#description').val()) {
$('#charCount').text($('#description').val().length);
}
}
// 初始化数字控制
function initNumberControls() {
$('#stockDecrement').on('click', function() {
const input = $('#stock');
const value = parseInt(input.val());
if (value > parseInt(input.attr('min'))) {
input.val(value - 1).trigger('change');
}
});
$('#stockIncrement').on('click', function() {
const input = $('#stock');
const value = parseInt(input.val());
input.val(value + 1).trigger('change');
});
}
// 初始化价格滑块
function initPriceSlider() {
$('#priceRange').on('input', function() {
$('#price').val($(this).val());
});
$('#price').on('input', function() {
const value = parseFloat($(this).val()) || 0;
$('#priceRange').val(Math.min(value, 500));
});
}
// 初始化字符计数器
function initCharCounter() {
$('#description').on('input', function() {
const count = $(this).val().length;
$('#charCount').text(count);
if (count > 2000) {
$('#charCount').addClass('text-danger');
} else {
$('#charCount').removeClass('text-danger');
}
});
}
// 初始化表单验证
ffunction initFormValidation() {
$('#bookForm').on('submit', function(e) {
// 如果表单正在提交中,阻止重复提交
if (isSubmitting) {
e.preventDefault();
showNotification('表单正在提交中,请勿重复点击', 'warning');
return false;
}
let isValid = true;
$('[required]').each(function() {
if (!$(this).val().trim()) {
isValid = false;
$(this).addClass('is-invalid');
// 添加错误提示
if (!$(this).next('.invalid-feedback').length) {
$(this).after(`<div class="invalid-feedback">此字段不能为空</div>`);
}
} else {
$(this).removeClass('is-invalid').next('.invalid-feedback').remove();
}
});
// 验证ISBN格式如果已填写
const isbn = $('#isbn').val().trim();
if (isbn) {
// 移除所有非数字、X和x字符后检查
const cleanIsbn = isbn.replace(/[^0-9Xx]/g, '');
const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/;
if (!isbnRegex.test(cleanIsbn)) {
isValid = false;
$('#isbn').addClass('is-invalid');
if (!$('#isbn').next('.invalid-feedback').length) {
$('#isbn').after(`<div class="invalid-feedback">ISBN格式不正确应为10位或13位</div>`);
}
}
}
if (!isValid) {
e.preventDefault();
// 滚动到第一个错误字段
$('html, body').animate({
scrollTop: $('.is-invalid:first').offset().top - 100
}, 500);
showNotification('请正确填写所有标记的字段', 'error');
} else {
// 设置表单锁定状态
isSubmitting = true;
// 修改提交按钮样式
const submitBtn = $(this).find('button[type="submit"]');
const originalHtml = submitBtn.html();
submitBtn.prop('disabled', true)
.html('<i class="fas fa-spinner fa-spin"></i> 保存中...');
// 显示提交中通知
showNotification('表单提交中...', 'info');
// 如果表单提交时间过长30秒后自动解锁
setTimeout(function() {
if (isSubmitting) {
isSubmitting = false;
submitBtn.prop('disabled', false).html(originalHtml);
showNotification('提交超时,请重试', 'warning');
}
}, 30000);
}
});
// 输入时移除错误样式
$('input, textarea, select').on('input change', function() {
$(this).removeClass('is-invalid').next('.invalid-feedback').remove();
});
}
// 还需要在服务端处理成功后重置状态
// 在页面加载完成时,添加监听服务器重定向事件
$(window).on('pageshow', function(event) {
if (event.originalEvent.persisted ||
(window.performance && window.performance.navigation.type === 2)) {
// 如果页面是从缓存加载的或通过后退按钮回到的
isSubmitting = false;
$('button[type="submit"]').prop('disabled', false)
.html('<i class="fas fa-save"></i> 保存图书');
}
});
// 绑定事件监听器
function attachEventListeners() {
// 文件选择处理
coverInput.on('change', handleCoverSelect);
// 裁剪控制
$('#rotateLeft').on('click', function() { cropper && cropper.rotate(-90); });
$('#rotateRight').on('click', function() { cropper && cropper.rotate(90); });
$('#zoomIn').on('click', function() { cropper && cropper.zoom(0.1); });
$('#zoomOut').on('click', function() { cropper && cropper.zoom(-0.1); });
$('#cropImage').on('click', applyCrop);
$('#removeCover').on('click', removeCover);
// 标签处理
tagInput.on('keydown', handleTagKeydown);
$('#addTagBtn').on('click', addTag);
$(document).on('click', '.tag-remove', removeTag);
// ISBN查询
$('#isbnLookup').on('click', lookupISBN);
// 预览按钮
$('#previewBtn').on('click', showPreview);
// 表单重置
$('#resetBtn').on('click', confirmReset);
}
// ========== 功能函数 ==========
// 更新表单进度条
function updateFormProgress() {
const requiredFields = $('[required]');
const filledFields = requiredFields.filter(function() {
return $(this).val() !== '';
});
const otherFields = $('input:not([required]), textarea:not([required]), select:not([required])').not('[type="file"]');
const filledOtherFields = otherFields.filter(function() {
return $(this).val() !== '';
});
let requiredWeight = 70; // 必填字段权重70%
let otherWeight = 30; // 非必填字段权重30%
let requiredProgress = requiredFields.length ? (filledFields.length / requiredFields.length) * requiredWeight : requiredWeight;
let otherProgress = otherFields.length ? (filledOtherFields.length / otherFields.length) * otherWeight : 0;
let totalProgress = Math.floor(requiredProgress + otherProgress);
$('#formProgress').css('width', totalProgress + '%').attr('aria-valuenow', totalProgress);
$('#progressText').text('完成 ' + totalProgress + '%');
if (totalProgress >= 100) {
$('.btn-primary').addClass('pulse');
} else {
$('.btn-primary').removeClass('pulse');
}
}
// 处理封面选择
function handleCoverSelect(e) {
const file = e.target.files[0];
if (!file) return;
// 验证文件类型
if (!file.type.match('image.*')) {
showNotification('请选择图片文件', 'warning');
return;
}
// 验证文件大小最大5MB
if (file.size > 5 * 1024 * 1024) {
showNotification('图片大小不能超过5MB', 'warning');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
// 先显示在预览框中,确保用户能立即看到上传的图片
coverPreview.html(`<img src="${e.target.result}" class="cover-image" alt="图书封面预览">`);
// 准备裁剪图片
$('#cropperImage').attr('src', e.target.result);
// 确保图片加载完成后再显示模态框
$('#cropperImage').on('load', function() {
// 打开模态框
$('#cropperModal').modal('show');
// 在模态框完全显示后初始化裁剪器
$('#cropperModal').on('shown.bs.modal', function() {
if (cropper) {
cropper.destroy();
}
try {
cropper = new Cropper(document.getElementById('cropperImage'), {
aspectRatio: 5 / 7,
viewMode: 2,
responsive: true,
guides: true,
background: true,
ready: function() {
console.log('Cropper初始化成功');
}
});
} catch (err) {
console.error('Cropper初始化失败:', err);
showNotification('图片处理工具初始化失败,请重试', 'error');
}
});
});
};
// 处理读取错误
reader.onerror = function() {
showNotification('读取图片失败,请重试', 'error');
};
reader.readAsDataURL(file);
}
// 应用裁剪
function applyCrop() {
if (!cropper) {
showNotification('图片处理工具未就绪,请重新上传', 'error');
$('#cropperModal').modal('hide');
return;
}
try {
const canvas = cropper.getCroppedCanvas({
width: 500,
height: 700,
fillColor: '#fff',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
});
if (!canvas) {
throw new Error('无法生成裁剪后的图片');
}
canvas.toBlob(function(blob) {
if (!blob) {
showNotification('图片处理失败,请重试', 'error');
return;
}
const url = URL.createObjectURL(blob);
coverPreview.html(`<img src="${url}" class="cover-image" alt="图书封面">`);
coverBlob = blob;
// 模拟File对象
const fileList = new DataTransfer();
const file = new File([blob], "cover.jpg", {type: "image/jpeg"});
fileList.items.add(file);
document.getElementById('cover').files = fileList.files;
$('#cropperModal').modal('hide');
showNotification('封面图片已更新', 'success');
}, 'image/jpeg', 0.95);
} catch (err) {
console.error('裁剪失败:', err);
showNotification('图片裁剪失败,请重试', 'error');
$('#cropperModal').modal('hide');
}
}
// 移除封面
function removeCover() {
coverPreview.html(`
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
<p class="placeholder-tip">点击上传或拖放图片至此处</p>
</div>
`);
coverInput.val('');
coverBlob = null;
}
// 渲染标签
function renderTags() {
tagsContainer.empty();
tags.forEach(tag => {
tagsContainer.append(`
<div class="tag">
<span class="tag-text">${tag}</span>
<button type="button" class="tag-remove" data-tag="${tag}">
<i class="fas fa-times"></i>
</button>
</div>
`);
});
tagsHiddenInput.val(tags.join(','));
}
// 添加标签
function addTag() {
const tag = tagInput.val().trim();
if (tag && !tags.includes(tag)) {
tags.push(tag);
renderTags();
tagInput.val('').focus();
}
}
// 处理标签输入键盘事件
function handleTagKeydown(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addTag();
}
}
// 移除标签
function removeTag() {
const tagToRemove = $(this).data('tag');
tags = tags.filter(t => t !== tagToRemove);
renderTags();
}
// ISBN查询
function lookupISBN() {
const isbn = $('#isbn').val().trim();
if (!isbn) {
showNotification('请先输入ISBN', 'warning');
return;
}
// 验证ISBN格式
const cleanIsbn = isbn.replace(/[^0-9Xx]/g, '');
const isbnRegex = /^(?:\d{10}|\d{13})$|^(?:\d{9}[Xx])$/;
if (!isbnRegex.test(cleanIsbn)) {
showNotification('ISBN格式不正确应为10位或13位', 'warning');
return;
}
$(this).html('<i class="fas fa-spinner fa-spin"></i>');
// 先检查ISBN是否已存在
$.get('/book/api/check-isbn', {isbn: isbn}, function(data) {
if (data.exists) {
$('#isbnLookup').html('<i class="fas fa-search"></i>');
showNotification(`ISBN "${isbn}" 已存在: 《${data.book_title}`, 'warning');
$('#isbn').addClass('is-invalid');
if (!$('#isbn').next('.invalid-feedback').length) {
$('#isbn').after(`<div class="invalid-feedback">此ISBN已被图书《${data.book_title}》使用</div>`);
}
} else {
// 继续查询外部API模拟
simulateISBNLookup(isbn);
}
}).fail(function() {
$('#isbnLookup').html('<i class="fas fa-search"></i>');
showNotification('服务器查询失败,请稍后再试', 'error');
});
}
// 模拟ISBN查询
function simulateISBNLookup(isbn) {
// 模拟API查询延迟
setTimeout(() => {
// 模拟查到的数据
if (isbn === '9787020002207') {
$('#title').val('红楼梦').trigger('blur');
$('#author').val('曹雪芹').trigger('blur');
$('#publisher').val('人民文学出版社').trigger('blur');
$('#publish_year').val('1996').trigger('blur');
$('#category_id').val('1').trigger('change');
tags = ['中国文学', '古典', '名著'];
renderTags();
$('#description').val('《红楼梦》是中国古代章回体长篇小说中国古典四大名著之一通行本共120回一般认为前80回是清代作家曹雪芹所著后40回作者有争议。小说以贾、史、王、薛四大家族的兴衰为背景以贾府的家庭琐事、闺阁闲情为脉络以贾宝玉、林黛玉、薛宝钗的爱情婚姻悲剧为主线刻画了以贾宝玉和金陵十二钗为中心的正邪两赋有情人的人性美和悲剧美。').trigger('input');
$('#price').val('59.70').trigger('input');
$('#priceRange').val('59.70');
showNotification('ISBN查询成功', 'success');
} else if (isbn === '9787544270878') {
$('#title').val('挪威的森林').trigger('blur');
$('#author').val('村上春树').trigger('blur');
$('#publisher').val('南海出版社').trigger('blur');
$('#publish_year').val('2017').trigger('blur');
$('#category_id').val('2').trigger('change');
tags = ['外国文学', '日本', '小说'];
renderTags();
$('#description').val('《挪威的森林》是日本作家村上春树创作的长篇小说首次出版于1987年。小说讲述了一个悲伤的爱情故事背景设定在20世纪60年代末的日本。主人公渡边纠缠在与平静的直子和开朗的绿子两人的感情中最终选择了生活。').trigger('input');
$('#price').val('39.50').trigger('input');
$('#priceRange').val('39.50');
showNotification('ISBN查询成功', 'success');
} else {
showNotification('未找到相关图书信息', 'warning');
}
$('#isbnLookup').html('<i class="fas fa-search"></i>');
updateFormProgress();
}, 1500);
}
// 显示预览
function showPreview() {
// 检查必填字段
if (!$('#title').val().trim() || !$('#author').val().trim()) {
showNotification('请至少填写书名和作者后再预览', 'warning');
return;
}
try {
// 确保所有值都有默认值防止undefined错误
const title = $('#title').val() || '未填写标题';
const author = $('#author').val() || '未填写作者';
const publisher = $('#publisher').val() || '-';
const isbn = $('#isbn').val() || '-';
const publishYear = $('#publish_year').val() || '-';
const description = $('#description').val() || '';
const stock = $('#stock').val() || '0';
let price = parseFloat($('#price').val()) || 0;
// 填充预览内容
$('#previewTitle').text(title);
$('#previewAuthor').text(author ? '作者: ' + author : '未填写作者');
$('#previewPublisher').text(publisher);
$('#previewISBN').text(isbn);
$('#previewYear').text(publishYear);
// 获取分类文本
const categoryId = $('#category_id').val();
const categoryText = categoryId ? $('#category_id option:selected').text() : '-';
$('#previewCategory').text(categoryText);
// 价格和库存
$('#previewPrice').text(price ? '¥' + price.toFixed(2) : '¥0.00');
$('#previewStock').text('库存: ' + stock);
// 标签
const previewTags = $('#previewTags');
previewTags.empty();
if (tags && tags.length > 0) {
tags.forEach(tag => {
previewTags.append(`<span class="preview-tag">${tag}</span>`);
});
} else {
previewTags.append('<span class="no-tags">暂无标签</span>');
}
// 描述
if (description) {
$('#previewDescription').html(`<p>${description.replace(/\n/g, '<br>')}</p>`);
} else {
$('#previewDescription').html(`<p class="placeholder-text">暂无简介内容</p>`);
}
// 封面
const previewCover = $('#previewCover');
previewCover.empty(); // 清空现有内容
if ($('#coverPreview img').length) {
const coverSrc = $('#coverPreview img').attr('src');
previewCover.html(`<img src="${coverSrc}" class="preview-cover-img" alt="封面预览">`);
} else {
previewCover.html(`
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
</div>
`);
}
// 显示预览模态框
$('#previewModal').modal('show');
console.log('预览模态框已显示');
} catch (err) {
console.error('生成预览时发生错误:', err);
showNotification('生成预览时出错,请重试', 'error');
}
}
// 确认重置表单
function confirmReset() {
if (confirm('确定要重置表单吗?所有已填写的内容将被清空。')) {
$('#bookForm')[0].reset();
removeCover();
tags = [];
renderTags();
updateFormProgress();
$('.select2').val(null).trigger('change');
$('#charCount').text('0');
showNotification('表单已重置', 'info');
}
}
// 通知提示函数
function showNotification(message, type) {
// 创建通知元素
const notification = $(`
<div class="notification ${type}-notification animate__animated animate__fadeInRight">
<div class="notification-icon">
<i class="fas ${getIconForType(type)}"></i>
</div>
<div class="notification-content">
<p>${message}</p>
</div>
<button class="notification-close">
<i class="fas fa-times"></i>
</button>
</div>
`);
// 添加到页面
if ($('.notification-container').length === 0) {
$('body').append('<div class="notification-container"></div>');
}
$('.notification-container').append(notification);
// 自动关闭
setTimeout(() => {
notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
setTimeout(() => {
notification.remove();
}, 500);
}, 5000);
// 点击关闭
notification.find('.notification-close').on('click', function() {
notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
setTimeout(() => {
notification.remove();
}, 500);
});
}
function getIconForType(type) {
switch(type) {
case 'success': return 'fa-check-circle';
case 'warning': return 'fa-exclamation-triangle';
case 'error': return 'fa-times-circle';
case 'info':
default: return 'fa-info-circle';
}
}
// 初始化页面
initialize();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}图书管理系统{% endblock %}</title>
<!-- 通用CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.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 -->
@ -95,7 +96,8 @@
</div>
<!-- 通用JavaScript -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 用户菜单下拉

View File

@ -15,16 +15,16 @@
<div class="page-header-wrapper">
<div class="page-header">
<div class="header-title-section">
<h1 class="page-title"><i class="fas fa-book-medical pulse-icon"></i> 添加新图书</h1>
<h1 class="page-title">添加新图书</h1>
<p class="subtitle">创建新书籍记录并添加到系统库存</p>
</div>
<div class="header-actions">
<a href="{{ url_for('book.book_list') }}" class="btn btn-light btn-icon-text">
<a href="{{ url_for('book.book_list') }}" class="btn-back">
<i class="fas fa-arrow-left"></i>
<span>返回列表</span>
</a>
<div class="form-progress">
<div class="progress">
<div class="progress-bar-container">
<div class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" id="formProgress"></div>
</div>
<span class="progress-text" id="progressText">完成 0%</span>
@ -35,71 +35,52 @@
<!-- 主表单区域 -->
<form method="POST" enctype="multipart/form-data" class="book-form" id="bookForm">
<div class="form-row">
<div class="form-grid">
<!-- 左侧表单区域 -->
<div class="col-lg-8">
<div class="form-main-content">
<!-- 基本信息卡片 -->
<div class="card form-card">
<div class="form-card">
<div class="card-header">
<div class="card-header-icon">
<i class="fas fa-info-circle"></i>
</div>
<div class="card-header-title">基本信息</div>
<span class="card-title">基本信息</span>
</div>
<div class="card-body">
<div class="form-section">
<div class="form-group">
<label for="title" class="form-label">书名 <span class="required">*</span></label>
<input type="text" class="form-control" id="title" name="title" required
placeholder="请输入完整图书名称" value="{{ book.title if book else '' }}">
<div class="form-help">完整准确的书名有助于读者查找</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="title" class="form-label">
<i class="fas fa-heading label-icon"></i>书名
<span class="required">*</span>
</label>
<input type="text" class="form-control custom-input floating-input" id="title" name="title" required>
<span class="floating-label">请输入完整图书名称</span>
<div class="form-text">完整准确的书名有助于读者查找</div>
<div class="form-group">
<label for="author" class="form-label">作者 <span class="required">*</span></label>
<input type="text" class="form-control" id="author" name="author" required
placeholder="请输入作者姓名" value="{{ book.author if book else '' }}">
</div>
<div class="form-group">
<label for="publisher" class="form-label">出版社</label>
<input type="text" class="form-control" id="publisher" name="publisher"
placeholder="请输入出版社名称" value="{{ book.publisher if book else '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="author" class="form-label">
<i class="fas fa-user-edit label-icon"></i>作者
<span class="required">*</span>
</label>
<input type="text" class="form-control custom-input floating-input" id="author" name="author" required>
<span class="floating-label">作者姓名</span>
</div>
<div class="form-group col-md-6">
<label for="publisher" class="form-label">
<i class="fas fa-building label-icon"></i>出版社
</label>
<input type="text" class="form-control custom-input floating-input" id="publisher" name="publisher">
<span class="floating-label">出版社名称</span>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="isbn" class="form-label">
<i class="fas fa-barcode label-icon"></i>ISBN
</label>
<div class="input-group">
<input type="text" class="form-control custom-input floating-input" id="isbn" name="isbn">
<span class="floating-label">国际标准图书编号</span>
<div class="input-group-append">
<button class="btn btn-primary isbn-lookup-btn" type="button" id="isbnLookup">
<i class="fas fa-search"></i>
</button>
</div>
<div class="form-group">
<label for="isbn" class="form-label">ISBN</label>
<div class="input-with-button">
<input type="text" class="form-control" id="isbn" name="isbn"
placeholder="例如: 978-7-XXXXX-XXX-X" value="{{ book.isbn if book else '' }}">
<button type="button" class="btn-append" id="isbnLookup">
<i class="fas fa-search"></i>
</button>
</div>
<div class="form-text">输入ISBN并点击查询按钮自动填充图书信息</div>
<div class="form-help">输入ISBN并点击查询按钮自动填充图书信息</div>
</div>
<div class="form-group col-md-6">
<label for="publish_year" class="form-label">
<i class="fas fa-calendar-alt label-icon"></i>出版年份
</label>
<input type="text" class="form-control custom-input floating-input" id="publish_year" name="publish_year">
<span class="floating-label">2023</span>
<div class="form-group">
<label for="publish_year" class="form-label">出版年份</label>
<input type="text" class="form-control" id="publish_year" name="publish_year"
placeholder="例如: 2023" value="{{ book.publish_year if book else '' }}">
</div>
</div>
</div>
@ -107,41 +88,36 @@
</div>
<!-- 分类和标签卡片 -->
<div class="card form-card">
<div class="form-card">
<div class="card-header">
<div class="card-header-icon">
<i class="fas fa-tags"></i>
</div>
<div class="card-header-title">分类与标签</div>
<span class="card-title">分类与标签</span>
</div>
<div class="card-body">
<div class="form-section">
<div class="form-row">
<div class="form-group col-md-6">
<label for="category_id" class="form-label">
<i class="fas fa-folder label-icon"></i>图书分类
</label>
<select class="form-control custom-select select2" id="category_id" name="category_id">
<div class="form-group">
<label for="category_id" class="form-label">图书分类</label>
<select class="form-control select2" id="category_id" name="category_id">
<option value="">选择分类...</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
<option value="{{ category.id }}" {% if book and book.category_id|string == category.id|string %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
<div class="form-text">为图书选择合适的分类以便于管理和查找</div>
<div class="form-help">为图书选择合适的分类以便于管理和查找</div>
</div>
<div class="form-group col-md-6">
<label for="tags" class="form-label">
<i class="fas fa-tag label-icon"></i>标签
</label>
<div class="tag-input-container">
<input type="text" class="form-control custom-input tag-input" id="tagInput" placeholder="输入标签后按回车添加">
<input type="hidden" id="tags" name="tags">
<button type="button" class="btn btn-sm btn-primary add-tag-btn">
<div class="form-group">
<label for="tagInput" class="form-label">标签</label>
<div class="tag-input-wrapper">
<input type="text" class="form-control" id="tagInput" placeholder="输入标签后按回车添加">
<input type="hidden" id="tags" name="tags" value="{{ book.tags if book else '' }}">
<button type="button" class="btn-tag-add" id="addTagBtn">
<i class="fas fa-plus"></i>
</button>
</div>
<div class="tags-container" id="tagsContainer"></div>
<div class="form-text">添加多个标签以提高图书的检索率</div>
<div class="form-help">添加多个标签以提高图书的检索率</div>
</div>
</div>
</div>
@ -149,25 +125,19 @@
</div>
<!-- 图书简介卡片 -->
<div class="card form-card">
<div class="form-card">
<div class="card-header">
<div class="card-header-icon">
<i class="fas fa-align-left"></i>
</div>
<div class="card-header-title">图书简介</div>
<span class="card-title">图书简介</span>
</div>
<div class="card-body">
<div class="form-section">
<div class="form-group">
<label for="description" class="form-label">
<i class="fas fa-file-alt label-icon"></i>内容简介
</label>
<textarea class="form-control custom-textarea" id="description" name="description" rows="8" placeholder="请输入图书的简要介绍..."></textarea>
<div class="form-text">
<div class="d-flex justify-content-between">
<span>简要描述图书的内容、特点和主要观点</span>
<span class="text-count"><span id="charCount">0</span>/2000</span>
</div>
<label for="description" class="form-label">内容简介</label>
<textarea class="form-control" id="description" name="description" rows="8"
placeholder="请输入图书的简要介绍...">{{ book.description if book else '' }}</textarea>
<div class="form-footer">
<div class="form-help">简要描述图书的内容、特点和主要观点</div>
<div class="char-counter"><span id="charCount">0</span>/2000</div>
</div>
</div>
</div>
@ -176,14 +146,11 @@
</div>
<!-- 右侧表单区域 -->
<div class="col-lg-4">
<div class="form-sidebar">
<!-- 封面图片卡片 -->
<div class="card form-card">
<div class="form-card">
<div class="card-header">
<div class="card-header-icon">
<i class="fas fa-image"></i>
</div>
<div class="card-header-title">封面图片</div>
<span class="card-title">封面图片</span>
</div>
<div class="card-body">
<div class="cover-preview-container">
@ -196,17 +163,17 @@
</div>
<div class="upload-options">
<div class="upload-btn-group">
<label for="cover" class="btn btn-primary custom-upload-btn">
<label for="cover" class="btn-upload">
<i class="fas fa-upload"></i> 上传图片
</label>
<button type="button" class="btn btn-outline-secondary btn-icon" id="removeCover">
<button type="button" class="btn-remove" id="removeCover">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<input type="file" id="cover" name="cover" class="form-control-file" accept="image/*" style="display:none;">
<div class="form-text text-center mt-2">
<small>推荐尺寸: 500×700px (竖版封面)</small><br>
<small>支持格式: JPG, PNG, WebP</small>
<div class="upload-tips">
<div>推荐尺寸: 500×700px (竖版封面)</div>
<div>支持格式: JPG, PNG, WebP</div>
</div>
</div>
</div>
@ -214,45 +181,35 @@
</div>
<!-- 库存和价格卡片 -->
<div class="card form-card">
<div class="form-card">
<div class="card-header">
<div class="card-header-icon">
<i class="fas fa-cubes"></i>
</div>
<div class="card-header-title">库存和价格</div>
<span class="card-title">库存和价格</span>
</div>
<div class="card-body">
<div class="form-section">
<div class="form-group">
<label for="stock" class="form-label">
<i class="fas fa-layer-group label-icon"></i>库存数量
</label>
<div class="input-number-group">
<button type="button" class="btn btn-outline-secondary input-number-decrement">
<i class="fas fa-minus"></i>
</button>
<input type="number" class="form-control custom-input text-center" id="stock" name="stock" min="0" value="0">
<button type="button" class="btn btn-outline-secondary input-number-increment">
<i class="fas fa-plus"></i>
</button>
<label for="stock" class="form-label">库存数量</label>
<div class="number-control">
<button type="button" class="number-btn decrement" id="stockDecrement"></button>
<input type="number" class="form-control text-center" id="stock" name="stock" min="0"
value="{{ book.stock if book else 0 }}">
<button type="button" class="number-btn increment" id="stockIncrement"></button>
</div>
</div>
<div class="form-group">
<label for="price" class="form-label">
<i class="fas fa-yen-sign label-icon"></i>价格
</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">¥</span>
</div>
<input type="number" class="form-control custom-input" id="price" name="price" step="0.01" min="0" placeholder="0.00">
<label for="price" class="form-label">价格</label>
<div class="price-input">
<span class="currency-symbol">¥</span>
<input type="number" class="form-control" id="price" name="price" step="0.01" min="0"
placeholder="0.00" value="{{ book.price if book else '' }}">
</div>
<div class="price-slider mt-3">
<input type="range" class="custom-range" id="priceRange" min="0" max="500" step="0.5" value="0">
<div class="d-flex justify-content-between">
<small>¥0</small>
<small>¥250</small>
<small>¥500</small>
<div class="price-slider">
<input type="range" class="range-slider" id="priceRange" min="0" max="500" step="0.5"
value="{{ book.price if book and book.price else 0 }}">
<div class="slider-marks">
<span>¥0</span>
<span>¥250</span>
<span>¥500</span>
</div>
</div>
</div>
@ -261,19 +218,17 @@
</div>
<!-- 提交按钮区域 -->
<div class="form-submit-container">
<div class="action-buttons">
<button type="submit" class="btn btn-primary btn-lg btn-block submit-btn pulse">
<i class="fas fa-save"></i> 保存图书
<div class="form-actions">
<button type="submit" class="btn-primary">
<i class="fas fa-save"></i> 保存图书
</button>
<div class="secondary-actions">
<button type="button" class="btn-secondary" id="previewBtn">
<i class="fas fa-eye"></i> 预览
</button>
<button type="reset" class="btn-secondary" id="resetBtn">
<i class="fas fa-undo"></i> 重置
</button>
<div class="secondary-buttons">
<button type="button" class="btn btn-outline-secondary btn-block" id="previewBtn">
<i class="fas fa-eye"></i> 预览
</button>
<button type="reset" class="btn btn-outline-danger btn-block reset-btn">
<i class="fas fa-undo"></i> 重置
</button>
</div>
</div>
<div class="form-tip">
<i class="fas fa-info-circle"></i>
@ -299,30 +254,28 @@
<div class="img-container">
<img id="cropperImage" src="" alt="图片预览">
</div>
</div>
<div class="modal-footer">
<div class="cropper-controls">
<div class="btn-group mr-2">
<button type="button" class="btn btn-outline-secondary" id="rotateLeft">
<div class="control-group">
<button type="button" class="control-btn" id="rotateLeft" title="向左旋转">
<i class="fas fa-undo"></i>
</button>
<button type="button" class="btn btn-outline-secondary" id="rotateRight">
<button type="button" class="control-btn" id="rotateRight" title="向右旋转">
<i class="fas fa-redo"></i>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary" id="zoomIn">
<i class="fas fa-search-plus"></i>
</button>
<button type="button" class="btn btn-outline-secondary" id="zoomOut">
<div class="control-group">
<button type="button" class="control-btn" id="zoomOut" title="缩小">
<i class="fas fa-search-minus"></i>
</button>
<button type="button" class="control-btn" id="zoomIn" title="放大">
<i class="fas fa-search-plus"></i>
</button>
</div>
</div>
<div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="cropImage">应用裁剪</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary modal-btn" data-dismiss="modal">取消</button>
<button type="button" class="btn-primary modal-btn" id="cropImage">应用裁剪</button>
</div>
</div>
</div>
@ -332,511 +285,74 @@
<div class="modal fade" id="previewModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-header preview-header">
<h5 class="modal-title">图书预览</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="book-preview-container">
<div class="row">
<div class="col-md-4">
<div class="book-preview-cover" id="previewCover">
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
</div>
<div class="modal-body preview-body">
<div class="book-preview">
<div class="preview-cover-section">
<div class="book-preview-cover" id="previewCover">
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
</div>
</div>
<div class="col-md-8">
<div class="book-preview-details">
<h3 id="previewTitle">书名加载中...</h3>
<p class="book-author" id="previewAuthor">作者加载中...</p>
<div class="book-info-section">
<div class="book-info-item">
<span class="info-label">出版社:</span>
<span class="info-value" id="previewPublisher">-</span>
</div>
<div class="book-info-item">
<span class="info-label">ISBN:</span>
<span class="info-value" id="previewISBN">-</span>
</div>
<div class="book-info-item">
<span class="info-label">出版年份:</span>
<span class="info-value" id="previewYear">-</span>
</div>
<div class="book-info-item">
<span class="info-label">分类:</span>
<span class="info-value" id="previewCategory">-</span>
</div>
<div class="book-info-item">
<span class="info-label">价格:</span>
<span class="info-value" id="previewPrice">-</span>
</div>
<div class="book-info-item">
<span class="info-label">库存:</span>
<span class="info-value" id="previewStock">-</span>
</div>
</div>
<div class="book-tags" id="previewTags"></div>
</div>
<div class="book-meta">
<div class="book-price" id="previewPrice">¥0.00</div>
<div class="book-stock" id="previewStock">库存: 0</div>
</div>
</div>
<div class="preview-details-section">
<h2 class="book-title" id="previewTitle">书名加载中...</h2>
<div class="book-author" id="previewAuthor">作者加载中...</div>
<div class="book-description" id="previewDescription">
<h4>图书简介</h4>
<p>简介加载中...</p>
<div class="book-info-grid">
<div class="info-item">
<div class="info-label">出版社</div>
<div class="info-value" id="previewPublisher">-</div>
</div>
<div class="info-item">
<div class="info-label">ISBN</div>
<div class="info-value" id="previewISBN">-</div>
</div>
<div class="info-item">
<div class="info-label">出版年份</div>
<div class="info-value" id="previewYear">-</div>
</div>
<div class="info-item">
<div class="info-label">分类</div>
<div class="info-value" id="previewCategory">-</div>
</div>
</div>
<div class="book-tags-preview" id="previewTags"></div>
<div class="book-description-preview">
<h3 class="section-title">图书简介</h3>
<div class="description-content" id="previewDescription">
<p class="placeholder-text">暂无简介内容</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" data-dismiss="modal" id="continueEditBtn">继续编辑</button>
<div class="modal-footer preview-footer">
<button type="button" class="btn-secondary modal-btn" data-dismiss="modal">关闭</button>
<button type="button" class="btn-primary modal-btn" data-dismiss="modal" id="continueEditBtn">继续编辑</button>
</div>
</div>
</div>
</div>
<!-- 通知容器 -->
<div class="notification-container"></div>
{% endblock %}
{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
<script>
$(document).ready(function() {
// 初始化Select2
$('.select2').select2({
placeholder: "选择分类...",
allowClear: true,
theme: "classic"
});
// 浮动标签效果
$('.floating-input').on('focus blur', function(e) {
$(this).parents('.form-group').toggleClass('focused', (e.type === 'focus' || this.value.length > 0));
}).trigger('blur');
// 表单进度条
updateFormProgress();
$('input, textarea, select').on('change keyup', function() {
updateFormProgress();
});
function updateFormProgress() {
const requiredFields = $('[required]');
const filledFields = requiredFields.filter(function() {
return $(this).val() !== '';
});
const otherFields = $('input:not([required]), textarea:not([required]), select:not([required])').not('[type="file"]');
const filledOtherFields = otherFields.filter(function() {
return $(this).val() !== '';
});
let requiredWeight = 70; // 必填字段权重70%
let otherWeight = 30; // 非必填字段权重30%
let requiredProgress = requiredFields.length ? (filledFields.length / requiredFields.length) * requiredWeight : requiredWeight;
let otherProgress = otherFields.length ? (filledOtherFields.length / otherFields.length) * otherWeight : 0;
let totalProgress = Math.floor(requiredProgress + otherProgress);
$('#formProgress').css('width', totalProgress + '%').attr('aria-valuenow', totalProgress);
$('#progressText').text('完成 ' + totalProgress + '%');
if (totalProgress >= 100) {
$('.submit-btn').addClass('pulse');
} else {
$('.submit-btn').removeClass('pulse');
}
}
// 封面图片处理
let cropper;
let coverBlob;
const coverPreview = $('#coverPreview');
const coverInput = $('#cover');
// 文件选择处理
coverInput.on('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
// 打开模态框并初始化裁剪
$('#cropperImage').attr('src', e.target.result);
$('#cropperModal').modal('show');
// 模态框显示后初始化Cropper
$('#cropperModal').on('shown.bs.modal', function() {
if (cropper) {
cropper.destroy();
}
cropper = new Cropper(document.getElementById('cropperImage'), {
aspectRatio: 5 / 7,
viewMode: 2,
responsive: true,
guides: true,
background: true
});
});
};
reader.readAsDataURL(file);
});
// 裁剪控制
$('#rotateLeft').on('click', function() {
cropper.rotate(-90);
});
$('#rotateRight').on('click', function() {
cropper.rotate(90);
});
$('#zoomIn').on('click', function() {
cropper.zoom(0.1);
});
$('#zoomOut').on('click', function() {
cropper.zoom(-0.1);
});
// 应用裁剪
$('#cropImage').on('click', function() {
const canvas = cropper.getCroppedCanvas({
width: 500,
height: 700,
fillColor: '#fff',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
});
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
coverPreview.html(`<img src="${url}" class="cover-image" alt="图书封面">`);
coverBlob = blob;
// 模拟File对象
const fileList = new DataTransfer();
const file = new File([blob], "cover.jpg", {type: "image/jpeg"});
fileList.items.add(file);
document.getElementById('cover').files = fileList.files;
$('#cropperModal').modal('hide');
}, 'image/jpeg', 0.95);
});
// 移除封面
$('#removeCover').on('click', function() {
coverPreview.html(`
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
<p class="placeholder-tip">点击上传或拖放图片至此处</p>
</div>
`);
coverInput.val('');
coverBlob = null;
});
// 拖放上传功能
coverPreview.on('dragover', function(e) {
e.preventDefault();
$(this).addClass('dragover');
}).on('dragleave drop', function(e) {
e.preventDefault();
$(this).removeClass('dragover');
}).on('drop', function(e) {
e.preventDefault();
const file = e.originalEvent.dataTransfer.files[0];
if (file && file.type.match('image.*')) {
coverInput[0].files = e.originalEvent.dataTransfer.files;
coverInput.trigger('change');
}
}).on('click', function() {
if (!$(this).find('img').length) {
coverInput.click();
}
});
// 标签处理
const tagInput = $('#tagInput');
const tagsContainer = $('#tagsContainer');
const tagsHiddenInput = $('#tags');
let tags = [];
function renderTags() {
tagsContainer.empty();
tags.forEach(tag => {
tagsContainer.append(`
<span class="tag-item">
${tag}
<i class="fas fa-times remove-tag" data-tag="${tag}"></i>
</span>
`);
});
tagsHiddenInput.val(tags.join(','));
}
function addTag() {
const tag = tagInput.val().trim();
if (tag && !tags.includes(tag)) {
tags.push(tag);
renderTags();
tagInput.val('').focus();
}
}
tagInput.on('keydown', function(e) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addTag();
}
});
$('.add-tag-btn').on('click', addTag);
$(document).on('click', '.remove-tag', function() {
const tagToRemove = $(this).data('tag');
tags = tags.filter(t => t !== tagToRemove);
renderTags();
});
// 数字输入控制
$('.input-number-decrement').on('click', function() {
const input = $(this).siblings('input');
const value = parseInt(input.val());
if (value > parseInt(input.attr('min'))) {
input.val(value - 1).trigger('change');
}
});
$('.input-number-increment').on('click', function() {
const input = $(this).siblings('input');
const value = parseInt(input.val());
input.val(value + 1).trigger('change');
});
// 价格滑块联动
$('#priceRange').on('input', function() {
$('#price').val($(this).val());
});
$('#price').on('input', function() {
const value = parseFloat($(this).val()) || 0;
$('#priceRange').val(Math.min(value, 500));
});
// 字符计数
$('#description').on('input', function() {
const count = $(this).val().length;
$('#charCount').text(count);
if (count > 2000) {
$('#charCount').addClass('text-danger');
} else {
$('#charCount').removeClass('text-danger');
}
});
// ISBN查询模拟
$('#isbnLookup').on('click', function() {
const isbn = $('#isbn').val().trim();
if (!isbn) return;
$(this).html('<i class="fas fa-spinner fa-spin"></i>');
// 模拟API查询延迟
setTimeout(() => {
// 模拟查到的数据
if (isbn === '9787020002207') {
$('#title').val('红楼梦').trigger('blur');
$('#author').val('曹雪芹').trigger('blur');
$('#publisher').val('人民文学出版社').trigger('blur');
$('#publish_year').val('1996').trigger('blur');
$('#category_id').val('1').trigger('change');
tags = ['中国文学', '古典', '名著'];
renderTags();
$('#description').val('《红楼梦》是中国古代章回体长篇小说中国古典四大名著之一通行本共120回一般认为前80回是清代作家曹雪芹所著后40回作者有争议。小说以贾、史、王、薛四大家族的兴衰为背景以贾府的家庭琐事、闺阁闲情为脉络以贾宝玉、林黛玉、薛宝钗的爱情婚姻悲剧为主线刻画了以贾宝玉和金陵十二钗为中心的正邪两赋有情人的人性美和悲剧美。').trigger('input');
$('#price').val('59.70').trigger('input');
// 显示成功通知
showNotification('ISBN查询成功', 'success');
} else {
showNotification('未找到相关图书信息', 'warning');
}
$(this).html('<i class="fas fa-search"></i>');
}, 1500);
});
// 表单预览
$('#previewBtn').on('click', function() {
// 填充预览内容
$('#previewTitle').text($('#title').val() || '未填写标题');
$('#previewAuthor').text($('#author').val() || '未填写作者');
$('#previewPublisher').text($('#publisher').val() || '-');
$('#previewISBN').text($('#isbn').val() || '-');
$('#previewYear').text($('#publish_year').val() || '-');
// 获取分类文本
const categoryId = $('#category_id').val();
const categoryText = categoryId ? $('#category_id option:selected').text() : '-';
$('#previewCategory').text(categoryText);
// 价格和库存
const price = parseFloat($('#price').val());
$('#previewPrice').text(price ? '¥' + price.toFixed(2) : '-');
$('#previewStock').text($('#stock').val() || '0');
// 标签
const previewTags = $('#previewTags');
previewTags.empty();
tags.forEach(tag => {
previewTags.append(`<span class="preview-tag">${tag}</span>`);
});
// 描述
const description = $('#description').val();
if (description) {
$('#previewDescription').html(`
<h4>图书简介</h4>
<p>${description.replace(/\n/g, '<br>')}</p>
`);
} else {
$('#previewDescription').html(`
<h4>图书简介</h4>
<p class="text-muted">未填写图书简介</p>
`);
}
// 封面
if ($('#coverPreview img').length) {
const coverSrc = $('#coverPreview img').attr('src');
$('#previewCover').html(`<img src="${coverSrc}" class="img-fluid" alt="封面预览">`);
} else {
$('#previewCover').html(`
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
</div>
`);
}
// 显示预览模态框
$('#previewModal').modal('show');
});
// 表单验证
$('#bookForm').on('submit', function(e) {
let isValid = true;
$('[required]').each(function() {
if (!$(this).val().trim()) {
isValid = false;
$(this).addClass('is-invalid');
// 添加错误提示
if (!$(this).next('.invalid-feedback').length) {
$(this).after(`<div class="invalid-feedback">此字段不能为空</div>`);
}
} else {
$(this).removeClass('is-invalid').next('.invalid-feedback').remove();
}
});
if (!isValid) {
e.preventDefault();
// 滚动到第一个错误字段
$('html, body').animate({
scrollTop: $('.is-invalid:first').offset().top - 100
}, 500);
showNotification('请填写所有必填字段', 'error');
} else {
showNotification('表单提交中...', 'info');
}
});
// 表单重置
$('.reset-btn').on('click', function() {
if (confirm('确定要重置表单吗?所有已填写的内容将被清空。')) {
$('#bookForm')[0].reset();
$('.floating-input').trigger('blur');
$('#coverPreview').html(`
<div class="no-cover-placeholder">
<i class="fas fa-book-open"></i>
<span>暂无封面</span>
<p class="placeholder-tip">点击上传或拖放图片至此处</p>
</div>
`);
tags = [];
renderTags();
updateFormProgress();
$('.select2').val(null).trigger('change');
$('#charCount').text('0');
showNotification('表单已重置', 'info');
}
});
// 输入时移除错误样式
$('input, textarea, select').on('input change', function() {
$(this).removeClass('is-invalid').next('.invalid-feedback').remove();
});
// 通知提示函数
function showNotification(message, type) {
// 创建通知元素
const notification = $(`
<div class="custom-notification notification-${type} animate__animated animate__fadeInRight">
<div class="notification-icon">
<i class="fas ${getIconForType(type)}"></i>
</div>
<div class="notification-content">
<p>${message}</p>
</div>
<button class="notification-close">
<i class="fas fa-times"></i>
</button>
</div>
`);
// 添加到页面
if ($('.notification-container').length === 0) {
$('body').append('<div class="notification-container"></div>');
}
$('.notification-container').append(notification);
// 自动关闭
setTimeout(() => {
notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
setTimeout(() => {
notification.remove();
}, 500);
}, 5000);
// 点击关闭
notification.find('.notification-close').on('click', function() {
notification.removeClass('animate__fadeInRight').addClass('animate__fadeOutRight');
setTimeout(() => {
notification.remove();
}, 500);
});
}
function getIconForType(type) {
switch(type) {
case 'success': return 'fa-check-circle';
case 'warning': return 'fa-exclamation-triangle';
case 'error': return 'fa-times-circle';
case 'info':
default: return 'fa-info-circle';
}
}
});
</script>
<script src="{{ url_for('static', filename='js/book-add.js') }}"></script>
{% endblock %}

View File

@ -109,8 +109,6 @@
{% 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>
@ -151,7 +149,7 @@
</div>
<!-- 借阅确认模态框 -->
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true">
<div class="modal fade" id="borrowModal" tabindex="-1" role="dialog" aria-labelledby="borrowModalLabel" aria-hidden="true" style="display: none;">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,9 @@ 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
numpy
pandas