-
+
+
-
+
分类
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 23ac704..372cfdf 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -12,7 +12,7 @@ const router = useRouter()
const transactionStore = useTransactionStore()
const settingsStore = useSettingsStore()
const uiStore = useUiStore()
-const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions } =
+const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions, sortedTransactions } =
storeToRefs(transactionStore)
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
useTransactionEntry()
@@ -44,6 +44,7 @@ onBeforeUnmount(() => {
})
const monthlyBudget = computed(() => settingsStore.monthlyBudget || 0)
+const budgetResetCycle = computed(() => settingsStore.budgetResetCycle || 'monthly')
const currentDayLabel = computed(
() =>
@@ -54,8 +55,46 @@ const currentDayLabel = computed(
}).format(new Date()),
)
+const periodStart = computed(() => {
+ const cycle = budgetResetCycle.value
+ const now = new Date()
+ if (cycle === 'weekly') {
+ const day = now.getDay() || 7 // 周一=1,周日=7
+ const diff = day - 1
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff)
+ }
+ if (cycle === 'monthly') {
+ const day = settingsStore.budgetMonthlyResetDay || 1
+ const safeDay = Math.min(Math.max(Number(day) || 1, 1), 28)
+ const candidate = new Date(now.getFullYear(), now.getMonth(), safeDay)
+ if (now >= candidate) {
+ return candidate
+ }
+ return new Date(now.getFullYear(), now.getMonth() - 1, safeDay)
+ }
+ if (cycle === 'custom') {
+ if (!settingsStore.budgetCustomStartDate) return null
+ return new Date(settingsStore.budgetCustomStartDate)
+ }
+ // none:不限制周期
+ return null
+})
+
+const periodExpense = computed(() => {
+ const cycle = budgetResetCycle.value
+ const start = periodStart.value
+ if (cycle === 'none' || !start) {
+ return totalExpense.value || 0
+ }
+ return (sortedTransactions.value || []).reduce((sum, tx) => {
+ if (tx.amount >= 0) return sum
+ const date = new Date(tx.date)
+ return date >= start ? sum + tx.amount : sum
+ }, 0)
+})
+
const budgetUsage = computed(() => {
- const expense = Math.abs(totalExpense.value || 0)
+ const expense = Math.abs(periodExpense.value || 0)
const budget = monthlyBudget.value || 0
if (!budget) return 0
return Math.min(expense / budget, 1)
@@ -63,7 +102,7 @@ const budgetUsage = computed(() => {
const balance = computed(() => (totalIncome.value || 0) + (totalExpense.value || 0))
const remainingBudget = computed(() =>
- Math.max((monthlyBudget.value || 0) - Math.abs(totalExpense.value || 0), 0),
+ Math.max((monthlyBudget.value || 0) - Math.abs(periodExpense.value || 0), 0),
)
const todayIncomeValue = computed(() => todaysIncome.value || 0)
@@ -91,18 +130,6 @@ const categoryMeta = {
const getCategoryMeta = (category) => categoryMeta[category] || categoryMeta.default
-const handleEditBudget = () => {
- const current = monthlyBudget.value || 0
- const input = window.prompt('设置本月预算(元)', current ? String(current) : '')
- if (input == null) return
- const numeric = Number(input)
- if (!Number.isFinite(numeric) || numeric < 0) {
- window.alert('请输入合法的预算金额')
- return
- }
- settingsStore.setMonthlyBudget(numeric)
-}
-
const goSettings = () => {
router.push({ name: 'settings' })
}
@@ -185,16 +212,16 @@ const openTransactionDetail = (tx) => {
-
本月预算
+
本期预算
{{ Math.round(budgetUsage * 100) }}%
@@ -205,9 +232,29 @@ const openTransactionDetail = (tx) => {
/>
- 已用 {{ formatCurrency(Math.abs(totalExpense)) }}
+ 已用 {{ formatCurrency(Math.abs(periodExpense)) }}
剩余 {{ formatCurrency(remainingBudget) }}
+
+
+ 周期:
+ {{
+ budgetResetCycle === 'weekly'
+ ? '每周重置'
+ : budgetResetCycle === 'none'
+ ? '不重置'
+ : budgetResetCycle === 'custom'
+ ? '自定义'
+ : '每月重置'
+ }}
+
+
+
diff --git a/src/views/ListView.vue b/src/views/ListView.vue
index 74578f4..5c13ca1 100644
--- a/src/views/ListView.vue
+++ b/src/views/ListView.vue
@@ -15,6 +15,7 @@ const categoryChips = [
{ label: '通勤', value: 'Transport', icon: 'ph-taxi' },
{ label: '健康', value: 'Health', icon: 'ph-heartbeat' },
{ label: '买菜', value: 'Groceries', icon: 'ph-basket' },
+ { label: '娱乐', value: 'Entertainment', icon: 'ph-game-controller' },
{ label: '收入', value: 'Income', icon: 'ph-wallet' },
{ label: '其他', value: 'Uncategorized', icon: 'ph-dots-three-outline' },
]
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
index 7f7429d..ef26aa7 100644
--- a/src/views/SettingsView.vue
+++ b/src/views/SettingsView.vue
@@ -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(() => {
/>
-
-
+
+
+
+
+
{{ settingsStore.profileName }}
- 本地优先 · 数据只存这台设备
+
数据只存放在本地
+
+
+
+ 预算管理
+
+
+
+
+
重置周期
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settingsStore.setCategoryBudget(cat.value, val)"
+ />
+
+
+
+
+
+
@@ -285,7 +454,7 @@ onMounted(() => {
-
Echo · Local-first · v{{ appVersion }}
+
Echo · v{{ appVersion }}