优化前端

This commit is contained in:
Jimmy
2026-05-07 15:18:30 +08:00
parent 5c7e489e1c
commit 031ec4d289
5 changed files with 377 additions and 92 deletions

View File

@@ -19,6 +19,16 @@ SAVE_PATH = OUTPUT_DIR / "receipt_details_full_clean.json"
def fetch_receipt_details_full():
log("INFO", "=== 🚚 启动收货明细报表全量抓取 (精简字段模式) ===")
page = get_page(port=9222)
# 尝试加载已有的存档,实现真正的断点累加
all_clean_items = []
if SAVE_PATH.exists():
try:
with open(SAVE_PATH, "r", encoding="utf-8") as f:
all_clean_items = json.load(f)
log("INFO", f"📦 已加载本地历史存档,包含 {len(all_clean_items)} 条数据。")
except Exception as e:
log("WARN", f"加载本地存档失败: {e},将从空列表开始。")
all_clean_items = []
try:
@@ -75,16 +85,80 @@ def fetch_receipt_details_full():
return
# =========================================================
# 第一页数据处理
# 第一页数据处理 (如果触发断点,则忽略第一页数据)
# =========================================================
log("OK", f"🎉 成功拦截到第一页数据HTTP: {packet.response.status}")
body = packet.response.body
data = body if isinstance(body, (dict, list)) else json.loads(body)
# 设定开始抓取的页码1表示从头开始抓全量数据
target_resume_page = 690
total_count = 0
if isinstance(data, dict) and "result" in data:
total_count = data["result"].get("totalCount", 0)
items = data["result"].get("items", [])
# 只有当不是断点续传即从第1页开始才把第一页的数据加入列表
if target_resume_page <= 1:
for item in items:
all_clean_items.append({
"采购订单号": item.get("purchaseOrderCode"),
"行号": item.get("rowsNum"),
"物料代码": item.get("materialCode"),
"物料名称": item.get("materialName"),
"物料规格": item.get("materialSpecification"),
"仓库代码": item.get("warehouseCode"),
"仓库名称": item.get("warehouseName"),
"供应商代码": item.get("supplierCode"),
"供应商名称": item.get("supplierName"),
"单位名称": item.get("unitName"),
"转换单位": item.get("convertUnitName"),
"收货单价": item.get("receivePrice"),
"收货时间": item.get("receiptTime"),
"进货数量": item.get("plannedPurchaseQuantity"),
"收货数量": item.get("convertGoodsQuantity") if item.get("convertGoodsQuantity") is not None else item.get("goodsQuantity"),
"收货总金额": item.get("receiveAmount")
})
log("OK", f"第一页清洗完成,提取了 {len(items)} 条数据。后端报告总条数: {total_count}")
else:
log("INFO", f"触发断点续传,跳过第一页的数据保存。后端报告总条数: {total_count}")
page_num = 1
# =========================================================
# 断点续传逻辑 (由于刚才中断在 711 页,我们需要跳到 712 页继续)
# =========================================================
if target_resume_page > 1:
log("INFO", f"🚀 触发断点续传机制!准备直接跳转到第 {target_resume_page} 页...")
# 尝试找页码输入框
jumper_input_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/span[3]/div/div//input'
input_ele = page.ele(jumper_input_xpath, timeout=5)
if not input_ele:
jumper_input_xpath = 'xpath://input[@type="number" and @aria-label=""]'
input_ele = page.ele(jumper_input_xpath, timeout=5)
if input_ele:
input_ele.clear()
input_ele.input(str(target_resume_page))
time.sleep(0.5)
input_ele.input('\n')
packet = page.listen.wait(timeout=15)
if not packet:
log("ERR", "断点跳转失败,未拦截到目标页的数据请求。")
return
log("OK", f"✅ 成功跳转至第 {target_resume_page} 页并截获数据!")
page_num = target_resume_page
# 读取并解析第 191 页的数据
body = packet.response.body
data = body if isinstance(body, (dict, list)) else json.loads(body)
if isinstance(data, dict) and "result" in data:
items = data["result"].get("items", [])
for item in items:
all_clean_items.append({
"采购订单号": item.get("purchaseOrderCode"),
@@ -100,18 +174,18 @@ def fetch_receipt_details_full():
"转换单位": item.get("convertUnitName"),
"收货单价": item.get("receivePrice"),
"收货时间": item.get("receiptTime"),
"进货数量": item.get("convertPlannedPurchaseQuantity") if item.get("convertPlannedPurchaseQuantity") is not None else item.get("plannedPurchaseQuantity"),
"进货数量": item.get("plannedPurchaseQuantity"),
"收货数量": item.get("convertGoodsQuantity") if item.get("convertGoodsQuantity") is not None else item.get("goodsQuantity"),
"收货总金额": item.get("receiveAmount")
})
log("OK", f"页清洗完成,提取 {len(items)} 条数据。后端报告总条数: {total_count}")
log("OK", f" {page_num} 页清洗完成,累计提取 {len(all_clean_items)} 条数据。")
else:
log("ERR", "找不到页码输入框,断点跳转失败,将从第 1 页继续!")
page_num = 1
# =========================================================
# 循环翻页抓取
# =========================================================
next_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/button[2]'
while True:
# 引入“类人”随机延迟2.5 秒到 5.5 秒之间随机)
@@ -125,9 +199,30 @@ def fetch_receipt_details_full():
log("INFO", f"☕️ 已经连续高强度翻了 {page_num} 页,触发风控规避机制,假装喝水休息 {long_delay:.2f} 秒...")
time.sleep(long_delay)
next_btn = page.ele(next_btn_xpath, timeout=5)
# 兼容多种 ElementUI 翻页按钮的特征
# 为了防止由于网络延迟导致的 DOM 元素短暂消失,我们加入重试机制
next_btn = None
for _ in range(3):
next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=3)
if next_btn:
break
time.sleep(1)
# 【修复】当跳页页数大于 400 页时,某些页面的 ElementUI 分页组件会为了节省 DOM 而卸载 next_btn
# 或者被包裹在隐藏容器里。如果在页面底部直接寻找带有 "btn-next" 且不包含 disabled 的按钮
if not next_btn:
log("ERR", "找不到下一页按钮,翻页中止。")
# 尝试备用定位方式:直接找右箭头图标所在的按钮
next_btn = page.ele('xpath://i[contains(@class, "el-icon-arrow-right")]/parent::button', timeout=3)
if not next_btn:
log("ERR", "重试 3 次后仍然找不到下一页按钮,可能是页面崩溃或会话超时,尝试强制刷新页面...")
page.refresh()
page.wait.load_start()
time.sleep(5)
# 刷新后尝试重新找一次
next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=5)
if not next_btn:
log("ERR", "刷新后依然找不到下一页按钮,彻底中止。")
break
# 检查按钮是否被禁用
@@ -212,6 +307,13 @@ def fetch_receipt_details_full():
with open(rescue_path, "w", encoding="utf-8") as f:
json.dump(all_clean_items, f, ensure_ascii=False, indent=2)
log("INFO", f"🆘 触发异常保存,抢救了 {len(all_clean_items)} 条数据。")
finally:
# 无论脚本正常结束还是异常退出,都强制停止监听,防止成为僵尸爬虫
try:
page.listen.stop()
log("INFO", "🛑 已释放浏览器监听资源。")
except:
pass
if __name__ == "__main__":
fetch_receipt_details_full()

View File

@@ -224,8 +224,17 @@ def fetch_receipt_details_incremental():
log("INFO", f"⏳ 停顿 {delay:.2f} 秒后点击下一页...")
time.sleep(delay)
next_btn_xpath = 'xpath://*[@id="app"]/div/div[1]/div[2]/div[2]/div[1]/div[2]/div/div[2]/div[1]/button[2]'
next_btn = page.ele(next_btn_xpath, timeout=5)
# 同步全量脚本的优化:重试机制与兼容的类名匹配
next_btn = None
for _ in range(3):
next_btn = page.ele('xpath://button[contains(@class, "btn-next")]', timeout=3)
if next_btn:
break
time.sleep(1)
# 备用定位方式:直接找右箭头图标所在的按钮
if not next_btn:
next_btn = page.ele('xpath://i[contains(@class, "el-icon-arrow-right")]/parent::button', timeout=3)
if next_btn:
try: next_btn.click()
@@ -236,7 +245,7 @@ def fetch_receipt_details_incremental():
log("ERR", f"{current_page + 1} 页请求超时!")
break
else:
log("ERR", "找不到下一页按钮!")
log("ERR", "重试 3 次后仍然找不到下一页按钮!")
break
current_page += 1
@@ -246,8 +255,13 @@ def fetch_receipt_details_incremental():
except Exception as e:
log("ERR", f"发生全局异常: {e}")
finally:
if 'conn' in locals() and conn:
conn.close()
if 'page' in locals() and page:
try:
page.listen.stop()
except Exception:
pass
if __name__ == "__main__":
fetch_receipt_details_incremental()

Binary file not shown.

View File

@@ -149,8 +149,6 @@ def get_receipts():
def sync_receipts():
"""触发后台运行收货明细增量抓取脚本(修复跨机器路径问题)"""
import sys
import io
import contextlib
if str(BROWSER_LOGIN_DIR) not in sys.path:
sys.path.insert(0, str(BROWSER_LOGIN_DIR))
@@ -158,16 +156,13 @@ def sync_receipts():
try:
from fetch_receipt_details_incremental import fetch_receipt_details_incremental
# 捕获函数内部的 print/log 输出
f = io.StringIO()
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
fetch_receipt_details_incremental()
# 将长时间运行的抓取任务放入后台线程,防止前端请求超时
threading.Thread(target=fetch_receipt_details_incremental, daemon=True).start()
logs = f.getvalue()
return jsonify({
"success": True,
"message": "增量同步执行完成",
"logs": logs
"message": "增量同步任务已在后台启动!请观察黑框控制台的运行日志。",
"logs": "任务已在后台运行..."
})
except ImportError:
return jsonify({"success": False, "message": "找不到增量抓取脚本或导入失败"}), 404
@@ -178,8 +173,6 @@ def sync_receipts():
def sync_bom():
"""触发后台运行 BOM 成本全量抓取脚本(修复跨机器路径问题)"""
import sys
import io
import contextlib
if str(BROWSER_LOGIN_DIR) not in sys.path:
sys.path.insert(0, str(BROWSER_LOGIN_DIR))
@@ -187,16 +180,13 @@ def sync_bom():
try:
from fetch_bom_cost_full_tree import fetch_bom_cost_tree
# 捕获函数内部的 print/log 输出
f = io.StringIO()
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
fetch_bom_cost_tree()
# 将长时间运行的抓取任务放入后台线程,防止前端请求超时
threading.Thread(target=fetch_bom_cost_tree, daemon=True).start()
logs = f.getvalue()
return jsonify({
"success": True,
"message": "BOM 同步执行完成",
"logs": logs
"message": "BOM 树抓取任务已在后台启动!预计耗时 10-20 分钟,请观察黑框控制台的运行日志。",
"logs": "任务已在后台运行..."
})
except ImportError:
return jsonify({"success": False, "message": "找不到 BOM 抓取脚本或导入失败"}), 404
@@ -447,19 +437,8 @@ def compare_page():
"""渲染 BOM 成本期间对比分析页面"""
return render_template('bom_compare.html')
@app.route('/api/bom_tree_compare/<parent_code>')
def get_bom_tree_compare(parent_code):
"""
为成本对比页面提供带时间段价格分析的 BOM 树数据。
前端需要传入两个时间段参数:
start_a, end_a (期间 A)
start_b, end_b (期间 B)
"""
start_a = request.args.get('start_a')
end_a = request.args.get('end_a')
start_b = request.args.get('start_b')
end_b = request.args.get('end_b')
def build_bom_compare_tree(parent_code, start_a, end_a, start_b, end_b):
"""构建用于成本对比分析的 BOM 树数据"""
conn = get_db_connection()
# 1. 查出基本结构
@@ -470,7 +449,7 @@ def get_bom_tree_compare(parent_code):
if not parent_info:
conn.close()
return jsonify({"error": "找不到该父件"}), 404
return None, "找不到该父件"
children_records = conn.execute(
'SELECT id, node_material_code, node_material_name, parent_node_id, bom_level, usage_qty FROM bom_child WHERE parent_material_code = ?',
@@ -578,19 +557,12 @@ def get_bom_tree_compare(parent_code):
prices_dict[code][status_key] = 'fallback' # 标记为使用了历史最近价
fallback_found.add(code)
# 3. 如果连期间之前的历史记录都没有,但它有基准价(说明是在期间之后才采购的),也算是 fallback 还是 missing
# 我们之前定义的是“无任何历史记录”才标红。所以如果它有最新基准价latest_price > 0
# 说明这东西买过,只是在 start_date 之前没买过。我们不应该把它标红missing而应该标为普通的没价格或者某种特殊颜色。
# 但为了严谨:既然我们在 start_date 之前没买过,意味着期初无库存/无价格,期间也没买,那这期间的价格只能是 0。
# 并且如果 latest_price > 0说明它并非“历史从未采购”我们把它标为 fallback 但价格是 0。
for code in missing_codes:
if code not in fallback_found:
prices_dict[code][period_key] = 0.0
if prices_dict[code].get('latest_price', 0) > 0:
# 有基准价,但在该期间和该期间之前都没买过,这不算“从未采购过”
prices_dict[code][status_key] = 'fallback'
else:
# 彻底查不到任何记录,才是真正的 missing
prices_dict[code][status_key] = 'missing'
# --- B. 处理期间 A ---
@@ -655,19 +627,11 @@ def get_bom_tree_compare(parent_code):
has_children = bool(node.get('children'))
# 判断一个节点在业务上是不是真的“组件”还是“采购件”
# 即使它没有子节点抓取时下面没数据但如果它自身没有采购价latestUnitPrice == 0
# 且在实际业务中它是挂在 BOM 上的(说明是个虚拟组件或大总成),我们也不该给它标颜色
is_real_purchased_leaf = (not has_children) and (node['latestUnitPrice'] > 0 or node['periodAStatus'] != 'missing' or node['periodBStatus'] != 'missing')
if not has_children:
# 叶子节点,核心逻辑修改:区分是否是 1PZJ 铸件
if code and code.startswith('1PZJ') and node.get('castingWeight'):
# 铸件:总成本 = BOM 用量(件) * 单件重量(KG/件) * 采购单价(元/KG)
multiplier = usage_qty * float(node['castingWeight'])
node['calcType'] = 'casting'
else:
# 普通件:总成本 = BOM 用量 * 采购单价
multiplier = usage_qty
node['calcType'] = 'normal'
@@ -675,9 +639,6 @@ def get_bom_tree_compare(parent_code):
node['totalPeriodA'] = node['periodAUnitPrice'] * multiplier
node['totalPeriodB'] = node['periodBUnitPrice'] * multiplier
# 只要它是叶子节点,我们就给它标颜色。
# 如果它真的是虚拟件(基准价是 0且 A B 都是 missing那就大大方方给它标红点
# 用户刚才反馈说有的子件有基准价但是连点都没标,就是因为之前那个 is_real_purchased_leaf 过滤得太严格或者有 Bug。
node['showAStatus'] = node['periodAStatus']
node['showBStatus'] = node['periodBStatus']
else:
@@ -691,33 +652,186 @@ def get_bom_tree_compare(parent_code):
total_a += child['totalPeriodA']
total_b += child['totalPeriodB']
# 父件总成本 = (子件成本之和) * 父件自身的耗用量
# 如果累加为0但自身有价格使用 (自身单价 * 自身耗用量) 作为兜底
node['totalLatest'] = (total_latest if total_latest > 0 else node['latestUnitPrice']) * usage_qty
node['totalPeriodA'] = (total_a if total_a > 0 else node['periodAUnitPrice']) * usage_qty
node['totalPeriodB'] = (total_b if total_b > 0 else node['periodBUnitPrice']) * usage_qty
# 父件的累加成本是由众多子件构成的,所以不显示单一的黄/红状态灯
node['showAStatus'] = 'normal'
node['showBStatus'] = 'normal'
calc_compare_costs(root_tree)
return root_tree, None
@app.route('/api/bom_tree_compare/<parent_code>')
def get_bom_tree_compare(parent_code):
"""
为成本对比页面提供带时间段价格分析的 BOM 树数据。
"""
start_a = request.args.get('start_a')
end_a = request.args.get('end_a')
start_b = request.args.get('start_b')
end_b = request.args.get('end_b')
root_tree, error = build_bom_compare_tree(parent_code, start_a, end_a, start_b, end_b)
if error:
return jsonify({"error": error}), 404
# 为了配合 ElementUI 的树形表格,根节点必须包装在数组里
return jsonify([root_tree])
def open_browser():
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from flask import send_file
@app.route('/api/export_compare/<parent_code>')
def export_compare(parent_code):
start_a = request.args.get('start_a')
end_a = request.args.get('end_a')
start_b = request.args.get('start_b')
end_b = request.args.get('end_b')
root_tree, error = build_bom_compare_tree(parent_code, start_a, end_a, start_b, end_b)
if error:
return jsonify({"error": error}), 404
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "BOM成本对比"
# 定义样式
header_fill = PatternFill(start_color="409EFF", end_color="409EFF", fill_type="solid")
header_font = Font(color="FFFFFF", bold=True)
center_align = Alignment(horizontal="center", vertical="center")
border = Border(left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin'))
headers = [
"物料代码", "物料名称", "BOM层级", "用量", "单位",
"最新单价", "最新总成本",
f"期间A单价 ({start_a or ''}{end_a or ''})", f"期间A总成本",
f"期间B单价 ({start_b or ''}{end_b or ''})", f"期间B总成本",
"单价差异 (B-A)", "总成本差异 (B-A)"
]
ws.append(headers)
for col_idx, cell in enumerate(ws[1], 1):
cell.fill = header_fill
cell.font = header_font
cell.alignment = center_align
cell.border = border
ws.column_dimensions[get_column_letter(col_idx)].width = 15
ws.column_dimensions['A'].width = 18
ws.column_dimensions['B'].width = 30
ws.column_dimensions['H'].width = 25
ws.column_dimensions['J'].width = 25
def write_node_to_excel(node, level=0):
# 差异计算逻辑(同前端)
period_a_unit = node.get('periodAUnitPrice', 0) or 0
period_b_unit = node.get('periodBUnitPrice', 0) or 0
diff_unit = period_b_unit - period_a_unit
period_a_total = node.get('totalPeriodA', 0) or 0
period_b_total = node.get('totalPeriodB', 0) or 0
diff_total = period_b_total - period_a_total
prefix = " " * level
row_data = [
node.get('materialCode', ''),
prefix + node.get('materialName', ''),
node.get('bomLevel', ''),
node.get('usageQty', 1),
node.get('unitName', ''),
node.get('latestUnitPrice', 0),
node.get('totalLatest', 0),
period_a_unit,
period_a_total,
period_b_unit,
period_b_total,
diff_unit,
diff_total
]
ws.append(row_data)
current_row = ws.max_row
# 简单格式化
for col_idx, cell in enumerate(ws[current_row], 1):
cell.border = border
if col_idx in [6, 7, 8, 9, 10, 11, 12, 13]:
cell.number_format = '0.00'
# 递归子节点
if node.get('children'):
for child in node['children']:
write_node_to_excel(child, level + 1)
write_node_to_excel(root_tree)
output = io.BytesIO()
wb.save(output)
output.seek(0)
filename = f"BOM成本对比_{parent_code}.xlsx"
return send_file(
output,
as_attachment=True,
download_name=filename,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
def open_browser(port):
"""延迟 1.5 秒后自动打开系统默认浏览器"""
time.sleep(1.5)
url = "http://127.0.0.1:5050"
url = f"http://127.0.0.1:{port}"
print(f"🚀 正在自动打开浏览器: {url}")
webbrowser.open(url)
def find_free_port(start_port=5050, max_port=5100):
"""自动寻找未被占用的端口,避免 Windows 10013 端口被拒错误"""
import socket
for port in range(start_port, max_port):
# 尝试通过创建 socket 并监听来确认端口是否真的可用
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 开启端口复用,避免 TIME_WAIT 状态导致误判
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
# 绑定到 127.0.0.1 而不是 0.0.0.0
s.bind(('127.0.0.1', port))
return port
except OSError:
continue
# 如果 5050-5100 都被占用或被拦截,尝试让系统随机分配一个端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('127.0.0.1', 0))
return s.getsockname()[1]
if __name__ == '__main__':
# 动态获取一个可用端口
try:
port = find_free_port()
except Exception as e:
print(f"⚠️ 无法自动获取端口,回退到默认端口: {e}")
port = 5050
# 启动前开启一个线程去拉起浏览器
threading.Thread(target=open_browser, daemon=True).start()
threading.Thread(target=open_browser, args=(port,), daemon=True).start()
# 启动后端服务
print("🚀 前端展示后端服务已启动!请在浏览器访问: http://127.0.0.1:5050")
# 更改默认端口为 5050避开 macOS 控制中心的 5000 端口占用
app.run(debug=False, host='0.0.0.0', port=5050)
print(f"🚀 前端展示后端服务已启动!请在浏览器访问: http://127.0.0.1:{port}")
# 智能判断:如果是通过 PyInstaller 打包运行的,则关闭热加载以避免多进程冲突和双开浏览器
is_frozen = getattr(sys, 'frozen', False)
# 更改为动态端口,避开被占用的端口。修改 host 为 127.0.0.1 避免 Windows 权限拦截
app.run(
debug=not is_frozen,
host='127.0.0.1',
port=port,
threaded=True,
use_reloader=not is_frozen
)

View File

@@ -211,7 +211,7 @@
<div class="toolbar" v-if="currentParentCode">
<!-- 第一行:期间选择 -->
<el-row :gutter="20" type="flex" align="middle" style="flex-wrap: wrap; margin-bottom: 10px;">
<el-col :span="11">
<el-col :span="10">
<span style="font-size: 14px; font-weight: bold; margin-right: 10px;">期间 A (基准期)</span>
<el-date-picker
v-model="periodA_start"
@@ -232,7 +232,7 @@
</el-date-picker>
</el-col>
<el-col :span="11">
<el-col :span="10">
<span style="font-size: 14px; font-weight: bold; margin-right: 10px;">期间 B (对比期)</span>
<el-date-picker
v-model="periodB_start"
@@ -252,8 +252,9 @@
style="width: 140px;">
</el-date-picker>
</el-col>
<el-col :span="2">
<el-col :span="4" style="display: flex; gap: 10px;">
<el-button type="primary" size="small" icon="el-icon-search" @click="fetchTreeData" :loading="loadingData">执行对比</el-button>
<el-button type="success" size="small" icon="el-icon-download" @click="exportExcel" :loading="exporting">导出 Excel</el-button>
</el-col>
</el-row>
@@ -418,6 +419,7 @@
parents: [],
loadingParents: false,
loadingData: false,
exporting: false,
currentParentCode: null,
tableData: [],
originalTableData: [],
@@ -562,6 +564,59 @@
};
this.tableData = filterInvisibleNodes(newData);
},
exportExcel() {
if (!this.currentParentCode) {
this.$message.warning('请先选择一个成品父件并执行对比');
return;
}
if (!this.periodA_start || !this.periodA_end || !this.periodB_start || !this.periodB_end) {
this.$message.warning('请完整选择期间A和期间B的时间范围');
return;
}
this.exporting = true;
const params = new URLSearchParams({
start_a: this.periodA_start,
end_a: this.periodA_end,
start_b: this.periodB_start,
end_b: this.periodB_end
});
axios({
url: `/api/export_compare/${this.currentParentCode}?${params.toString()}`,
method: 'GET',
responseType: 'blob' // 重要:设置响应类型为 blob
})
.then(response => {
// 创建下载链接
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
// 从响应头中获取文件名,如果没有则使用默认文件名
const contentDisposition = response.headers['content-disposition'];
let fileName = `BOM成本对比_${this.currentParentCode}.xlsx`;
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (fileNameMatch && fileNameMatch.length === 2) {
fileName = decodeURIComponent(fileNameMatch[1]);
}
}
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
this.exporting = false;
this.$message.success('导出成功');
})
.catch(error => {
this.exporting = false;
this.$message.error('导出失败,请重试');
});
}
}
})