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.book import Book, Category
from app.models.user import db from app.models.user import db
from app.utils.auth import login_required, permission_required # 修改导入替换admin_required为permission_required from app.utils.auth import login_required, permission_required # 修改导入替换admin_required为permission_required
@ -9,7 +9,9 @@ import datetime
import pandas as pd import pandas as pd
import uuid import uuid
from app.models.log import Log from app.models.log import Log
from io import BytesIO
import xlsxwriter
from sqlalchemy import text
book_bp = Blueprint('book', __name__) book_bp = Blueprint('book', __name__)
@ -675,7 +677,7 @@ def delete_category(category_id):
# 批量导入图书 # 批量导入图书
@book_bp.route('/import', methods=['GET', 'POST']) @book_bp.route('/import', methods=['GET', 'POST'])
@login_required @login_required
@permission_required('import_export_books') # 替换 @admin_required @permission_required('import_export_books')
def import_books(): def import_books():
if request.method == 'POST': if request.method == 'POST':
if 'file' not in request.files: if 'file' not in request.files:
@ -689,41 +691,152 @@ def import_books():
if file and file.filename.endswith(('.xlsx', '.xls')): if file and file.filename.endswith(('.xlsx', '.xls')):
try: try:
# 添加详细日志
current_app.logger.info(f"开始导入Excel文件: {file.filename}")
# 读取Excel文件 # 读取Excel文件
df = pd.read_excel(file) 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 success_count = 0
update_count = 0 # 新增:更新计数
error_count = 0 error_count = 0
errors = [] 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(): for index, row in df.iterrows():
try: try:
current_app.logger.debug(f"处理第{index + 2}行数据")
# 检查必填字段 # 检查必填字段
if pd.isna(row.get('title')) or pd.isna(row.get('author')): 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 error_count += 1
continue continue
# 检查ISBN是否已存在 # 检查ISBN是否已存在
isbn = row.get('isbn') isbn = row.get('isbn')
if isbn and not pd.isna(isbn) and Book.query.filter_by(isbn=str(isbn)).first(): existing_book = None
errors.append(f'{index + 2}行: ISBN {isbn} 已存在')
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 error_count += 1
continue 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( book = Book(
title=row.get('title'), title=row.get('title'),
author=row.get('author'), author=row.get('author'),
publisher=row.get('publisher') if not pd.isna(row.get('publisher')) else None, 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, 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, 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, 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, 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, stock=stock,
price=float(row.get('price')) if not pd.isna(row.get('price')) else None, price=price,
status=1, status=1,
created_at=datetime.datetime.now(), created_at=datetime.datetime.now(),
updated_at=datetime.datetime.now() updated_at=datetime.datetime.now()
@ -733,6 +846,8 @@ def import_books():
# 提交以获取book的id # 提交以获取book的id
db.session.flush() db.session.flush()
current_app.logger.debug(f"书籍添加成功: {book.title}, ID: {book.id}")
# 创建库存日志 # 创建库存日志
if book.stock > 0: if book.stock > 0:
from app.models.inventory import InventoryLog from app.models.inventory import InventoryLog
@ -746,30 +861,59 @@ def import_books():
changed_at=datetime.datetime.now() changed_at=datetime.datetime.now()
) )
db.session.add(inventory_log) db.session.add(inventory_log)
current_app.logger.debug(f"库存日志添加成功: 书籍ID {book.id}, 数量 {book.stock}")
success_count += 1 success_count += 1
except Exception as e:
errors.append(f'{index + 2}行: {str(e)}')
error_count += 1
except Exception as 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() # 每行错误单独回滚
try:
# 最终提交所有成功的记录
if success_count > 0 or update_count > 0:
db.session.commit() db.session.commit()
current_app.logger.info(f"导入完成: 新增{success_count}条,更新{update_count}条,失败{error_count}")
# 记录操作日志 # 记录操作日志
Log.add_log( Log.add_log(
action='批量导入图书', action='批量导入图书',
user_id=current_user.id, user_id=current_user.id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"批量导入图书: 成功{success_count}条,失败{error_count}条,文件名:{file.filename}" description=f"批量导入图书: 新增{success_count}条,更新{update_count}条,失败{error_count}条,文件名:{file.filename}"
) )
flash(f'导入完成: 成功{success_count}条,失败{error_count}', 'info') # 输出详细的错误信息
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: if errors:
flash('<br>'.join(errors[:10]) + (f'<br>...等共{len(errors)}个错误' if len(errors) > 10 else ''), error_display = '<br>'.join(errors[:10])
'warning') if len(errors) > 10:
error_display += f'<br>...等共{len(errors)}个错误'
flash(error_display, 'warning')
return redirect(url_for('book.book_list')) 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: except Exception as e:
db.session.rollback()
# 记录详细错误日志
error_msg = f"批量导入图书失败: {str(e)}"
current_app.logger.error(error_msg)
# 记录操作失败日志 # 记录操作失败日志
Log.add_log( Log.add_log(
action='批量导入图书失败', action='批量导入图书失败',
@ -788,72 +932,87 @@ def import_books():
# 导出图书 # 导出图书
@book_bp.route('/export') @book_bp.route('/export', methods=['GET'])
@login_required @login_required
@permission_required('import_export_books') # 替换 @admin_required @permission_required('import_export_books')
def export_books(): def export_books():
# 获取查询参数 try:
search = request.args.get('search', '') # 获取所有活跃的图书
category_id = request.args.get('category_id', type=int) 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( headers = ['ID', '书名', '作者', '出版社', '分类', '标签', 'ISBN', '出版年份',
(Book.title.contains(search)) | '描述', '封面链接', '库存', '价格', '创建时间', '更新时间']
(Book.author.contains(search)) | for col_num, header in enumerate(headers):
(Book.isbn.contains(search)) worksheet.write(0, col_num, header)
)
if category_id: # 不使用status过滤条件因为Category模型没有status字段
query = query.filter_by(category_id=category_id) 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)}")
books = query.all() # 添加数据行
for row_num, book in enumerate(books, 1):
# 获取分类名称(如果有)
category_name = category_dict.get(book.category_id, "") if book.category_id else ""
# 创建DataFrame # 格式化日期
data = [] created_at = book.created_at.strftime('%Y-%m-%d %H:%M:%S') if book.created_at else ""
for book in books: updated_at = book.updated_at.strftime('%Y-%m-%d %H:%M:%S') if book.updated_at else ""
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) # 写入图书数据
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)
# 创建临时文件 # 关闭工作簿
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') workbook.close()
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) output.seek(0)
# 写入Excel
df.to_excel(filepath, index=False)
# 记录操作日志 # 记录操作日志
Log.add_log( Log.add_log(
action='导出图书', action='导出图书',
user_id=current_user.id, user_id=current_user.id,
ip_address=request.remote_addr, ip_address=request.remote_addr,
description=f"导出图书数据: {len(data)}条记录, 查询条件: 搜索={search}, 分类ID={category_id}" description=f"导出{len(books)}本图书"
) )
# 提供下载链接 # 返回文件
return redirect(url_for('static', filename=f'temp/{filename}')) 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'
)
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') @book_bp.route('/test-permissions')
@ -931,87 +1090,219 @@ def browse_books():
@book_bp.route('/template/download') @book_bp.route('/template/download')
@login_required @login_required
@permission_required('import_export_books') # 替换 @admin_required @permission_required('import_export_books')
def download_template(): def download_template():
"""生成并下载Excel图书导入模板""" """下载图书导入模板"""
# 创建一个简单的DataFrame作为模板 import tempfile
data = { import os
'title': ['三体', '解忧杂货店'], from openpyxl import Workbook
'author': ['刘慈欣', '东野圭吾'], from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
'publisher': ['重庆出版社', '南海出版公司'], from openpyxl.utils import get_column_letter
'category_id': [2, 1], # 对应于科幻小说和文学分类 from flask import after_this_request, current_app as app, send_file
'tags': ['科幻,宇宙', '治愈,悬疑'],
'isbn': ['9787229100605', '9787544270878'], # 创建工作簿和工作表
'publish_year': ['2008', '2014'], wb = Workbook()
'description': ['中国著名科幻小说,三体世界的故事。', '通过信件为人们解忧的杂货店故事。'], ws = wb.active
'cover_url': ['', ''], # 示例中为空 ws.title = "图书导入模板"
'stock': [10, 5],
'price': [45.00, 39.80] # 定义样式
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() for col, width in column_widths.items():
category_map = {cat.id: cat.name for cat in categories} 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: instructions_ws = wb.create_sheet(title="填写说明")
df.to_excel(writer, sheet_name='图书数据示例', index=False)
# 创建说明工作表 # 设置说明工作表的列宽
info_df = pd.DataFrame({ instructions_ws.column_dimensions['A'].width = 15
'字段名': ['title', 'author', 'publisher', 'category_id', 'tags', instructions_ws.column_dimensions['B'].width = 20
'isbn', 'publish_year', 'description', 'cover_url', 'stock', 'price'], instructions_ws.column_dimensions['C'].width = 60
'说明': ['图书标题 (必填)', '作者名称 (必填)', '出版社', '分类ID (对应系统分类)',
'标签 (多个标签用逗号分隔)', 'ISBN编号', '出版年份', '图书简介',
'封面图片URL', '库存数量', '价格'],
'示例': ['三体', '刘慈欣', '重庆出版社', '2 (科幻小说)', '科幻,宇宙',
'9787229100605', '2008', '中国著名科幻小说...', 'http://example.com/cover.jpg', '10', '45.00'],
'是否必填': ['', '', '', '', '', '', '', '', '', '', '']
})
info_df.to_excel(writer, sheet_name='填写说明', index=False)
# 创建分类ID工作表 # 添加说明标题
category_df = pd.DataFrame({ instructions_ws.append(["字段名", "说明", "备注"])
'分类ID': list(category_map.keys()),
'分类名称': list(category_map.values())
})
category_df.to_excel(writer, sheet_name='分类对照表', index=False)
# 设置响应 # 设置标题行样式
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') for col_idx in range(1, 4):
filename = f'book_import_template_{timestamp}.xlsx' 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( notes = {
action='下载图书导入模板', 'title': "图书的完整名称,例如:'哈利·波特与魔法石'。必须填写且不能为空。",
user_id=current_user.id, 'author': "图书的作者名称,例如:'J.K.罗琳'。必须填写且不能为空。如有多个作者,请用逗号分隔。",
ip_address=request.remote_addr, 'publisher': "出版社名称,例如:'人民文学出版社'",
description="下载图书批量导入Excel模板" 'category_id': "图书分类的系统ID。请在系统的分类管理页面查看ID。如果不确定可以留空系统将使用默认分类。",
'tags': "图书的标签,多个标签请用英文逗号分隔,例如:'魔幻,冒险,青少年文学'",
'isbn': "图书的ISBN号码例如'9787020042494'。请输入完整的13位或10位ISBN。",
'publish_year': "图书的出版年份,例如:'2000'。请输入4位数字年份。",
'description': "图书的简介或摘要。可以输入详细的描述信息,系统支持换行和基本格式。",
'cover_url': "图书封面的在线URL地址。如果有图片网址系统将自动下载并设置为封面。",
'stock': "图书的库存数量,例如:'10'。请输入整数。",
'price': "图书的价格,例如:'39.5'。可以输入小数。"
}
# 添加字段说明
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'], "")
])
# 设置单元格样式
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:
wb.save(temp_path)
# 注册一个函数在响应完成后删除临时文件
@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'
) )
# 直接返回生成的文件 # 添加防止缓存的头信息
from flask import send_file response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
from io import BytesIO response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
# 将文件转换为二进制流 return response
output = BytesIO() except Exception as e:
with open('book_import_template.xlsx', 'rb') as f: # 确保在出错时也删除临时文件
output.write(f.read())
output.seek(0)
# 删除临时文件
try: try:
os.remove('book_import_template.xlsx') os.unlink(temp_path)
except: except:
pass pass
app.logger.error("生成模板文件出错: %s", str(e))
# 通过send_file返回 return {"error": "生成模板文件失败,请稍后再试"}, 500
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=filename
)

View File

@ -22,28 +22,72 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// 美化表单提交按钮的点击效果 // 监听表单提交
const importBtn = document.querySelector('.import-btn'); const form = document.querySelector('form');
if (importBtn) { if (form) {
importBtn.addEventListener('click', function(e) { form.addEventListener('submit', function(e) {
const fileInput = document.getElementById('file');
if (!fileInput || !fileInput.files || !fileInput.files.length) { if (!fileInput || !fileInput.files || !fileInput.files.length) {
e.preventDefault(); e.preventDefault();
showMessage('请先选择要导入的Excel文件', 'warning'); showMessage('请先选择要导入的Excel文件', 'warning');
return; return;
} }
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在导入...'; const importBtn = document.querySelector('.import-btn');
this.disabled = true; if (importBtn) {
importBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在导入...';
importBtn.disabled = true;
}
// 添加花朵飘落动画效果 // 添加花朵飘落动画效果
addFallingElements(10); 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(); 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) { function checkFileAndPreview(file) {
if (!file) return; if (!file) return;

View File

@ -68,10 +68,11 @@
<main class="main-content"> <main class="main-content">
<!-- 顶部导航 --> <!-- 顶部导航 -->
<header class="top-bar"> <header class="top-bar">
<div class="search-container"> <!-- 修改搜索容器为表单 -->
<i class="fas fa-search search-icon"></i> <form action="{{ url_for('book.browse_books') }}" method="GET" class="search-container" id="global-search-form">
<input type="text" placeholder="搜索图书..." class="search-input"> <i class="fas fa-search search-icon" id="search-submit-icon"></i>
</div> <input type="text" name="search" placeholder="搜索图书..." class="search-input" id="global-search-input">
</form>
<div class="user-menu"> <div class="user-menu">
<div class="notifications dropdown"> <div class="notifications dropdown">
<a href="#" class="notification-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <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> </script>

View File

@ -64,9 +64,11 @@
<div class="template-download animate__animated animate__pulse animate__infinite animate__slower"> <div class="template-download animate__animated animate__pulse animate__infinite animate__slower">
<p>不确定如何开始? 下载我们精心准备的模板:</p> <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模板 <i class="fas fa-download"></i> 下载Excel模板
</a> </button>
<!-- 用于显示下载成功或失败的消息 -->
<div id="downloadMessage" style="margin-top: 10px; display: none;"></div>
</div> </div>
</div> </div>
</div> </div>
@ -82,9 +84,91 @@
<div class="flower flower-1"></div> <div class="flower flower-1"></div>
<div class="flower flower-2"></div> <div class="flower flower-2"></div>
</div> </div>
<!-- 基本模板隐藏在页面里,以便在无法请求服务器时使用 -->
<div style="display:none;">
<a id="directDownloadLink" href="#" download="book_import_template.xlsx"></a>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/book-import.js') }}"></script> <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 %} {% endblock %}

View File

@ -11,3 +11,6 @@ pillow==9.5.0
numpy numpy
pandas pandas
flask-login flask-login
openpyxl
xlrd
xlsxwriter