feat: 添加预算管理功能,支持分类预算和重置周期设置,优化用户界面
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user