Appearance
实现 贷款计算器 工具
分类:general | 标签:贷款、房贷、车贷、计算器 支持房贷、车贷、消费贷,等额本息/等额本金计算,月供明细与方案对比
2.1 功能说明
支持房贷、车贷、消费贷,等额本息/等额本金计算,月供明细与方案对比
核心能力
- 贷款计算器
- 组合贷
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
本工具目录 LoanCalculator/ 下除 index.vue 外,还包含以下源文件:
CalculatorPage.vue(1382 行)
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
goBack()monthlyRate()installmentPayment()buildSchedule()sum()money()signMoney()num()calcCombo()calcPrepay()calcLpr()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElTabs, ElTabPane, ElButton, ElIcon } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import CalculatorPage from './CalculatorPage.vue'
import FeedbackForm from '@/components/FeedbackForm.vue'
const router = useRouter()
const activeTab = ref('calculator')
const goBack = () => {
router.push('/general')
}
/* ---------------- 通用金融算法 ---------------- */
function monthlyRate(annualPct) {
return Number(annualPct) / 100 / 12
}
function installmentPayment(principal, i, n) {
if (i === 0) return principal / n
const t = Math.pow(1 + i, n)
return principal * i * t / (t - 1)
}
// 生成还款计划:equal-installment 等额本息,equal-principal 等额本金
function buildSchedule(principal, annualPct, months, method) {
const i = monthlyRate(annualPct)
const rows = []
let balance = principal
if (method === 'equal-principal') {
const p0 = principal / months
for (let k = 1; k <= months; k++) {
const interest = balance * i
const p = k === months ? balance : p0
balance = Math.max(0, balance - p)
rows.push({ k, payment: p + interest, principal: p, interest, balance })
}
} else {
const M = installmentPayment(principal, i, months)
for (let k = 1; k <= months; k++) {
const interest = balance * i
let p = M - interest
if (k === months || p > balance) p = balance
balance = Math.max(0, balance - p)
rows.push({ k, payment: p + interest, principal: p, interest, balance })
}
}
return rows
}
function sum(rows, key) {
return rows.reduce((a, r) => a + r[key], 0)
}
function money(n) {
if (!isFinite(n)) return '-'
return Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' 元'
}
function signMoney(n) {
if (!isFinite(n)) return '-'
if (Math.abs(n) < 0.005) return '基准'
return (n > 0 ? '+' : '') + money(n)
}
function num(v) {
const n = Number(v)
return isFinite(n) ? n : NaN
}
/* ---------------- 组合贷 ---------------- */
const combo = reactive({
bizAmount: '80', bizRate: '3.85', bizYears: '30',
fundAmount: '40', fundRate: '2.85', fundYears: '30',
method: 'equal-installment'
})
const comboResult = ref(null)
const comboError = ref('')
function calcCombo() {
comboError.value = ''
comboResult.value = null
const bizP = num(combo.bizAmount) * 10000
const fundP = num(combo.fundAmount) * 10000
const bizN = Math.round(num(combo.bizYears) * 12)
const fundN = Math.round(num(combo.fundYears) * 12)
const bizR = num(combo.bizRate)
const fundR = num(combo.fundRate)
if (!(bizP >= 0) || !(fundP >= 0) || bizP + fundP <= 0) { comboError.value = '请输入有效的贷款金额,两部分之和需大于 0'; return }
if (bizP > 0 && (!(bizN > 0) || !(bizR >= 0))) { comboError.value = '请输入有效的商贷年限与利率'; return }
if (fundP > 0 && (!(fundN > 0) || !(fundR >= 0))) { comboError.value = '请输入有效的公积金年限与利率'; return }
if (bizN > 600 || fundN > 600) { comboError.value = '贷款年限过长,请控制在 50 年以内'; return }
const bizRows = bizP > 0 ? buildSchedule(bizP, bizR, bizN, combo.method) : []
const fundRows = fundP > 0 ? buildSchedule(fundP, fundR, fundN, combo.method) : []
const months = Math.max(bizRows.length, fundRows.length)
const merged = []
for (let k = 0; k < months; k++) {
const a = bizRows[k]
const b = fundRows[k]
merged.push({
k: k + 1,
payment: (a ? a.payment : 0) + (b ? b.payment : 0),
principal: (a ? a.principal : 0) + (b ? b.principal : 0),
interest: (a ? a.interest : 0) + (b ? b.interest : 0),
balance: (a ? a.balance : 0) + (b ? b.balance : 0)
})
}
const preview = merged.filter(r => r.k <= 12 || (r.k - 1) % 12 === 0 || r.k === months)
comboResult.value = {
months,
totalPrincipal: bizP + fundP,
totalInterest: sum(merged, 'interest'),
totalPayment: sum(merged, 'payment'),
firstPayment: merged[0].payment,
lastPayment: merged[merged.length - 1].payment,
bizInterest: sum(bizRows, 'interest'),
fundInterest: sum(fundRows, 'interest'),
bizFirst: bizRows.length ? bizRows[0].payment : 0,
fundFirst: fundRows.length ? fundRows[0].payment : 0,
preview
}
}
/* ---------------- 提前还款 ---------------- */
const prepay = reactive({
amount: '100', rate: '3.85', years: '30', method: 'equal-installment',
paidMonths: '24', prepayAmount: '20'
})
const prepayResult = ref(null)
const prepayError = ref('')
function calcPrepay() {
prepayError.value = ''
prepayResult.value = null
const P = num(prepay.amount) * 10000
const rate = num(prepay.rate)
const n = Math.round(num(prepay.years) * 12)
const k = Math.round(num(prepay.paidMonths))
const A = num(prepay.prepayAmount) * 10000
const i = monthlyRate(rate)
if (!(P > 0)) { prepayError.value = '请输入有效的贷款金额'; return }
if (!(rate >= 0)) { prepayError.value = '请输入有效的年利率'; return }
if (!(n > 0) || n > 600) { prepayError.value = '请输入有效的贷款年限(不超过 50 年)'; return }
if (!(k >= 0) || k >= n) { prepayError.value = '已还期数需大于等于 0 且小于总期数 ' + n; return }
if (!(A > 0)) { prepayError.value = '请输入有效的提前还款金额'; return }
const origin = buildSchedule(P, rate, n, prepay.method)
const paidRows = origin.slice(0, k)
const paidInterest = sum(paidRows, 'interest')
const balanceBefore = k === 0 ? P : origin[k - 1].balance
const originRestInterest = sum(origin.slice(k), 'interest')
const restMonths = n - k
if (A >= balanceBefore) {
prepayResult.value = {
originFirst: origin[0].payment,
paidInterest,
balanceBefore,
balanceAfter: 0,
originRestInterest,
restMonths,
settled: true
}
return
}
const balanceAfter = balanceBefore - A
// 方案一:缩短期限(保持月供 / 保持每月本金不变)
let shortenMonths = 0
let shortenInterest = 0
let shortenPayment = 0
if (prepay.method === 'equal-principal') {
const p0 = P / n
let bal = balanceAfter
shortenPayment = p0 + bal * i
while (bal > 0.005 && shortenMonths < 1200) {
const interest = bal * i
const p = Math.min(p0, bal)
bal -= p
shortenInterest += interest
shortenMonths++
}
} else {
const M = installmentPayment(P, i, n)
shortenPayment = M
if (M <= balanceAfter * i + 0.005) {
prepayError.value = '原月供不足以覆盖剩余本金的月利息,无法按缩短期限方案测算'
return
}
let bal = balanceAfter
while (bal > 0.005 && shortenMonths < 1200) {
const interest = bal * i
const p = Math.min(M - interest, bal)
bal -= p
shortenInterest += interest
shortenMonths++
}
}
// 方案二:减少月供(保持剩余期数不变)
const reduceRows = buildSchedule(balanceAfter, rate, restMonths, prepay.method)
const reduceInterest = sum(reduceRows, 'interest')
prepayResult.value = {
originFirst: origin[0].payment,
paidInterest,
balanceBefore,
balanceAfter,
originRestInterest,
restMonths,
settled: false,
shorten: {
payment: shortenPayment,
months: shortenMonths,
interest: shortenInterest,
saved: originRestInterest - shortenInterest
},
reduce: {
payment: reduceRows[0].payment,
months: restMonths,
interest: reduceInterest,
saved: originRestInterest - reduceInterest
}
}
}
/* ---------------- LPR 浮动 ---------------- */
const LPR_PRESETS = ['3.1', '3.5', '3.85', '4.2']
const lpr = reactive({
amount: '100', years: '30', base: '3.85', bps: '-60,-30,0,30,60', method: 'equal-installment'
})
const lprRows = ref([])
const lprError = ref('')
function calcLpr() {
lprError.value = ''
lprRows.value = []
const P = num(lpr.amount) * 10000
const n = Math.round(num(lpr.years) * 12)
const base = num(lpr.base)
if (!(P > 0)) { lprError.value = '请输入有效的贷款金额'; return }
if (!(n > 0) || n > 600) { lprError.value = '请输入有效的贷款年限(不超过 50 年)'; return }
if (!(base >= 0)) { lprError.value = '请输入有效的 LPR 基准利率'; return }
const bpList = String(lpr.bps).split(/[,,\s]+/).map(s => s.trim()).filter(Boolean).map(Number)
if (!bpList.length || bpList.some(b => !isFinite(b))) { lprError.value = '加减点需为逗号分隔的数字,如 -60,-30,0,30'; return }
if (bpList.some(b => base + b / 100 < 0)) { lprError.value = '减点后的执行利率不能为负数'; return }
const rows = bpList.map(bp => {
const rate = base + bp / 100
const schedule = buildSchedule(P, rate, n, lpr.method)
return {
bp,
bpLabel: (bp > 0 ? '+' : '') + bp + ' bp',
rateLabel: rate.toFixed(2) + '%',
firstPayment: schedule[0].payment,
totalInterest: sum(schedule, 'interest'),
totalPayment: sum(schedule, 'payment')
}
})
const baseRow = rows.find(r => r.bp === 0) || rows[0]
rows.forEach(r => {
r.diffPayment = r.firstPayment - baseRow.firstPayment
r.diffInterest = r.totalInterest - baseRow.totalInterest
})
rows.sort((a, b) => a.bp - b.bp)
lprRows.value = rows
}2.3 效果截图
