Files
contract_template_tool/contract_template_tool.html

465 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>