fix_api_export_and_import_and_homepage_search

This commit is contained in:
superlishunqin 2025-05-16 22:50:25 +08:00
parent 04cc629988
commit 6246b9730f
15 changed files with 636 additions and 167 deletions

View File

@ -1,4 +1,4 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify, send_file
from app.models.book import Book, Category
from app.models.user import db
from app.utils.auth import login_required, permission_required # 修改导入替换admin_required为permission_required
@ -9,7 +9,9 @@ import datetime
import pandas as pd
import uuid
from app.models.log import Log
from io import BytesIO
import xlsxwriter
from sqlalchemy import text
book_bp = Blueprint('book', __name__)
@ -675,7 +677,7 @@ def delete_category(category_id):
# 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST'])
@login_required
@permission_required('import_export_books') # 替换 @admin_required
@permission_required('import_export_books')
def import_books():
if request.method == 'POST':
if 'file' not in request.files:
@ -689,41 +691,152 @@ def import_books():
if file and file.filename.endswith(('.xlsx', '.xls')):
try:
# 添加详细日志
current_app.logger.info(f"开始导入Excel文件: {file.filename}")
# 读取Excel文件
df = pd.read_excel(file)
current_app.logger.info(f"成功读取Excel文件共有{len(df)}行数据")
# 打印DataFrame的列名和前几行数据帮助诊断问题
current_app.logger.info(f"Excel文件列名: {df.columns.tolist()}")
current_app.logger.info(f"Excel文件前两行数据:\n{df.head(2)}")
success_count = 0
update_count = 0 # 新增:更新计数
error_count = 0
errors = []
# 检查必填列是否存在
required_columns = ['title', 'author']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
error_msg = f"Excel文件缺少必要的列: {', '.join(missing_columns)}"
current_app.logger.error(error_msg)
flash(error_msg, 'danger')
return redirect(request.url)
# 处理每一行数据
for index, row in df.iterrows():
try:
current_app.logger.debug(f"处理第{index + 2}行数据")
# 检查必填字段
if pd.isna(row.get('title')) or pd.isna(row.get('author')):
errors.append(f'{index + 2}行: 书名或作者为空')
error_msg = f'{index + 2}行: 书名或作者为空'
current_app.logger.warning(error_msg)
errors.append(error_msg)
error_count += 1
continue
# 处理数据类型转换
try:
stock = int(row.get('stock')) if not pd.isna(row.get('stock')) else 0
except ValueError:
error_msg = f'{index + 2}行: 库存数量必须是整数'
current_app.logger.warning(error_msg)
errors.append(error_msg)
error_count += 1
continue
try:
price = float(row.get('price')) if not pd.isna(row.get('price')) else None
except ValueError:
error_msg = f'{index + 2}行: 价格必须是数字'
current_app.logger.warning(error_msg)
errors.append(error_msg)
error_count += 1
continue
try:
category_id = int(row.get('category_id')) if not pd.isna(row.get('category_id')) else None
if category_id and not Category.query.get(category_id):
error_msg = f'{index + 2}行: 分类ID {category_id} 不存在'
current_app.logger.warning(error_msg)
errors.append(error_msg)
error_count += 1
continue
except ValueError:
error_msg = f'{index + 2}行: 分类ID必须是整数'
current_app.logger.warning(error_msg)
errors.append(error_msg)
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
existing_book = None
if isbn and not pd.isna(isbn):
isbn = str(isbn)
existing_book = Book.query.filter_by(isbn=isbn).first()
# 如果ISBN已存在检查状态
if existing_book:
if existing_book.status == 1:
# 活跃的图书,不允许重复导入
error_msg = f'{index + 2}行: ISBN {isbn} 已存在于活跃图书中'
current_app.logger.warning(error_msg)
errors.append(error_msg)
error_count += 1
continue
else:
# 已软删除的图书,更新它
current_app.logger.info(f"{index + 2}行: 发现已删除的ISBN {isbn},将更新该记录")
# 更新图书信息
existing_book.title = row.get('title')
existing_book.author = row.get('author')
existing_book.publisher = row.get('publisher') if not pd.isna(
row.get('publisher')) else None
existing_book.category_id = category_id
existing_book.tags = row.get('tags') if not pd.isna(row.get('tags')) else None
existing_book.publish_year = str(row.get('publish_year')) if not pd.isna(
row.get('publish_year')) else None
existing_book.description = row.get('description') if not pd.isna(
row.get('description')) else None
existing_book.cover_url = row.get('cover_url') if not pd.isna(
row.get('cover_url')) else None
existing_book.price = price
existing_book.status = 1 # 重新激活图书
existing_book.updated_at = datetime.datetime.now()
# 处理库存变更
stock_change = stock - existing_book.stock
existing_book.stock = stock
# 创建库存日志
if stock_change != 0:
from app.models.inventory import InventoryLog
change_type = '入库' if stock_change > 0 else '出库'
inventory_log = InventoryLog(
book_id=existing_book.id,
change_type=change_type,
change_amount=abs(stock_change),
after_stock=stock,
operator_id=current_user.id,
remark='批量导入更新图书',
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
update_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,
category_id=category_id,
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,
isbn=isbn,
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,
stock=stock,
price=price,
status=1,
created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now()
@ -733,6 +846,8 @@ def import_books():
# 提交以获取book的id
db.session.flush()
current_app.logger.debug(f"书籍添加成功: {book.title}, ID: {book.id}")
# 创建库存日志
if book.stock > 0:
from app.models.inventory import InventoryLog
@ -746,30 +861,59 @@ def import_books():
changed_at=datetime.datetime.now()
)
db.session.add(inventory_log)
current_app.logger.debug(f"库存日志添加成功: 书籍ID {book.id}, 数量 {book.stock}")
success_count += 1
except Exception as e:
errors.append(f'{index + 2}行: {str(e)}')
error_msg = f'{index + 2}行: {str(e)}'
current_app.logger.error(f"处理第{index + 2}行时出错: {str(e)}")
errors.append(error_msg)
error_count += 1
db.session.rollback() # 每行错误单独回滚
db.session.commit()
try:
# 最终提交所有成功的记录
if success_count > 0 or update_count > 0:
db.session.commit()
# 记录操作日志
Log.add_log(
action='批量导入图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"批量导入图书: 成功{success_count}条,失败{error_count}条,文件名:{file.filename}"
)
current_app.logger.info(f"导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}")
flash(f'导入完成: 成功{success_count}条,失败{error_count}', 'info')
if errors:
flash('<br>'.join(errors[:10]) + (f'<br>...等共{len(errors)}个错误' if len(errors) > 10 else ''),
'warning')
# 记录操作日志
Log.add_log(
action='批量导入图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"批量导入图书: 新增{success_count}条,更新{update_count}条,失败{error_count}条,文件名:{file.filename}"
)
return redirect(url_for('book.book_list'))
# 输出详细的错误信息
if success_count > 0 or update_count > 0:
flash(f'导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}', 'success')
else:
flash(f'导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}', 'warning')
if errors:
error_display = '<br>'.join(errors[:10])
if len(errors) > 10:
error_display += f'<br>...等共{len(errors)}个错误'
flash(error_display, 'warning')
return redirect(url_for('book.book_list'))
except Exception as commit_error:
db.session.rollback()
error_msg = f"提交数据库事务失败: {str(commit_error)}"
current_app.logger.error(error_msg)
flash(error_msg, 'danger')
return redirect(request.url)
except Exception as e:
db.session.rollback()
# 记录详细错误日志
error_msg = f"批量导入图书失败: {str(e)}"
current_app.logger.error(error_msg)
# 记录操作失败日志
Log.add_log(
action='批量导入图书失败',
@ -788,72 +932,87 @@ def import_books():
# 导出图书
@book_bp.route('/export')
@book_bp.route('/export', methods=['GET'])
@login_required
@permission_required('import_export_books') # 替换 @admin_required
@permission_required('import_export_books')
def export_books():
# 获取查询参数
search = request.args.get('search', '')
category_id = request.args.get('category_id', type=int)
try:
# 获取所有活跃的图书
books = Book.query.filter_by(status=1).all()
query = Book.query
# 创建工作簿和工作表
output = BytesIO()
workbook = xlsxwriter.Workbook(output)
worksheet = workbook.add_worksheet()
if search:
query = query.filter(
(Book.title.contains(search)) |
(Book.author.contains(search)) |
(Book.isbn.contains(search))
# 添加表头
headers = ['ID', '书名', '作者', '出版社', '分类', '标签', 'ISBN', '出版年份',
'描述', '封面链接', '库存', '价格', '创建时间', '更新时间']
for col_num, header in enumerate(headers):
worksheet.write(0, col_num, header)
# 不使用status过滤条件因为Category模型没有status字段
category_dict = {}
try:
categories = db.session.execute(
text("SELECT id, name FROM categories")
).fetchall()
for cat in categories:
category_dict[cat[0]] = cat[1]
except Exception as ex:
current_app.logger.warning(f"获取分类失败: {str(ex)}")
# 添加数据行
for row_num, book in enumerate(books, 1):
# 获取分类名称(如果有)
category_name = category_dict.get(book.category_id, "") if book.category_id 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 ""
# 写入图书数据
worksheet.write(row_num, 0, book.id)
worksheet.write(row_num, 1, book.title)
worksheet.write(row_num, 2, book.author)
worksheet.write(row_num, 3, book.publisher or "")
worksheet.write(row_num, 4, category_name)
worksheet.write(row_num, 5, book.tags or "")
worksheet.write(row_num, 6, book.isbn or "")
worksheet.write(row_num, 7, book.publish_year or "")
worksheet.write(row_num, 8, book.description or "")
worksheet.write(row_num, 9, book.cover_url or "")
worksheet.write(row_num, 10, book.stock)
worksheet.write(row_num, 11, float(book.price) if book.price else 0)
worksheet.write(row_num, 12, created_at)
worksheet.write(row_num, 13, updated_at)
# 关闭工作簿
workbook.close()
# 设置响应
output.seek(0)
# 记录操作日志
Log.add_log(
action='导出图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"导出了{len(books)}本图书"
)
if category_id:
query = query.filter_by(category_id=category_id)
# 返回文件
return send_file(
output,
as_attachment=True,
download_name=f"图书导出_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx",
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
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)
# 记录操作日志
Log.add_log(
action='导出图书',
user_id=current_user.id,
ip_address=request.remote_addr,
description=f"导出图书数据: {len(data)}条记录, 查询条件: 搜索={search}, 分类ID={category_id}"
)
# 提供下载链接
return redirect(url_for('static', filename=f'temp/{filename}'))
except Exception as e:
current_app.logger.error(f"导出图书失败: {str(e)}")
flash(f'导出失败: {str(e)}', 'danger')
return redirect(url_for('book.book_list'))
@book_bp.route('/test-permissions')
@ -931,87 +1090,219 @@ def browse_books():
@book_bp.route('/template/download')
@login_required
@permission_required('import_export_books') # 替换 @admin_required
@permission_required('import_export_books')
def download_template():
"""生成并下载Excel图书导入模板"""
# 创建一个简单的DataFrame作为模板
data = {
'title': ['三体', '解忧杂货店'],
'author': ['刘慈欣', '东野圭吾'],
'publisher': ['重庆出版社', '南海出版公司'],
'category_id': [2, 1], # 对应于科幻小说和文学分类
'tags': ['科幻,宇宙', '治愈,悬疑'],
'isbn': ['9787229100605', '9787544270878'],
'publish_year': ['2008', '2014'],
'description': ['中国著名科幻小说,三体世界的故事。', '通过信件为人们解忧的杂货店故事。'],
'cover_url': ['', ''], # 示例中为空
'stock': [10, 5],
'price': [45.00, 39.80]
"""下载图书导入模板"""
import tempfile
import os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from flask import after_this_request, current_app as app, send_file
# 创建工作簿和工作表
wb = Workbook()
ws = wb.active
ws.title = "图书导入模板"
# 定义样式
header_font = Font(name='微软雅黑', size=11, bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
required_font = Font(name='微软雅黑', size=11, bold=True, color="FF0000")
optional_font = Font(name='微软雅黑', size=11)
center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
left_alignment = Alignment(vertical='center', wrap_text=True)
# 定义边框样式
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# 定义列宽
column_widths = {
'A': 30, # title
'B': 20, # author
'C': 20, # publisher
'D': 15, # category_id
'E': 25, # tags
'F': 15, # isbn
'G': 15, # publish_year
'H': 40, # description
'I': 30, # cover_url
'J': 10, # stock
'K': 10, # price
}
# 创建一个分类ID和名称的映射
categories = Category.query.all()
category_map = {cat.id: cat.name for cat in categories}
# 设置列宽
for col, width in column_widths.items():
ws.column_dimensions[col].width = width
# 创建一个pandas DataFrame
df = pd.DataFrame(data)
# 定义字段信息
fields = [
{'name': 'title', 'desc': '图书标题', 'required': True, 'example': '哈利·波特与魔法石'},
{'name': 'author', 'desc': '作者名称', 'required': True, 'example': 'J.K.罗琳'},
{'name': 'publisher', 'desc': '出版社', 'required': False, 'example': '人民文学出版社'},
{'name': 'category_id', 'desc': '分类ID', 'required': False, 'example': '1'},
{'name': 'tags', 'desc': '标签', 'required': False, 'example': '魔幻,冒险,青少年文学'},
{'name': 'isbn', 'desc': 'ISBN编号', 'required': False, 'example': '9787020042494'},
{'name': 'publish_year', 'desc': '出版年份', 'required': False, 'example': '2000'},
{'name': 'description', 'desc': '图书简介', 'required': False,
'example': '这是一本关于魔法的书籍,讲述了小男孩哈利...'},
{'name': 'cover_url', 'desc': '封面图片URL', 'required': False, 'example': 'https://example.com/covers/hp.jpg'},
{'name': 'stock', 'desc': '库存数量', 'required': False, 'example': '10'},
{'name': 'price', 'desc': '价格', 'required': False, 'example': '39.5'},
]
# 添加标题行
ws.append([f"{field['name']}" for field in fields])
# 设置标题行样式
for col_idx, field in enumerate(fields, start=1):
cell = ws.cell(row=1, column=col_idx)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_alignment
cell.border = thin_border
# 添加示例数据行
ws.append([field['example'] for field in fields])
# 设置示例行样式
for col_idx, field in enumerate(fields, start=1):
cell = ws.cell(row=2, column=col_idx)
cell.alignment = left_alignment
cell.border = thin_border
if field['required']:
cell.font = required_font
else:
cell.font = optional_font
# 冻结首行
ws.freeze_panes = "A2"
# 添加说明工作表
with pd.ExcelWriter('book_import_template.xlsx', engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='图书数据示例', index=False)
instructions_ws = wb.create_sheet(title="填写说明")
# 创建说明工作表
info_df = pd.DataFrame({
'字段名': ['title', 'author', 'publisher', 'category_id', 'tags',
'isbn', 'publish_year', 'description', 'cover_url', 'stock', 'price'],
'说明': ['图书标题 (必填)', '作者名称 (必填)', '出版社', '分类ID (对应系统分类)',
'标签 (多个标签用逗号分隔)', 'ISBN编号', '出版年份', '图书简介',
'封面图片URL', '库存数量', '价格'],
'示例': ['三体', '刘慈欣', '重庆出版社', '2 (科幻小说)', '科幻,宇宙',
'9787229100605', '2008', '中国著名科幻小说...', 'http://example.com/cover.jpg', '10', '45.00'],
'是否必填': ['', '', '', '', '', '', '', '', '', '', '']
})
info_df.to_excel(writer, sheet_name='填写说明', index=False)
# 设置说明工作表的列宽
instructions_ws.column_dimensions['A'].width = 15
instructions_ws.column_dimensions['B'].width = 20
instructions_ws.column_dimensions['C'].width = 60
# 创建分类ID工作表
category_df = pd.DataFrame({
'分类ID': list(category_map.keys()),
'分类名称': list(category_map.values())
})
category_df.to_excel(writer, sheet_name='分类对照表', index=False)
# 添加说明标题
instructions_ws.append(["字段名", "说明", "备注"])
# 设置响应
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
filename = f'book_import_template_{timestamp}.xlsx'
# 设置标题行样式
for col_idx in range(1, 4):
cell = instructions_ws.cell(row=1, column=col_idx)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_alignment
cell.border = thin_border
# 记录操作日志
Log.add_log(
action='下载图书导入模板',
user_id=current_user.id,
ip_address=request.remote_addr,
description="下载图书批量导入Excel模板"
)
# 字段详细说明
notes = {
'title': "图书的完整名称,例如:'哈利·波特与魔法石'。必须填写且不能为空。",
'author': "图书的作者名称,例如:'J.K.罗琳'。必须填写且不能为空。如有多个作者,请用逗号分隔。",
'publisher': "出版社名称,例如:'人民文学出版社'",
'category_id': "图书分类的系统ID。请在系统的分类管理页面查看ID。如果不确定可以留空系统将使用默认分类。",
'tags': "图书的标签,多个标签请用英文逗号分隔,例如:'魔幻,冒险,青少年文学'",
'isbn': "图书的ISBN号码例如'9787020042494'。请输入完整的13位或10位ISBN。",
'publish_year': "图书的出版年份,例如:'2000'。请输入4位数字年份。",
'description': "图书的简介或摘要。可以输入详细的描述信息,系统支持换行和基本格式。",
'cover_url': "图书封面的在线URL地址。如果有图片网址系统将自动下载并设置为封面。",
'stock': "图书的库存数量,例如:'10'。请输入整数。",
'price': "图书的价格,例如:'39.5'。可以输入小数。"
}
# 直接返回生成的文件
from flask import send_file
from io import BytesIO
# 添加字段说明
for idx, field in enumerate(fields, start=2):
required_text = "【必填】" if field['required'] else "【选填】"
instructions_ws.append([
field['name'],
f"{field['desc']} {required_text}",
notes.get(field['name'], "")
])
# 将文件转换为二进制流
output = BytesIO()
with open('book_import_template.xlsx', 'rb') as f:
output.write(f.read())
output.seek(0)
# 设置单元格样式
for col_idx in range(1, 4):
cell = instructions_ws.cell(row=idx, column=col_idx)
cell.alignment = left_alignment
cell.border = thin_border
if field['required'] and col_idx == 2:
cell.font = required_font
else:
cell.font = optional_font
# 添加示例行
instructions_ws.append(["", "", ""])
instructions_ws.append(["示例", "", "以下是完整的示例数据:"])
example_data = [
{"title": "哈利·波特与魔法石", "author": "J.K.罗琳", "publisher": "人民文学出版社",
"category_id": "1", "tags": "魔幻,冒险,青少年文学", "isbn": "9787020042494",
"publish_year": "2000", "description": "这是一本关于魔法的书籍,讲述了小男孩哈利...",
"cover_url": "https://example.com/covers/hp.jpg", "stock": "10", "price": "39.5"},
{"title": "三体", "author": "刘慈欣", "publisher": "重庆出版社",
"category_id": "2", "tags": "科幻,硬科幻,中国科幻", "isbn": "9787536692930",
"publish_year": "2008", "description": "文化大革命如火如荼进行的同时...",
"cover_url": "https://example.com/covers/threebody.jpg", "stock": "15", "price": "59.8"},
{"title": "平凡的世界", "author": "路遥", "publisher": "北京十月文艺出版社",
"category_id": "3", "tags": "文学,现实主义,农村", "isbn": "9787530216781",
"publish_year": "2017", "description": "这是一部全景式地表现中国当代城乡社会...",
"cover_url": "https://example.com/covers/world.jpg", "stock": "8", "price": "128.0"}
]
# 添加3个完整示例到主模板工作表
for example in example_data:
row_data = [example.get(field['name'], '') for field in fields]
ws.append(row_data)
# 设置示例数据样式
for row_idx in range(3, 6):
for col_idx in range(1, len(fields) + 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.alignment = left_alignment
cell.border = thin_border
# 使用临时文件保存工作簿
fd, temp_path = tempfile.mkstemp(suffix='.xlsx')
os.close(fd)
# 删除临时文件
try:
os.remove('book_import_template.xlsx')
except:
pass
wb.save(temp_path)
# 通过send_file返回
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=filename
)
# 注册一个函数在响应完成后删除临时文件
@after_this_request
def remove_file(response):
try:
os.unlink(temp_path)
except Exception as error:
app.logger.error("删除临时文件出错: %s", error)
return response
response = send_file(
temp_path,
as_attachment=True,
download_name='图书导入模板.xlsx',
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
# 添加防止缓存的头信息
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
except Exception as e:
# 确保在出错时也删除临时文件
try:
os.unlink(temp_path)
except:
pass
app.logger.error("生成模板文件出错: %s", str(e))
return {"error": "生成模板文件失败,请稍后再试"}, 500

View File

@ -22,28 +22,72 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// 美化表单提交按钮的点击效果
const importBtn = document.querySelector('.import-btn');
if (importBtn) {
importBtn.addEventListener('click', function(e) {
// 监听表单提交
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
const fileInput = document.getElementById('file');
if (!fileInput || !fileInput.files || !fileInput.files.length) {
e.preventDefault();
showMessage('请先选择要导入的Excel文件', 'warning');
return;
}
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在导入...';
this.disabled = true;
const importBtn = document.querySelector('.import-btn');
if (importBtn) {
importBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在导入...';
importBtn.disabled = true;
}
// 添加花朵飘落动画效果
addFallingElements(10);
// 设置超时处理如果30秒后还没响应提示用户
window.importTimeout = setTimeout(function() {
showMessage('导入处理时间较长,请耐心等待...', 'info');
}, 30000);
});
}
// 美化表单提交按钮的点击效果
const importBtn = document.querySelector('.import-btn');
if (importBtn) {
importBtn.addEventListener('click', function(e) {
// 按钮的点击效果已由表单提交事件处理
// 避免重复处理
if (!form || form.reportValidity() === false) {
e.preventDefault();
}
});
}
// 为浮动元素添加动画
initFloatingElements();
// 检查页面中的flash消息
checkFlashMessages();
});
// 检查页面中的flash消息
function checkFlashMessages() {
// Flask的flash消息通常会渲染为带有特定类的元素
const flashMessages = document.querySelectorAll('.alert');
if (flashMessages && flashMessages.length > 0) {
// 如果存在flash消息说明页面是提交后重新加载的
// 恢复按钮状态
const importBtn = document.querySelector('.import-btn');
if (importBtn) {
importBtn.innerHTML = '<i class="fas fa-upload"></i> 开始导入';
importBtn.disabled = false;
}
// 清除可能的超时
if (window.importTimeout) {
clearTimeout(window.importTimeout);
}
}
}
// 检查文件类型并尝试预览
function checkFileAndPreview(file) {
if (!file) return;

View File

@ -68,10 +68,11 @@
<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>
<!-- 修改搜索容器为表单 -->
<form action="{{ url_for('book.browse_books') }}" method="GET" class="search-container" id="global-search-form">
<i class="fas fa-search search-icon" id="search-submit-icon"></i>
<input type="text" name="search" placeholder="搜索图书..." class="search-input" id="global-search-input">
</form>
<div class="user-menu">
<div class="notifications dropdown">
<a href="#" class="notification-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -175,6 +176,52 @@
}
});
}
// 搜索功能
const searchForm = document.getElementById('global-search-form');
const searchInput = document.getElementById('global-search-input');
const searchIcon = document.getElementById('search-submit-icon');
if (searchForm && searchInput && searchIcon) {
// 智能判断搜索提交目标 - 根据当前页面设置合适的搜索结果页
function updateSearchFormAction() {
const currentPath = window.location.pathname;
if (currentPath.includes('/book/admin/list')) {
searchForm.action = "{{ url_for('book.admin_book_list') }}";
} else if (currentPath.includes('/book/list')) {
searchForm.action = "{{ url_for('book.book_list') }}";
} else if (currentPath.includes('/book/browse')) {
searchForm.action = "{{ url_for('book.browse_books') }}";
} else {
searchForm.action = "{{ url_for('book.browse_books') }}";
}
}
updateSearchFormAction();
// 点击搜索图标时提交表单
searchIcon.addEventListener('click', function() {
if (searchInput.value.trim() !== '') {
updateSearchFormAction();
searchForm.submit();
}
});
// 输入框按回车键时提交表单
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
if (searchInput.value.trim() === '') {
e.preventDefault(); // 防止空搜索提交
} else {
updateSearchFormAction();
// 表单自然会提交,不需要额外代码
}
}
});
// 添加视觉反馈,让用户知道搜索图标可点击
searchIcon.style.cursor = 'pointer';
}
});
</script>

View File

@ -64,9 +64,11 @@
<div class="template-download animate__animated animate__pulse animate__infinite animate__slower">
<p>不确定如何开始? 下载我们精心准备的模板:</p>
<a href="{{ url_for('book.download_template') }}" class="btn download-btn">
<button type="button" id="downloadTemplateBtn" class="btn download-btn">
<i class="fas fa-download"></i> 下载Excel模板
</a>
</button>
<!-- 用于显示下载成功或失败的消息 -->
<div id="downloadMessage" style="margin-top: 10px; display: none;"></div>
</div>
</div>
</div>
@ -82,9 +84,91 @@
<div class="flower flower-1"></div>
<div class="flower flower-2"></div>
</div>
<!-- 基本模板隐藏在页面里,以便在无法请求服务器时使用 -->
<div style="display:none;">
<a id="directDownloadLink" href="#" download="book_import_template.xlsx"></a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/book-import.js') }}"></script>
<script>
// 添加下载模板的JavaScript代码
document.addEventListener('DOMContentLoaded', function() {
const downloadBtn = document.getElementById('downloadTemplateBtn');
const messageDiv = document.getElementById('downloadMessage');
const directLink = document.getElementById('directDownloadLink');
if (downloadBtn) {
downloadBtn.addEventListener('click', function(e) {
e.preventDefault();
// 显示下载中状态
const originalText = downloadBtn.innerHTML;
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 准备下载中...';
downloadBtn.disabled = true;
// 方法1: 使用fetch API请求文件并手动触发下载
fetch('{{ url_for("book.download_template") }}', {
method: 'GET',
credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
throw new Error('下载请求失败,状态码: ' + response.status);
}
return response.blob();
})
.then(blob => {
// 创建临时URL
const url = window.URL.createObjectURL(blob);
// 设置下载链接
directLink.href = url;
directLink.download = 'book_import_template.xlsx';
// 触发点击
directLink.click();
// 释放URL对象
window.URL.revokeObjectURL(url);
// 显示成功消息
messageDiv.innerHTML = '<div class="alert alert-success">模板下载成功!</div>';
messageDiv.style.display = 'block';
// 3秒后隐藏消息
setTimeout(() => {
messageDiv.style.display = 'none';
}, 3000);
})
.catch(error => {
console.error('下载失败:', error);
// 显示错误消息
messageDiv.innerHTML = '<div class="alert alert-danger">下载失败,请稍后重试。</div>';
messageDiv.style.display = 'block';
// 方法2: 如果fetch失败尝试使用window.open在新标签页下载
window.open('{{ url_for("book.download_template") }}', '_blank');
})
.finally(() => {
// 恢复按钮状态
downloadBtn.innerHTML = originalText;
downloadBtn.disabled = false;
});
// 方法3: 超时后,如果以上方法都失败,提供直接链接让用户手动点击
setTimeout(function() {
if (messageDiv.innerHTML.includes('下载失败') || messageDiv.style.display === 'none') {
messageDiv.innerHTML = '<div class="alert alert-warning">下载似乎没有开始。<a href="{{ url_for("book.download_template") }}" target="_blank" download class="alert-link">点击这里</a>手动下载。</div>';
messageDiv.style.display = 'block';
}
}, 5000);
});
}
});
</script>
{% endblock %}

View File

@ -10,4 +10,7 @@ email-validator==2.0.0
pillow==9.5.0
numpy
pandas
flask-login
flask-login
openpyxl
xlrd
xlsxwriter