Merge pull request '修复pdf导出' (#8) from xiongyuxing/gongxue-base:main into main
This commit is contained in:
@@ -38,11 +38,166 @@ const typeMap: Record<string, string> = {
|
||||
water: '水费',
|
||||
electricity: '电费',
|
||||
cleaning: '保洁费',
|
||||
rent: '租金',
|
||||
damage: '损坏赔偿',
|
||||
penalty: '罚款',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
const escapeHtml = (value: unknown) =>
|
||||
String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
|
||||
const money = (value: unknown) => `¥${Number(value || 0).toFixed(2)}`;
|
||||
|
||||
const buildBillPrintHtml = (bill: any) => {
|
||||
const studentName = bill.student?.name || '-';
|
||||
const status = statusMap[bill.status]?.text || bill.status || '-';
|
||||
const generatedAt = bill.generatedAt
|
||||
? dayjs(bill.generatedAt).format('YYYY-MM-DD HH:mm')
|
||||
: dayjs().format('YYYY-MM-DD HH:mm');
|
||||
const hasDeposit = Number(bill.availableDeposit || 0) > 0;
|
||||
const items = bill.items || [];
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>账单_${escapeHtml(studentName)}_${escapeHtml(bill.id)}</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 0; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
color: #000;
|
||||
background: #f5f5f5;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", Arial, sans-serif;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
.page {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
margin: 0 auto;
|
||||
padding: 50px;
|
||||
background: #fff;
|
||||
}
|
||||
h1 { margin: 0; text-align: center; font-size: 20px; line-height: 1.35; font-weight: 700; }
|
||||
.generated-time { margin-top: 8px; text-align: center; color: #666; font-size: 10px; }
|
||||
.basic-info { margin-top: 24px; font-size: 12px; line-height: 1.8; }
|
||||
.section-title { margin: 16px 0 6px; font-size: 14px; font-weight: 700; text-decoration: underline; }
|
||||
.amount-summary { font-size: 12px; line-height: 1.75; }
|
||||
.total { color: #007aff; font-size: 14px; font-weight: 700; }
|
||||
.deposit { color: #52c41a; font-size: 11px; }
|
||||
.deposit-applied { color: #fa8c16; font-size: 11px; }
|
||||
.after-deposit { color: #ff3b30; font-size: 14px; font-weight: 700; }
|
||||
table { width: 100%; border-collapse: collapse; table-layout: fixed; margin-top: 8px; }
|
||||
th, td { padding: 5px 6px; border-bottom: 1px solid #ccc; font-size: 9px; line-height: 1.45; text-align: left; vertical-align: top; word-break: break-word; }
|
||||
th { color: #333; font-weight: 700; }
|
||||
td.amount, th.amount { text-align: right; white-space: nowrap; }
|
||||
.footer { margin-top: 34px; text-align: center; color: #999; font-size: 8px; }
|
||||
.print-actions {
|
||||
position: fixed;
|
||||
right: 18px;
|
||||
top: 18px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.print-actions button {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #1f6feb;
|
||||
border-radius: 4px;
|
||||
background: #1f6feb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
@media print {
|
||||
body { background: #fff; }
|
||||
.page { margin: 0; }
|
||||
.print-actions { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-actions">
|
||||
<button onclick="window.print()">打印 / 另存为 PDF</button>
|
||||
</div>
|
||||
<main class="page">
|
||||
<h1>恭学教育基地水电费账单</h1>
|
||||
<div class="generated-time">生成时间: ${escapeHtml(generatedAt)}</div>
|
||||
|
||||
<div class="basic-info">
|
||||
<div>学生姓名: ${escapeHtml(studentName)}</div>
|
||||
<div>计费周期: ${escapeHtml(bill.periodStart)} ~ ${escapeHtml(bill.periodEnd)}</div>
|
||||
<div>账单状态: ${escapeHtml(status)}</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<div class="section-title">费用汇总</div>
|
||||
<div class="amount-summary">
|
||||
<div>分摊费用: ${escapeHtml(money(bill.sharedAmount))}</div>
|
||||
<div>个人费用: ${escapeHtml(money(bill.personalAmount))}</div>
|
||||
<div class="total">应付总额: ${escapeHtml(money(bill.totalAmount))}</div>
|
||||
${
|
||||
hasDeposit
|
||||
? `<div class="deposit">可用押金: ${escapeHtml(money(bill.availableDeposit))}</div>
|
||||
<div class="deposit-applied">押金抵扣: -${escapeHtml(money(bill.depositApplied))}</div>
|
||||
<div class="after-deposit">抵扣后应付: ${escapeHtml(money(bill.amountAfterDeposit ?? bill.totalAmount))}</div>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-title">费用明细</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 22%;">费用类型</th>
|
||||
<th>说明</th>
|
||||
<th style="width: 11%;">天数</th>
|
||||
<th style="width: 12%;">总人天</th>
|
||||
<th class="amount" style="width: 16%;">金额(元)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${
|
||||
items.length
|
||||
? items
|
||||
.map(
|
||||
(item: any) => `<tr>
|
||||
<td>${escapeHtml(typeMap[item.expenseType] || item.expenseType || '-')}</td>
|
||||
<td>${escapeHtml(item.description || '-')}</td>
|
||||
<td>${escapeHtml(item.days || 0)}</td>
|
||||
<td>${escapeHtml(item.totalRoomDays || 0)}</td>
|
||||
<td class="amount">${escapeHtml(Number(item.studentAmount || 0).toFixed(2))}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('')
|
||||
: '<tr><td colspan="5" style="text-align:center; color:#999;">暂无费用明细</td></tr>'
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="footer">
|
||||
本账单由恭学教育基地管理系统自动生成
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => window.print(), 250);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
const BillsPage: React.FC = () => {
|
||||
const [bills, setBills] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -186,11 +341,24 @@ const BillsPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleExportPdf = (billId: number) => {
|
||||
downloadBlob(`/bills/export/pdf/${billId}`, `账单_${billId}.pdf`).catch(() =>
|
||||
message.error('导出失败'),
|
||||
);
|
||||
};
|
||||
const handleExportPdf = useCallback(async (billId: number) => {
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) {
|
||||
message.error('无法打开打印窗口,请允许浏览器弹窗后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
printWindow.document.write('<!doctype html><title>账单加载中</title><body>账单加载中...</body>');
|
||||
try {
|
||||
const bill = await api.get(`/bills/${billId}`);
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(buildBillPrintHtml(bill));
|
||||
printWindow.document.close();
|
||||
} catch (e: any) {
|
||||
printWindow.close();
|
||||
message.error(e?.message || '账单数据加载失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{ title: '学生', width: 120, render: (_: any, r: any) => r.student?.name || '-' },
|
||||
|
||||
Reference in New Issue
Block a user