465 lines
13 KiB
HTML
465 lines
13 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>租屋契約工具</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>
|