fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
SunCheng
2026-02-20 22:07:09 +08:00
parent 3c3172fc81
commit a7414c792e
11 changed files with 498 additions and 201 deletions

View File

@@ -508,6 +508,11 @@ const handleQueryBills = async () => {
}
const percentage = computed(() => {
// 优先使用后端返回的 usagePercentage 字段
if (props.budget.usagePercentage !== undefined && props.budget.usagePercentage !== null) {
return Math.round(props.budget.usagePercentage)
}
// 降级方案:如果后端没有返回该字段,前端计算
if (!props.budget.limit) {
return 0
}

View File

@@ -92,50 +92,68 @@
<van-icon name="balance-o" />
收入明细
</div>
<div class="detail-table">
<div
v-for="item in currentBudget.details.incomeItems"
:key="item.id"
class="detail-item"
>
<div class="item-header">
<span class="item-name">{{ item.name }}</span>
<van-tag
size="mini"
:type="item.type === 1 ? 'default' : 'primary'"
<div class="rich-html-content">
<table>
<thead>
<tr>
<th>名称</th>
<th>预算额度</th>
<th>实际金额</th>
<th>计算用</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in currentBudget.details.incomeItems"
:key="item.id"
>
{{ item.type === 1 ? '月度' : '年度' }}
</van-tag>
</div>
<div class="item-amounts">
<div class="amount-row">
<span class="amount-label">预算</span>
<span class="amount-value">¥{{ formatMoney(item.budgetLimit) }}</span>
</div>
<div class="amount-row">
<span class="amount-label">实际</span>
<span
class="amount-value"
:class="{ warning: item.isOverBudget }"
>
¥{{ formatMoney(item.actualAmount) }}
</span>
</div>
<div class="amount-row highlight">
<span class="amount-label">计算用</span>
<span class="amount-value income">¥{{ formatMoney(item.effectiveAmount) }}</span>
</div>
</div>
<div class="item-note">
<van-tag
size="mini"
plain
:type="item.isOverBudget ? 'warning' : 'success'"
>
{{ item.calculationNote }}
</van-tag>
</div>
</div>
<td>
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<span>{{ item.name }}</span>
<van-tag
size="mini"
plain
:type="item.type === 1 ? 'default' : 'primary'"
>
{{ item.type === 1 ? '月' : '年' }}
</van-tag>
<van-tag
v-if="item.isArchived"
size="mini"
type="success"
>
已归档
</van-tag>
</div>
</td>
<td>{{ formatMoney(item.budgetLimit) }}</td>
<td>
<span
class="income-value"
:class="{ 'expense-value': item.isOverBudget }"
>
{{ formatMoney(item.actualAmount) }}
</span>
</td>
<td>
<span class="income-value">{{ formatMoney(item.effectiveAmount) }}</span>
</td>
</tr>
</tbody>
</table>
<p style="margin-top: 12px;">
<strong>收入预算合计:</strong>
<template v-if="hasArchivedIncome">
已归档 <span class="income-value"><strong>{{ formatMoney(archivedIncomeTotal) }}</strong></span>
+ 未来预算 <span class="income-value"><strong>{{ formatMoney(futureIncomeTotal) }}</strong></span>
= <span class="income-value"><strong>{{ formatMoney(currentBudget.details.summary.totalIncomeBudget) }}</strong></span>
</template>
<template v-else>
<span class="income-value">
<strong>{{ formatMoney(currentBudget.details.summary.totalIncomeBudget) }}</strong>
</span>
</template>
</p>
</div>
</div>
@@ -145,58 +163,70 @@
<van-icon name="bill-o" />
支出明细
</div>
<div class="detail-table">
<div
v-for="item in currentBudget.details.expenseItems"
:key="item.id"
class="detail-item"
:class="{ overbudget: item.isOverBudget }"
>
<div class="item-header">
<span class="item-name">{{ item.name }}</span>
<van-tag
size="mini"
:type="item.type === 1 ? 'default' : 'primary'"
<div class="rich-html-content">
<table>
<thead>
<tr>
<th>名称</th>
<th>预算额度</th>
<th>实际金额</th>
<th>计算用</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in currentBudget.details.expenseItems"
:key="item.id"
>
{{ item.type === 1 ? '月度' : '年度' }}
</van-tag>
</div>
<div class="item-amounts">
<div class="amount-row">
<span class="amount-label">预算</span>
<span class="amount-value">¥{{ formatMoney(item.budgetLimit) }}</span>
</div>
<div class="amount-row">
<span class="amount-label">实际</span>
<span
class="amount-value"
:class="{ danger: item.isOverBudget }"
>
¥{{ formatMoney(item.actualAmount) }}
</span>
</div>
<div class="amount-row highlight">
<span class="amount-label">计算用</span>
<span class="amount-value expense">¥{{ formatMoney(item.effectiveAmount) }}</span>
</div>
</div>
<div class="item-note">
<van-tag
size="mini"
plain
:type="item.isOverBudget ? 'danger' : 'default'"
>
{{ item.calculationNote }}
</van-tag>
<van-tag
v-if="item.isOverBudget"
size="mini"
type="danger"
>
超支
</van-tag>
</div>
</div>
<td>
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<span>{{ item.name }}</span>
<van-tag
size="mini"
plain
:type="item.type === 1 ? 'default' : 'primary'"
>
{{ item.type === 1 ? '月' : '年' }}
</van-tag>
<van-tag
v-if="item.isArchived"
size="mini"
type="success"
>
已归档
</van-tag>
<van-tag
v-if="item.isOverBudget"
size="mini"
type="danger"
>
超支
</van-tag>
</div>
</td>
<td>{{ formatMoney(item.budgetLimit) }}</td>
<td>
<span class="expense-value">{{ formatMoney(item.actualAmount) }}</span>
</td>
<td>
<span class="expense-value">{{ formatMoney(item.effectiveAmount) }}</span>
</td>
</tr>
</tbody>
</table>
<p style="margin-top: 12px;">
<strong>支出预算合计:</strong>
<template v-if="hasArchivedExpense">
已归档 <span class="expense-value"><strong>{{ formatMoney(archivedExpenseTotal) }}</strong></span>
+ 未来预算 <span class="expense-value"><strong>{{ formatMoney(futureExpenseTotal) }}</strong></span>
= <span class="expense-value"><strong>{{ formatMoney(currentBudget.details.summary.totalExpenseBudget) }}</strong></span>
</template>
<template v-else>
<span class="expense-value">
<strong>{{ formatMoney(currentBudget.details.summary.totalExpenseBudget) }}</strong>
</span>
</template>
</p>
</div>
</div>
@@ -206,28 +236,27 @@
<van-icon name="calculator-o" />
计算汇总
</div>
<div class="formula-box">
<div class="formula-row">
<span class="formula-label">收入合计</span>
<span class="formula-value income">
¥{{ formatMoney(currentBudget.details.summary.totalIncomeBudget) }}
<div class="rich-html-content">
<h3>计算公式</h3>
<p>
<strong>收入预算合计:</strong>
<span class="income-value">
<strong>{{ formatMoney(currentBudget.details.summary.totalIncomeBudget) }}</strong>
</span>
</div>
<div class="formula-row">
<span class="formula-label">支出合计</span>
<span class="formula-value expense">
¥{{ formatMoney(currentBudget.details.summary.totalExpenseBudget) }}
</p>
<p>
<strong>支出预算合计</strong>
<span class="expense-value">
<strong>{{ formatMoney(currentBudget.details.summary.totalExpenseBudget) }}</strong>
</span>
</div>
<div class="formula-row highlight">
<span class="formula-label">计划存款</span>
<span class="formula-value primary">
¥{{ formatMoney(currentBudget.details.summary.plannedSavings) }}
</p>
<p>
<strong>计划存款</strong>
{{ currentBudget.details.summary.calculationFormula }}
= <span class="highlight">
<strong>{{ formatMoney(currentBudget.details.summary.plannedSavings) }}</strong>
</span>
</div>
</div>
<div class="formula-text">
{{ currentBudget.details.summary.calculationFormula }}
</p>
</div>
</div>
</div>
@@ -400,6 +429,45 @@ const incomeCurrent = computed(() => matchedIncomeBudget.value?.current || 0)
const expenseLimit = computed(() => matchedExpenseBudget.value?.limit || 0)
const expenseCurrent = computed(() => matchedExpenseBudget.value?.current || 0)
// 归档和未来预算的汇总 (仅用于年度存款计划)
const hasArchivedIncome = computed(() => {
if (!currentBudget.value?.details) return false
return currentBudget.value.details.incomeItems.some(item => item.isArchived)
})
const archivedIncomeTotal = computed(() => {
if (!currentBudget.value?.details) return 0
return currentBudget.value.details.incomeItems
.filter(item => item.isArchived)
.reduce((sum, item) => sum + item.effectiveAmount, 0)
})
const futureIncomeTotal = computed(() => {
if (!currentBudget.value?.details) return 0
return currentBudget.value.details.incomeItems
.filter(item => !item.isArchived)
.reduce((sum, item) => sum + item.effectiveAmount, 0)
})
const hasArchivedExpense = computed(() => {
if (!currentBudget.value?.details) return false
return currentBudget.value.details.expenseItems.some(item => item.isArchived)
})
const archivedExpenseTotal = computed(() => {
if (!currentBudget.value?.details) return 0
return currentBudget.value.details.expenseItems
.filter(item => item.isArchived)
.reduce((sum, item) => sum + item.effectiveAmount, 0)
})
const futureExpenseTotal = computed(() => {
if (!currentBudget.value?.details) return 0
return currentBudget.value.details.expenseItems
.filter(item => !item.isArchived)
.reduce((sum, item) => sum + item.effectiveAmount, 0)
})
// 辅助函数
const formatMoney = (val) => {
return parseFloat(val || 0).toLocaleString(undefined, {
@@ -647,98 +715,13 @@ const getProgressColor = (budget) => {
padding: 0 8px;
}
/* 明细表格样式 */
/* 明细表格样式 - 使用 rich-html-content 统一样式 */
.detail-tables {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-table {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
background-color: var(--van-light-gray);
border-radius: 8px;
padding: 12px;
border-left: 3px solid var(--van-gray-4);
}
.detail-item.overbudget {
border-left-color: var(--van-danger-color);
background-color: rgba(245, 34, 45, 0.05);
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.item-name {
font-size: 15px;
font-weight: 600;
color: var(--van-text-color);
}
.item-amounts {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 8px;
}
.amount-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
.amount-row.highlight {
padding-top: 6px;
margin-top: 4px;
border-top: 1px dashed var(--van-border-color);
font-weight: 600;
}
.amount-label {
color: var(--van-text-color-2);
}
.amount-value {
font-family: DIN Alternate, system-ui;
font-weight: 600;
color: var(--van-text-color);
}
.amount-value.income {
color: var(--van-success-color);
}
.amount-value.expense {
color: var(--van-danger-color);
}
.amount-value.warning {
color: var(--van-warning-color);
}
.amount-value.danger {
color: var(--van-danger-color);
}
.item-note {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.formula-row {
display: flex;
justify-content: space-between;