feat: 完成数据分析和用户体验优化功能

- 实现完整的数据分析和图表功能
  - 创建 ECharts 图表组件(饼图、折线图、柱状图)
  - 开发分析服务和数据处理逻辑
  - 实现智能洞察和趋势分析
  - 支持灵活的时间范围筛选

- 完善用户体验和界面优化
  - 添加动画和交互效果组件
  - 创建骨架屏加载状态
  - 实现悬浮操作按钮和快捷操作
  - 开发完整的帮助系统
  - 支持手势操作和键盘快捷键

- 完成分类和账户管理功能
  - 创建分类管理界面和表单
  - 实现账户管理和余额统计
  - 支持自定义图标和颜色
  - 完善数据管理页面

- 实现通知监听和自动记账功能
  - 配置 Android 开发环境
  - 开发通知监听 Capacitor 插件
  - 实现前端通知处理逻辑
  - 支持多平台支付通知解析

- 技术改进
  - 完善 TypeScript 类型定义
  - 优化组件架构和状态管理
  - 增强 CSS 动画系统
  - 提升移动端适配性
This commit is contained in:
2025-08-18 11:34:25 +08:00
parent 4ea91a0f59
commit 8180d7a2ec
49 changed files with 6970 additions and 465 deletions

View File

@@ -33,9 +33,23 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
<!-- Notification Listener Service -->
<service
android:name=".NotificationListenerService"
android:label="@string/notification_listener_service"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>

View File

@@ -2,4 +2,12 @@ package com.example.bill;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(android.os.Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 注册自定义插件
registerPlugin(NotificationListenerPlugin.class);
}
}

View File

@@ -0,0 +1,157 @@
package com.example.bill;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ComponentName;
import android.provider.Settings;
import android.text.TextUtils;
import android.os.Bundle;
import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "NotificationListener")
public class NotificationListenerPlugin extends Plugin {
private static final String TAG = "NotificationListenerPlugin";
private BroadcastReceiver paymentReceiver;
@Override
public void load() {
super.load();
setupPaymentReceiver();
}
@PluginMethod
public void checkPermission(PluginCall call) {
boolean hasPermission = isNotificationServiceEnabled();
JSObject result = new JSObject();
result.put("hasPermission", hasPermission);
call.resolve(result);
}
@PluginMethod
public void requestPermission(PluginCall call) {
if (!isNotificationServiceEnabled()) {
// 打开通知访问设置页面
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
JSObject result = new JSObject();
result.put("success", true);
result.put("message", "已打开通知访问设置页面,请手动启用权限");
call.resolve(result);
} else {
JSObject result = new JSObject();
result.put("success", true);
result.put("message", "通知监听权限已启用");
call.resolve(result);
}
}
@PluginMethod
public void startListening(PluginCall call) {
if (!isNotificationServiceEnabled()) {
call.reject("通知监听权限未启用");
return;
}
// 启动通知监听服务
try {
Intent serviceIntent = new Intent(getContext(), NotificationListenerService.class);
getContext().startService(serviceIntent);
JSObject result = new JSObject();
result.put("success", true);
result.put("message", "通知监听服务已启动");
call.resolve(result);
} catch (Exception e) {
Log.e(TAG, "启动通知监听服务失败: " + e.getMessage());
call.reject("启动通知监听服务失败: " + e.getMessage());
}
}
@PluginMethod
public void stopListening(PluginCall call) {
try {
Intent serviceIntent = new Intent(getContext(), NotificationListenerService.class);
getContext().stopService(serviceIntent);
JSObject result = new JSObject();
result.put("success", true);
result.put("message", "通知监听服务已停止");
call.resolve(result);
} catch (Exception e) {
Log.e(TAG, "停止通知监听服务失败: " + e.getMessage());
call.reject("停止通知监听服务失败: " + e.getMessage());
}
}
private boolean isNotificationServiceEnabled() {
String pkgName = getContext().getPackageName();
final String flat = Settings.Secure.getString(getContext().getContentResolver(),
"enabled_notification_listeners");
if (!TextUtils.isEmpty(flat)) {
final String[] names = flat.split(":");
for (String name : names) {
final ComponentName cn = ComponentName.unflattenFromString(name);
if (cn != null) {
if (TextUtils.equals(pkgName, cn.getPackageName())) {
return true;
}
}
}
}
return false;
}
private void setupPaymentReceiver() {
paymentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if ("com.example.bill.PAYMENT_DETECTED".equals(intent.getAction())) {
Bundle paymentBundle = intent.getBundleExtra("paymentInfo");
if (paymentBundle != null) {
JSObject paymentData = bundleToJSObject(paymentBundle);
notifyListeners("paymentDetected", paymentData);
Log.d(TAG, "支付信息已发送到前端: " + paymentData.toString());
}
}
}
};
IntentFilter filter = new IntentFilter("com.example.bill.PAYMENT_DETECTED");
getContext().registerReceiver(paymentReceiver, filter);
}
private JSObject bundleToJSObject(Bundle bundle) {
JSObject jsObject = new JSObject();
jsObject.put("type", bundle.getString("type", ""));
jsObject.put("amount", bundle.getDouble("amount", 0.0));
jsObject.put("merchant", bundle.getString("merchant", ""));
jsObject.put("account", bundle.getString("account", ""));
jsObject.put("cardNumber", bundle.getString("cardNumber", ""));
jsObject.put("packageName", bundle.getString("packageName", ""));
jsObject.put("rawText", bundle.getString("rawText", ""));
jsObject.put("timestamp", bundle.getLong("timestamp", 0));
return jsObject;
}
@Override
protected void handleOnDestroy() {
super.handleOnDestroy();
if (paymentReceiver != null) {
try {
getContext().unregisterReceiver(paymentReceiver);
} catch (Exception e) {
Log.e(TAG, "注销广播接收器失败: " + e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,285 @@
package com.example.bill;
import android.app.Notification;
import android.content.Intent;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.HashMap;
import java.util.Map;
public class NotificationListenerService extends NotificationListenerService {
private static final String TAG = "NotificationListener";
// 支付应用包名
private static final String[] PAYMENT_PACKAGES = {
"com.eg.android.AlipayGphone", // 支付宝
"com.tencent.mm", // 微信
"com.unionpay", // 银联
"com.chinamworld.bocmbci", // 中国银行
"com.icbc", // 工商银行
"com.ccb.ccbnetpay", // 建设银行
"cmb.pb", // 招商银行
"com.abc.mobile.android" // 农业银行
};
// 支付正则表达式模式
private static final Map<String, Pattern[]> PAYMENT_PATTERNS = new HashMap<>();
static {
// 支付宝支付模式
PAYMENT_PATTERNS.put("com.eg.android.AlipayGphone", new Pattern[]{
Pattern.compile("成功付款([\\d,]+\\.\\d{2})元给(.+?)。"),
Pattern.compile("成功收款([\\d,]+\\.\\d{2})元,来自(.+?)。"),
Pattern.compile("您向(.+?)付款([\\d,]+\\.\\d{2})元"),
Pattern.compile("您收到(.+?)转账([\\d,]+\\.\\d{2})元")
});
// 微信支付模式
PAYMENT_PATTERNS.put("com.tencent.mm", new Pattern[]{
Pattern.compile("微信支付收款([\\d,]+\\.\\d{2})元\\(来自(.+?)\\)"),
Pattern.compile("已成功向(.+?)付款([\\d,]+\\.\\d{2})元"),
Pattern.compile("微信转账收款([\\d,]+\\.\\d{2})元"),
Pattern.compile("已向(.+?)转账([\\d,]+\\.\\d{2})元")
});
// 银行卡支付模式(通用)
Pattern[] bankPatterns = {
Pattern.compile("您尾号(\\d{4})的.*?账户.*?支出.*?([\\d,]+\\.\\d{2})元.*?余额.*?([\\d,]+\\.\\d{2})元"),
Pattern.compile("您尾号(\\d{4})的.*?账户.*?收入.*?([\\d,]+\\.\\d{2})元.*?余额.*?([\\d,]+\\.\\d{2})元"),
Pattern.compile(".*?消费.*?([\\d,]+\\.\\d{2})元.*?商户(.+?)"),
Pattern.compile(".*?转账.*?([\\d,]+\\.\\d{2})元.*?收款人(.+?)")
};
// 为各个银行应用添加通用模式
PAYMENT_PATTERNS.put("com.chinamworld.bocmbci", bankPatterns);
PAYMENT_PATTERNS.put("com.icbc", bankPatterns);
PAYMENT_PATTERNS.put("com.ccb.ccbnetpay", bankPatterns);
PAYMENT_PATTERNS.put("cmb.pb", bankPatterns);
PAYMENT_PATTERNS.put("com.abc.mobile.android", bankPatterns);
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
String packageName = sbn.getPackageName();
// 检查是否是支付相关应用
if (!isPaymentApp(packageName)) {
return;
}
Notification notification = sbn.getNotification();
Bundle extras = notification.extras;
if (extras != null) {
String title = extras.getString(Notification.EXTRA_TITLE, "");
String text = extras.getString(Notification.EXTRA_TEXT, "");
String bigText = extras.getString(Notification.EXTRA_BIG_TEXT, "");
// 使用 bigText 如果可用,否则使用 text
String content = bigText.isEmpty() ? text : bigText;
Log.d(TAG, "收到通知 - 包名: " + packageName + ", 标题: " + title + ", 内容: " + content);
// 解析支付信息
PaymentInfo paymentInfo = parsePaymentInfo(packageName, title, content);
if (paymentInfo != null) {
Log.d(TAG, "解析到支付信息: " + paymentInfo.toString());
// 发送支付信息到前端
sendPaymentInfoToFrontend(paymentInfo);
}
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
// 通知被移除时的处理
}
private boolean isPaymentApp(String packageName) {
for (String paymentPackage : PAYMENT_PACKAGES) {
if (paymentPackage.equals(packageName)) {
return true;
}
}
return false;
}
private PaymentInfo parsePaymentInfo(String packageName, String title, String content) {
Pattern[] patterns = PAYMENT_PATTERNS.get(packageName);
if (patterns == null) {
return null;
}
String fullText = title + " " + content;
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(fullText);
if (matcher.find()) {
PaymentInfo info = new PaymentInfo();
info.packageName = packageName;
info.rawText = fullText;
info.timestamp = System.currentTimeMillis();
try {
// 根据不同的模式解析不同的字段
if (packageName.equals("com.eg.android.AlipayGphone")) {
parseAlipayInfo(matcher, info, pattern);
} else if (packageName.equals("com.tencent.mm")) {
parseWechatInfo(matcher, info, pattern);
} else {
parseBankInfo(matcher, info, pattern);
}
return info;
} catch (Exception e) {
Log.e(TAG, "解析支付信息失败: " + e.getMessage());
}
}
}
return null;
}
private void parseAlipayInfo(Matcher matcher, PaymentInfo info, Pattern pattern) {
String patternStr = pattern.pattern();
if (patternStr.contains("成功付款")) {
info.type = "expense";
info.amount = parseAmount(matcher.group(1));
info.merchant = matcher.group(2);
info.account = "支付宝";
} else if (patternStr.contains("成功收款")) {
info.type = "income";
info.amount = parseAmount(matcher.group(1));
info.merchant = matcher.group(2);
info.account = "支付宝";
} else if (patternStr.contains("您向")) {
info.type = "expense";
info.merchant = matcher.group(1);
info.amount = parseAmount(matcher.group(2));
info.account = "支付宝";
} else if (patternStr.contains("您收到")) {
info.type = "income";
info.merchant = matcher.group(1);
info.amount = parseAmount(matcher.group(2));
info.account = "支付宝";
}
}
private void parseWechatInfo(Matcher matcher, PaymentInfo info, Pattern pattern) {
String patternStr = pattern.pattern();
if (patternStr.contains("收款")) {
info.type = "income";
info.amount = parseAmount(matcher.group(1));
if (matcher.groupCount() > 1) {
info.merchant = matcher.group(2);
}
info.account = "微信";
} else if (patternStr.contains("付款")) {
info.type = "expense";
info.merchant = matcher.group(1);
info.amount = parseAmount(matcher.group(2));
info.account = "微信";
} else if (patternStr.contains("转账收款")) {
info.type = "income";
info.amount = parseAmount(matcher.group(1));
info.account = "微信";
info.merchant = "微信转账";
} else if (patternStr.contains("已向")) {
info.type = "expense";
info.merchant = matcher.group(1);
info.amount = parseAmount(matcher.group(2));
info.account = "微信";
}
}
private void parseBankInfo(Matcher matcher, PaymentInfo info, Pattern pattern) {
String patternStr = pattern.pattern();
if (patternStr.contains("支出")) {
info.type = "expense";
info.cardNumber = matcher.group(1);
info.amount = parseAmount(matcher.group(2));
info.account = "银行卡(" + info.cardNumber + ")";
} else if (patternStr.contains("收入")) {
info.type = "income";
info.cardNumber = matcher.group(1);
info.amount = parseAmount(matcher.group(2));
info.account = "银行卡(" + info.cardNumber + ")";
} else if (patternStr.contains("消费")) {
info.type = "expense";
info.amount = parseAmount(matcher.group(1));
info.merchant = matcher.group(2);
info.account = "银行卡";
} else if (patternStr.contains("转账")) {
info.type = "expense";
info.amount = parseAmount(matcher.group(1));
info.merchant = matcher.group(2);
info.account = "银行卡";
}
}
private double parseAmount(String amountStr) {
// 移除逗号和其他非数字字符,保留小数点
String cleanAmount = amountStr.replaceAll("[,]", "");
try {
return Double.parseDouble(cleanAmount);
} catch (NumberFormatException e) {
Log.e(TAG, "解析金额失败: " + amountStr);
return 0.0;
}
}
private void sendPaymentInfoToFrontend(PaymentInfo paymentInfo) {
// 通过广播或其他方式发送到前端
Intent intent = new Intent("com.example.bill.PAYMENT_DETECTED");
intent.putExtra("paymentInfo", paymentInfo.toBundle());
sendBroadcast(intent);
Log.d(TAG, "支付信息已发送到前端: " + paymentInfo.toString());
}
// 支付信息数据类
public static class PaymentInfo {
public String type; // "income" 或 "expense"
public double amount; // 金额
public String merchant; // 商户名称
public String account; // 账户名称
public String cardNumber; // 银行卡号(后四位)
public String packageName; // 应用包名
public String rawText; // 原始通知文本
public long timestamp; // 时间戳
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putString("type", type);
bundle.putDouble("amount", amount);
bundle.putString("merchant", merchant != null ? merchant : "");
bundle.putString("account", account != null ? account : "");
bundle.putString("cardNumber", cardNumber != null ? cardNumber : "");
bundle.putString("packageName", packageName);
bundle.putString("rawText", rawText);
bundle.putLong("timestamp", timestamp);
return bundle;
}
@Override
public String toString() {
return "PaymentInfo{" +
"type='" + type + '\'' +
", amount=" + amount +
", merchant='" + merchant + '\'' +
", account='" + account + '\'' +
", cardNumber='" + cardNumber + '\'' +
", packageName='" + packageName + '\'' +
", timestamp=" + timestamp +
'}';
}
}
}

View File

@@ -1,7 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">bill</string>
<string name="title_activity_main">bill</string>
<string name="app_name">个人账单</string>
<string name="title_activity_main">个人账单</string>
<string name="package_name">com.example.bill</string>
<string name="custom_url_scheme">com.example.bill</string>
<string name="notification_listener_service">账单通知监听服务</string>
</resources>