From 82fc35b7c9be58be0bcf91a62d1a51379300a322 Mon Sep 17 00:00:00 2001 From: Jafeng <2998840497@qq.com> Date: Tue, 2 Dec 2025 17:50:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=A2=84=E7=AE=97?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E9=A2=84=E7=AE=97=E5=92=8C=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E8=AE=BE=E7=BD=AE=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/echo/app/MainActivity.java | 14 +- src/App.vue | 29 ++- src/components/EchoInput.vue | 101 +++++++++ src/stores/settings.js | 59 ++++++ src/views/AddEntryView.vue | 91 +++++--- src/views/HomeView.vue | 85 ++++++-- src/views/ListView.vue | 1 + src/views/SettingsView.vue | 195 ++++++++++++++++-- 8 files changed, 517 insertions(+), 58 deletions(-) create mode 100644 src/components/EchoInput.vue diff --git a/android/app/src/main/java/com/echo/app/MainActivity.java b/android/app/src/main/java/com/echo/app/MainActivity.java index b28fd4a..113bbf1 100644 --- a/android/app/src/main/java/com/echo/app/MainActivity.java +++ b/android/app/src/main/java/com/echo/app/MainActivity.java @@ -1,10 +1,13 @@ package com.echo.app; import android.os.Bundle; +import android.view.View; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsControllerCompat; import com.getcapacitor.BridgeActivity; import com.echo.app.notification.NotificationBridgePlugin; -// 主 Activity:这里显式注册自定义的 NotificationBridge 插件,打通原生通知监听到前端的桥接 +// 主 Activity:注册自定义插件,并启用沉浸式状态栏,使前端 Warm 背景延伸到系统栏区域 public class MainActivity extends BridgeActivity { @Override public void onCreate(Bundle savedInstanceState) { @@ -12,5 +15,14 @@ public class MainActivity extends BridgeActivity { // 否则 Bridge 已经创建完成,JS 侧会报 "plugin is not implemented on android" registerPlugin(NotificationBridgePlugin.class); super.onCreate(savedInstanceState); + + // 让内容绘制到状态栏/导航栏后面,实现沉浸式效果 + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + + // 使用浅色背景 + 深色状态栏图标,避免黑色条块感 + View decorView = getWindow().getDecorView(); + WindowInsetsControllerCompat insetsController = + new WindowInsetsControllerCompat(getWindow(), decorView); + insetsController.setAppearanceLightStatusBars(true); } } diff --git a/src/App.vue b/src/App.vue index f274dd9..768beb3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,7 @@  diff --git a/src/components/EchoInput.vue b/src/components/EchoInput.vue new file mode 100644 index 0000000..7fb190f --- /dev/null +++ b/src/components/EchoInput.vue @@ -0,0 +1,101 @@ + + + + diff --git a/src/stores/settings.js b/src/stores/settings.js index 6f58ab7..b046abf 100644 --- a/src/stores/settings.js +++ b/src/stores/settings.js @@ -7,6 +7,18 @@ export const useSettingsStore = defineStore( const notificationCaptureEnabled = ref(true) const aiAutoCategoryEnabled = ref(false) const monthlyBudget = ref(12000) + const budgetResetCycle = ref('monthly') // monthly | weekly | none | custom + const budgetMonthlyResetDay = ref(1) // 每月第几天重置,1-28 + const budgetCustomStartDate = ref('') // 自定义起始日(ISO 日期字符串) + const categoryBudgets = ref({ + Food: 0, + Transport: 0, + Health: 0, + Groceries: 0, + Entertainment: 0, + Uncategorized: 0, + }) + const categoryBudgetEnabled = ref(false) const profileName = ref('Echo 用户') const profileAvatar = ref('') @@ -23,6 +35,38 @@ export const useSettingsStore = defineStore( monthlyBudget.value = Number.isFinite(numeric) && numeric >= 0 ? numeric : 0 } + const setBudgetResetCycle = (value) => { + const allowed = ['monthly', 'weekly', 'none', 'custom'] + budgetResetCycle.value = allowed.includes(value) ? value : 'monthly' + } + + const setCategoryBudget = (category, value) => { + const numeric = Number(value) + const safe = Number.isFinite(numeric) && numeric >= 0 ? numeric : 0 + categoryBudgets.value = { + ...categoryBudgets.value, + [category]: safe, + } + } + + const setCategoryBudgetEnabled = (value) => { + categoryBudgetEnabled.value = !!value + } + + const setBudgetMonthlyResetDay = (value) => { + const n = Number(value) + if (!Number.isFinite(n)) { + budgetMonthlyResetDay.value = 1 + return + } + const clamped = Math.min(Math.max(Math.round(n), 1), 28) + budgetMonthlyResetDay.value = clamped + } + + const setBudgetCustomStartDate = (value) => { + budgetCustomStartDate.value = value || '' + } + const setProfileName = (value) => { profileName.value = (value || '').trim() || 'Echo 用户' } @@ -35,11 +79,21 @@ export const useSettingsStore = defineStore( notificationCaptureEnabled, aiAutoCategoryEnabled, monthlyBudget, + budgetResetCycle, + budgetMonthlyResetDay, + budgetCustomStartDate, + categoryBudgets, + categoryBudgetEnabled, profileName, profileAvatar, setNotificationCaptureEnabled, setAiAutoCategoryEnabled, setMonthlyBudget, + setBudgetResetCycle, + setBudgetMonthlyResetDay, + setBudgetCustomStartDate, + setCategoryBudget, + setCategoryBudgetEnabled, setProfileName, setProfileAvatar, } @@ -50,6 +104,11 @@ export const useSettingsStore = defineStore( 'notificationCaptureEnabled', 'aiAutoCategoryEnabled', 'monthlyBudget', + 'budgetResetCycle', + 'budgetMonthlyResetDay', + 'budgetCustomStartDate', + 'categoryBudgets', + 'categoryBudgetEnabled', 'profileName', 'profileAvatar', ], diff --git a/src/views/AddEntryView.vue b/src/views/AddEntryView.vue index 715d4e0..05cc5b9 100644 --- a/src/views/AddEntryView.vue +++ b/src/views/AddEntryView.vue @@ -2,6 +2,7 @@ import { computed, reactive, ref, watch } from 'vue' import { useTransactionStore } from '../stores/transactions' import { useUiStore } from '../stores/ui' +import EchoInput from '../components/EchoInput.vue' const transactionStore = useTransactionStore() const uiStore = useUiStore() @@ -9,8 +10,11 @@ const uiStore = useUiStore() const editingId = computed(() => uiStore.editingTransactionId || '') const feedback = ref('') const saving = ref(false) +const dragStartY = ref(0) +const dragging = ref(false) +const dragOffset = ref(0) -const categories = ['Food', 'Transport', 'Health', 'Groceries', 'Income', 'Uncategorized'] +const categories = ['Food', 'Transport', 'Health', 'Groceries', 'Entertainment', 'Income', 'Uncategorized'] const toDatetimeLocal = (value) => { const date = value ? new Date(value) : new Date() @@ -109,15 +113,58 @@ const deleteEntry = async () => { closePanel() } +const onDragStart = (event) => { + const touch = event.touches?.[0] + if (!touch) return + dragStartY.value = touch.clientY + dragging.value = true + dragOffset.value = 0 +} + +const onDragMove = (event) => { + if (!dragging.value) return + const touch = event.touches?.[0] + if (!touch) return + const delta = touch.clientY - dragStartY.value + dragOffset.value = delta > 0 ? delta : 0 +} + +const onDragEnd = () => { + if (!dragging.value) return + dragging.value = false + if (dragOffset.value > 80) { + closePanel() + } else { + dragOffset.value = 0 + } +} + +const sheetStyle = computed(() => { + if (!dragOffset.value) return {} + return { + transform: `translateY(${dragOffset.value}px)`, + transition: dragging.value ? 'none' : 'transform 0.2s ease-out', + } +}) + transactionStore.ensureInitialized()