第一次上传
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<view class="app-page" :class="themeClass">
|
||||
<section-card title="月度预算" subtitle="设置总预算与分类预算,控制消费节奏">
|
||||
<view class="budget-hero surface-strong">
|
||||
<view>
|
||||
<text class="tiny-text">本月总预算</text>
|
||||
<text class="budget-number">{{ formatCurrency(totalBudget) }}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="tiny-text">本月已支出</text>
|
||||
<text class="budget-number negative">{{ formatCurrency(spentTotal) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: usagePercentWidth }"></view>
|
||||
</view>
|
||||
<view class="budget-foot">
|
||||
<text class="tiny-text">使用进度 {{ usagePercentLabel }}</text>
|
||||
<text class="tiny-text">{{ remainingBudget >= 0 ? `剩余 ${formatCurrency(remainingBudget)}` : `已超支 ${formatCurrency(Math.abs(remainingBudget))}` }}</text>
|
||||
</view>
|
||||
<view class="budget-foot secondary-foot">
|
||||
<text class="tiny-text">{{ totalBudget ? `日均可用 ${formatCurrency(dailyAllowance)}` : '设置预算后可查看剩余额度分配' }}</text>
|
||||
</view>
|
||||
<view class="editor-row">
|
||||
<view class="input-shell"><input v-model="totalBudgetInput" type="digit" placeholder="输入本月总预算" /></view>
|
||||
<view class="primary-button save-btn" @click="saveTotalBudget">保存</view>
|
||||
</view>
|
||||
</section-card>
|
||||
|
||||
<section-card v-if="overBudgetRows.length" title="超支提醒" subtitle="当前分类预算已被突破,建议尽快调整">
|
||||
<view class="alert-list">
|
||||
<view v-for="item in overBudgetRows" :key="item.id" class="alert-item">
|
||||
<text class="alert-title">{{ item.name }}</text>
|
||||
<text class="alert-text">预算 {{ formatCurrency(item.budget) }},已支出 {{ formatCurrency(item.spent) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</section-card>
|
||||
|
||||
<section-card title="分类预算" subtitle="为高频分类分别设置预算,减少超支风险">
|
||||
<view class="category-list">
|
||||
<view v-for="item in categoryRows" :key="item.id" class="category-card surface-strong">
|
||||
<view class="category-head">
|
||||
<view class="category-title-row">
|
||||
<view class="category-dot" :style="{ background: item.color }"></view>
|
||||
<text class="category-title">{{ item.name }}</text>
|
||||
</view>
|
||||
<text class="tiny-text">已花 {{ formatCurrency(item.spent) }}</text>
|
||||
</view>
|
||||
<view class="progress-track thin-track">
|
||||
<view class="progress-fill" :style="{ width: item.progressWidth }"></view>
|
||||
</view>
|
||||
<view class="budget-foot">
|
||||
<text class="tiny-text">预算 {{ formatCurrency(item.budget) }}</text>
|
||||
<text class="tiny-text" :class="item.budget > 0 && item.spent > item.budget ? 'negative' : ''">{{ item.progressLabel }}</text>
|
||||
</view>
|
||||
<view class="editor-row">
|
||||
<view class="input-shell"><input v-model="categoryBudgetDrafts[item.id]" type="digit" placeholder="设置分类预算" /></view>
|
||||
<view class="ghost-button save-btn" @click="saveCategoryBudget(item.id)">保存</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</section-card>
|
||||
|
||||
<app-tab-bar current="budget" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import SectionCard from '../../components/SectionCard.vue'
|
||||
import AppTabBar from '../../components/AppTabBar.vue'
|
||||
import { useAppStore } from '../../utils/store'
|
||||
import { getDaysLeftInMonth, isSameMonth, toMonthKey } from '../../utils/date'
|
||||
import { clampPercent, formatCurrency, formatPercent } from '../../utils/money'
|
||||
|
||||
const store = useAppStore()
|
||||
const currentMonth = computed(() => toMonthKey())
|
||||
const themeClass = computed(() => (store.state.settings.theme === 'dark' ? 'theme-dark' : ''))
|
||||
const totalBudgetInput = ref(String(store.state.budgets.total || ''))
|
||||
const categoryBudgetDrafts = reactive({})
|
||||
|
||||
const expenseBills = computed(() => store.state.bills.filter((item) => item.type === 'expense' && isSameMonth(item.date, currentMonth.value)))
|
||||
const spentTotal = computed(() => expenseBills.value.reduce((sum, item) => sum + Number(item.amount), 0))
|
||||
const totalBudget = computed(() => Number(store.state.budgets.total) || 0)
|
||||
const remainingBudget = computed(() => totalBudget.value - spentTotal.value)
|
||||
const dailyAllowance = computed(() => Math.max(remainingBudget.value, 0) / Math.max(1, getDaysLeftInMonth(currentMonth.value)))
|
||||
const usagePercentLabel = computed(() => (totalBudget.value ? formatPercent(spentTotal.value / Math.max(totalBudget.value, 1)) : '未设置'))
|
||||
const usagePercentWidth = computed(() => clampPercent(spentTotal.value / Math.max(totalBudget.value || 1, 1)))
|
||||
|
||||
const categoryRows = computed(() => store.state.categories.expense.map((category) => {
|
||||
const spent = expenseBills.value
|
||||
.filter((item) => item.categoryId === category.id)
|
||||
.reduce((sum, item) => sum + Number(item.amount), 0)
|
||||
const budget = Number(store.state.budgets.categoryBudgets[category.id] || 0)
|
||||
return {
|
||||
...category,
|
||||
spent,
|
||||
budget,
|
||||
progressLabel: budget ? formatPercent(spent / Math.max(budget, 1)) : '未设置',
|
||||
progressWidth: clampPercent(spent / Math.max(budget || 1, 1))
|
||||
}
|
||||
}))
|
||||
|
||||
const overBudgetRows = computed(() => categoryRows.value.filter((item) => item.budget > 0 && item.spent > item.budget))
|
||||
|
||||
watch(
|
||||
() => store.state.budgets.total,
|
||||
(value) => {
|
||||
totalBudgetInput.value = String(value || '')
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
categoryRows,
|
||||
(rows) => {
|
||||
rows.forEach((item) => {
|
||||
categoryBudgetDrafts[item.id] = String(item.budget || '')
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function saveTotalBudget() {
|
||||
store.setBudgetTotal(totalBudgetInput.value)
|
||||
uni.showToast({ title: '总预算已保存', icon: 'none' })
|
||||
}
|
||||
|
||||
function saveCategoryBudget(categoryId) {
|
||||
store.setCategoryBudget(categoryId, categoryBudgetDrafts[categoryId])
|
||||
uni.showToast({ title: '分类预算已保存', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.budget-hero,
|
||||
.category-card {
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.budget-hero,
|
||||
.category-head,
|
||||
.category-title-row,
|
||||
.budget-foot,
|
||||
.editor-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.secondary-foot {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.budget-number {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
overflow: hidden;
|
||||
height: 18rpx;
|
||||
margin: 24rpx 0 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.thin-track {
|
||||
height: 14rpx;
|
||||
margin: 18rpx 0 14rpx;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #5f8df5 0%, #1f6f5f 100%);
|
||||
}
|
||||
|
||||
.editor-row {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.editor-row .input-shell {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
flex: 0 0 180rpx;
|
||||
}
|
||||
|
||||
.alert-list,
|
||||
.category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 22rpx;
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.category-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user