<div id="loan-calculator">
<div class="container">
<div class="form-group loanType">
<label>貸款類型</label>
<label><input type="radio" name="type" class="general" checked> 一般貸款</label>
<label><input type="radio" name="type" class="youth"> 青安貸款</label>
<label><input type="radio" name="type" class="combine"> 組合貸款</label>
</div>
<!-- 一般/青安 表單 -->
<form class="loan-form" data-form="general">
<div class="form-group">
<label>計算方式</label>
<select class="js-calc-type">
<option value="loan">按貸款總額</option>
<option value="house">按房屋總價</option>
</select>
</div>
<div class="form-group js-loan-amount-group">
<label>貸款總額</label>
<input type="number" class="js-loan-amount" placeholder="請輸入貸款總額">
<span class="unit">萬元</span>
</div>
<div class="js-house-group hidden">
<div class="form-group">
<label>房屋總價</label>
<input type="number" class="js-house-price" placeholder="請輸入房屋總價">
<span class="unit">萬元</span>
</div>
<div class="form-group">
<label>自備款</label>
<input type="number" class="js-down-payment" placeholder="請輸入自備款">
<span class="unit">萬元</span>
</div>
</div>
<div class="form-group">
<label>貸款期限</label>
<select class="js-years"></select>
</div>
<div class="form-group">
<label>寬限期</label>
<select class="gracePeriod"></select>
</div>
<div class="form-group">
<label>利率方式</label>
<select class="js-rate-type">
<option value="single">單一利率</option>
<option value="multi">多段利率</option>
</select>
</div>
<div class="form-group single-rate">
<label>年利率</label>
<select class="bankSelect"></select>
<input type="number" class="rateDisplay" placeholder="例如 2.125">
<span class="suffix">%</span>
</div>
<div class="multiRateSection hidden">
<div class="form-group rate-row">
<label>第一段利率</label>
<input type="number" class="start-month js-start" placeholder="起始月"> ~
<input type="number" class="end-month js-end" placeholder="結束月"> 月
<input type="number" class="rate js-rate" placeholder="利率%">
</div>
<div class="form-group rate-row">
<label>第二段利率</label>
<input type="number" class="start-month js-start" placeholder="起始月"> ~
<input type="number" class="end-month js-end" placeholder="結束月"> 月
<input type="number" class="rate js-rate" placeholder="利率%">
</div>
<div class="form-group rate-row">
<label>第三段利率</label>
<input type="number" class="start-month js-start" placeholder="起始月"> ~
<input type="number" class="end-month js-end" placeholder="結束月"> 月
<input type="number" class="rate js-rate" placeholder="利率%">
</div>
</div>
<div class="form-actions">
<button type="button" class="submit-btn" onclick="calculateLoan(this)">開始計算</button>
</div>
</form>
<!-- 組合貸款表單 -->
<form class="combine-form hidden js-form" data-form="combine">
<div class="form-group js-calc-type">
<label>計算方式</label>
<select class="calcType">
<option value="loan">按貸款總額</option>
<option value="house">按房屋總價</option>
</select>
</div>
<div class="form-group js-loan-amount-group">
<label>貸款總額</label>
<input type="number" class="js-input js-loan-amount" data-field="totalAmount" placeholder="請輸入貸款總額">
<span class="unit">萬元</span>
</div>
<div class="js-house-group hidden">
<div class="form-group">
<label>房屋總價</label>
<input type="number" class="js-house-price" placeholder="請輸入房屋總價">
<span class="unit">萬元</span>
</div>
<div class="form-group">
<label>自備款</label>
<input type="number" class="js-down-payment" placeholder="請輸入自備款">
<span class="unit">萬元</span>
</div>
</div>
<section class="segment youth" data-segment="youth">
<div class="form-group">
<label>青安貸款金額</label>
<input type="number" class="js-input" data-field="amount" placeholder="請輸入青安貸款金額">
<span class="unit">萬元</span>
</div>
<div class="form-group">
<label>寬限期</label>
<select class="js-select gracePeriod" data-field="graceYears"></select>
</div>
<div class="rates-panel js-multirates" data-field="multiRates">
<div class="form-group rate-row js-rate-row">
<label>第一段</label>
<input type="number" class="js-start" placeholder="起始月">
<span class="sep">~</span>
<input type="number" class="js-end" placeholder="結束月">
<span class="suffix">月</span>
<input type="number" class="js-rate" placeholder="利率%">
<span class="suffix">%</span>
</div>
<div class="form-group rate-row js-rate-row">
<label>第二段</label>
<input type="number" class="js-start" placeholder="起始月">
<span class="sep">~</span>
<input type="number" class="js-end" placeholder="結束月">
<span class="suffix">月</span>
<input type="number" class="js-rate" placeholder="利率%">
<span class="suffix">%</span>
</div>
<div class="form-group rate-row js-rate-row">
<label>第三段</label>
<input type="number" class="js-start" placeholder="起始月">
<span class="sep">~</span>
<input type="number" class="js-end" placeholder="結束月">
<span class="suffix">月</span>
<input type="number" class="js-rate" placeholder="利率%">
<span class="suffix">%</span>
</div>
</div>
</section>
<section class="segment general" data-segment="general">
<h4>一般貸款</h4>
<div class="form-group">
<label>一般貸款金額</label>
<input type="number" class="js-input" data-field="amount" placeholder="請輸入一般貸款金額">
<span class="unit">萬元</span>
</div>
<div class="form-group">
<label>寬限期</label>
<select class="js-select gracePeriod" data-field="graceYears"></select>
</div>
<div class="form-group">
<label>利率方式</label>
<select class="js-select js-rate-type" data-field="rateType">
<option value="single">單一利率</option>
<option value="multi">多段利率</option>
</select>
</div>
<div class="rates-panel js-single-rate single-rate" data-field="singleRate">
<div class="form-group">
<label>年利率</label>
<select class="bankSelect"></select>
<input type="number" class="js-input rateDisplay" data-field="annualRate" placeholder="例如 2.125">
<span class="suffix">%</span>
</div>
</div>
<div class="rates-panel js-multirates hidden" data-field="multiRates">
<div class="form-group rate-row js-rate-row">
<label>第一段</label>
<input type="number" class="js-start" placeholder="起始月">
<span class="sep">~</span>
<input type="number" class="js-end" placeholder="結束月">
<span class="suffix">月</span>
<input type="number" class="js-rate" placeholder="利率%">
<span class="suffix">%</span>
</div>
<div class="form-group rate-row js-rate-row">
<label>第二段</label>
<input type="number" class="js-start" placeholder="起始月">
<span class="sep">~</span>
<input type="number" class="js-end" placeholder="結束月">
<span class="suffix">月</span>
<input type="number" class="js-rate" placeholder="利率%">
<span class="suffix">%</span>
</div>
<div class="form-group rate-row js-rate-row">
<label>第三段</label>
<input type="number" class="js-start" placeholder="起始月">
<span class="sep">~</span>
<input type="number" class="js-end" placeholder="結束月">
<span class="suffix">月</span>
<input type="number" class="js-rate" placeholder="利率%">
<span class="suffix">%</span>
</div>
</div>
</section>
<div class="form-actions">
<button type="button" class="submit-btn" onclick="calculateLoan(this)">開始計算</button>
</div>
</form>
<!-- 右側結果 -->
<div class="result-container">
<div class="result-card result-eqpi">
<h3>本息平均攤還</h3>
<p class="monthly">0 元/月</p>
<ul class="detail-list">
<li class="principal">本金(0萬元)</li>
<li class="interest">利息(0萬元)</li>
<li class="total">本息合計(0萬元)</li>
</ul>
</div>
<div class="result-card result-eq">
<h3>本金平均攤還</h3>
<p class="monthly">0 元/月</p>
<p style="font-size:12px;color:gray;">每月遞減</p>
<ul class="detail-list">
<li class="principal">本金(0萬元)</li>
<li class="interest">利息(0萬元)</li>
<li class="total">本息合計(0萬元)</li>
</ul>
</div>
</div>
</div>
<!-- 內嵌銀行利率 JSON(Bricks 可放 HTML 分頁) -->
<script type="application/json" id="bankRatesData">
[
{ "bank": "兆豐國際商業銀行", "rate": 0.812 },
{ "bank": "華南商業銀行", "rate": 1.062 },
{ "bank": "臺灣銀行", "rate": 1.94 },
{ "bank": "第一商業銀行", "rate": 2.06 }
]
</script>
</div>
/* 只影響 #loan-calculator 裡的樣式,避免被主題/Bricks 蓋掉 */
#loan-calculator .hidden { display: none !important; }
/* 版面:左表單 / 右結果 */
#loan-calculator .container {
display: grid;
grid-template-columns: minmax(340px, 520px) 1fr;
gap: 24px;
align-items: start;
}
/* 表單卡片 */
#loan-calculator .loan-form,
#loan-calculator .combine-form {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
}
/* 一般欄位三欄版型:標籤 / 欄位 / 單位 */
#loan-calculator .loan-form .form-group:not(.loanType):not(.rate-row):not(.single-rate),
#loan-calculator .combine-form .form-group:not(.loanType):not(.rate-row):not(.single-rate) {
display: grid;
grid-template-columns: 96px 1fr auto;
column-gap: 10px;
align-items: center;
margin-bottom: 14px;
}
/* 基本表單元素 */
#loan-calculator .form-group label { margin: 0 0 6px 0; }
#loan-calculator .form-group input,
#loan-calculator .form-group select {
padding: 8px 10px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 6px;
box-sizing: border-box;
}
#loan-calculator .form-group input[type="radio"] { margin-right: 4px; }
#loan-calculator .unit,
#loan-calculator .suffix,
#loan-calculator .sep { color:#6b7280; font-size: 14px; }
/* 單一利率區 */
#loan-calculator .single-rate { display: grid; grid-template-columns: 96px 1fr auto; column-gap: 10px; align-items: center; }
#loan-calculator .single-rate label { margin: 0; }
/* 多段利率行 */
#loan-calculator .rate-row{
display: grid;
grid-template-columns: 88px 70px 14px 70px 18px 1fr 14px;
column-gap: 6px;
align-items: center;
margin-bottom: 10px;
}
#loan-calculator .rate-row label{ margin:0; white-space:nowrap; font-weight:600; color:#374151; }
#loan-calculator .rate-row input{
min-width:0; height:34px; padding:6px 8px; font-size:14px;
border:1px solid #ccc; border-radius:6px;
}
#loan-calculator .rate-row input[type=number]::-webkit-outer-spin-button,
#loan-calculator .rate-row input[type=number]::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
#loan-calculator .rate-row input[type=number]{ -moz-appearance:textfield; }
/* 結果卡片 */
#loan-calculator .result-container { display:flex; gap:24px; }
#loan-calculator .result-card {
border: 1px solid #eee; background:#fff; border-radius:8px;
width: 260px; padding: 16px; text-align:center;
box-shadow: 0 2px 8px rgba(0,0,0,.05);
}
#loan-calculator .result-card h3 { font-size:16px; margin:0 0 8px; }
#loan-calculator .result-card .monthly { font-size:22px; font-weight:700; color:#2c3e50; margin:8px 0; }
#loan-calculator .result-card .detail-list { list-style:none; padding:0; margin:0; text-align:left; font-size:13px; color:#333; }
#loan-calculator .result-card .detail-list li { margin:4px 0; }
/* RWD */
@media (max-width: 960px) {
#loan-calculator .container { grid-template-columns: 1fr; }
#loan-calculator .result-container { flex-direction: column; }
}
/* ========= Root & Helpers ========= */
const ROOT = document.getElementById('loan-calculator') || document;
const $ = (sel, root = ROOT) => root.querySelector(sel);
const $$ = (sel, root = ROOT) => Array.from(root.querySelectorAll(sel));
/* ========= Forms ========= */
const generalForm = $('form.loan-form');
const combineForm = $('form.combine-form');
function switchForm(mode) {
if (!generalForm || !combineForm) return;
if (mode === 'combine') {
generalForm.classList.add('hidden');
combineForm.classList.remove('hidden');
} else {
combineForm.classList.add('hidden');
generalForm.classList.remove('hidden');
}
}
/* ========= 下拉初始化:寬限期 / 年期 ========= */
function populateGracePeriods(root = ROOT) {
$$('select.gracePeriod', root).forEach(select => {
const max = Number(select.dataset.maxYears) || 5;
const def = Number(select.dataset.defaultYears) || 0;
select.innerHTML = '';
for (let i = 0; i <= max; i++) {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = `${i} 年`;
if (i === def) opt.selected = true;
select.appendChild(opt);
}
});
}
function populateLoanYears(root = ROOT) {
$$('select.js-years, select.loanYears, #loanYears', root).forEach(select => {
const def = Number(select.dataset.defaultYears) || 30;
select.innerHTML = '';
for (let i = 1; i <= 40; i++) {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = `${i} 年(${i * 12} 個月)`;
if (i === def) opt.selected = true;
select.appendChild(opt);
}
});
}
/* ========= 計算方式切換(貸款總額 ↔ 房屋總價/自備款) ========= */
function setupCalcTypeToggle(form) {
if (!form) return;
const calcType = form.querySelector('select.js-calc-type, select.calcType, #calcType');
const loanGroup = form.querySelector('.js-loan-amount-group, #loanAmountGroup');
const houseGrp = form.querySelector('.js-house-group, #houseGroup, .houseGroup');
if (!calcType || !loanGroup || !houseGrp) return;
const toggleDisabled = (el, disabled) => {
el.querySelectorAll('input, select, textarea, button').forEach(c => c.disabled = disabled);
};
const update = () => {
const isHouse = calcType.value === 'house';
// 貸款總額:隱藏 + 禁用 + 清空
loanGroup.classList.toggle('hidden', isHouse);
toggleDisabled(loanGroup, isHouse);
if (isHouse) {
const loanInput = loanGroup.querySelector('.js-loan-amount, #loanAmount');
if (loanInput) loanInput.value = '';
}
// 房屋總價/自備款:顯示 + 啟用
houseGrp.classList.toggle('hidden', !isHouse);
toggleDisabled(houseGrp, !isHouse);
};
calcType.addEventListener('change', update);
update();
}
/* ========= 銀行利率下拉 ========= */
function initBankRateWidgets(data) {
$$('.bankSelect').forEach(select => {
const box = select.closest('.single-rate, .js-single-rate, .form-group') || select.parentElement;
const display = box ? $('.rateDisplay', box) : null;
select.innerHTML = '';
const custom = document.createElement('option');
custom.value = 'custom';
custom.textContent = '自訂利率';
select.appendChild(custom);
(Array.isArray(data) ? data : []).forEach(item => {
const opt = document.createElement('option');
opt.value = item.rate;
opt.textContent = `${item.rate}% 起(${item.bank})`;
select.appendChild(opt);
});
if (display && data?.length) display.value = data[0].rate;
if (select.options.length > 1) select.selectedIndex = 1;
select.addEventListener('change', function () {
if (display && this.value !== 'custom') display.value = this.value;
});
if (display) {
display.addEventListener('input', () => { select.value = 'custom'; });
}
});
}
function initBankRateWidgetsFromJson() {
let bankRates = [];
const el = document.getElementById('bankRatesData');
if (el) {
try { bankRates = JSON.parse(el.textContent.trim()); }
catch (e) { console.error('bankRatesData JSON 解析失敗:', e); }
} else {
console.warn('找不到 #bankRatesData 節點(年利率下拉將只有「自訂利率」)');
}
initBankRateWidgets(bankRates);
}
/* ========= 一般表單:利率方式切換 ========= */
function setupGeneralRateType() {
const form = $('form.loan-form');
if (!form) return;
const select = $('.js-rate-type, .rateTypeSelect, #rateTypeSelect', form);
const single = $('.single-rate, .js-single-rate', form);
let multi = $('.multiRateSection, #multiRateSection, .rates-panel.js-multirates', form);
if (!multi) {
const row = $('.rate-row', form);
if (row) multi = row.closest('div');
}
if (!select || !single || !multi) return;
const update = () => {
const isMulti = select.value === 'multi';
single.classList.toggle('hidden', isMulti);
multi.classList.toggle('hidden', !isMulti);
};
update();
select.addEventListener('change', update);
}
/* ========= 組合表單:一般段利率方式切換 ========= */
function setupCombineRateType() {
const form = $('form.combine-form');
if (!form) return;
$$('.segment.general .js-rate-type, .segment.general .rateTypeSelect, .segment.general #rateTypeSelect', form)
.forEach(select => {
const seg = select.closest('.segment.general');
const singleBox = $('.js-single-rate, .single-rate', seg);
const multiBox = $('.js-multirates', seg);
if (!singleBox || !multiBox) return;
const update = () => {
const isMulti = select.value === 'multi';
singleBox.classList.toggle('hidden', isMulti);
multiBox.classList.toggle('hidden', !isMulti);
};
update();
select.addEventListener('change', update);
});
}
/* ========= 多段利率驗證 ========= */
function validateMultiRates(scope, loanYears) {
const totalMonths = loanYears * 12;
const starts = $$('.js-start, .start-month', scope);
const ends = $$('.js-end, .end-month', scope);
const rates = $$('.js-rate, .rate', scope);
const segs = [];
const L = Math.max(starts.length, ends.length, rates.length);
for (let i = 0; i < L; i++) {
const s = parseInt(starts[i]?.value, 10);
const e = parseInt(ends[i]?.value, 10);
const r = parseFloat(rates[i]?.value);
if ([s, e, r].every(v => Number.isNaN(v))) continue; // 全空略過
if ([s, e, r].some (v => Number.isNaN(v))) return alert(`第 ${i + 1} 段利率輸入不完整`), null;
if (s < 1 || e < s || e > totalMonths) return alert(`第 ${i + 1} 段月份輸入不正確 (1 ~ ${totalMonths})`), null;
segs.push({ startMonth: s, endMonth: e, rate: r });
}
if (segs.length === 0) return alert('請至少輸入一段利率'), null;
segs.sort((a,b) => a.startMonth - b.startMonth);
for (let i = 0; i < segs.length; i++) {
if (i === 0 && segs[i].startMonth !== 1) return alert('第一段必須從第 1 個月開始'), null;
if (i > 0 && segs[i].startMonth !== segs[i-1].endMonth + 1)
return alert(`第 ${i + 1} 段必須緊接上一段月份(${segs[i-1].endMonth + 1} 開始)`), null;
}
if (segs.at(-1).endMonth !== totalMonths) return alert(`最後一段必須剛好到第 ${totalMonths} 月`), null;
return segs;
}
/* ========= 核心計算 ========= */
function calcEqualPrincipalInterest(principal, months, graceMonths = 0, monthlyRate) {
if (months <= graceMonths) return { monthlyPay: 0, totalInterest: 0, totalPayment: 0 };
const realMonths = months - graceMonths;
const monthlyPay = monthlyRate === 0
? principal / realMonths
: (principal * monthlyRate * Math.pow(1 + monthlyRate, realMonths)) /
(Math.pow(1 + monthlyRate, realMonths) - 1);
const interestDuringGrace = principal * monthlyRate * graceMonths;
const totalPayment = interestDuringGrace + monthlyPay * realMonths;
const totalInterest = totalPayment - principal;
return { monthlyPay, totalInterest, totalPayment };
}
function calcEqualPrincipal(principal, months, graceMonths = 0, monthlyRate) {
if (months <= graceMonths) return { monthlyPrincipal: 0, totalInterest: 0, totalPayment: 0, firstMonthPay: 0 };
const realMonths = months - graceMonths;
const monthlyPrincipal = principal / realMonths;
let totalInterest = 0;
for (let i = 0; i < realMonths; i++) {
const remain = principal - monthlyPrincipal * i;
totalInterest += remain * monthlyRate;
}
const interestDuringGrace = principal * monthlyRate * graceMonths;
totalInterest += interestDuringGrace;
const totalPayment = principal + totalInterest;
const firstMonthPay = monthlyPrincipal + principal * monthlyRate;
return { monthlyPrincipal, totalInterest, totalPayment, firstMonthPay };
}
function calcMultiRatePI(principal, rateSegments, graceMonths = 0) {
let remain = principal, ti = 0, tp = 0, first = 0, idx = 0;
const lastMonth = rateSegments.at(-1).endMonth;
for (const seg of rateSegments) {
const months = seg.endMonth - seg.startMonth + 1;
const r = seg.rate / 100 / 12;
for (let i = 0; i < months; i++) {
idx++;
if (idx <= graceMonths) {
const interest = remain * r; ti += interest; tp += interest;
if (idx === 1) first = interest;
continue;
}
const remainMonths = lastMonth - idx + 1;
const pay = (remain * r * Math.pow(1 + r, remainMonths)) /
(Math.pow(1 + r, remainMonths) - 1);
const interest = remain * r;
const principalPay = pay - interest;
remain -= principalPay; ti += interest; tp += pay;
if (idx === graceMonths + 1) first = pay;
}
}
return { firstMonthPay: first, totalInterest: ti, totalPayment: tp };
}
function calcMultiRateP(principal, rateSegments, graceMonths = 0) {
const totalMonths = rateSegments.reduce((s, seg) => s + (seg.endMonth - seg.startMonth + 1), 0);
const realMonths = totalMonths - graceMonths;
const monthlyPrincipal = principal / realMonths;
let remain = principal, ti = 0, tp = 0, first = 0, idx = 0;
for (const seg of rateSegments) {
const months = seg.endMonth - seg.startMonth + 1;
const r = seg.rate / 100 / 12;
for (let i = 0; i < months; i++) {
idx++;
if (idx <= graceMonths) {
const interest = remain * r; ti += interest; tp += interest;
if (idx === 1) first = interest;
continue;
}
const interest = remain * r;
const pay = monthlyPrincipal + interest;
remain -= monthlyPrincipal; ti += interest; tp += pay;
if (idx === graceMonths + 1) first = pay;
}
}
return { firstMonthPay: first, totalInterest: ti, totalPayment: tp, monthlyPrincipal };
}
/* ========= 組合貸款:三欄同步(輸入時自動補,不跳警示) ========= */
function setupCombineTriadSync() {
const form = $('form.combine-form');
if (!form) return;
const totalInput = form.querySelector('.js-loan-amount[data-field="totalAmount"]') || form.querySelector('.js-loan-amount');
const youthInput = form.querySelector('.segment[data-segment="youth"] .js-input[data-field="amount"]');
const generalInput = form.querySelector('.segment[data-segment="general"] .js-input[data-field="amount"]');
if (!totalInput || !youthInput || !generalInput) return;
let updating = false;
const num = v => { const n = Number(v); return Number.isFinite(n) ? n : NaN; };
const clamp = v => (Number.isFinite(v) ? Math.max(0, v) : '');
const syncFrom = source => {
if (updating) return; updating = true;
let t = num(totalInput.value), y = num(youthInput.value), g = num(generalInput.value);
const hasT = Number.isFinite(t), hasY = Number.isFinite(y), hasG = Number.isFinite(g);
if (source === 'total') {
if (hasT && hasY && !hasG) generalInput.value = clamp(t - y);
else if (hasT && hasG && !hasY) youthInput.value = clamp(t - g);
} else if (source === 'youth') {
if (hasT && !hasG) generalInput.value = clamp(t - y);
else if (!hasT && hasG) totalInput.value = clamp(y + g);
} else if (source === 'general') {
if (hasT && !hasY) youthInput.value = clamp(t - g);
else if (!hasT && hasY) totalInput.value = clamp(y + g);
}
updating = false;
};
totalInput.addEventListener('input', () => syncFrom('total'));
youthInput.addEventListener('input', () => syncFrom('youth'));
generalInput.addEventListener('input',() => syncFrom('general'));
syncFrom('total');
}
/* ========= 結果呈現 ========= */
function getEqpiCard() { return $('#result-eqpi') || $('.result-card.result-eqpi'); }
function getEqpCard() { return $('#result-eqp') || $('.result-card.result-eq'); }
function updateResultCards(eqPI, eqP, loanAmount) {
const eqpi = getEqpiCard(), eqp = getEqpCard(); if (!eqpi || !eqp) return;
eqpi.querySelector('.monthly').textContent = `${Math.round(eqPI.monthlyPay).toLocaleString()} 元/月`;
eqpi.querySelector('.principal').textContent = `本金(${Math.round(loanAmount/10000)} 萬元)`;
eqpi.querySelector('.interest').textContent = `利息(${Math.round(eqPI.totalInterest/10000)} 萬元)`;
eqpi.querySelector('.total').textContent = `本息合計(${Math.round(eqPI.totalPayment/10000)} 萬元)`;
eqp.querySelector('.monthly').textContent = `${Math.round(eqP.firstMonthPay).toLocaleString()} 元/月`;
eqp.querySelector('.principal').textContent = `本金(${Math.round(loanAmount/10000)} 萬元)`;
eqp.querySelector('.interest').textContent = `利息(${Math.round(eqP.totalInterest/10000)} 萬元)`;
eqp.querySelector('.total').textContent = `本息合計(${Math.round(eqP.totalPayment/10000)} 萬元)`;
}
function updateMultiResultCard(multiPI, multiP, loanAmount) {
const eqpi = getEqpiCard(), eqp = getEqpCard(); if (!eqpi || !eqp) return;
eqpi.querySelector('.monthly').textContent = `${Math.round(multiPI.firstMonthPay).toLocaleString()} 元/月`;
eqpi.querySelector('.principal').textContent = `本金(${Math.round(loanAmount/10000)} 萬元)`;
eqpi.querySelector('.interest').textContent = `利息(${Math.round(multiPI.totalInterest/10000)} 萬元)`;
eqpi.querySelector('.total').textContent = `本息合計(${Math.round(multiPI.totalPayment/10000)} 萬元)`;
eqp.querySelector('.monthly').textContent = `${Math.round(multiP.firstMonthPay).toLocaleString()} 元/月`;
eqp.querySelector('.principal').textContent = `本金(${Math.round(loanAmount/10000)} 萬元)`;
eqp.querySelector('.interest').textContent = `利息(${Math.round(multiP.totalInterest/10000)} 萬元)`;
eqp.querySelector('.total').textContent = `本息合計(${Math.round(multiP.totalPayment/10000)} 萬元)`;
}
/* ========= 主計算 ========= */
function calculateLoan(triggerEl) {
const form = triggerEl?.closest('form') || $('form.loan-form:not(.hidden), form.combine-form:not(.hidden)');
if (!form) return;
const getVal = sel => $(sel, form)?.value ?? '';
const loanYears = Number(getVal('select.js-years, .loanYears, #loanYears')) || 30;
const loanMonths = loanYears * 12;
const graceYears = Number(getVal('select.gracePeriod')) || 0;
const graceMonths= graceYears * 12;
const calcType = getVal('select.js-calc-type, select.calcType, #calcType') || 'loan';
let loanAmount = 0;
if (calcType === 'loan') {
loanAmount = Number(getVal('.js-loan-amount, #loanAmount')) * 10000 || 0;
} else {
const house = Number(getVal('.js-house-price, #housePrice')) * 10000 || 0;
const down = Number(getVal('.js-down-payment, #downPayment')) * 10000 || 0;
loanAmount = Math.max(house - down, 0);
}
// 一般/青安(同表單)
if (form.classList.contains('loan-form')) {
const rateType = getVal('.js-rate-type, .rateTypeSelect, #rateTypeSelect') || 'single';
if (rateType === 'multi') {
const segs = validateMultiRates(form, loanYears); if (!segs) return;
const pi = calcMultiRatePI(loanAmount, segs, graceMonths);
const p = calcMultiRateP (loanAmount, segs, graceMonths);
updateMultiResultCard(pi, p, loanAmount);
} else {
const annual = Number(getVal('.rateDisplay, #rateDisplay')) || 0;
const r = annual / 100 / 12;
const pi = calcEqualPrincipalInterest(loanAmount, loanMonths, graceMonths, r);
const p = calcEqualPrincipal (loanAmount, loanMonths, graceMonths, r);
updateResultCards(pi, p, loanAmount);
}
return;
}
// 組合
if (form.classList.contains('combine-form')) {
// 三欄驗算 & 自動補(單位:萬元)
const totalEl = form.querySelector('.js-loan-amount[data-field="totalAmount"]') || form.querySelector('.js-loan-amount');
const youthEl = form.querySelector('.segment[data-segment="youth"] .js-input[data-field="amount"]');
const generalEl = form.querySelector('.segment[data-segment="general"] .js-input[data-field="amount"]');
let t = Number(totalEl?.value ?? NaN), y = Number(youthEl?.value ?? NaN), g = Number(generalEl?.value ?? NaN);
const hasT = Number.isFinite(t), hasY = Number.isFinite(y), hasG = Number.isFinite(g);
const filled = [hasT, hasY, hasG].filter(Boolean).length;
if (filled < 2) { alert('請至少輸入下列三者中的二者:\n- 貸款總額\n- 青安金額\n- 一般金額'); return; }
if (!hasT && hasY && hasG) { t = y + g; totalEl.value = t >= 0 ? t : 0; }
else if (hasT && !hasY && hasG) { y = t - g; youthEl.value = y >= 0 ? y : 0; }
else if (hasT && hasY && !hasG) { g = t - y; generalEl.value = g >= 0 ? g : 0; }
if ([t, y, g].every(Number.isFinite) && Math.abs(t - (y + g)) > 1e-6) {
alert('「貸款總額」必須等於「青安金額」+「一般金額」'); return;
}
const youthAmt = (Number(youthEl.value) || 0) * 10000;
const genAmt = (Number(generalEl.value) || 0) * 10000;
// 青安(多段)
const youthSeg = $('.segment[data-segment="youth"], .segment.youth', form);
const youthGY = Number($('.gracePeriod', youthSeg)?.value || 0);
const youthGM = youthGY * 12;
const youthY = Number($('.js-years', youthSeg)?.value || loanYears);
const youthRs = youthAmt > 0 ? validateMultiRates(youthSeg, youthY) : null;
if (youthAmt > 0 && !youthRs) return;
let youthPI = { firstMonthPay: 0, totalInterest: 0, totalPayment: 0 };
let youthP = { firstMonthPay: 0, totalInterest: 0, totalPayment: 0 };
if (youthAmt > 0 && youthRs) {
youthPI = calcMultiRatePI(youthAmt, youthRs, youthGM);
youthP = calcMultiRateP (youthAmt, youthRs, youthGM);
}
// 一般(單一或多段)
const genSeg = $('.segment[data-segment="general"], .segment.general', form);
const genGY = Number($('.gracePeriod', genSeg)?.value || 0);
const genGM = genGY * 12;
const genY = Number($('.js-years', genSeg)?.value || loanYears);
const genType = $('.js-rate-type, .rateTypeSelect, #rateTypeSelect', genSeg)?.value || 'single';
let genPI = { firstMonthPay: 0, totalInterest: 0, totalPayment: 0 };
let genP = { firstMonthPay: 0, totalInterest: 0, totalPayment: 0 };
if (genAmt > 0) {
if (genType === 'multi') {
const segs = validateMultiRates(genSeg, genY); if (!segs) return;
genPI = calcMultiRatePI(genAmt, segs, genGM);
genP = calcMultiRateP (genAmt, segs, genGM);
} else {
const annual = Number($('.rateDisplay', genSeg)?.value || 0);
const r = annual / 100 / 12;
genPI = calcEqualPrincipalInterest(genAmt, genY * 12, genGM, r);
genP = calcEqualPrincipal (genAmt, genY * 12, genGM, r);
}
}
const totalAmount = youthAmt + genAmt;
updateMultiResultCard(
{
firstMonthPay: (youthPI.firstMonthPay||0) + (genPI.firstMonthPay||0),
totalInterest: (youthPI.totalInterest||0) + (genPI.totalInterest||0),
totalPayment : (youthPI.totalPayment ||0) + (genPI.totalPayment ||0),
},
{
firstMonthPay: (youthP.firstMonthPay||0) + (genP.firstMonthPay||0),
totalInterest: (youthP.totalInterest||0) + (genP.totalInterest||0),
totalPayment : (youthP.totalPayment ||0) + (genP.totalPayment ||0),
},
totalAmount
);
}
}
/* ========= 貸款類型切換(一般/青安/組合) ========= */
function setupTypeRadios() {
$$('input[name="type"]').forEach(radio => {
radio.addEventListener('change', () => {
const selected = $('input[name="type"]:checked');
if (!selected) return;
if (selected.classList.contains('combine')) {
switchForm('combine'); return;
}
switchForm('general');
// 青安 → 強制多段
const form = generalForm;
const rateTypeWrap =
($('.js-rate-type, .rateTypeSelect, #rateTypeSelect', form)?.closest('.form-group')) || null;
const single = $('.single-rate, .js-single-rate', form);
let multi = $('.multiRateSection, #multiRateSection, .rates-panel.js-multirates', form);
if (!multi) {
const row = $('.rate-row', form);
if (row) multi = row.closest('div');
}
if (!single || !multi) return;
if (selected.classList.contains('youth')) {
if (rateTypeWrap) rateTypeWrap.style.display = 'none';
single.classList.add('hidden');
multi.classList.remove('hidden');
} else {
if (rateTypeWrap) rateTypeWrap.style.display = '';
setupGeneralRateType();
}
});
});
}
/* ========= 一鍵初始化(Bricks:直接呼叫) ========= */
function initLoanCalculator() {
populateGracePeriods();
populateLoanYears();
initBankRateWidgetsFromJson();
setupCombineTriadSync();
const checked = $('input[name="type"]:checked');
switchForm(checked && checked.classList.contains('combine') ? 'combine' : 'general');
$$('form.loan-form, form.combine-form').forEach(setupCalcTypeToggle);
setupGeneralRateType();
setupCombineRateType();
setupTypeRadios();
}
/* 讓按鈕 onclick 可呼叫 */
window.calculateLoan = calculateLoan;
/* 直接初始化(Bricks Code 元素的 JS 會在元素渲染後執行) */
initLoanCalculator();