新增租屋契約工具與 HTML 版本備份

This commit is contained in:
2026-05-14 17:32:13 +08:00
commit f7e6cc124a
4 changed files with 1466 additions and 0 deletions

464
contract_template_tool.html Normal file
View File

@@ -0,0 +1,464 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>租屋契約工具</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<style>
:root {
--bg: #f3f4f6;
--panel: rgba(255, 255, 255, 0.94);
--panel-strong: #ffffff;
--text: #2b2118;
--muted: #746657;
--line: #d9dde3;
--accent: #a34b2a;
--accent-strong: #7f3519;
--shadow: 0 18px 45px rgba(82, 51, 28, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", "Noto Sans TC", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(163, 75, 42, 0.06), transparent 30%),
linear-gradient(135deg, #fafbfc 0%, #eef1f4 100%);
}
.shell {
width: min(1180px, calc(100% - 32px));
margin: 24px auto;
padding: 24px;
border: 1px solid rgba(217, 221, 227, 0.8);
border-radius: 24px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(12px);
box-shadow: var(--shadow);
}
.hero {
display: flex;
justify-content: space-between;
align-items: end;
gap: 16px;
margin-bottom: 20px;
}
h1 {
margin: 0;
font-size: clamp(28px, 4vw, 42px);
line-height: 1.05;
letter-spacing: 0.02em;
}
.subtitle {
margin: 8px 0 0;
color: var(--muted);
font-size: 15px;
}
.meta {
color: var(--muted);
font-size: 13px;
white-space: nowrap;
}
.layout {
display: grid;
grid-template-columns: 360px 1fr;
gap: 20px;
align-items: start;
}
.panel {
background: var(--panel);
border: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 20px;
padding: 18px;
}
.panel h2 {
margin: 0 0 12px;
font-size: 18px;
}
.field-list {
display: grid;
gap: 12px;
}
.field-card {
padding: 12px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(248, 250, 252, 0.96);
}
label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--panel-strong);
color: var(--text);
font: inherit;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 12px;
}
button {
border: 0;
border-radius: 999px;
padding: 11px 16px;
font: inherit;
cursor: pointer;
transition: transform 0.18s ease;
}
button:hover {
transform: translateY(-1px);
}
.primary {
color: #fff9f3;
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
}
.secondary {
color: var(--text);
background: #eceff3;
}
.output {
white-space: pre-wrap;
min-height: 640px;
padding: 16px 16px 32px;
border-radius: 16px;
border: 1px dashed var(--line);
background: #ffffff;
line-height: 1.75;
}
.hint {
margin-top: 12px;
color: var(--muted);
font-size: 13px;
}
.status {
min-height: 20px;
margin-top: 10px;
color: var(--accent-strong);
font-size: 13px;
}
.pdf-export {
width: 794px;
padding: 16px 16px 32px;
border: 1px dashed var(--line);
border-radius: 16px;
background: #ffffff;
color: var(--text);
font-family: "Segoe UI", "Noto Sans TC", sans-serif;
font-size: 16px;
line-height: 1.75;
white-space: pre-wrap;
}
@media print {
body {
background: #fff;
}
.shell {
width: auto;
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: #fff;
box-shadow: none;
}
.hero,
.panel:first-child,
.toolbar,
.hint,
.status {
display: none !important;
}
.layout {
display: block;
}
.panel:last-child {
border: 0;
padding: 0;
background: #fff;
}
.panel:last-child h2 {
display: none;
}
.output {
min-height: auto;
padding: 16px 16px 32px;
border: 1px dashed var(--line);
border-radius: 16px;
background: #fff;
}
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
.hero {
flex-direction: column;
align-items: start;
}
}
</style>
</head>
<body>
<div class="shell">
<div class="hero">
<div>
<h1>租屋契約工具</h1>
<p class="subtitle">直接填 3 個欄位,右邊會自動產生完整契約內容。</p>
</div>
<div class="meta" id="timeLabel">台北時間:載入中</div>
</div>
<div class="layout">
<section class="panel">
<h2>填寫資料</h2>
<div class="field-list" id="fields"></div>
</section>
<section class="panel">
<h2>帶入後內容</h2>
<div class="toolbar">
<button class="primary" id="exportPdf">匯出 PDF</button>
<button class="secondary" id="sharePdf">分享 PDF</button>
<button class="secondary" id="printDoc">列印 / 另存 PDF</button>
</div>
<div class="output" id="output"></div>
<div class="hint">提示PDF 輸出目前跟第二版一樣,走簡單文字框樣式。</div>
<div class="status" id="status"></div>
</section>
</div>
</div>
<script>
const defaultTemplate = `房屋租賃契約
立契約書人 出租人 廖雅惠 (以下簡稱為甲方)
承租人 (以下簡稱為乙方)
因房屋租賃事件,訂立本契約,雙方同意之條件如左:
房屋所在地及使用範圍:台中市西屯區西平里文華路 217-5 號 2 樓 A 室
第二條 租賃期限:自民國 114 年 6 月 18 日至 115 年 6 月 17 日止計 1 年。
第三條 租金:
1. 每月租金新台幣 {{每月租金}} 元,每月 {{繳款日期}} 日以前繳納。
2. 保證金新台幣 {{保證金}} 元,於租賃期滿交還。
3. 包(管理費、網路、安博盒子)
4. 附(電視、冷氣、冰箱、洗衣機、雙人床、書桌、衣櫃、椅子)
第四條 使用租賃物之限制:
1. 本房屋係供住家之用。
2. 未經甲方同意,乙方不得將房屋全部或一部轉租、出借、頂讓,或以其他變相方法由他人使用房屋。
3. 乙方於租賃期滿應即將房屋遷讓交還,不得向甲方請求遷移費或任何費用。
4. 房屋不得供非法使用,或存放危險物品影響公共安全。
5. 房屋有改裝設施之必要,乙方取得甲方之同意後得自行裝設,但不得損害原有建築,乙方於交還房屋時應負責回復原狀,不可以在牆上張貼任何物品如海報、公佈欄。
第五條 危險負擔:乙方應以善良管理人之注意使用房屋,除因天災地變等不可抗拒之情形外,因乙方之過失致房屋毀損,應負損害賠償之責。房屋因自然之損壞有修繕必要時,由甲方負責修理。
第六條 違約處罰:
1. 乙方違反約定方法使用房屋,或拖欠租金,經甲方催告限期繳納仍不支付時,甲方得終止租約。
2. 乙方於終止租約或租賃期滿不交還房屋,自終止租約或租賃期滿之歷日起,乙方應支付按房租壹倍計算之違約金。
第七條 其他特約事項:
1. 乙方遷出時,如遺留傢俱雜物不搬者,視為放棄,應由甲方處理及留存最後一期水電單以便結水電及退押金。
2. 本契約租賃期限未滿,住滿半年需扣一個月押金,但需提前一個月告知及配合看屋。及一個月房租沒付房東可請房客遷出。
3. 退租時必須將房屋打掃乾淨,否則收 800 元清潔費,不得養寵物,房間禁菸,被發現強制退租及扣 2 個月押金。
4. 合約到期前一個月如不續租,須告知房東,並配合給其他房客看屋。
5. 牆壁上不可張貼紙張或釘任何物品例如公佈欄。
6. 如惡意輕生必須賠償甲方當初購屋款及裝潢費用新台幣 150 萬元整。
7. 電費 1 度 5 元,每月與房租計算。
第八條 應受強制執行之事項:承租人給付租金或期限屆滿交還租賃物,出租人返還保證金,如不履行應逕送強制執行。
出租人:廖雅惠
地址:台中市西屯區西平里文華路 217-5 號 2 樓 A 室
電話0918298185
承租人:
簽章:
身份證號:
緊急聯絡人:
地址:
電話:
匯款帳號:
中華民國 年 月 日`;
const fieldsContainer = document.getElementById("fields");
const output = document.getElementById("output");
const timeLabel = document.getElementById("timeLabel");
const status = document.getElementById("status");
const values = {
"每月租金": "",
"繳款日期": "",
"保證金": ""
};
const allowedFields = ["每月租金", "繳款日期", "保證金"];
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 compiledText() {
return defaultTemplate.replace(/{{\s*([^{}]+)\s*}}/g, (_, key) => {
const name = key.trim();
return values[name] ?? "";
});
}
function renderFields() {
fieldsContainer.innerHTML = "";
allowedFields.forEach(name => {
const card = document.createElement("div");
card.className = "field-card";
const label = document.createElement("label");
label.setAttribute("for", `field-${name}`);
label.textContent = name;
const input = document.createElement("input");
input.id = `field-${name}`;
input.type = "text";
input.value = values[name] || "";
input.placeholder = `請輸入 ${name}`;
input.addEventListener("input", () => {
values[name] = input.value;
renderOutput();
});
card.append(label, input);
fieldsContainer.appendChild(card);
});
}
function renderOutput() {
output.textContent = compiledText();
}
function buildFileName() {
const rent = (values["每月租金"] || "未填租金").replace(/[\\/:*?"<>|]/g, "_");
const day = (values["繳款日期"] || "未填日期").replace(/[\\/:*?"<>|]/g, "_");
return `租屋契約_租金${rent}_日期${day}.pdf`;
}
function createPdfElement() {
const div = document.createElement("div");
div.className = "pdf-export";
div.textContent = output.textContent;
return div;
}
async function exportPdfBlob() {
const pdfElement = createPdfElement();
return window.html2pdf().set({
margin: 10,
filename: buildFileName(),
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" }
}).from(pdfElement).outputPdf("blob");
}
document.getElementById("printDoc").addEventListener("click", () => {
setStatus("已開啟列印視窗,可另存為 PDF。");
window.print();
});
document.getElementById("exportPdf").addEventListener("click", async () => {
try {
setStatus("正在匯出 PDF...");
const pdfElement = createPdfElement();
await window.html2pdf().set({
margin: 10,
filename: buildFileName(),
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" }
}).from(pdfElement).save();
setStatus("PDF 已匯出。");
} catch (error) {
setStatus("匯出失敗,請改用列印 / 另存 PDF。");
}
});
document.getElementById("sharePdf").addEventListener("click", async () => {
try {
if (!navigator.share || !navigator.canShare) {
throw new Error("not-supported");
}
setStatus("正在產生分享用 PDF...");
const blob = await exportPdfBlob();
const file = new File([blob], buildFileName(), { type: "application/pdf" });
if (!navigator.canShare({ files: [file] })) {
throw new Error("cannot-share-file");
}
await navigator.share({
title: "租屋契約 PDF",
text: "這是帶入後的租屋契約。",
files: [file]
});
setStatus("已開啟分享視窗。");
} catch (error) {
setStatus("分享失敗或瀏覽器不支援,請先匯出 PDF。");
}
});
renderFields();
renderOutput();
updateTaipeiTime();
setInterval(updateTaipeiTime, 1000);
</script>
</body>
</html>