feat: 添加预算管理功能,支持分类预算和重置周期设置,优化用户界面

This commit is contained in:
2025-12-02 17:50:26 +08:00
parent fae5a408ff
commit 82fc35b7c9
8 changed files with 517 additions and 58 deletions

View File

@@ -2,6 +2,7 @@
import { computed, onMounted, ref } from 'vue'
import NotificationBridge, { isNativeNotificationBridgeAvailable } from '../lib/notificationBridge'
import { useSettingsStore } from '../stores/settings'
import EchoInput from '../components/EchoInput.vue'
// 从 Vite 注入的版本号(来源于 package.json用于在设置页展示
// eslint-disable-next-line no-undef
@@ -9,6 +10,7 @@ const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0
const settingsStore = useSettingsStore()
const fileInputRef = ref(null)
const editingProfileName = ref(false)
const notificationCaptureEnabled = computed({
get: () => settingsStore.notificationCaptureEnabled,
@@ -56,13 +58,49 @@ const toggleAiAutoCategory = () => {
aiAutoCategoryEnabled.value = !aiAutoCategoryEnabled.value
}
const handleEditProfileName = () => {
const current = settingsStore.profileName || 'Echo 用户'
const input = window.prompt('修改昵称', current)
if (input == null) return
settingsStore.setProfileName(input)
}
const profileNameModel = computed({
get: () => settingsStore.profileName,
set: (value) => settingsStore.setProfileName(value),
})
const budgetAmount = computed({
get: () => (settingsStore.monthlyBudget || 0).toString(),
set: (value) => settingsStore.setMonthlyBudget(value),
})
const budgetCycle = computed({
get: () => settingsStore.budgetResetCycle,
set: (value) => settingsStore.setBudgetResetCycle(value),
})
const categoryBudgets = computed(() => settingsStore.categoryBudgets || {})
const categoryBudgetEnabled = computed({
get: () => settingsStore.categoryBudgetEnabled,
set: (value) => settingsStore.setCategoryBudgetEnabled(value),
})
const monthlyResetDayModel = computed({
get: () => (settingsStore.budgetMonthlyResetDay || 1).toString(),
set: (value) => settingsStore.setBudgetMonthlyResetDay(value),
})
const customStartDateModel = computed({
get: () => settingsStore.budgetCustomStartDate || '',
set: (value) => settingsStore.setBudgetCustomStartDate(value),
})
const hasAnyCategoryBudget = computed(() =>
Object.values(categoryBudgets.value || {}).some((v) => (v || 0) > 0),
)
const budgetCategories = [
{ value: 'Food', label: '餐饮' },
{ value: 'Transport', label: '通勤' },
{ value: 'Health', label: '健康' },
{ value: 'Groceries', label: '买菜' },
{ value: 'Entertainment', label: '娱乐' },
{ value: 'Uncategorized', label: '其他' },
]
const handleAvatarClick = () => {
if (fileInputRef.value) {
fileInputRef.value.click()
@@ -137,21 +175,152 @@ onMounted(() => {
/>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="font-bold text-lg text-stone-800">
<div class="flex items-center gap-2 mb-1">
<template v-if="editingProfileName">
<input
v-model="profileNameModel"
class="bg-stone-50 rounded-full px-3 py-1 text-sm font-bold text-stone-800 border border-stone-200 focus:outline-none focus:border-stone-400"
placeholder="输入要显示的昵称"
/>
</template>
<h3
v-else
class="font-bold text-lg text-stone-800"
>
{{ settingsStore.profileName }}
</h3>
<button
class="text-[11px] text-stone-400 underline-offset-2 hover:text-stone-600"
@click="handleEditProfileName"
class="w-6 h-6 rounded-full bg-stone-50 flex items-center justify-center text-[10px] text-stone-500 border border-stone-100"
@click="editingProfileName = !editingProfileName"
>
编辑
<i class="ph-bold ph-pencil-simple" />
</button>
</div>
<p class="text-xs text-stone-400">本地优先 · 数据只存这台设备</p>
<p class="text-xs text-stone-400 mb-2">数据只存放在本地</p>
</div>
</div>
<!-- 预算管理 -->
<section>
<h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2">
预算管理
</h3>
<div class="bg-white rounded-3xl p-4 shadow-sm border border-stone-100 space-y-4">
<EchoInput
v-model="budgetAmount"
label="全部预算金额(元)"
type="number"
inputmode="decimal"
placeholder="例如 8000"
/>
<div class="flex items-center justify-between">
<span class="text-[11px] text-stone-400 font-bold">重置周期</span>
<div class="flex gap-2">
<button
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
:class="
budgetCycle === 'monthly'
? 'bg-gradient-warm text-white border-transparent shadow-sm'
: 'bg-stone-50 text-stone-500 border-stone-200'
"
@click="budgetCycle = 'monthly'"
>
每月
</button>
<button
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
:class="
budgetCycle === 'weekly'
? 'bg-gradient-warm text-white border-transparent shadow-sm'
: 'bg-stone-50 text-stone-500 border-stone-200'
"
@click="budgetCycle = 'weekly'"
>
每周
</button>
<button
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
:class="
budgetCycle === 'none'
? 'bg-gradient-warm text-white border-transparent shadow-sm'
: 'bg-stone-50 text-stone-500 border-stone-200'
"
@click="budgetCycle = 'none'"
>
不重置
</button>
<button
class="px-3 py-1.5 rounded-full text-[11px] font-bold border transition"
:class="
budgetCycle === 'custom'
? 'bg-gradient-warm text-white border-transparent shadow-sm'
: 'bg-stone-50 text-stone-500 border-stone-200'
"
@click="budgetCycle = 'custom'"
>
自定义
</button>
</div>
</div>
<div v-if="budgetCycle === 'monthly'" class="mt-3">
<EchoInput
v-model="monthlyResetDayModel"
label="每月第几天重置1-28"
type="number"
inputmode="numeric"
placeholder="例如 1 表示每月 1 号"
/>
</div>
<div v-else-if="budgetCycle === 'custom'" class="mt-3">
<EchoInput
v-model="customStartDateModel"
label="自定义起始日"
type="date"
hint="从该日期开始作为预算统计区间的起点"
/>
</div>
<div class="pt-2 space-y-2">
<div class="flex items-center justify-between">
<p class="text-[11px] text-stone-400 font-bold">分类预算可选</p>
<button
class="w-10 h-5 rounded-full px-0.5 flex items-center transition-all duration-200"
:class="
categoryBudgetEnabled
? 'bg-orange-400 justify-end'
: 'bg-stone-200 justify-start'
"
@click="categoryBudgetEnabled = !categoryBudgetEnabled"
>
<div class="w-4 h-4 bg-white rounded-full shadow-sm" />
</button>
</div>
<div
v-if="categoryBudgetEnabled || hasAnyCategoryBudget"
class="space-y-3"
>
<div
v-for="cat in budgetCategories"
:key="cat.value"
class="border border-stone-100 rounded-2xl px-3 py-2 bg-stone-50/40"
>
<EchoInput
:model-value="(categoryBudgets[cat.value] || 0) ? String(categoryBudgets[cat.value]) : ''"
:label="`${cat.label}预算(元)`"
type="number"
inputmode="decimal"
placeholder="留空则不启用该分类预算"
@update:modelValue="(val) => settingsStore.setCategoryBudget(cat.value, val)"
/>
</div>
</div>
</div>
</div>
</section>
<!-- 自动化 & AI -->
<section>
<h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2">
@@ -285,7 +454,7 @@ onMounted(() => {
</section>
<div class="text-center pt-4">
<p class="text-xs text-stone-300">Echo · Local-first · v{{ appVersion }}</p>
<p class="text-xs text-stone-300">Echo · v{{ appVersion }}</p>
</div>
</div>
</template>