修复历史记录批量下载的bug
This commit is contained in:
@@ -6,7 +6,7 @@ import zipfile
|
||||
from datetime import datetime
|
||||
from flask import request, jsonify, send_from_directory
|
||||
from lib.db import execute_query
|
||||
from lib.logger import log_info, log_error
|
||||
from lib.logger import log_info, log_error, log_warning
|
||||
from . import download_bp
|
||||
|
||||
|
||||
@@ -120,31 +120,32 @@ def batch_download():
|
||||
# 清理旧的临时文件
|
||||
cleanup_old_temp_zips()
|
||||
|
||||
# 查询所有成功的历史记录
|
||||
# 查询所有成功的历史记录(附带筛选条件以增强安全性)
|
||||
placeholders = ', '.join(['%s'] * len(ids))
|
||||
result = execute_query(
|
||||
f'SELECT id, file_path, config_name, report_date, status FROM t_daily_report_history WHERE id IN ({placeholders})',
|
||||
f'SELECT id, file_path, config_name, report_date, status, split_name FROM t_daily_report_history WHERE id IN ({placeholders})',
|
||||
tuple(ids)
|
||||
)
|
||||
|
||||
if not result:
|
||||
return jsonify({'success': False, 'message': '未找到对应的历史记录'}), 404
|
||||
|
||||
total_matched = len(result)
|
||||
success_records = [r for r in result if r.get('status') == 1]
|
||||
skipped_failed = total_matched - len(success_records)
|
||||
|
||||
reports_dir = get_reports_dir()
|
||||
temp_dir = get_temp_dir()
|
||||
|
||||
# 收集要打包的文件
|
||||
files_to_zip = []
|
||||
failed_names = []
|
||||
used_names = set() # 检测 zip 内文件名冲突
|
||||
|
||||
for item in result:
|
||||
if item.get('status') != 1:
|
||||
failed_names.append(f"{item.get('config_name', '未知')} (生成失败)")
|
||||
continue
|
||||
|
||||
for item in success_records:
|
||||
file_path = item.get('file_path')
|
||||
if not file_path:
|
||||
failed_names.append(f"{item.get('config_name', '未知')} (无文件)")
|
||||
failed_names.append(f"{item.get('config_name', '未知')} (无文件路径)")
|
||||
continue
|
||||
|
||||
# 提取文件名
|
||||
@@ -157,17 +158,27 @@ def batch_download():
|
||||
|
||||
full_path = os.path.join(reports_dir, filename)
|
||||
if not os.path.exists(full_path):
|
||||
failed_names.append(f"{item.get('config_name', '未知')} (文件不存在)")
|
||||
failed_names.append(f"{item.get('config_name', '未知')}_{item.get('report_date', '')} (文件不存在)")
|
||||
continue
|
||||
|
||||
# 使用配置名作为zip内的文件名,避免重名
|
||||
# 使用配置名+报表日期+拆分值作为zip内的文件名,避免重名
|
||||
config_name = item.get('config_name', '未知')
|
||||
report_date = item.get('report_date', '')
|
||||
ext = os.path.splitext(filename)[1]
|
||||
split_name = item.get('split_name', '')
|
||||
ext = os.path.splitext(filename)[1] or '.xlsx'
|
||||
|
||||
base_parts = [config_name]
|
||||
if report_date:
|
||||
zip_inner_name = f"{config_name}_{report_date}{ext}"
|
||||
else:
|
||||
zip_inner_name = f"{config_name}{ext}"
|
||||
base_parts.append(str(report_date))
|
||||
if split_name:
|
||||
base_parts.append(str(split_name))
|
||||
base_name = '_'.join(base_parts)
|
||||
zip_inner_name = f"{base_name}{ext}"
|
||||
|
||||
# 处理文件名冲突:如果同名,追加记录ID
|
||||
if zip_inner_name in used_names:
|
||||
zip_inner_name = f"{base_name}_{item.get('id', '')}{ext}"
|
||||
used_names.add(zip_inner_name)
|
||||
|
||||
files_to_zip.append({
|
||||
'full_path': full_path,
|
||||
@@ -187,16 +198,23 @@ def batch_download():
|
||||
for file_info in files_to_zip:
|
||||
zf.write(file_info['full_path'], file_info['inner_name'])
|
||||
|
||||
log_info(f'[下载API] 批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)} 个', 'download')
|
||||
log_info(f'[下载API] 批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)} 个,跳过 {skipped_failed} 个失败记录', 'download')
|
||||
if failed_names:
|
||||
log_warning(f'[下载API] 缺失/无效文件列表: {failed_names[:20]}', 'download')
|
||||
|
||||
# 返回zip文件
|
||||
# 返回zip文件,通过响应头返回统计信息
|
||||
from flask import send_file
|
||||
return send_file(
|
||||
response = send_file(
|
||||
zip_path,
|
||||
as_attachment=True,
|
||||
download_name=zip_filename,
|
||||
mimetype='application/zip'
|
||||
)
|
||||
response.headers['X-Total-Matched'] = str(total_matched)
|
||||
response.headers['X-Files-Zipped'] = str(len(files_to_zip))
|
||||
response.headers['X-Files-Missing'] = str(len(failed_names))
|
||||
response.headers['X-Skipped-Failed'] = str(skipped_failed)
|
||||
return response
|
||||
except Exception as e:
|
||||
log_error(f'[下载API] 批量下载错误: {e}', 'download')
|
||||
import traceback
|
||||
@@ -235,9 +253,8 @@ def batch_download_by_filter():
|
||||
where_clauses.append('DATE(end_time) <= %s')
|
||||
params.append(end_date)
|
||||
|
||||
if status is not None and status != '':
|
||||
where_clauses.append('status = %s')
|
||||
params.append(int(status))
|
||||
# 注意:批量导出只导出成功记录(status=1),忽略用户传入的 status 筛选
|
||||
# 因为失败的记录没有文件可下载
|
||||
|
||||
if config_name:
|
||||
where_clauses.append('config_name LIKE %s')
|
||||
@@ -245,9 +262,17 @@ def batch_download_by_filter():
|
||||
|
||||
where_sql = ' AND '.join(where_clauses)
|
||||
|
||||
# 查询所有匹配的成功记录
|
||||
# 先查询总数,用于日志记录
|
||||
count_result = execute_query(
|
||||
f'SELECT COUNT(*) as total FROM t_daily_report_history WHERE {where_sql}',
|
||||
tuple(params)
|
||||
)
|
||||
total_matched = count_result[0]['total'] if count_result else 0
|
||||
log_info(f'[下载API] 按筛选匹配到 {total_matched} 条成功记录', 'download')
|
||||
|
||||
# 查询所有匹配的成功记录(按报表日期正序排列,方便用户查看)
|
||||
result = execute_query(
|
||||
f'SELECT id, file_path, config_name, report_date, status FROM t_daily_report_history WHERE {where_sql} ORDER BY create_time DESC',
|
||||
f'SELECT id, file_path, config_name, report_date, status, split_name FROM t_daily_report_history WHERE {where_sql} ORDER BY report_date ASC, config_name ASC, id ASC',
|
||||
tuple(params)
|
||||
)
|
||||
|
||||
@@ -260,11 +285,12 @@ def batch_download_by_filter():
|
||||
# 收集要打包的文件
|
||||
files_to_zip = []
|
||||
failed_names = []
|
||||
used_names = set() # 用于检测 zip 内文件名冲突
|
||||
|
||||
for item in result:
|
||||
file_path = item.get('file_path')
|
||||
if not file_path:
|
||||
failed_names.append(f"{item.get('config_name', '未知')} (无文件)")
|
||||
failed_names.append(f"{item.get('config_name', '未知')}_{item.get('report_date', '')} (无文件路径)")
|
||||
continue
|
||||
|
||||
# 提取文件名
|
||||
@@ -277,25 +303,40 @@ def batch_download_by_filter():
|
||||
|
||||
full_path = os.path.join(reports_dir, filename)
|
||||
if not os.path.exists(full_path):
|
||||
failed_names.append(f"{item.get('config_name', '未知')} (文件不存在)")
|
||||
failed_names.append(f"{item.get('config_name', '未知')}_{item.get('report_date', '')} (文件不存在)")
|
||||
continue
|
||||
|
||||
# 使用配置名作为zip内的文件名,避免重名
|
||||
# 构建 zip 内文件名:配置名_报表日期(避免重名)
|
||||
config_name_val = item.get('config_name', '未知')
|
||||
report_date = item.get('report_date', '')
|
||||
ext = os.path.splitext(filename)[1]
|
||||
split_name = item.get('split_name', '')
|
||||
ext = os.path.splitext(filename)[1] or '.xlsx'
|
||||
|
||||
# 基础名称:配置名_报表日期
|
||||
base_parts = [config_name_val]
|
||||
if report_date:
|
||||
zip_inner_name = f"{config_name_val}_{report_date}{ext}"
|
||||
else:
|
||||
zip_inner_name = f"{config_name_val}{ext}"
|
||||
base_parts.append(str(report_date))
|
||||
if split_name:
|
||||
base_parts.append(str(split_name))
|
||||
base_name = '_'.join(base_parts)
|
||||
zip_inner_name = f"{base_name}{ext}"
|
||||
|
||||
# 处理文件名冲突:如果同名,追加记录ID
|
||||
if zip_inner_name in used_names:
|
||||
zip_inner_name = f"{base_name}_{item.get('id', '')}{ext}"
|
||||
used_names.add(zip_inner_name)
|
||||
|
||||
files_to_zip.append({
|
||||
'full_path': full_path,
|
||||
'inner_name': zip_inner_name
|
||||
})
|
||||
|
||||
log_info(f'[下载API] 匹配 {total_matched} 条记录,成功打包 {len(files_to_zip)} 个文件,缺失 {len(failed_names)} 个文件', 'download')
|
||||
if failed_names:
|
||||
log_warning(f'[下载API] 缺失的文件列表: {failed_names[:20]}', 'download')
|
||||
|
||||
if len(files_to_zip) == 0:
|
||||
return jsonify({'success': False, 'message': '没有可下载的文件'}), 400
|
||||
return jsonify({'success': False, 'message': '没有可下载的文件(匹配的记录文件均不存在)'}), 400
|
||||
|
||||
# 生成zip文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
@@ -309,16 +350,78 @@ def batch_download_by_filter():
|
||||
|
||||
log_info(f'[下载API] 按筛选批量打包完成,共 {len(files_to_zip)} 个文件,失败 {len(failed_names)} 个', 'download')
|
||||
|
||||
# 返回zip文件
|
||||
# 返回zip文件,通过自定义头部返回统计信息
|
||||
from flask import send_file
|
||||
return send_file(
|
||||
response = send_file(
|
||||
zip_path,
|
||||
as_attachment=True,
|
||||
download_name=zip_filename,
|
||||
mimetype='application/zip'
|
||||
)
|
||||
# 通过响应头返回统计信息,便于前端诊断
|
||||
response.headers['X-Total-Matched'] = str(total_matched)
|
||||
response.headers['X-Files-Zipped'] = str(len(files_to_zip))
|
||||
response.headers['X-Files-Missing'] = str(len(failed_names))
|
||||
return response
|
||||
except Exception as e:
|
||||
log_error(f'[下载API] 按筛选批量下载错误: {e}', 'download')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@download_bp.route('/download/batch-filter-count', methods=['POST'])
|
||||
def batch_download_filter_count():
|
||||
"""按筛选条件统计可导出的记录数
|
||||
在批量导出前调用,让用户确认导出范围。
|
||||
"""
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
config_name = data.get('config_name', '').strip()
|
||||
start_date = data.get('start_date', '').strip()
|
||||
end_date = data.get('end_date', '').strip()
|
||||
|
||||
# 构建查询条件(与批量导出完全一致,只统计成功记录)
|
||||
where_clauses = ['status = 1']
|
||||
params = []
|
||||
|
||||
if start_date:
|
||||
where_clauses.append('DATE(start_time) >= %s')
|
||||
params.append(start_date)
|
||||
|
||||
if end_date:
|
||||
where_clauses.append('DATE(end_time) <= %s')
|
||||
params.append(end_date)
|
||||
|
||||
if config_name:
|
||||
where_clauses.append('config_name LIKE %s')
|
||||
params.append(f'%{config_name}%')
|
||||
|
||||
where_sql = ' AND '.join(where_clauses)
|
||||
|
||||
# 统计总记录数
|
||||
count_result = execute_query(
|
||||
f'SELECT COUNT(*) as total FROM t_daily_report_history WHERE {where_sql}',
|
||||
tuple(params)
|
||||
)
|
||||
total = count_result[0]['total'] if count_result else 0
|
||||
|
||||
# 统计日期范围
|
||||
date_range = execute_query(
|
||||
f'SELECT MIN(report_date) as min_date, MAX(report_date) as max_date FROM t_daily_report_history WHERE {where_sql}',
|
||||
tuple(params)
|
||||
)
|
||||
min_date = date_range[0]['min_date'] if date_range else None
|
||||
max_date = date_range[0]['max_date'] if date_range else None
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'total': total,
|
||||
'min_date': str(min_date) if min_date else '',
|
||||
'max_date': str(max_date) if max_date else ''
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
log_error(f'[下载API] 统计可导出记录数错误: {e}', 'download')
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
@@ -16,59 +16,153 @@ function onHistoryStatusChange() {
|
||||
loadHistoryWithDate(1);
|
||||
}
|
||||
|
||||
// 批量下载历史记录
|
||||
// 统一的批量下载:勾选时下载选中记录,未勾选时按筛选条件下载全部匹配记录
|
||||
async function batchDownloadHistory() {
|
||||
// 同步当前筛选条件
|
||||
const configNameInput = document.getElementById('history-config-name');
|
||||
if (configNameInput) {
|
||||
historyConfigName = configNameInput.value.trim();
|
||||
}
|
||||
historyStatus = document.getElementById('history-status-filter').value;
|
||||
historyStartDate = document.getElementById('history-start-date').value;
|
||||
historyEndDate = document.getElementById('history-end-date').value;
|
||||
|
||||
const checkboxes = document.querySelectorAll('.history-checkbox:checked');
|
||||
if (checkboxes.length === 0) {
|
||||
alert('请至少选择一条记录');
|
||||
const hasSelection = checkboxes.length > 0;
|
||||
|
||||
if (hasSelection) {
|
||||
// 模式1:下载勾选的记录
|
||||
const ids = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
const failedCount = Array.from(checkboxes).filter(cb => cb.dataset.status !== '1').length;
|
||||
const successCount = checkboxes.length - failedCount;
|
||||
|
||||
if (successCount === 0) {
|
||||
alert('选中的记录都是生成失败的,无法下载');
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
if (!confirm(`选中的 ${checkboxes.length} 条记录中有 ${failedCount} 条是生成失败的,无法下载。\n是否打包下载成功的 ${successCount} 条?`)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!confirm(`确定要下载选中的 ${successCount} 条报表吗?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/download/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: ids })
|
||||
});
|
||||
await handleDownloadResponse(response);
|
||||
} catch (error) {
|
||||
alert('下载失败: ' + error.message);
|
||||
}
|
||||
} else {
|
||||
// 模式2:按筛选条件下载全部匹配的成功记录
|
||||
const filterDescParts = [];
|
||||
if (historyConfigName) filterDescParts.push(`配置名称含"${historyConfigName}"`);
|
||||
if (historyStartDate) filterDescParts.push(`开始日期 ${historyStartDate}`);
|
||||
if (historyEndDate) filterDescParts.push(`结束日期 ${historyEndDate}`);
|
||||
const filterDesc = filterDescParts.length > 0 ? filterDescParts.join(',') : '全部记录';
|
||||
|
||||
// 查询匹配数量
|
||||
let matchCount = 0;
|
||||
let dateRange = '';
|
||||
try {
|
||||
const countResponse = await fetch('/api/download/batch-filter-count', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config_name: historyConfigName,
|
||||
start_date: historyStartDate,
|
||||
end_date: historyEndDate
|
||||
})
|
||||
});
|
||||
const countResult = await countResponse.json();
|
||||
if (countResult.success) {
|
||||
matchCount = countResult.data.total || 0;
|
||||
if (countResult.data.min_date && countResult.data.max_date) {
|
||||
dateRange = `\n报表日期范围: ${countResult.data.min_date} 至 ${countResult.data.max_date}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('查询导出数量失败:', e);
|
||||
}
|
||||
|
||||
if (matchCount === 0) {
|
||||
alert(`没有符合条件的成功记录可导出。\n筛选条件: ${filterDesc}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`未选中任何记录,将按当前筛选条件导出全部匹配的成功记录。\n\n筛选条件: ${filterDesc}\n匹配记录数: ${matchCount} 条${dateRange}\n\n注意: 仅导出生成成功的报表。\n是否继续?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/download/batch-filter', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config_name: historyConfigName,
|
||||
start_date: historyStartDate,
|
||||
end_date: historyEndDate,
|
||||
status: historyStatus
|
||||
})
|
||||
});
|
||||
await handleDownloadResponse(response);
|
||||
} catch (error) {
|
||||
alert('下载失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理下载响应(两种模式共用)
|
||||
async function handleDownloadResponse(response) {
|
||||
if (!response.ok) {
|
||||
try {
|
||||
const result = await response.json();
|
||||
alert('下载失败: ' + (result.message || '未知错误'));
|
||||
} catch (e) {
|
||||
alert('下载失败: 服务器返回错误');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||||
|
||||
// 检查是否有生成失败的记录
|
||||
const failedCount = Array.from(checkboxes).filter(cb => cb.dataset.status !== '1').length;
|
||||
const successCount = checkboxes.length - failedCount;
|
||||
|
||||
if (successCount === 0) {
|
||||
alert('选中的记录都是生成失败的,无法下载');
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
if (!confirm(`选中的 ${checkboxes.length} 条记录中有 ${failedCount} 条是生成失败的,无法下载。\n是否打包下载成功的 ${successCount} 条?`)) {
|
||||
return;
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '');
|
||||
a.download = `批量下载_${timestamp}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
// 显示统计信息(如果有)
|
||||
const totalMatched = response.headers.get('X-Total-Matched');
|
||||
const filesZipped = response.headers.get('X-Files-Zipped');
|
||||
const filesMissing = response.headers.get('X-Files-Missing');
|
||||
const skippedFailed = response.headers.get('X-Skipped-Failed');
|
||||
if (totalMatched && filesZipped) {
|
||||
const missing = filesMissing ? parseInt(filesMissing) : 0;
|
||||
const skipped = skippedFailed ? parseInt(skippedFailed) : 0;
|
||||
let msg = `下载完成!\n`;
|
||||
msg += `匹配记录: ${totalMatched} 条\n`;
|
||||
msg += `成功打包: ${filesZipped} 个文件`;
|
||||
if (missing > 0) {
|
||||
msg += `\n缺失文件: ${filesMissing} 个(历史文件可能已被清理)`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/download/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: ids })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '');
|
||||
a.download = `批量下载_${timestamp}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} else {
|
||||
try {
|
||||
const result = await response.json();
|
||||
alert('下载失败: ' + (result.message || '未知错误'));
|
||||
} catch (e) {
|
||||
alert('下载失败: 服务器返回错误');
|
||||
}
|
||||
if (skipped > 0) {
|
||||
msg += `\n跳过失败记录: ${skipped} 个`;
|
||||
}
|
||||
if (missing > 0 || skipped > 0) {
|
||||
alert(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('下载失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,69 +469,5 @@ async function batchDeleteHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导出当前筛选条件下的所有记录(与搜索条件同步)
|
||||
async function batchDownloadFilteredHistory() {
|
||||
// 同步当前筛选条件
|
||||
const configNameInput = document.getElementById('history-config-name');
|
||||
if (configNameInput) {
|
||||
historyConfigName = configNameInput.value.trim();
|
||||
}
|
||||
historyStatus = document.getElementById('history-status-filter').value;
|
||||
historyStartDate = document.getElementById('history-start-date').value;
|
||||
historyEndDate = document.getElementById('history-end-date').value;
|
||||
|
||||
// 构建筛选条件描述
|
||||
const filterDescParts = [];
|
||||
if (historyStatus) {
|
||||
filterDescParts.push(historyStatus === '1' ? '成功' : '失败');
|
||||
}
|
||||
if (historyConfigName) {
|
||||
filterDescParts.push(`配置名称含"${historyConfigName}"`);
|
||||
}
|
||||
if (historyStartDate) {
|
||||
filterDescParts.push(`开始日期 ${historyStartDate}`);
|
||||
}
|
||||
if (historyEndDate) {
|
||||
filterDescParts.push(`结束日期 ${historyEndDate}`);
|
||||
}
|
||||
const filterDesc = filterDescParts.length > 0 ? filterDescParts.join(',') : '全部记录';
|
||||
|
||||
if (!confirm(`将导出符合当前筛选条件的所有成功记录(${filterDesc})。\n注意:仅导出生成成功的报表。\n是否继续?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/download/batch-filter', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config_name: historyConfigName,
|
||||
start_date: historyStartDate,
|
||||
end_date: historyEndDate,
|
||||
status: historyStatus
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '');
|
||||
a.download = `批量导出_${timestamp}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} else {
|
||||
try {
|
||||
const result = await response.json();
|
||||
alert('导出失败: ' + (result.message || '未知错误'));
|
||||
} catch (e) {
|
||||
alert('导出失败: 服务器返回错误');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('导出失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
// 批量导出当前筛选条件下的所有记录 - 已合并到 batchDownloadHistory 函数
|
||||
// 该函数已被移除,功能整合到统一的 batchDownloadHistory 中
|
||||
|
||||
@@ -140,8 +140,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-danger" onclick="batchDeleteHistory()">批量删除</button>
|
||||
<button class="btn btn-danger" onclick="batchDownloadHistory()">批量下载</button>
|
||||
<button class="btn btn-success" onclick="batchDownloadFilteredHistory()">批量导出(当前筛选)</button>
|
||||
<button class="btn btn-success" onclick="batchDownloadHistory()">批量下载</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
|
||||
Reference in New Issue
Block a user