feat: 添加通知设置功能,支持用户开启/关闭通知捕获,优化设置页功能显示,升级kotlin构建版本

This commit is contained in:
2025-11-28 11:37:33 +08:00
parent a2c5525a2e
commit 6b916a4a4b
8 changed files with 237 additions and 58 deletions

View File

@@ -18,7 +18,7 @@ android {
} }
} }
// <EFBFBD><EFBFBD>ʽͳһ Java / Kotlin <EFBFBD>ı<EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>£<EFBFBD>Java 21<EFBFBD><EFBFBD> // 显式统一 Java / Kotlin 的编译目标版本,保持与 Capacitor 生成配置一致(Java 21
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_21 sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21
@@ -52,4 +52,5 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
} }
apply from: 'capacitor.build.gradle' apply from: 'capacitor.build.gradle'

View File

@@ -1,9 +1,10 @@
package com.echo.app.notification package com.echo.app.notification
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.provider.Settings
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.getcapacitor.JSArray import com.getcapacitor.JSArray
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
@@ -12,7 +13,7 @@ import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
// Capacitor <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>˱<EFBFBD>¶֪ͨȨ<EFBFBD>޼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ж<EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD>Լ<EFBFBD>֪ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD> // Capacitor 插件:向前端暴露通知权限、通知队列以及通知事件
@CapacitorPlugin(name = "NotificationBridge") @CapacitorPlugin(name = "NotificationBridge")
class NotificationBridgePlugin : Plugin() { class NotificationBridgePlugin : Plugin() {
@@ -21,7 +22,7 @@ class NotificationBridgePlugin : Plugin() {
override fun load() { override fun load() {
super.load() super.load()
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> NotificationBridgeService <EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD>ڹ㲥<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><EFBFBD>Ϊ JS <EFBFBD>¼<EFBFBD> // 监听 NotificationBridgeService 发送的应用内广播,并转发为 JS 事件
val filter = IntentFilter("com.echo.app.NOTIFICATION_POSTED") val filter = IntentFilter("com.echo.app.NOTIFICATION_POSTED")
notificationPostedReceiver = object : BroadcastReceiver() { notificationPostedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -52,10 +53,19 @@ class NotificationBridgePlugin : Plugin() {
@PluginMethod @PluginMethod
fun requestPermission(call: PluginCall) { fun requestPermission(call: PluginCall) {
// <EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><EFBFBD>ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> // 当前仅返回权限状态,实际的引导文案和跳转由前端控制
hasPermission(call) hasPermission(call)
} }
@PluginMethod
fun openNotificationSettings(call: PluginCall) {
// 跳转到系统的“通知使用权”设置页面,引导用户手动开启监听权限
val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
call.resolve()
}
@PluginMethod @PluginMethod
fun getPendingNotifications(call: PluginCall) { fun getPendingNotifications(call: PluginCall) {
val queue = NotificationStorage.list(context) val queue = NotificationStorage.list(context)
@@ -80,4 +90,4 @@ class NotificationBridgePlugin : Plugin() {
NotificationStorage.clear(context) NotificationStorage.clear(context)
call.resolve() call.resolve()
} }
} }

View File

@@ -1,10 +1,13 @@
<resources> <resources>
<!-- <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ں<EFBFBD> Capacitor Ĭ<><C4AC><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD> --> <!-- 应用主主题:为 Capacitor WebView 启用沉浸式状态栏和导航栏 -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">false</item> <item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style> </style>
<!-- <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Activity ר<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>⣨Ŀǰ<EFBFBD><EFBFBD> AppTheme һ<>£<EFBFBD><C2A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɽ<EFBFBD><C9BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3> --> <!-- 启动 Activity 专用主题(当前与 AppTheme 一致,后续可单独定制) -->
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme" /> <style name="AppTheme.NoActionBarLaunch" parent="AppTheme" />
</resources> </resources>

View File

@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.8.22' ext.kotlin_version = '2.0.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.0.0' classpath 'com.android.tools.build:gradle:8.4.2'
classpath 'com.google.gms:google-services:4.3.15' classpath 'com.google.gms:google-services:4.3.15'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@@ -26,3 +26,5 @@ allprojects {
task clean(type: Delete) { task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@@ -1,4 +1,4 @@
<script setup> <script setup>
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import BottomDock from './components/BottomDock.vue' import BottomDock from './components/BottomDock.vue'
@@ -12,20 +12,18 @@ onMounted(() => {
</script> </script>
<template> <template>
<!-- 整体应用容器居中展示移动端画布 --> <!-- 整体应用容器居中展示移动端画布配合沉浸式状态栏使用安全区域内边距 -->
<div class="min-h-screen bg-warmOffwhite text-stone-800 flex items-center justify-center"> <div class="min-h-screen bg-warmOffwhite text-stone-800 flex items-center justify-center">
<div <div
class="h-screen max-h-[844px] max-w-md w-full mx-auto bg-warmOffwhite shadow-2xl relative overflow-hidden flex flex-col" class="h-screen max-h-[844px] max-w-md w-full mx-auto bg-warmOffwhite shadow-2xl relative overflow-hidden flex flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom);"
> >
<!-- 顶部状态栏占位 -->
<div class="h-8 w-full shrink-0" />
<!-- 主内容区域 vue-router 控制具体页面 --> <!-- 主内容区域 vue-router 控制具体页面 -->
<main class="flex-1 overflow-y-auto hide-scrollbar px-5 pt-2 pb-28 relative"> <main class="flex-1 overflow-y-auto hide-scrollbar px-5 pt-2 pb-28 relative">
<RouterView /> <RouterView />
</main> </main>
<!-- 底部 Dock 导航 --> <!-- 底部 Dock 导航 -->
<BottomDock /> <BottomDock />
</div> </div>
</div> </div>

View File

@@ -10,6 +10,7 @@ import {
transformNotificationToTransaction, transformNotificationToTransaction,
} from '../services/notificationRuleService' } from '../services/notificationRuleService'
import NotificationBridge, { isNativeNotificationBridgeAvailable } from '../lib/notificationBridge' import NotificationBridge, { isNativeNotificationBridgeAvailable } from '../lib/notificationBridge'
import { useSettingsStore } from '../stores/settings'
const notifications = ref([]) const notifications = ref([])
const processingId = ref('') const processingId = ref('')
@@ -35,6 +36,7 @@ const ensureRulesReady = async () => {
export const useTransactionEntry = () => { export const useTransactionEntry = () => {
const transactionStore = useTransactionStore() const transactionStore = useTransactionStore()
const settingsStore = useSettingsStore()
// 原生端:在首次同步前做一次权限校验与请求 // 原生端:在首次同步前做一次权限校验与请求
const ensureNativePermission = async () => { const ensureNativePermission = async () => {
@@ -83,6 +85,11 @@ export const useTransactionEntry = () => {
if (syncing.value) return if (syncing.value) return
syncing.value = true syncing.value = true
try { try {
// 若用户关闭了通知自动捕获,则不从原生/模拟队列拉取新通知,只清空待确认列表
if (!settingsStore.notificationCaptureEnabled) {
notifications.value = []
return
}
if (nativeBridgeReady) { if (nativeBridgeReady) {
await ensureNativePermission() await ensureNativePermission()
} }

31
src/stores/settings.js Normal file
View File

@@ -0,0 +1,31 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettingsStore = defineStore(
'settings',
() => {
const notificationCaptureEnabled = ref(true)
const aiAutoCategoryEnabled = ref(false)
const setNotificationCaptureEnabled = (value) => {
notificationCaptureEnabled.value = !!value
}
const setAiAutoCategoryEnabled = (value) => {
aiAutoCategoryEnabled.value = !!value
}
return {
notificationCaptureEnabled,
aiAutoCategoryEnabled,
setNotificationCaptureEnabled,
setAiAutoCategoryEnabled,
}
},
{
persist: {
paths: ['notificationCaptureEnabled', 'aiAutoCategoryEnabled'],
},
},
)

View File

@@ -1,66 +1,184 @@
<script setup> <script setup>
// 设置页当前只展示静态 UI后续会接入真实设置状态和持久化逻辑 import { computed, onMounted, ref } from 'vue'
import NotificationBridge, { isNativeNotificationBridgeAvailable } from '../lib/notificationBridge'
import { useSettingsStore } from '../stores/settings'
const settingsStore = useSettingsStore()
const notificationCaptureEnabled = computed({
get: () => settingsStore.notificationCaptureEnabled,
set: (value) => settingsStore.setNotificationCaptureEnabled(value),
})
const aiAutoCategoryEnabled = computed({
get: () => settingsStore.aiAutoCategoryEnabled,
set: (value) => settingsStore.setAiAutoCategoryEnabled(value),
})
const nativeBridgeReady = isNativeNotificationBridgeAvailable()
const notificationPermissionGranted = ref(true)
const checkingPermission = ref(false)
const checkNotificationPermission = async () => {
if (!nativeBridgeReady) return
checkingPermission.value = true
try {
const result = await NotificationBridge.hasPermission()
notificationPermissionGranted.value = !!result?.granted
} catch {
notificationPermissionGranted.value = false
} finally {
checkingPermission.value = false
}
}
const toggleNotificationCapture = async () => {
const next = !notificationCaptureEnabled.value
notificationCaptureEnabled.value = next
if (next && nativeBridgeReady) {
await checkNotificationPermission()
if (!notificationPermissionGranted.value) {
try {
await NotificationBridge.openNotificationSettings()
} catch {
// 插件调用失败时忽略,由下方文案提示用户
}
}
}
}
const toggleAiAutoCategory = () => {
aiAutoCategoryEnabled.value = !aiAutoCategoryEnabled.value
}
const handleExportData = () => {
window.alert('导出功能即将上线:届时可以一键导出 CSV / Excel。当前版本建议先通过截图或复制方式备份关键信息。')
}
const handleClearCache = () => {
const ok = window.confirm('确认清除缓存吗?这不会删除正式账本数据,但会重置筛选偏好等本地设置。')
if (!ok) return
try {
// 暂时只清理本地设置缓存,避免误删账本
localStorage.removeItem('settings')
window.alert('已清除设置缓存,部分偏好将在下次启动时重置。')
} catch {
window.alert('清除缓存失败,可稍后重试。')
}
}
onMounted(() => {
void checkNotificationPermission()
})
</script> </script>
<template> <template>
<div class="space-y-6 animate-fade-in pb-10"> <div class="space-y-6 animate-fade-in pb-10">
<h2 class="text-2xl font-extrabold text-stone-800">设置</h2> <h2 class="text-2xl font-extrabold text-stone-800">设置</h2>
<!-- Profile Section --> <!-- 个人信息 / 简短说明 -->
<div class="bg-white rounded-3xl p-5 flex items-center gap-4 shadow-sm border border-stone-100"> <div class="bg-white rounded-3xl p-5 flex items-center gap-4 shadow-sm border border-stone-100">
<img <img
src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" src="https://api.dicebear.com/7.x/avataaars/svg?seed=Echo"
alt="Avatar" alt="Avatar"
class="w-16 h-16 rounded-full bg-stone-100" class="w-16 h-16 rounded-full bg-stone-100"
/> />
<div class="flex-1"> <div class="flex-1">
<h3 class="font-bold text-lg text-stone-800">Alex Chen</h3> <h3 class="font-bold text-lg text-stone-800">Echo 用户</h3>
<p class="text-xs text-stone-400">Pro 会员 (2025.12 到期)</p> <p class="text-xs text-stone-400">本地优先 · 数据只存这台设备</p>
</div> </div>
<button
class="w-8 h-8 rounded-full bg-stone-50 flex items-center justify-center text-stone-400 active:bg-stone-100 transition"
>
<i class="ph-bold ph-pencil-simple" />
</button>
</div> </div>
<!-- Settings Group 1: Automation & AI --> <!-- 自动化 & AI -->
<section> <section>
<h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2"> <h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2">
自动化 &amp; AI 自动化 &amp; AI
</h3> </h3>
<div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100"> <div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100">
<div class="flex items-center justify-between p-4 border-b border-stone-50"> <!-- 自动捕获支付通知 -->
<div class="flex items-center gap-3"> <div class="flex flex-col gap-1 border-b border-stone-50">
<div <div class="flex items-center justify-between p-4">
class="w-8 h-8 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center" <div class="flex items-center gap-3">
> <div
<i class="ph-fill ph-bell-ringing" /> class="w-8 h-8 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center"
>
<i class="ph-fill ph-bell-ringing" />
</div>
<div>
<p class="font-bold text-stone-700 text-sm">自动捕获支付通知</p>
<p class="text-[11px] text-stone-400 mt-0.5">
监听支付宝 / 微信 / 银行 App 通知在本机自动生成记账草稿
</p>
</div>
</div> </div>
<span class="font-bold text-stone-700 text-sm">自动捕获支付通知</span> <button
class="w-11 h-6 rounded-full px-0.5 flex items-center transition-all duration-200"
:class="
notificationCaptureEnabled
? 'bg-orange-400 justify-end'
: 'bg-stone-200 justify-start'
"
@click="toggleNotificationCapture"
>
<div class="w-4 h-4 bg-white rounded-full shadow-sm" />
</button>
</div> </div>
<!-- 开关目前为静态 UI后续接入真实状态 -->
<div class="w-11 h-6 bg-orange-400 rounded-full relative cursor-pointer"> <div v-if="notificationCaptureEnabled" class="px-4 pb-4">
<div class="absolute right-1 top-1 w-4 h-4 bg-white rounded-full shadow-sm" /> <div
v-if="!notificationPermissionGranted"
class="bg-orange-50 border border-orange-200 rounded-2xl px-3 py-2 flex items-start gap-2"
>
<i class="ph-bold ph-warning text-orange-500 mt-0.5" />
<div class="text-[11px] text-orange-700 leading-relaxed">
<p>系统尚未授予 Echo通知使用权否则无法自动捕获通知</p>
<button
class="mt-1 text-[11px] font-bold text-orange-600 underline"
@click="NotificationBridge.openNotificationSettings()"
>
去系统设置开启
</button>
</div>
</div>
<p v-else class="text-[11px] text-emerald-600 flex items-center gap-1">
<i class="ph-bold ph-check-circle" />
通知权限已开启Echo 会在收到新通知后自动刷新首页
</p>
</div> </div>
</div> </div>
<div class="flex items-center justify-between p-4">
<div class="flex items-center gap-3"> <!-- AI 自动分类开关预留 -->
<div <div class="flex flex-col gap-1 p-4">
class="w-8 h-8 rounded-full bg-purple-50 text-purple-500 flex items-center justify-center" <div class="flex items-center justify-between">
> <div class="flex items-center gap-3">
<i class="ph-fill ph-magic-wand" /> <div
class="w-8 h-8 rounded-full bg-purple-50 text-purple-500 flex items-center justify-center"
>
<i class="ph-fill ph-magic-wand" />
</div>
<div>
<p class="font-bold text-stone-700 text-sm">AI 自动分类预留</p>
<p class="text-[11px] text-stone-400 mt-0.5">
开启后未来会优先使用云端 AI 对商户进行分类与标签分析
</p>
</div>
</div> </div>
<span class="font-bold text-stone-700 text-sm">AI 自动分类</span> <button
</div> class="w-11 h-6 rounded-full px-0.5 flex items-center transition-all duration-200"
<div class="w-11 h-6 bg-orange-400 rounded-full relative cursor-pointer"> :class="aiAutoCategoryEnabled ? 'bg-orange-400 justify-end' : 'bg-stone-200 justify-start'"
<div class="absolute right-1 top-1 w-4 h-4 bg-white rounded-full shadow-sm" /> @click="toggleAiAutoCategory"
>
<div class="w-4 h-4 bg-white rounded-full shadow-sm" />
</button>
</div> </div>
<p v-if="aiAutoCategoryEnabled" class="px-4 pb-1 text-[11px] text-purple-600">
当前版本仅记录偏好后续接入云端 AI 后会自动生效
</p>
</div> </div>
</div> </div>
</section> </section>
<!-- Settings Group 2: General --> <!-- 通用设置 -->
<section> <section>
<h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2"> <h3 class="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3 ml-2">
通用 通用
@@ -68,6 +186,7 @@
<div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100"> <div class="bg-white rounded-3xl overflow-hidden shadow-sm border border-stone-100">
<button <button
class="flex w-full items-center justify-between p-4 border-b border-stone-50 active:bg-stone-50 transition text-left" class="flex w-full items-center justify-between p-4 border-b border-stone-50 active:bg-stone-50 transition text-left"
@click="handleExportData"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div <div
@@ -75,27 +194,35 @@
> >
<i class="ph-fill ph-export" /> <i class="ph-fill ph-export" />
</div> </div>
<span class="font-bold text-stone-700 text-sm">导出账单数据 (Excel)</span> <div>
<p class="font-bold text-stone-700 text-sm">导出账单数据预留</p>
<p class="text-[11px] text-stone-400 mt-0.5">未来支持导出为 CSV / Excel 文件</p>
</div>
</div> </div>
<i class="ph-bold ph-caret-right text-stone-300" /> <i class="ph-bold ph-caret-right text-stone-300" />
</button> </button>
<div class="flex items-center justify-between p-4 active:bg-stone-50 transition"> <button
class="flex w-full items-center justify-between p-4 active:bg-stone-50 transition text-left"
@click="handleClearCache"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div <div
class="w-8 h-8 rounded-full bg-red-50 text-red-500 flex items-center justify-center" class="w-8 h-8 rounded-full bg-red-50 text-red-500 flex items-center justify-center"
> >
<i class="ph-fill ph-trash" /> <i class="ph-fill ph-trash" />
</div> </div>
<span class="font-bold text-stone-700 text-sm">清除缓存</span> <div>
<p class="font-bold text-stone-700 text-sm">清除缓存设置</p>
<p class="text-[11px] text-stone-400 mt-0.5">仅清理本地偏好不影响正式账本数据</p>
</div>
</div> </div>
<span class="text-xs text-stone-400">128 MB</span> <span class="text-xs text-stone-400"> KB</span>
</div> </button>
</div> </div>
</section> </section>
<div class="text-center pt-4"> <div class="text-center pt-4">
<p class="text-xs text-stone-300">Version 1.0.2 Beta</p> <p class="text-xs text-stone-300">Echo · Local-first Beta</p>
</div> </div>
</div> </div>
</template> </template>