425 lines
18 KiB
HTML
425 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-Hant">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>租屋契約 PDF 工具</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--bg: #eee4d6;
|
||
--paper: #ffffff;
|
||
--ink: #222222;
|
||
--muted: #6c6258;
|
||
--line: #d4c7b8;
|
||
--accent: #8a4a27;
|
||
--accent-2: #6f3517;
|
||
--panel: rgba(255, 250, 244, 0.9);
|
||
--shadow: 0 18px 45px rgba(67, 43, 24, 0.12);
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
color: var(--ink);
|
||
font-family: "PMingLiU", "MingLiU", "Noto Serif TC", serif;
|
||
background:
|
||
radial-gradient(circle at top left, rgba(138, 74, 39, 0.1), transparent 26%),
|
||
linear-gradient(135deg, #f7f1e8, var(--bg));
|
||
}
|
||
.page {
|
||
width: min(1380px, calc(100% - 32px));
|
||
margin: 24px auto;
|
||
padding: 24px;
|
||
border: 1px solid rgba(212, 199, 184, 0.9);
|
||
border-radius: 24px;
|
||
background: rgba(255, 247, 239, 0.78);
|
||
backdrop-filter: blur(10px);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.hero {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: end;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
h1 {
|
||
margin: 0;
|
||
font-family: "Microsoft JhengHei", "Noto Sans TC", sans-serif;
|
||
font-size: clamp(28px, 4vw, 40px);
|
||
line-height: 1.05;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.subtitle {
|
||
margin: 8px 0 0;
|
||
color: var(--muted);
|
||
font-family: "Microsoft JhengHei", "Noto Sans TC", sans-serif;
|
||
font-size: 15px;
|
||
}
|
||
.meta {
|
||
color: var(--muted);
|
||
font-family: "Microsoft JhengHei", "Noto Sans TC", sans-serif;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 330px 1fr;
|
||
gap: 22px;
|
||
align-items: start;
|
||
}
|
||
.controls {
|
||
position: sticky;
|
||
top: 20px;
|
||
padding: 18px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 20px;
|
||
background: var(--panel);
|
||
box-shadow: var(--shadow);
|
||
font-family: "Microsoft JhengHei", "Noto Sans TC", sans-serif;
|
||
}
|
||
.controls h2 {
|
||
margin: 0 0 10px;
|
||
font-size: 18px;
|
||
}
|
||
.controls p {
|
||
margin: 0 0 16px;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
.field { margin-bottom: 14px; }
|
||
.field label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
font-size: 14px;
|
||
color: var(--muted);
|
||
}
|
||
.field input {
|
||
width: 100%;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 14px;
|
||
background: #fffdfa;
|
||
font: inherit;
|
||
font-size: 16px;
|
||
color: var(--ink);
|
||
}
|
||
.toolbar {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 18px;
|
||
}
|
||
button {
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 12px 16px;
|
||
font: inherit;
|
||
cursor: pointer;
|
||
transition: transform 0.18s ease, opacity 0.18s ease;
|
||
}
|
||
button:hover { transform: translateY(-1px); }
|
||
.primary {
|
||
color: #fffaf5;
|
||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||
}
|
||
.secondary {
|
||
color: var(--ink);
|
||
background: #eadcc9;
|
||
}
|
||
.preview-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.paper {
|
||
width: 210mm;
|
||
min-height: 297mm;
|
||
padding: 22mm 18mm;
|
||
border: 1px solid #d7d1c8;
|
||
background: var(--paper);
|
||
box-shadow: 0 24px 60px rgba(38, 25, 14, 0.12);
|
||
color: #111111;
|
||
line-height: 1.72;
|
||
font-size: 18px;
|
||
}
|
||
.paper.pdf-export-mode {
|
||
width: 174mm;
|
||
min-height: 0;
|
||
padding: 0;
|
||
border: 0;
|
||
box-shadow: none;
|
||
background: #ffffff;
|
||
}
|
||
.contract-title {
|
||
margin: 0 0 14px;
|
||
text-align: center;
|
||
font-size: 24px;
|
||
letter-spacing: 0.2em;
|
||
font-weight: 700;
|
||
}
|
||
.contract-title,
|
||
.contract-line,
|
||
.signature-row {
|
||
break-inside: avoid;
|
||
page-break-inside: avoid;
|
||
}
|
||
.contract-line {
|
||
white-space: pre-wrap;
|
||
min-height: 1.75em;
|
||
}
|
||
.indent {
|
||
padding-left: 2em;
|
||
text-indent: -2em;
|
||
}
|
||
.signature-block { margin-top: 18px; }
|
||
.signature-row { white-space: pre-wrap; }
|
||
.fill {
|
||
display: inline-block;
|
||
min-width: 3em;
|
||
padding: 0 0.15em;
|
||
}
|
||
.hint {
|
||
margin-top: 14px;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
.status {
|
||
min-height: 22px;
|
||
margin-top: 10px;
|
||
color: var(--accent-2);
|
||
font-size: 13px;
|
||
}
|
||
@media (max-width: 1100px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
.controls { position: static; }
|
||
.preview-wrap { overflow: auto; justify-content: start; }
|
||
}
|
||
@page {
|
||
size: A4;
|
||
margin: 22mm 18mm;
|
||
}
|
||
@media print {
|
||
body { background: #ffffff; }
|
||
.page {
|
||
width: auto;
|
||
margin: 0;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 0;
|
||
background: #ffffff;
|
||
box-shadow: none;
|
||
}
|
||
.hero, .controls { display: none !important; }
|
||
.layout { display: block; }
|
||
.preview-wrap { display: block; }
|
||
.paper {
|
||
width: auto;
|
||
min-height: auto;
|
||
margin: 0;
|
||
padding: 0;
|
||
border: 0;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="hero">
|
||
<div>
|
||
<h1>租屋契約 PDF 工具</h1>
|
||
<p class="subtitle">只改 `每月租金`、`繳款日期`、`保證金`,右邊維持接近原始 `.doc` 的契約排版,並可匯出或分享 PDF。</p>
|
||
</div>
|
||
<div class="meta" id="timeLabel">台北時間:載入中</div>
|
||
</div>
|
||
<div class="layout">
|
||
<aside class="controls">
|
||
<h2>填寫欄位</h2>
|
||
<p>這版固定只改 3 個地方,其他條文、地址、設備、租期都照原本文件排版顯示。</p>
|
||
<div class="field">
|
||
<label for="rentInput">每月租金</label>
|
||
<input id="rentInput" type="text" placeholder="例如:6500">
|
||
</div>
|
||
<div class="field">
|
||
<label for="payDayInput">繳款日期</label>
|
||
<input id="payDayInput" type="text" placeholder="例如:5">
|
||
</div>
|
||
<div class="field">
|
||
<label for="depositInput">保證金</label>
|
||
<input id="depositInput" type="text" placeholder="例如:12000">
|
||
</div>
|
||
<div class="toolbar">
|
||
<button class="primary" id="exportPdfBtn">匯出 PDF</button>
|
||
<button class="secondary" id="sharePdfBtn">分享 PDF</button>
|
||
<button class="secondary" id="printBtn">列印 / 另存 PDF</button>
|
||
<button class="secondary" id="copyTextBtn">複製契約文字</button>
|
||
</div>
|
||
<div class="hint">如果瀏覽器不支援直接分享檔案,還是可以先按「匯出 PDF」存檔,再用 Line、Email 或其他方式傳出去。</div>
|
||
<div class="status" id="status"></div>
|
||
</aside>
|
||
<section class="preview-wrap">
|
||
<article class="paper" id="contractPaper">
|
||
<h2 class="contract-title">房屋租賃契約</h2>
|
||
<div class="contract-line"> 立契約書人 出租人 廖雅惠 (以下簡稱為甲方)</div>
|
||
<div class="contract-line"> 承租人 (以下簡稱為乙方)</div>
|
||
<div class="contract-line">因房屋租賃事件,訂立本契約,雙方同意之條件如左:</div>
|
||
<div class="contract-line">房屋所在地及使用範圍:台中市西屯區西平里文華路 217-5 號 2 樓 A 室</div>
|
||
<div class="contract-line">第二條 租賃期限:自民國 114 年 6 月 18 日至 115 年 6 月 17 日止計 1 年。</div>
|
||
<div class="contract-line">第三條 租金:1. 每月租金新台幣 <span class="fill" id="rentValue"></span> 元,每月 <span class="fill" id="payDayValue"></span> 日以前繳納。</div>
|
||
<div class="contract-line"> 2. 保證金新台幣 <span class="fill" id="depositValue"></span> 元,於租賃期滿交還</div>
|
||
<div class="contract-line"> 3. 包 (管理費 網路 安博盒子 )</div>
|
||
<div class="contract-line"> 4. 附( 電視 冷氣 冰箱 洗衣機 雙人床 書桌 衣櫃 椅子 )</div>
|
||
<div class="contract-line">第四條 使用租賃物之限制:</div>
|
||
<div class="contract-line indent">1. 本房屋係供住家之用</div>
|
||
<div class="contract-line indent">2. 未經甲方同意,乙方不得將房屋全部或一部轉租、出借、頂讓,或以其他變相方法由他人使用房屋。</div>
|
||
<div class="contract-line indent">3. 乙方於租賃期滿應即將房屋遷讓交還,不得向甲方請求遷移費或任何費用。</div>
|
||
<div class="contract-line indent">4. 房屋不得供非法使用,或存放危險物品影響公共安全。</div>
|
||
<div class="contract-line indent">5. 房屋有改裝設施之必要,乙方取得甲方之同意後得自行裝設,但不得損害原有建築,乙方於交還房屋時應負責回復原狀不可以在牆上張貼任何物品如海報公佈欄</div>
|
||
<div class="contract-line">第五條 危險負擔:乙方應以善良管理人之注意使用房屋,除因天災地變等不可抗拒之情形外,因乙方之過失致房屋毀損,應負損害賠償之責。房屋因自然之損壞有修繕必要時,由甲方負責修理。</div>
|
||
<div class="contract-line">第六條 違約處罰:</div>
|
||
<div class="contract-line indent">1. 乙方違反約定方法使用房屋,或拖欠租金,經甲方催告限期繳納仍不支付時,甲方得終止租約。</div>
|
||
<div class="contract-line indent">2. 乙方於終止租約或租賃期滿不交還房屋,自終止租約或租賃期滿之歷日起,乙方應支付按房租壹倍計算之違約金。</div>
|
||
<div class="contract-line">第七條 其他特約事項:</div>
|
||
<div class="contract-line indent">1. 乙方遷出時,如遺留傢俱雜物不搬者,視為放棄,應由甲方處理及留存最後一期水電單以便結水電及退押金。</div>
|
||
<div class="contract-line indent">2. 本契約租賃期限未滿,住滿半年需扣一個月押金但需提前一個月告知及配合看屋。及一個月房租沒付房東可請房客遷出。</div>
|
||
<div class="contract-line indent">3. 退租時必須將房屋打掃乾淨,否收 800 元清潔費,不得養寵物 房間禁菸被發現強制退租及扣 2 個月押金。</div>
|
||
<div class="contract-line indent">4. 合約到期前一個月如不續租,須告知房東,並配合給其他房客看屋</div>
|
||
<div class="contract-line indent">5. 牆壁上不可張貼紙張或釘任何物品例如公佈欄</div>
|
||
<div class="contract-line indent">6. 如惡意輕生必須賠償甲方當初購屋款及裝潢費用新台幣 150 萬元整.</div>
|
||
<div class="contract-line indent">7. 電費 1 度 5 元,每月與房租計算</div>
|
||
<div class="contract-line">第八條 應受強制執行之事項:承租人給付租金或期限屆滿交還租賃物出租人返還保證金,如不履行應逕送強制執行。</div>
|
||
<div class="signature-block">
|
||
<div class="signature-row"> 出租人: 廖雅惠 簽章:</div>
|
||
<div class="signature-row">地 址:台中市西屯區西平里文華路 217-5 號 2 樓 A 室</div>
|
||
<div class="signature-row">電 話:0918298185</div>
|
||
<div class="signature-row">承租人: 簽章:</div>
|
||
<div class="signature-row">身份證號: 緊急聯絡人:</div>
|
||
<div class="signature-row">地 址:</div>
|
||
<div class="signature-row">電 話:</div>
|
||
<div class="signature-row">匯款帳號:</div>
|
||
<div class="signature-row">中 華 民 國 年 月 日</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const timeLabel = document.getElementById("timeLabel");
|
||
const status = document.getElementById("status");
|
||
const rentInput = document.getElementById("rentInput");
|
||
const payDayInput = document.getElementById("payDayInput");
|
||
const depositInput = document.getElementById("depositInput");
|
||
const rentValue = document.getElementById("rentValue");
|
||
const payDayValue = document.getElementById("payDayValue");
|
||
const depositValue = document.getElementById("depositValue");
|
||
const contractPaper = document.getElementById("contractPaper");
|
||
const pdfPageMargin = 22;
|
||
const pdfSideMargin = 18;
|
||
|
||
function updateTaipeiTime() {
|
||
const formatter = new Intl.DateTimeFormat("zh-TW", {
|
||
timeZone: "Asia/Taipei", year: "numeric", month: "2-digit", day: "2-digit",
|
||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
||
});
|
||
timeLabel.textContent = `台北時間:${formatter.format(new Date())}`;
|
||
}
|
||
function setStatus(message) { status.textContent = message; }
|
||
function syncValues() {
|
||
rentValue.textContent = rentInput.value || " ";
|
||
payDayValue.textContent = payDayInput.value || " ";
|
||
depositValue.textContent = depositInput.value || " ";
|
||
}
|
||
function buildFileName() {
|
||
const rent = (rentInput.value || "未填租金").replace(/[\\/:*?"<>|]/g, "_");
|
||
const payDay = (payDayInput.value || "未填日期").replace(/[\\/:*?"<>|]/g, "_");
|
||
return `租屋契約_逢甲A_租金${rent}_繳款日${payDay}.pdf`;
|
||
}
|
||
|
||
function buildPdfOptions() {
|
||
return {
|
||
margin: [pdfPageMargin, pdfSideMargin, pdfPageMargin, pdfSideMargin],
|
||
filename: buildFileName(),
|
||
image: { type: "jpeg", quality: 0.98 },
|
||
html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
|
||
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
|
||
pagebreak: {
|
||
mode: ["css", "legacy"],
|
||
avoid: [".contract-title", ".contract-line", ".signature-row"]
|
||
}
|
||
};
|
||
}
|
||
|
||
async function withPdfExportMode(callback) {
|
||
contractPaper.classList.add("pdf-export-mode");
|
||
try {
|
||
await callback();
|
||
} finally {
|
||
contractPaper.classList.remove("pdf-export-mode");
|
||
}
|
||
}
|
||
|
||
async function exportPdfFile() {
|
||
await withPdfExportMode(() => {
|
||
return window.html2pdf().set(buildPdfOptions()).from(contractPaper).save();
|
||
});
|
||
}
|
||
|
||
async function createPdfBlob() {
|
||
let blob;
|
||
await withPdfExportMode(async () => {
|
||
const worker = window.html2pdf().set(buildPdfOptions()).from(contractPaper).toPdf();
|
||
const pdf = await worker.get("pdf");
|
||
blob = pdf.output("blob");
|
||
});
|
||
return blob;
|
||
}
|
||
function buildContractText() {
|
||
return [
|
||
"房屋租賃契約",
|
||
" 立契約書人 出租人 廖雅惠 (以下簡稱為甲方)",
|
||
"承租人 (以下簡稱為乙方)",
|
||
"因房屋租賃事件,訂立本契約,雙方同意之條件如左:",
|
||
"房屋所在地及使用範圍:台中市西屯區西平里文華路 217-5 號 2 樓 A 室",
|
||
"第二條 租賃期限:自民國 114 年 6 月 18 日至 115 年 6 月 17 日止計 1 年。",
|
||
`第三條 租金:1. 每月租金新台幣 ${rentInput.value || ""} 元,每月 ${payDayInput.value || ""} 日以前繳納。`,
|
||
` 2. 保證金新台幣 ${depositInput.value || ""} 元,於租賃期滿交還`,
|
||
" 3. 包 (管理費 網路 安博盒子 )",
|
||
" 4. 附( 電視 冷氣 冰箱 洗衣機 雙人床 書桌 衣櫃 椅子 )"
|
||
].join("\n");
|
||
}
|
||
rentInput.addEventListener("input", syncValues);
|
||
payDayInput.addEventListener("input", syncValues);
|
||
depositInput.addEventListener("input", syncValues);
|
||
document.getElementById("copyTextBtn").addEventListener("click", async () => {
|
||
try { await navigator.clipboard.writeText(buildContractText()); setStatus("已複製契約文字。"); }
|
||
catch { setStatus("複製失敗,請檢查瀏覽器剪貼簿權限。"); }
|
||
});
|
||
document.getElementById("printBtn").addEventListener("click", () => {
|
||
setStatus("已開啟列印視窗,可選擇另存為 PDF。");
|
||
window.print();
|
||
});
|
||
document.getElementById("exportPdfBtn").addEventListener("click", async () => {
|
||
try { setStatus("正在匯出 PDF..."); await exportPdfFile(); setStatus(`PDF 已匯出:${buildFileName()}`); }
|
||
catch { setStatus("匯出失敗,改用列印另存 PDF 也可以。"); }
|
||
});
|
||
document.getElementById("sharePdfBtn").addEventListener("click", async () => {
|
||
try {
|
||
if (!navigator.share || !navigator.canShare) throw new Error("目前瀏覽器不支援檔案分享");
|
||
setStatus("正在產生分享用 PDF...");
|
||
const blob = await createPdfBlob();
|
||
const file = new File([blob], buildFileName(), { type: "application/pdf" });
|
||
if (!navigator.canShare({ files: [file] })) throw new Error("這個裝置不支援分享 PDF 檔");
|
||
await navigator.share({ title: "租屋契約 PDF", text: "這是已帶入租金資料的租屋契約。", files: [file] });
|
||
setStatus("PDF 已開啟分享視窗。");
|
||
} catch {
|
||
setStatus("分享失敗或瀏覽器不支援,請先匯出 PDF 後再傳送。");
|
||
}
|
||
});
|
||
updateTaipeiTime();
|
||
syncValues();
|
||
setInterval(updateTaipeiTime, 1000);
|
||
</script>
|
||
</body>
|
||
</html>
|