fix:修复无法导出账单pdf

This commit is contained in:
2026-07-16 11:25:59 +08:00
parent fa086e4c1c
commit 2beedc22af
2 changed files with 128 additions and 4 deletions

View File

@@ -0,0 +1,110 @@
interface BillPrintItem {
expenseType?: string | null;
description?: string | null;
days?: number | null;
totalRoomDays?: number | null;
studentAmount?: number | string | null;
}
export interface BillPrintData {
id: number;
student?: { name?: string | null } | null;
periodStart?: string | null;
periodEnd?: string | null;
status?: string | null;
sharedAmount?: number | string | null;
personalAmount?: number | string | null;
totalAmount?: number | string | null;
paidAmount?: number | string | null;
outstandingAmount?: number | string | null;
items?: BillPrintItem[] | null;
}
const statusLabels: Record<string, string> = {
unpaid: '待支付',
partially_paid: '部分支付',
paid: '已结清',
cancelled: '已取消',
};
const escapeHtml = (value: unknown) =>
String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
const money = (value: unknown) => `¥${Number(value || 0).toFixed(2)}`;
export const buildBillPrintHtml = (bill: BillPrintData) => {
const rows = (bill.items ?? [])
.map(
(item) => `
<tr>
<td>${escapeHtml(item.expenseType || '')}</td>
<td>${escapeHtml(item.description || '')}</td>
<td>${Number(item.days || 0)}</td>
<td>${Number(item.totalRoomDays || 0)}</td>
<td>${Number(item.studentAmount || 0).toFixed(2)}</td>
</tr>`,
)
.join('');
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>账单_${bill.id}</title>
<style>
@page { size: A4; margin: 50pt; }
* { box-sizing: border-box; }
body {
margin: 0;
color: #000;
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
font-size: 12px;
line-height: 1.5;
}
h1 { margin: 0; font-size: 20px; font-weight: 600; text-align: center; }
h2 { margin: 20px 0 6px; font-size: 14px; text-decoration: underline; }
p { margin: 2px 0; }
.generated { margin: 6px 0 18px; color: #666; font-size: 10px; text-align: center; }
.total { color: #007aff; font-size: 14px; font-weight: 600; }
.paid { color: #389e0d; font-size: 11px; }
.outstanding { color: ${Number(bill.outstandingAmount || 0) > 0 ? '#ff3b30' : '#389e0d'}; font-size: 14px; font-weight: 600; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 9px; }
th, td { padding: 4px 0; text-align: left; vertical-align: top; word-break: break-word; }
th { color: #333; font-size: 10px; font-weight: 400; border-bottom: 1px solid #ccc; }
th:nth-child(1) { width: 24%; }
th:nth-child(2) { width: 36%; }
th:nth-child(3), th:nth-child(4) { width: 12%; }
th:nth-child(5) { width: 16%; }
.empty { padding: 12px 0; color: #999; text-align: center; }
.footer { margin-top: 34px; color: #999; font-size: 8px; text-align: center; }
</style>
</head>
<body>
<h1>恭学教育基地水电费账单</h1>
<div class="generated">生成时间: ${escapeHtml(new Date().toLocaleString('zh-CN'))}</div>
<p>学生姓名: ${escapeHtml(bill.student?.name || '-')}</p>
<p>计费周期: ${escapeHtml(bill.periodStart || '-')} ~ ${escapeHtml(bill.periodEnd || '-')}</p>
<p>账单状态: ${escapeHtml(statusLabels[bill.status || ''] || bill.status || '-')}</p>
<h2>费用汇总</h2>
<p>分摊费用: ${money(bill.sharedAmount)}</p>
<p>个人费用: ${money(bill.personalAmount)}</p>
<p class="total">应付总额: ${money(bill.totalAmount)}</p>
<p class="paid">已扣余额: ${money(bill.paidAmount)}</p>
<p class="outstanding">待补缴: ${money(bill.outstandingAmount)}</p>
<h2>费用明细</h2>
<table>
<thead><tr><th>费用类型</th><th>说明</th><th>天数</th><th>总人天</th><th>金额(元)</th></tr></thead>
<tbody>${rows || '<tr><td class="empty" colspan="5">暂无费用明细</td></tr>'}</tbody>
</table>
<div class="footer">本账单由恭学教育基地管理系统自动生成</div>
<script>window.addEventListener('load', function () { setTimeout(function () { window.print(); }, 100); });</script>
</body>
</html>`;
};

View File

@@ -24,6 +24,7 @@ import api from '../../api';
import PermissionButton from '../../components/PermissionButton';
import { downloadBlob } from '../../utils/download';
import { message } from '../../ui/app-message';
import { buildBillPrintHtml, type BillPrintData } from './bill-print';
const statusMap: Record<string, { text: string; color: string }> = {
@@ -165,10 +166,23 @@ const BillsPage: React.FC = () => {
);
};
const handleExportPdf = (billId: number) => {
downloadBlob(`/bills/export/pdf/${billId}`, `账单_${billId}.pdf`).catch(() =>
message.error('导出失败'),
);
const handleExportPdf = async (billId: number) => {
const printWindow = window.open('', '_blank');
if (!printWindow) {
message.error('浏览器阻止了打印窗口,请允许弹出窗口后重试');
return;
}
printWindow.document.write('<p style="font-family:sans-serif;padding:24px">正在加载账单...</p>');
try {
const bill = await api.get<BillPrintData>(`/bills/${billId}`);
printWindow.document.open();
printWindow.document.write(buildBillPrintHtml(bill));
printWindow.document.close();
} catch (error: any) {
printWindow.close();
message.error(error?.message || '账单加载失败');
}
};
const columns = useMemo(() => [