初始化專案:租屋契約 PDF 產生器
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
public/app.js
Normal file
166
public/app.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const form = document.querySelector('#contractForm');
|
||||
const templateSelect = document.querySelector('#template');
|
||||
const submitButton = document.querySelector('#submitButton');
|
||||
const downloadButton = document.querySelector('#downloadButton');
|
||||
const shareButton = document.querySelector('#shareButton');
|
||||
const message = document.querySelector('#message');
|
||||
const resultTitle = document.querySelector('#resultTitle');
|
||||
const connectionStatus = document.querySelector('#connectionStatus');
|
||||
|
||||
let currentPdfBlob = null;
|
||||
let currentPdfFileName = '租屋契約.pdf';
|
||||
|
||||
loadTemplates();
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
setBusy(true);
|
||||
resetPdf();
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch('/api/contracts/pdf', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template: formData.get('template'),
|
||||
monthlyRent: formData.get('monthlyRent'),
|
||||
paymentDay: formData.get('paymentDay'),
|
||||
deposit: formData.get('deposit'),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
throw new Error(errorBody.error || 'PDF 產生失敗。');
|
||||
}
|
||||
|
||||
currentPdfBlob = await response.blob();
|
||||
currentPdfFileName = getFileNameFromDisposition(response.headers.get('Content-Disposition'))
|
||||
|| buildDefaultFileName(formData.get('template'));
|
||||
|
||||
resultTitle.textContent = 'PDF 已產生';
|
||||
message.textContent = currentPdfFileName;
|
||||
downloadButton.disabled = false;
|
||||
shareButton.disabled = !canShareCurrentPdf();
|
||||
} catch (error) {
|
||||
resultTitle.textContent = '產生失敗';
|
||||
message.textContent = error.message;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
});
|
||||
|
||||
downloadButton.addEventListener('click', () => {
|
||||
if (!currentPdfBlob) {
|
||||
return;
|
||||
}
|
||||
downloadPdf();
|
||||
});
|
||||
|
||||
shareButton.addEventListener('click', async () => {
|
||||
if (!currentPdfBlob) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = new File([currentPdfBlob], currentPdfFileName, { type: 'application/pdf' });
|
||||
if (!navigator.canShare || !navigator.canShare({ files: [file] })) {
|
||||
downloadPdf();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.share({
|
||||
title: '租屋契約 PDF',
|
||||
files: [file],
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
message.textContent = '分享失敗,已保留下載按鈕。';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
const response = await fetch('/api/templates');
|
||||
if (!response.ok) {
|
||||
throw new Error('無法讀取範本。');
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
templateSelect.replaceChildren(
|
||||
...body.templates.map((name) => new Option(name, name)),
|
||||
);
|
||||
|
||||
if (body.templates.length === 0) {
|
||||
templateSelect.append(new Option('templates 資料夾沒有 .doc 範本', ''));
|
||||
templateSelect.disabled = true;
|
||||
submitButton.disabled = true;
|
||||
connectionStatus.textContent = '缺少範本';
|
||||
message.textContent = '請先將 .doc 範本放進 templates 資料夾。';
|
||||
return;
|
||||
}
|
||||
|
||||
connectionStatus.textContent = '可使用';
|
||||
} catch (error) {
|
||||
templateSelect.append(new Option('讀取失敗', ''));
|
||||
templateSelect.disabled = true;
|
||||
submitButton.disabled = true;
|
||||
connectionStatus.textContent = '離線';
|
||||
message.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function setBusy(isBusy) {
|
||||
submitButton.disabled = isBusy || templateSelect.disabled;
|
||||
submitButton.textContent = isBusy ? '產生中' : '產生 PDF';
|
||||
}
|
||||
|
||||
function resetPdf() {
|
||||
currentPdfBlob = null;
|
||||
downloadButton.disabled = true;
|
||||
shareButton.disabled = true;
|
||||
resultTitle.textContent = '產生中';
|
||||
message.textContent = '正在建立 PDF。';
|
||||
}
|
||||
|
||||
function canShareCurrentPdf() {
|
||||
if (!currentPdfBlob || !navigator.canShare) {
|
||||
return false;
|
||||
}
|
||||
const file = new File([currentPdfBlob], currentPdfFileName, { type: 'application/pdf' });
|
||||
return navigator.canShare({ files: [file] });
|
||||
}
|
||||
|
||||
function downloadPdf() {
|
||||
const url = URL.createObjectURL(currentPdfBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = currentPdfFileName;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function getFileNameFromDisposition(disposition) {
|
||||
if (!disposition) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (utf8Match) {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
}
|
||||
|
||||
const asciiMatch = disposition.match(/filename="([^"]+)"/i);
|
||||
return asciiMatch ? asciiMatch[1] : '';
|
||||
}
|
||||
|
||||
function buildDefaultFileName(templateName) {
|
||||
const baseName = String(templateName || '租屋契約').replace(/\.[^.]+$/, '');
|
||||
return `${baseName}.pdf`;
|
||||
}
|
||||
Reference in New Issue
Block a user