From 8180d7a2ec0e97060c2199ce12415e43b9293dcf Mon Sep 17 00:00:00 2001 From: Jafeng <2998840497@qq.com> Date: Mon, 18 Aug 2025 11:34:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现完整的数据分析和图表功能 - 创建 ECharts 图表组件(饼图、折线图、柱状图) - 开发分析服务和数据处理逻辑 - 实现智能洞察和趋势分析 - 支持灵活的时间范围筛选 - 完善用户体验和界面优化 - 添加动画和交互效果组件 - 创建骨架屏加载状态 - 实现悬浮操作按钮和快捷操作 - 开发完整的帮助系统 - 支持手势操作和键盘快捷键 - 完成分类和账户管理功能 - 创建分类管理界面和表单 - 实现账户管理和余额统计 - 支持自定义图标和颜色 - 完善数据管理页面 - 实现通知监听和自动记账功能 - 配置 Android 开发环境 - 开发通知监听 Capacitor 插件 - 实现前端通知处理逻辑 - 支持多平台支付通知解析 - 技术改进 - 完善 TypeScript 类型定义 - 优化组件架构和状态管理 - 增强 CSS 动画系统 - 提升移动端适配性 --- .kiro/specs/personal-billing-app/tasks.md | 57 +- .../android/app/src/main/AndroidManifest.xml | 14 + .../java/com/example/bill/MainActivity.java | 10 +- .../bill/NotificationListenerPlugin.java | 157 +++ .../bill/NotificationListenerService.java | 285 +++++ .../app/src/main/res/values/strings.xml | 5 +- frontend/package-lock.json | 1005 ++++++++++------- frontend/package.json | 5 +- frontend/postcss.config.js | 2 +- frontend/src/assets/main.css | 196 +++- .../src/components/account/AccountForm.vue | 358 ++++++ .../src/components/account/AccountList.vue | 220 ++++ .../components/analytics/AnalyticsPreview.vue | 104 ++ .../src/components/category/CategoryForm.vue | 329 ++++++ .../src/components/category/CategoryList.vue | 173 +++ frontend/src/components/charts/BarChart.vue | 147 +++ frontend/src/components/charts/BaseChart.vue | 75 ++ frontend/src/components/charts/LineChart.vue | 145 +++ frontend/src/components/charts/PieChart.vue | 134 +++ .../src/components/common/AnimatedButton.vue | 185 +++ frontend/src/components/common/BaseButton.vue | 20 +- frontend/src/components/common/BaseIcon.vue | 10 +- .../common/FloatingActionButton.vue | 250 ++++ frontend/src/components/common/HelpSystem.vue | 212 ++++ .../src/components/common/LoadingSkeleton.vue | 107 ++ .../components/common/OfflineIndicator.vue | 2 +- .../src/components/common/PageTransition.vue | 172 +++ .../src/components/common/QuickActions.vue | 58 + .../notification/NotificationSettings.vue | 255 +++++ .../notification/PaymentConfirmation.vue | 209 ++++ .../transaction/TransactionCard.vue | 4 +- .../transaction/TransactionSearch.vue | 2 +- frontend/src/composables/useGestures.ts | 222 ++++ frontend/src/main.ts | 5 + frontend/src/plugins/notification-listener.ts | 51 + frontend/src/repositories/index.ts | 7 +- frontend/src/router/index.ts | 5 + frontend/src/services/analytics.ts | 414 +++++++ frontend/src/services/database.ts | 106 +- .../src/services/notification-listener.ts | 297 +++++ frontend/src/stores/analytics.ts | 238 ++++ frontend/src/stores/notification.ts | 227 ++++ frontend/src/types/index.ts | 37 +- frontend/src/views/AnalyticsView.vue | 341 +++++- frontend/src/views/HomeView.vue | 115 +- frontend/src/views/ManagementView.vue | 284 +++++ frontend/src/views/SettingsView.vue | 129 ++- frontend/src/views/TransactionsView.vue | 39 +- frontend/tailwind.config.js | 11 +- 49 files changed, 6970 insertions(+), 465 deletions(-) create mode 100644 frontend/android/app/src/main/java/com/example/bill/NotificationListenerPlugin.java create mode 100644 frontend/android/app/src/main/java/com/example/bill/NotificationListenerService.java create mode 100644 frontend/src/components/account/AccountForm.vue create mode 100644 frontend/src/components/account/AccountList.vue create mode 100644 frontend/src/components/analytics/AnalyticsPreview.vue create mode 100644 frontend/src/components/category/CategoryForm.vue create mode 100644 frontend/src/components/category/CategoryList.vue create mode 100644 frontend/src/components/charts/BarChart.vue create mode 100644 frontend/src/components/charts/BaseChart.vue create mode 100644 frontend/src/components/charts/LineChart.vue create mode 100644 frontend/src/components/charts/PieChart.vue create mode 100644 frontend/src/components/common/AnimatedButton.vue create mode 100644 frontend/src/components/common/FloatingActionButton.vue create mode 100644 frontend/src/components/common/HelpSystem.vue create mode 100644 frontend/src/components/common/LoadingSkeleton.vue create mode 100644 frontend/src/components/common/PageTransition.vue create mode 100644 frontend/src/components/common/QuickActions.vue create mode 100644 frontend/src/components/notification/NotificationSettings.vue create mode 100644 frontend/src/components/notification/PaymentConfirmation.vue create mode 100644 frontend/src/composables/useGestures.ts create mode 100644 frontend/src/plugins/notification-listener.ts create mode 100644 frontend/src/services/analytics.ts create mode 100644 frontend/src/services/notification-listener.ts create mode 100644 frontend/src/stores/analytics.ts create mode 100644 frontend/src/stores/notification.ts create mode 100644 frontend/src/views/ManagementView.vue diff --git a/.kiro/specs/personal-billing-app/tasks.md b/.kiro/specs/personal-billing-app/tasks.md index d6acfd4..d1e178f 100644 --- a/.kiro/specs/personal-billing-app/tasks.md +++ b/.kiro/specs/personal-billing-app/tasks.md @@ -64,6 +64,7 @@ - [x] 3. 开发核心交易管理界面 + - [x] 3.1 实现基础 UI 组件库 @@ -96,59 +97,89 @@ - 开发筛选结果的实时更新和状态管理 - _需求: 3.1, 3.2, 3.3, 3.4, 3.5_ -- [ ] 4. 开发通知监听和自动记账功能 - - [ ] 4.1 配置 Android 开发环境 +- [x] 4. 开发通知监听和自动记账功能 + + + + - [x] 4.1 配置 Android 开发环境 + + - 在 Windows 11 上配置 Android Studio 和 SDK - 设置 Android 模拟器和真机调试环境 - 创建基础的 Android 构建和调试流程 - _需求: 1.1_ - - [ ] 4.2 开发通知监听 Capacitor 插件 + - [x] 4.2 开发通知监听 Capacitor 插件 + - 创建自定义 Capacitor 插件,实现 NotificationListenerService - 编写 Java/Kotlin 代码监听系统通知并解析支付信息 - 实现通知数据的正则表达式解析和格式化 - _需求: 1.1, 1.2, 1.4_ - - [ ] 4.3 实现前端通知处理逻辑 + - [x] 4.3 实现前端通知处理逻辑 + - 集成通知监听插件到 Vue 应用中 - 创建通知确认对话框,支持用户确认和编辑自动捕获的交易 - 实现通知权限申请和状态管理 - _需求: 1.2, 1.3, 1.5_ -- [ ] 5. 实现分类和账户管理功能 - - [ ] 5.1 开发分类管理功能 +- [x] 5. 实现分类和账户管理功能 + + + + - [x] 5.1 开发分类管理功能 + + - 创建分类管理界面,支持添加、编辑、删除分类 - 实现分类图标和颜色选择器 - 开发默认分类的初始化和管理逻辑 - _需求: 8.1, 8.2, 8.5_ - - [ ] 5.2 实现账户管理功能 + - [x] 5.2 实现账户管理功能 + - 创建支付账户管理界面和 CRUD 操作 - 实现账户类型选择和图标配置 - 开发账户余额跟踪和统计功能 - _需求: 8.3, 8.4_ -- [ ] 6. 创建数据分析和图表功能 - - [ ] 6.1 实现本地数据分析逻辑 +- [x] 6. 创建数据分析和图表功能 + + + + + - [x] 6.1 实现本地数据分析逻辑 + + - 开发本地数据的汇总统计、趋势分析、分类统计功能 - 实现数据聚合查询和性能优化 - 创建分析数据的缓存机制 - _需求: 5.1, 5.2, 5.3, 5.4_ - - [ ] 6.2 开发图表组件和分析页面 + + + - [x] 6.2 开发图表组件和分析页面 + + - 集成 ECharts,创建饼图、折线图、柱状图组件 - 实现 AnalyticsPage,展示支出构成、趋势分析和关键指标 - 开发交互式图表和时间段选择功能 - _需求: 5.1, 5.2, 5.3, 5.4, 5.5_ -- [ ] 7. 完善用户体验和界面优化 - - [ ] 7.1 添加动画和交互效果 +- [x] 7. 完善用户体验和界面优化 + + + + - [x] 7.1 添加动画和交互效果 + + - 实现页面转场动画和按钮交互效果 - 开发列表项的添加删除动画 - 创建加载状态和骨架屏组件 - _需求: 6.2_ - - [ ] 7.2 优化用户体验细节 + - [x] 7.2 优化用户体验细节 + + - 实现悬浮操作按钮和快速添加功能 - 开发手势操作和快捷键支持 - 创建用户引导和帮助系统 diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml index 340e7df..bfc91c4 100644 --- a/frontend/android/app/src/main/AndroidManifest.xml +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -33,9 +33,23 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"> + + + + + + + + + + diff --git a/frontend/android/app/src/main/java/com/example/bill/MainActivity.java b/frontend/android/app/src/main/java/com/example/bill/MainActivity.java index d8b4184..d2ada66 100644 --- a/frontend/android/app/src/main/java/com/example/bill/MainActivity.java +++ b/frontend/android/app/src/main/java/com/example/bill/MainActivity.java @@ -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); + } +} diff --git a/frontend/android/app/src/main/java/com/example/bill/NotificationListenerPlugin.java b/frontend/android/app/src/main/java/com/example/bill/NotificationListenerPlugin.java new file mode 100644 index 0000000..2968e16 --- /dev/null +++ b/frontend/android/app/src/main/java/com/example/bill/NotificationListenerPlugin.java @@ -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()); + } + } + } +} \ No newline at end of file diff --git a/frontend/android/app/src/main/java/com/example/bill/NotificationListenerService.java b/frontend/android/app/src/main/java/com/example/bill/NotificationListenerService.java new file mode 100644 index 0000000..4ee3f72 --- /dev/null +++ b/frontend/android/app/src/main/java/com/example/bill/NotificationListenerService.java @@ -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 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 + + '}'; + } + } +} \ No newline at end of file diff --git a/frontend/android/app/src/main/res/values/strings.xml b/frontend/android/app/src/main/res/values/strings.xml index e22fda3..b47a54e 100644 --- a/frontend/android/app/src/main/res/values/strings.xml +++ b/frontend/android/app/src/main/res/values/strings.xml @@ -1,7 +1,8 @@ - bill - bill + 个人账单 + 个人账单 com.example.bill com.example.bill + 账单通知监听服务 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2267d08..a46c6c6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,13 +12,14 @@ "@capacitor/android": "^7.4.2", "@capacitor/cli": "^7.4.2", "@capacitor/core": "^7.4.2", + "echarts": "^5.6.0", "pinia": "^3.0.3", "vue": "^3.5.18", + "vue-echarts": "^7.0.3", "vue-router": "^4.5.1" }, "devDependencies": { "@playwright/test": "^1.54.1", - "@tailwindcss/postcss": "^4.1.11", "@tsconfig/node22": "^22.0.2", "@types/jsdom": "^21.1.7", "@types/node": "^22.16.5", @@ -38,7 +39,7 @@ "npm-run-all2": "^8.0.4", "postcss": "^8.5.6", "prettier": "3.6.2", - "tailwindcss": "^4.1.11", + "tailwindcss": "^3.4.0", "typescript": "~5.8.0", "vite": "^7.0.6", "vite-plugin-vue-devtools": "^8.0.0", @@ -1737,19 +1738,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2314,349 +2302,6 @@ "win32" ] }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmmirror.com/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@tailwindcss/oxide/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.1.11.tgz", - "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "postcss": "^8.4.41", - "tailwindcss": "4.1.11" - } - }, "node_modules/@tsconfig/node22": { "version": "22.0.2", "resolved": "https://registry.npmmirror.com/@tsconfig/node22/-/node22-22.0.2.tgz", @@ -3692,6 +3337,34 @@ "node": ">=14" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", @@ -3801,6 +3474,19 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/birpc": { "version": "2.5.0", "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz", @@ -3936,6 +3622,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001735", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", @@ -4001,6 +3697,44 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", @@ -4242,16 +3976,48 @@ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.4.tgz", @@ -4325,20 +4091,6 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", @@ -5056,6 +4808,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5153,6 +4915,19 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", @@ -5289,6 +5064,35 @@ "dev": true, "license": "ISC" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", @@ -5759,6 +5563,8 @@ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -5795,6 +5601,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5816,6 +5623,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5837,6 +5645,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5858,6 +5667,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5879,6 +5689,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5900,6 +5711,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5921,6 +5733,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5942,6 +5755,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5963,6 +5777,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5984,6 +5799,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5992,6 +5808,23 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmmirror.com/localforage/-/localforage-1.10.0.tgz", @@ -6189,6 +6022,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", @@ -6271,6 +6116,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", @@ -6420,6 +6275,26 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", @@ -6586,6 +6461,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", @@ -6671,6 +6553,16 @@ "node": ">=0.10" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pinia": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz", @@ -6692,6 +6584,16 @@ } } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/playwright": { "version": "1.54.2", "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.54.2.tgz", @@ -6766,6 +6668,119 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, "node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", @@ -6908,6 +6923,16 @@ ], "license": "MIT" }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/read-package-json-fast": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", @@ -6936,6 +6961,40 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7495,6 +7554,39 @@ "dev": true, "license": "MIT" }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/superjson": { "version": "2.2.2", "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz", @@ -7520,6 +7612,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -7544,20 +7649,51 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", "dev": true, "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, "engines": { - "node": ">=6" + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" } }, "node_modules/tar": { @@ -7592,6 +7728,29 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/through2/-/through2-4.0.2.tgz", @@ -7781,6 +7940,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", @@ -8358,6 +8524,51 @@ "dev": true, "license": "MIT" }, + "node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-echarts": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-7.0.3.tgz", + "integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==", + "license": "MIT", + "dependencies": { + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.5.1", + "vue": "^2.7.0 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/runtime-core": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { "version": "10.2.0", "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", @@ -8739,6 +8950,19 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", @@ -8774,6 +8998,21 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" } } } diff --git a/frontend/package.json b/frontend/package.json index f882664..72bb769 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,13 +22,14 @@ "@capacitor/android": "^7.4.2", "@capacitor/cli": "^7.4.2", "@capacitor/core": "^7.4.2", + "echarts": "^5.6.0", "pinia": "^3.0.3", "vue": "^3.5.18", + "vue-echarts": "^7.0.3", "vue-router": "^4.5.1" }, "devDependencies": { "@playwright/test": "^1.54.1", - "@tailwindcss/postcss": "^4.1.11", "@tsconfig/node22": "^22.0.2", "@types/jsdom": "^21.1.7", "@types/node": "^22.16.5", @@ -48,7 +49,7 @@ "npm-run-all2": "^8.0.4", "postcss": "^8.5.6", "prettier": "3.6.2", - "tailwindcss": "^4.1.11", + "tailwindcss": "^3.4.0", "typescript": "~5.8.0", "vite": "^7.0.6", "vite-plugin-vue-devtools": "^8.0.0", diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index af9d8dc..e99ebc2 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - '@tailwindcss/postcss': {}, + tailwindcss: {}, autoprefixer: {}, }, } \ No newline at end of file diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index c501a94..4011ee6 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -5,7 +5,7 @@ /* 自定义基础样式 */ @layer base { body { - @apply bg-background text-text-primary font-sans; + @apply bg-background text-primary font-sans; } * { @@ -32,6 +32,30 @@ @apply hover:bg-primary hover:text-white; } + .btn-outline { + @apply btn-neumorphic bg-transparent text-gray-700 border-border; + @apply hover:bg-background hover:border-primary; + } + + .btn-ghost { + @apply px-6 py-3 rounded-xl bg-transparent text-gray-500 border-transparent; + @apply transition-all duration-200 ease-out; + @apply hover:bg-background hover:text-gray-700; + } + + /* 按钮尺寸 */ + .btn-sm { + @apply px-4 py-2 text-sm; + } + + .btn-md { + @apply px-6 py-3 text-base; + } + + .btn-lg { + @apply px-8 py-4 text-lg; + } + /* Neumorphic 卡片样式 */ .card-neumorphic { @apply bg-surface rounded-xl shadow-neumorphic border border-border; @@ -63,4 +87,174 @@ .shadow-soft { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } + + /* 动画工具类 */ + .animate-fade-in { + animation: fadeIn 0.3s ease-out; + } + + .animate-slide-up { + animation: slideUp 0.3s ease-out; + } + + .animate-slide-down { + animation: slideDown 0.3s ease-out; + } + + .animate-slide-left { + animation: slideLeft 0.3s ease-out; + } + + .animate-slide-right { + animation: slideRight 0.3s ease-out; + } + + .animate-scale-in { + animation: scaleIn 0.3s ease-out; + } + + .animate-bounce-in { + animation: bounceIn 0.5s ease-out; + } + + /* 悬浮效果 */ + .hover-lift { + @apply transition-transform duration-200 ease-out; + } + + .hover-lift:hover { + @apply transform -translate-y-1; + } + + /* 脉冲效果 */ + .pulse-ring { + @apply absolute inset-0 rounded-full animate-ping; + } + + /* 渐变背景 */ + .bg-gradient-primary { + @apply bg-gradient-to-r from-primary to-primary-light; + } + + .bg-gradient-success { + @apply bg-gradient-to-r from-accent-green to-green-400; + } + + .bg-gradient-danger { + @apply bg-gradient-to-r from-accent-red to-red-400; + } +} + +/* 动画关键帧 */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideDown { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideLeft { + from { + transform: translateX(20px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideRight { + from { + transform: translateX(-20px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: scale(0.9); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes bounceIn { + 0% { + transform: scale(0.3); + opacity: 0; + } + 50% { + transform: scale(1.05); + } + 70% { + transform: scale(0.9); + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +/* 页面转场动画 */ +.page-enter-active, +.page-leave-active { + transition: all 0.3s ease; +} + +.page-enter-from { + opacity: 0; + transform: translateX(20px); +} + +.page-leave-to { + opacity: 0; + transform: translateX(-20px); +} + +/* 列表项动画 */ +.list-item-enter-active, +.list-item-leave-active { + transition: all 0.3s ease; +} + +.list-item-enter-from, +.list-item-leave-to { + opacity: 0; + transform: translateX(30px); +} + +.list-item-move { + transition: transform 0.3s ease; } diff --git a/frontend/src/components/account/AccountForm.vue b/frontend/src/components/account/AccountForm.vue new file mode 100644 index 0000000..8715829 --- /dev/null +++ b/frontend/src/components/account/AccountForm.vue @@ -0,0 +1,358 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/account/AccountList.vue b/frontend/src/components/account/AccountList.vue new file mode 100644 index 0000000..613f83b --- /dev/null +++ b/frontend/src/components/account/AccountList.vue @@ -0,0 +1,220 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/analytics/AnalyticsPreview.vue b/frontend/src/components/analytics/AnalyticsPreview.vue new file mode 100644 index 0000000..17c23ce --- /dev/null +++ b/frontend/src/components/analytics/AnalyticsPreview.vue @@ -0,0 +1,104 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/category/CategoryForm.vue b/frontend/src/components/category/CategoryForm.vue new file mode 100644 index 0000000..2cec87e --- /dev/null +++ b/frontend/src/components/category/CategoryForm.vue @@ -0,0 +1,329 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/category/CategoryList.vue b/frontend/src/components/category/CategoryList.vue new file mode 100644 index 0000000..9e33586 --- /dev/null +++ b/frontend/src/components/category/CategoryList.vue @@ -0,0 +1,173 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/charts/BarChart.vue b/frontend/src/components/charts/BarChart.vue new file mode 100644 index 0000000..5347dda --- /dev/null +++ b/frontend/src/components/charts/BarChart.vue @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/charts/BaseChart.vue b/frontend/src/components/charts/BaseChart.vue new file mode 100644 index 0000000..f089246 --- /dev/null +++ b/frontend/src/components/charts/BaseChart.vue @@ -0,0 +1,75 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/charts/LineChart.vue b/frontend/src/components/charts/LineChart.vue new file mode 100644 index 0000000..d015475 --- /dev/null +++ b/frontend/src/components/charts/LineChart.vue @@ -0,0 +1,145 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/charts/PieChart.vue b/frontend/src/components/charts/PieChart.vue new file mode 100644 index 0000000..aa1f1ee --- /dev/null +++ b/frontend/src/components/charts/PieChart.vue @@ -0,0 +1,134 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/AnimatedButton.vue b/frontend/src/components/common/AnimatedButton.vue new file mode 100644 index 0000000..5439510 --- /dev/null +++ b/frontend/src/components/common/AnimatedButton.vue @@ -0,0 +1,185 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/common/BaseButton.vue b/frontend/src/components/common/BaseButton.vue index 4cfffa3..11ed4fb 100644 --- a/frontend/src/components/common/BaseButton.vue +++ b/frontend/src/components/common/BaseButton.vue @@ -5,24 +5,38 @@ { 'btn-primary': variant === 'primary', 'btn-secondary': variant === 'secondary', + 'btn-outline': variant === 'outline', + 'btn-ghost': variant === 'ghost', + 'btn-sm': size === 'sm', + 'btn-md': size === 'md', + 'btn-lg': size === 'lg', + 'opacity-50 cursor-not-allowed': disabled || loading, }, $attrs.class, ]" - :disabled="disabled" + :disabled="disabled || loading" @click="$emit('click', $event)" > - +
+
+ +
+ + + \ No newline at end of file diff --git a/frontend/src/components/common/HelpSystem.vue b/frontend/src/components/common/HelpSystem.vue new file mode 100644 index 0000000..7d256f1 --- /dev/null +++ b/frontend/src/components/common/HelpSystem.vue @@ -0,0 +1,212 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/LoadingSkeleton.vue b/frontend/src/components/common/LoadingSkeleton.vue new file mode 100644 index 0000000..fa920fb --- /dev/null +++ b/frontend/src/components/common/LoadingSkeleton.vue @@ -0,0 +1,107 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/common/OfflineIndicator.vue b/frontend/src/components/common/OfflineIndicator.vue index e7d6f56..2219057 100644 --- a/frontend/src/components/common/OfflineIndicator.vue +++ b/frontend/src/components/common/OfflineIndicator.vue @@ -179,7 +179,7 @@ function handleClose() { } // 自动隐藏成功消息 -let hideTimer: NodeJS.Timeout | null = null +let hideTimer: ReturnType | null = null function scheduleAutoHide() { if (hideTimer) { diff --git a/frontend/src/components/common/PageTransition.vue b/frontend/src/components/common/PageTransition.vue new file mode 100644 index 0000000..96d3337 --- /dev/null +++ b/frontend/src/components/common/PageTransition.vue @@ -0,0 +1,172 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/common/QuickActions.vue b/frontend/src/components/common/QuickActions.vue new file mode 100644 index 0000000..e8c0fd2 --- /dev/null +++ b/frontend/src/components/common/QuickActions.vue @@ -0,0 +1,58 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/notification/NotificationSettings.vue b/frontend/src/components/notification/NotificationSettings.vue new file mode 100644 index 0000000..75a5b90 --- /dev/null +++ b/frontend/src/components/notification/NotificationSettings.vue @@ -0,0 +1,255 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/notification/PaymentConfirmation.vue b/frontend/src/components/notification/PaymentConfirmation.vue new file mode 100644 index 0000000..546ce9b --- /dev/null +++ b/frontend/src/components/notification/PaymentConfirmation.vue @@ -0,0 +1,209 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/transaction/TransactionCard.vue b/frontend/src/components/transaction/TransactionCard.vue index 9095453..03c82dd 100644 --- a/frontend/src/components/transaction/TransactionCard.vue +++ b/frontend/src/components/transaction/TransactionCard.vue @@ -90,7 +90,7 @@ + + + + @@ -113,19 +119,82 @@ import { ref, computed, onMounted } from 'vue' import { useRouter } from 'vue-router' import BaseCard from '@/components/common/BaseCard.vue' import BaseButton from '@/components/common/BaseButton.vue' -import BaseInput from '@/components/common/BaseInput.vue' +import QuickActions from '@/components/common/QuickActions.vue' +import FloatingActionButton from '@/components/common/FloatingActionButton.vue' +import HelpSystem from '@/components/common/HelpSystem.vue' import { useDatabaseStore } from '@/stores/database' import { useOfflineStore } from '@/stores/offline' const router = useRouter() -const testInput = ref('') const databaseStore = useDatabaseStore() const offlineStore = useOfflineStore() +const showHelp = ref(false) // 计算属性 const connectionStatus = computed(() => offlineStore.connectionStatus) const offlineStats = computed(() => offlineStore.getOfflineStats()) +// 快捷操作配置 +const quickActions = computed(() => [ + { + id: 'add-expense', + icon: '💸', + label: '记支出', + description: '快速添加支出', + color: '#ef4444', + action: () => router.push('/transactions/add?type=expense') + }, + { + id: 'add-income', + icon: '💰', + label: '记收入', + description: '快速添加收入', + color: '#10b981', + action: () => router.push('/transactions/add?type=income') + }, + { + id: 'view-analytics', + icon: '📊', + label: '数据分析', + description: '查看统计图表', + color: '#3b82f6', + action: () => router.push('/analytics') + }, + { + id: 'manage-data', + icon: '⚙️', + label: '数据管理', + description: '管理分类账户', + color: '#8b5cf6', + action: () => router.push('/management') + } +]) + +// FAB 操作配置 +const fabActions = computed(() => [ + { + id: 'add-expense', + icon: 'minus', + label: '添加支出', + class: 'bg-accent-red hover:bg-red-600 focus:ring-accent-red', + action: () => router.push('/transactions/add?type=expense') + }, + { + id: 'add-income', + icon: 'plus', + label: '添加收入', + class: 'bg-accent-green hover:bg-green-600 focus:ring-accent-green', + action: () => router.push('/transactions/add?type=income') + }, + { + id: 'quick-scan', + icon: 'search', + label: '快速查找', + class: 'bg-accent-blue hover:bg-blue-600 focus:ring-accent-blue', + action: () => router.push('/transactions') + } +]) + // 测试添加交易 async function addTestTransaction() { try { @@ -149,6 +218,16 @@ function goToTransactions() { router.push('/transactions') } +// 处理快捷操作 +function handleQuickAction(action: any) { + action.action() +} + +// 处理 FAB 操作 +function handleFabAction(action: any) { + action.action() +} + onMounted(() => { // 确保数据库已初始化 if (!databaseStore.isInitialized) { diff --git a/frontend/src/views/ManagementView.vue b/frontend/src/views/ManagementView.vue new file mode 100644 index 0000000..fe06797 --- /dev/null +++ b/frontend/src/views/ManagementView.vue @@ -0,0 +1,284 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 05c7802..f07b274 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -1,22 +1,143 @@ \ No newline at end of file diff --git a/frontend/src/views/TransactionsView.vue b/frontend/src/views/TransactionsView.vue index 4544d09..07e72af 100644 --- a/frontend/src/views/TransactionsView.vue +++ b/frontend/src/views/TransactionsView.vue @@ -150,13 +150,12 @@ - + main-icon="plus" + :actions="fabActions" + @action="handleFabAction" + /> @@ -165,6 +164,7 @@ import { ref, computed, onMounted, watch } from 'vue' import { useRouter } from 'vue-router' import BaseCard from '@/components/common/BaseCard.vue' import BaseIcon from '@/components/common/BaseIcon.vue' +import FloatingActionButton from '@/components/common/FloatingActionButton.vue' import TransactionList from '@/components/transaction/TransactionList.vue' import TransactionSearch from '@/components/transaction/TransactionSearch.vue' import TransactionFilter from '@/components/transaction/TransactionFilter.vue' @@ -282,6 +282,24 @@ const stats = computed(() => { } }) +// FAB 操作 +const fabActions = computed(() => [ + { + id: 'add-expense', + icon: 'minus', + label: '添加支出', + class: 'bg-accent-red hover:bg-red-600 focus:ring-accent-red', + action: () => handleAddTransaction('expense') + }, + { + id: 'add-income', + icon: 'plus', + label: '添加收入', + class: 'bg-accent-green hover:bg-green-600 focus:ring-accent-green', + action: () => handleAddTransaction('income') + } +]) + // 方法 async function loadTransactions() { loading.value = true @@ -384,6 +402,15 @@ function handleAdd() { router.push('/transactions/add') } +function handleAddTransaction(type?: 'income' | 'expense') { + const route = type ? `/transactions/add?type=${type}` : '/transactions/add' + router.push(route) +} + +function handleFabAction(action: any) { + action.action() +} + function handleSelectionChange(ids: number[]) { selectedIds.value = ids } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 360310a..71fa42d 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -27,21 +27,20 @@ export default { blue: '#3B82F6', // 蓝色 - 收入相关 green: '#22C55E', // 绿色 - 成功状态 red: '#EF4444', // 红色 - 支出相关 + orange: '#F97316', // 橙色 - 警告状态 }, + // 自定义颜色 background: '#F5F5F4', // 背景色 surface: '#FFFFFF', // 卡片背景 - text: { - primary: '#333333', // 主要文本 - secondary: '#666666', // 次要文本 - }, border: '#E5E5E5', // 边框色 - - // 扩展的语义化颜色 success: '#22C55E', warning: '#F59E0B', error: '#EF4444', info: '#3B82F6', + 'text-primary': '#333333', // 主要文本 + 'text-secondary': '#666666', // 次要文本 }, + fontFamily: { sans: ['Inter', 'Poppins', 'system-ui', 'sans-serif'], },