Skip to content

实现 贷款计算器 工具

分类:general | 标签:贷款、房贷、车贷、计算器 支持房贷、车贷、消费贷,等额本息/等额本金计算,月供明细与方案对比

2.1 功能说明

支持房贷、车贷、消费贷,等额本息/等额本金计算,月供明细与方案对比

核心能力

  • 贷款计算器
  • 组合贷

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-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 效果截图

贷款计算器 效果截图

工具访问地址:https://www.i91tools.com/tools/loan-calculator