From a83cda63f5be0a751482e21f9259c7568ddc6a88 Mon Sep 17 00:00:00 2001 From: handsomeAq Date: Wed, 23 Sep 2026 15:33:59 +0800 Subject: [PATCH] =?UTF-8?q?UI=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .codebuddy/memory/MEMORY.md | 21 +- android/app/src/main/AndroidManifest.xml | 2 +- docs/Flutter打包与真机安装指南.md | 12 +- ios/Runner/Info.plist | 2 +- lib/app.dart | 2 +- lib/core/module/app_module.dart | 68 ++++++ lib/pages/assistant/assistant_page.dart | 165 ++++++++++++++ .../bill_home_page.dart} | 85 +++++-- .../bill_statistics_page.dart} | 43 +++- .../widgets/bill_group_card.dart | 0 .../bill/widgets/bill_module_summary.dart | 47 ++++ .../{home => bill}/widgets/budget_card.dart | 0 .../widgets/category_pie_card.dart | 0 .../widgets/category_rank_list.dart | 0 .../widgets/month_switcher.dart | 0 lib/pages/home/dashboard_page.dart | 214 ++++++++++++++++++ lib/pages/login/login_page.dart | 2 +- lib/pages/mine/mine_page.dart | 74 ++++-- .../note/widgets/note_module_summary.dart | 48 ++++ lib/pages/shell/main_shell.dart | 43 +++- lib/providers/note_provider.dart | 9 + test/page_layout_test.dart | 106 +++++++++ web/index.html | 6 +- web/manifest.json | 6 +- 24 files changed, 874 insertions(+), 81 deletions(-) create mode 100644 lib/core/module/app_module.dart create mode 100644 lib/pages/assistant/assistant_page.dart rename lib/pages/{home/home_page.dart => bill/bill_home_page.dart} (70%) rename lib/pages/{statistics/statistics_page.dart => bill/bill_statistics_page.dart} (82%) rename lib/pages/{home => bill}/widgets/bill_group_card.dart (100%) create mode 100644 lib/pages/bill/widgets/bill_module_summary.dart rename lib/pages/{home => bill}/widgets/budget_card.dart (100%) rename lib/pages/{statistics => bill}/widgets/category_pie_card.dart (100%) rename lib/pages/{statistics => bill}/widgets/category_rank_list.dart (100%) rename lib/pages/{home => bill}/widgets/month_switcher.dart (100%) create mode 100644 lib/pages/home/dashboard_page.dart create mode 100644 lib/pages/note/widgets/note_module_summary.dart create mode 100644 test/page_layout_test.dart diff --git a/.codebuddy/memory/MEMORY.md b/.codebuddy/memory/MEMORY.md index bed302a..9226b98 100644 --- a/.codebuddy/memory/MEMORY.md +++ b/.codebuddy/memory/MEMORY.md @@ -1,7 +1,7 @@ -# 项目长期记忆:强宝爱记账 +# 项目长期记忆:强宝小助手 ## 项目概况 -- 名称:`qiangbao_accounting`(强宝爱记账),一个 Flutter 记账类 App。 +- 名称:`qiangbao_accounting`(强宝小助手),一个 Flutter 记账类 App。 - 定位:由 uniApp 项目 `pages/login/login.vue` 复刻迁移而来(见 `login_page.dart` 顶部注释)。 - 环境:Flutter **3.47.4** stable / Dart **3.13.3**(Windows 开发)。 - 目标平台:仅 `android/`、`ios/`、`web/`,未配置桌面端。 @@ -16,9 +16,18 @@ ## 架构与关键文件 - `lib/app.dart`:根组件 `QiangbaoApp extends ConsumerWidget`。配置 M3 主题,用 `auth.isLoggedIn` 在 `MainShell` / `LoginPage` 之间分发。全局 `navigatorKey` 已抽到 `lib/core/navigation/navigator_key.dart`(避免 app.dart ↔ 网络层循环依赖),供网络层 401 跳转与全局 toast 使用。 - `lib/core/network/auth_redirect.dart`:网络层与 Riverpod 的桥接(`configureAuthRedirect(container)` 在 `main.dart` 调用),提供 `forceLogout()` / `logoutAndGoLogin()`。 -- `lib/pages/shell/main_shell.dart`:主框架,4 个 Tab(首页/记账/统计/我的),用 `IndexedStack` 保留各页状态,`NavigationBar`(M3)承载导航,默认 `_index = 1`(记账)。`initState` 里有登录态兜底跳转。 +- `lib/pages/shell/main_shell.dart`:主框架,4 个 Tab 是**角色**不是功能 —— 首页(仪表盘)/记录(全局录入)/助手(AI)/我的,用 `IndexedStack` 保留各页状态,`NavigationBar`(M3)承载导航,默认 `_index = 0`(首页)。`initState` 里有登录态兜底跳转。 - `lib/pages/login/login_page.dart`:登录/注册页,含毛玻璃卡片(`BackdropFilter`)、动画背景圆球(`AnimationController` + `Stack` + `Positioned` + `Transform.translate`)、记住密码(`utils/crypto.dart` 加解密)。 -- 目前只有「记账」页(`AddPage`,含 `manual_entry_tab.dart`、`ocr_tab.dart`、`ocr_result_card.dart`)是真实实现,首页/统计/我的仍是 `_PlaceholderPage`。 +- 首页、统计、我的**都已是真实实现**(不是占位页)。记账录入页在 `pages/add/`(`AddPage`,手动记账 / OCR 双 Tab)。 + +### 模块框架(2026-09-23 做的「阶段 A」) +- **`lib/core/module/app_module.dart` 是模块注册表**,`appModules` 列表是唯一的真相来源:首页仪表盘的卡片、`MinePage` 的「功能模块」列表都从它生成。**新增一个模块 = 往列表加一条 + 写页面**,导航/首页/设置页都不用改。 +- 每个 `AppModule` 有:`id`(稳定标识,将来做排序/开关/AI 可读范围时持久化用)、`name`、`blurb`、`icon`(emoji,和项目卡片风格一致)、`open`(打开入口页的 `WidgetBuilder`)、**可选的 `summary`**(首页卡片上的摘要 widget,由模块自己提供,`null` 就不显示)。 +- **具体模块不进底部 Tab**。代价是打开 App 多一次点击才能看到账单,换来的是「以后加模块不用改布局」。这是用户明确选的。 +- 目录按模块划分:`pages/bill/`(记账:首页/统计/编辑 + `widgets/`)、`pages/note/`、`pages/add/`(记录 Tab 的录入)、`pages/home/`(全局仪表盘)、`pages/assistant/`(AI 占位)。 +- 「记录」Tab 目前直接就是记账录入 —— 只有一种记录类型。**等第二种记录类型(运动/投资)出现时才需要在它上面加一层「选类型」**。 +- 统计**不再是顶层 Tab**,从记账首页右上角图标进(`BillStatisticsPage`)。 +- `pages/bill/bill_home_page.dart` 和 `bill_statistics_page.dart` 都是被 push 进来的,所以顶部自绘了返回箭头,底部留白也按 `Navigator.canPop()` 判断(有导航栏时留 90,没有时留 24)。 ## 设计规范(配色) - 品牌主色:`#C8956E`(驼色/焦糖色),在 `app.dart` 中作为 `ColorScheme.fromSeed` 的 seedColor。 @@ -35,6 +44,8 @@ - 已知风险:① `--dart-define` 在 release 下同样生效,可击穿"正式包走生产"的保证;② `kDebugMode` 在 profile 模式为 false → profile 包会连生产,宜改用 `kReleaseMode`。 ## 待办 / 可优化点 +- **模块的排序与开关还没做**(注册表里 `id` 已为此预留);AI 的「可读范围」也需要按 `id` 配置。 +- **`test/page_layout_test.dart` 在守布局**:用 320 / 393 / 480 三种宽度断言仪表盘和记账首页不溢出。改这两个页面的横向布局后跑一下 —— 它已经抓到过一次真 bug(记账首页顶部四个元素挤一行,在 393 宽的常规机上溢出 32px)。 - 硬编码颜色可迁移为 M3 角色色(`colorScheme.primary` 等),并补暗色模式。 - `api_env.dart` 优先用 `kReleaseMode` 显式判断,并统一生产地址(域名 + HTTPS);多环境改用 `--dart-define-from-file`;加运行时自检/启动日志显示当前 baseUrl。 - iOS 若需访问明文 HTTP,需在 `Info.plist` 增加 ATS 例外(`NSAllowsLocalNetworking` 或域例外)。 @@ -43,7 +54,7 @@ - Flutter 3.47 起 Material/Cupertino 已解耦为 `material_ui` / `cupertino_ui` 包,`package:flutter/material.dart` 仍可用,暂无需迁移。 ## 应用名称与图标(已落地) -- 显示名统一为 **强宝爱记账**:`AndroidManifest.xml` 的 `android:label`、`Info.plist` 的 `CFBundleDisplayName`、`web/index.html` 的 `` 与 `apple-mobile-web-app-title`;`web/manifest.json` 用 `short_name = 强宝记账`(4 字以内避免桌面截断)。 +- 显示名统一为 **强宝小助手**:`AndroidManifest.xml` 的 `android:label`、`Info.plist` 的 `CFBundleDisplayName`、`web/index.html` 的 `<title>` 与 `apple-mobile-web-app-title`;`web/manifest.json` 用 `short_name = 强宝助手`(4 字以内避免桌面截断)。 - 图标链路:`tools/make_app_icon.py`(去水印 + 抠图 + 缩放)→ `assets/icon/app_icon.png` + `app_icon_fg.png` → `dart run flutter_launcher_icons`。 - 图标配置在 `pubspec.yaml` 末尾的 `flutter_launcher_icons` 段;dev 依赖已加 `flutter_launcher_icons: ^0.14.0`。 - 素材归档在 `assets/icon/_source/`(`raw_bear.png` 采用、`raw_bear_dog.png` 备选)。`assets/icon/` 未声明为 Flutter assets,不会进包。 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4ed8197..6a18d88 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET"/> <application - android:label="强宝爱记账" + android:label="强宝小助手" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true"> diff --git a/docs/Flutter打包与真机安装指南.md b/docs/Flutter打包与真机安装指南.md index 19df81d..08e019b 100644 --- a/docs/Flutter打包与真机安装指南.md +++ b/docs/Flutter打包与真机安装指南.md @@ -1,6 +1,6 @@ # Flutter 打包 APK 与真机安装指南 -> 项目:强宝爱记账(qiangbao_accounting) +> 项目:强宝小助手(qiangbao_accounting) > 环境:Windows 11 + Flutter 3.47.4 / Dart 3.13.3 > 测试机:小米 M2007J22C(Android 12 / MIUI 14,设备号 `9dr4gyqgivl7tw5l`) @@ -288,11 +288,11 @@ version: 1.0.0+1 | 平台 | 位置 | 当前值 | |---|---|---| -| Android 桌面名 | `android/app/src/main/AndroidManifest.xml` 的 `android:label` | `强宝爱记账` | -| iOS 桌面名 | `ios/Runner/Info.plist` 的 `CFBundleDisplayName` | `强宝爱记账` | -| Web 标签页 | `web/index.html` 的 `<title>` | `强宝爱记账` | -| Web 主屏名 | `web/index.html` 的 `apple-mobile-web-app-title` | `强宝爱记账` | -| Web PWA | `web/manifest.json` 的 `name` / `short_name` | `强宝爱记账` / `强宝记账` | +| Android 桌面名 | `android/app/src/main/AndroidManifest.xml` 的 `android:label` | `强宝小助手` | +| iOS 桌面名 | `ios/Runner/Info.plist` 的 `CFBundleDisplayName` | `强宝小助手` | +| Web 标签页 | `web/index.html` 的 `<title>` | `强宝小助手` | +| Web 主屏名 | `web/index.html` 的 `apple-mobile-web-app-title` | `强宝小助手` | +| Web PWA | `web/manifest.json` 的 `name` / `short_name` | `强宝小助手` / `强宝助手` | | 包名 | `android/app/build.gradle.kts` 的 `applicationId` | `com.qiangbao.qiangbao_accounting`(**上架后不可改**) | > `short_name` 保持 4 个汉字以内,否则 Android 桌面图标下方会被截断成省略号。 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 71f4e69..4dba7fd 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -11,7 +11,7 @@ <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleDisplayName</key> - <string>强宝爱记账</string> + <string>强宝小助手</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> diff --git a/lib/app.dart b/lib/app.dart index b8dc6f0..cf70aeb 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -13,7 +13,7 @@ class QiangbaoApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final auth = ref.watch(authProvider); return MaterialApp( - title: '强宝爱记账', + title: '强宝小助手', navigatorKey: navigatorKey, theme: ThemeData( colorScheme: ColorScheme.fromSeed( diff --git a/lib/core/module/app_module.dart b/lib/core/module/app_module.dart new file mode 100644 index 0000000..9161079 --- /dev/null +++ b/lib/core/module/app_module.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../../pages/bill/bill_home_page.dart'; +import '../../pages/bill/widgets/bill_module_summary.dart'; +import '../../pages/note/note_list_page.dart'; +import '../../pages/note/widgets/note_module_summary.dart'; + +/// 一个功能模块要对外暴露的东西:它是谁、怎么打开自己。 +/// +/// 首页仪表盘的卡片网格、「我的」里的模块列表都从 [appModules] 生成 —— +/// 新增一个模块 = 往列表里加一条 + 写这个模块的页面, +/// 布局层(导航栏、首页、设置页)一行都不用改。 +@immutable +class AppModule { + const AppModule({ + required this.id, + required this.name, + required this.blurb, + required this.icon, + required this.open, + this.summary, + }); + + /// 稳定标识。将来做「模块排序 / 开关 / AI 可读范围」时要靠它持久化。 + final String id; + + /// 模块名,显示在卡片和列表上 + final String name; + + /// 一句话说明,让用户在首页就能看懂这个模块是干什么的 + final String blurb; + + /// 用 emoji 当图标 —— 和项目现有的卡片风格一致(笔记入口、预算提示都是 emoji) + final String icon; + + /// 打开模块入口页 + final WidgetBuilder open; + + /// 首页卡片上的摘要。 + /// + /// 由模块自己提供,因为只有模块知道该摘要什么;返回 null 表示这个模块 + /// 暂时没有可显示的数据(卡片就只显示名字和说明)。 + final Widget Function(BuildContext context)? summary; +} + +/// 全部模块。**顺序 = 首页卡片顺序 = 「我的」里的列表顺序。** +/// +/// 现在只有两个真模块。加「投资」「运动」「服务器」时照抄一条即可 —— +/// 注意「记录」Tab 目前直接就是记账录入,等第二种记录类型出现时, +/// 才需要在它上面加一层「选类型」。 +final List<AppModule> appModules = [ + AppModule( + id: 'bill', + name: '记账', + blurb: '收支记录、统计与预算', + icon: '💰', + open: (_) => const BillHomePage(), + summary: (_) => const BillModuleSummary(), + ), + AppModule( + id: 'note', + name: '笔记', + blurb: 'Markdown 知识库,随时查看和记录', + icon: '📝', + open: (_) => const NoteListPage(), + summary: (_) => const NoteModuleSummary(), + ), +]; diff --git a/lib/pages/assistant/assistant_page.dart b/lib/pages/assistant/assistant_page.dart new file mode 100644 index 0000000..2b3d616 --- /dev/null +++ b/lib/pages/assistant/assistant_page.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +/// 助手页(占位)。 +/// +/// 现在是骨架里预留的位置 —— 先让布局一次到位,阶段 B 接 AI 时只填内容、 +/// 不再动导航结构。同时它也是首页那个输入框的落地页。 +class AssistantPage extends StatelessWidget { + const AssistantPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFFE5CC), Color(0xFFFFD4A8), Color(0xFFFFC08A)], + ), + ), + child: SafeArea( + child: Column( + children: [ + _header(context), + Expanded( + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(24, 40, 24, 0), + children: [ + const Center(child: Text('✨', style: TextStyle(fontSize: 46))), + const SizedBox(height: 16), + const Center( + child: Text( + 'AI 助手正在建设中', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + ), + const SizedBox(height: 8), + const Center( + child: Text( + '位置已经预留好,接入后不用再改布局', + style: TextStyle(fontSize: 12.5, color: Color(0xFF8D6E63)), + ), + ), + const SizedBox(height: 28), + _planCard(), + const SizedBox(height: 90), + ], + ), + ), + ], + ), + ), + ), + ); + } + + /// 用 Tab 打开时没有上一页,就不显示返回箭头 + Widget _header(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 6, 16, 6), + child: Row( + children: [ + if (Navigator.of(context).canPop()) + InkWell( + onTap: () => Navigator.of(context).pop(), + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(Icons.arrow_back, size: 22, color: Color(0xFF5D4037)), + ), + ), + const Padding( + padding: EdgeInsets.only(left: 6), + child: Text( + '助手', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF5D4037), + ), + ), + ), + ], + ), + ); + } + + Widget _planCard() { + const items = [ + ('一句话记账', '「昨天买咖啡 32」→ 自动填好分类和金额'), + ('笔记问答', '从你的知识库里找答案,而不是重读一遍'), + ('服务器状态', '查状态、看日志 —— 只走预定义动作,不给任意 shell'), + ('跨模块查询', '「上周服务器相关的花费」这类要跨模块的问题'), + ]; + + return Container( + padding: const EdgeInsets.fromLTRB(18, 16, 18, 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '规划中的能力', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 12), + for (final (title, desc) in items) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 5), + child: Icon(Icons.circle, size: 5, color: Color(0xFFC8956E)), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 2), + Text( + desc, + style: const TextStyle(fontSize: 12, color: Color(0xFF8D6E63)), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 14), + ], + const Divider(height: 1, color: Color(0xFFF0E6D8)), + const SizedBox(height: 10), + const Text( + '隐私:涉及你笔记里的服务器信息时,会在请求发出前先脱敏。', + style: TextStyle(fontSize: 11.5, color: Color(0xFFBCAAA4), height: 1.5), + ), + const SizedBox(height: 8), + ], + ), + ); + } +} diff --git a/lib/pages/home/home_page.dart b/lib/pages/bill/bill_home_page.dart similarity index 70% rename from lib/pages/home/home_page.dart rename to lib/pages/bill/bill_home_page.dart index 665eb50..62caab3 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/bill/bill_home_page.dart @@ -11,14 +11,19 @@ import '../../utils/amount_util.dart'; import '../../utils/bill_grouping.dart'; import '../../utils/date_util.dart'; import '../../widgets/state_views.dart'; -import '../bill/bill_edit_page.dart'; +import 'bill_edit_page.dart'; +import 'bill_statistics_page.dart'; import 'widgets/bill_group_card.dart'; import 'widgets/budget_card.dart'; import 'widgets/month_switcher.dart'; -/// 首页:月份切换 + 本月汇总 + 预算环 + 按日期分组的账单列表 -class HomePage extends ConsumerWidget { - const HomePage({super.key}); +/// 记账模块的首页:月份切换 + 本月汇总 + 预算环 + 按日期分组的账单列表。 +/// +/// 这个页面同时承担两个入口角色: +/// · 从首页仪表盘的「记账」卡片进来(会被 push,这时有返回箭头) +/// · 直接 push 进来时底部没有导航栏,所以底部留白要按情况给 +class BillHomePage extends ConsumerWidget { + const BillHomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -48,7 +53,7 @@ class HomePage extends ConsumerWidget { physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), children: [ - _header(anchor), + _header(context), const SizedBox(height: 14), _summaryCard(anchor, statsAsync), const SizedBox(height: 12), @@ -59,7 +64,8 @@ class HomePage extends ConsumerWidget { _budgetUnavailableTip(), const SizedBox(height: 16), ..._billSection(context, ref, billsAsync), - const SizedBox(height: 90), // 底部导航避让 + // 被 push 进来时底部没有导航栏,就不用留出避让高度 + SizedBox(height: Navigator.of(context).canPop() ? 24 : 90), ], ), ), @@ -68,24 +74,67 @@ class HomePage extends ConsumerWidget { ); } - // ---- 顶部标题 + 月份切换 ---- - Widget _header(DateTime anchor) { - return Row( + // ---- 顶部:返回 + 标题 + 统计入口 / 月份切换 ---- + // + // 为什么要分两行:月份切换条本身就有 ~206px 宽,再并上返回箭头(34)、 + // 标题(80)、统计图标(34),一行要约 364px —— 而 393 宽的常规手机只有 + // 361px 可用,实测溢出 32px。分两行之后每行都很宽松。 + Widget _header(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '我的账单', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF5D4037), - ), + Row( + children: [ + if (Navigator.of(context).canPop()) ...[ + _iconButton( + icon: Icons.arrow_back, + tooltip: '返回', + onTap: () => Navigator.of(context).pop(), + ), + const SizedBox(width: 6), + ], + const Text( + '我的账单', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF5D4037), + ), + ), + const Spacer(), + _iconButton( + icon: Icons.bar_chart, + tooltip: '统计分析', + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const BillStatisticsPage()), + ), + ), + ], ), - const Spacer(), + const SizedBox(height: 10), const MonthSwitcher(), ], ); } + Widget _iconButton({ + required IconData icon, + required String tooltip, + required VoidCallback onTap, + }) { + return Tooltip( + message: tooltip, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon(icon, size: 22, color: const Color(0xFF5D4037)), + ), + ), + ); + } + // ---- 本月汇总 ---- Widget _summaryCard(DateTime anchor, AsyncValue<StatisticsResponse> statsAsync) { final stats = statsAsync.value; @@ -162,7 +211,7 @@ class HomePage extends ConsumerWidget { if (bills.isEmpty) { return const EmptyView( title: '本月暂无账单记录', - tip: '点击底部「记账」添加,或用 OCR 识别小票', + tip: '点击底部「记录」添加,或用 OCR 识别小票', ); } return Column( diff --git a/lib/pages/statistics/statistics_page.dart b/lib/pages/bill/bill_statistics_page.dart similarity index 82% rename from lib/pages/statistics/statistics_page.dart rename to lib/pages/bill/bill_statistics_page.dart index 605c499..acac188 100644 --- a/lib/pages/statistics/statistics_page.dart +++ b/lib/pages/bill/bill_statistics_page.dart @@ -10,8 +10,10 @@ import 'widgets/category_pie_card.dart'; import 'widgets/category_rank_list.dart'; /// 统计页:按日/周/月切换 + 汇总 + 分类占比饼图 + 分类明细 -class StatisticsPage extends ConsumerWidget { - const StatisticsPage({super.key}); +/// +/// 属于记账模块,从账单首页右上角进(不是顶层 Tab)。 +class BillStatisticsPage extends ConsumerWidget { + const BillStatisticsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -34,19 +36,13 @@ class StatisticsPage extends ConsumerWidget { physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), children: [ - const Text( - '统计分析', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF5D4037), - ), - ), + _titleRow(context), const SizedBox(height: 14), _tabBar(tab, ref), const SizedBox(height: 14), _content(ref, statsAsync), - const SizedBox(height: 90), // 底部导航避让 + // 被 push 进来时底部没有导航栏,就不用留出避让高度 + SizedBox(height: Navigator.of(context).canPop() ? 24 : 90), ], ), ), @@ -54,6 +50,31 @@ class StatisticsPage extends ConsumerWidget { ); } + // ---- 顶部标题(带返回,因为这个页面是 push 进来的) ---- + Widget _titleRow(BuildContext context) { + return Row( + children: [ + if (Navigator.of(context).canPop()) + InkWell( + onTap: () => Navigator.of(context).pop(), + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(Icons.arrow_back, size: 22, color: Color(0xFF5D4037)), + ), + ), + const Text( + '统计分析', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF5D4037), + ), + ), + ], + ); + } + // ---- 时间粒度切换 ---- Widget _tabBar(StatisticsTab current, WidgetRef ref) { return Container( diff --git a/lib/pages/home/widgets/bill_group_card.dart b/lib/pages/bill/widgets/bill_group_card.dart similarity index 100% rename from lib/pages/home/widgets/bill_group_card.dart rename to lib/pages/bill/widgets/bill_group_card.dart diff --git a/lib/pages/bill/widgets/bill_module_summary.dart b/lib/pages/bill/widgets/bill_module_summary.dart new file mode 100644 index 0000000..0a6a047 --- /dev/null +++ b/lib/pages/bill/widgets/bill_module_summary.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../providers/bill_provider.dart'; +import '../../../utils/amount_util.dart'; + +/// 首页仪表盘上「记账」卡片的摘要:当前查看月份的收入 / 支出 / 结余。 +/// +/// 故意和账单首页共用 `monthAnchorProvider`,而不是自己取 `DateTime.now()`: +/// · 两边看到的永远是同一个月,不会出现「卡片写 9 月、点进去是 8 月」 +/// · provider 已经是现成的,不会多打一次接口 +/// 所以标签用「N月」而不是「本月」。 +class BillModuleSummary extends ConsumerWidget { + const BillModuleSummary({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final anchor = ref.watch(monthAnchorProvider); + final stats = ref.watch(monthlyStatisticsProvider(anchor)).value; + + return Row( + children: [ + Expanded( + child: _item('${anchor.month}月支出', stats?.totalExpense, const Color(0xFFFA3534)), + ), + Expanded( + child: _item('结余', stats?.balance, const Color(0xFF5D4037)), + ), + ], + ); + } + + Widget _item(String label, double? amount, Color color) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF8D6E63))), + const SizedBox(height: 3), + Text( + // 统计还没回来时显示占位,避免卡片高度跳动 + amount == null ? '--' : '¥${formatAmount(amount)}', + style: TextStyle(fontSize: 14.5, fontWeight: FontWeight.bold, color: color), + ), + ], + ); + } +} diff --git a/lib/pages/home/widgets/budget_card.dart b/lib/pages/bill/widgets/budget_card.dart similarity index 100% rename from lib/pages/home/widgets/budget_card.dart rename to lib/pages/bill/widgets/budget_card.dart diff --git a/lib/pages/statistics/widgets/category_pie_card.dart b/lib/pages/bill/widgets/category_pie_card.dart similarity index 100% rename from lib/pages/statistics/widgets/category_pie_card.dart rename to lib/pages/bill/widgets/category_pie_card.dart diff --git a/lib/pages/statistics/widgets/category_rank_list.dart b/lib/pages/bill/widgets/category_rank_list.dart similarity index 100% rename from lib/pages/statistics/widgets/category_rank_list.dart rename to lib/pages/bill/widgets/category_rank_list.dart diff --git a/lib/pages/home/widgets/month_switcher.dart b/lib/pages/bill/widgets/month_switcher.dart similarity index 100% rename from lib/pages/home/widgets/month_switcher.dart rename to lib/pages/bill/widgets/month_switcher.dart diff --git a/lib/pages/home/dashboard_page.dart b/lib/pages/home/dashboard_page.dart new file mode 100644 index 0000000..2d1f9c0 --- /dev/null +++ b/lib/pages/home/dashboard_page.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; + +import '../../core/module/app_module.dart'; +import '../assistant/assistant_page.dart'; + +/// 首页仪表盘:AI 输入入口 + 模块卡片网格。 +/// +/// 这个页面是「模块目录」,不是某个模块的主页 —— 卡片全部从 [appModules] +/// 生成,所以将来加模块时**这个文件不需要改**。 +/// +/// 副作用是:打开 App 第一眼看不到账单了,要点一下「记账」卡片才进账单首页。 +/// 这是刻意的 —— 记账不再是主角。 +class DashboardPage extends StatelessWidget { + const DashboardPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFFFE5CC), Color(0xFFFFD4A8), Color(0xFFFFC08A)], + ), + ), + child: SafeArea( + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + children: [ + _header(), + const SizedBox(height: 16), + _aiEntry(context), + const SizedBox(height: 22), + const Text( + '功能模块', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 10), + _moduleGrid(context), + const SizedBox(height: 90), // 底部导航避让 + ], + ), + ), + ), + ); + } + + // ---- 标题 + 问候 ---- + Widget _header() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '强宝小助手', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFF5D4037), + letterSpacing: 1, + ), + ), + const SizedBox(height: 4), + Text( + _greeting(), + style: const TextStyle(fontSize: 13, color: Color(0xFF8D6E63)), + ), + ], + ); + } + + String _greeting() { + final hour = DateTime.now().hour; + if (hour < 6) return '夜深了,注意休息'; + if (hour < 12) return '早上好,今天想做点什么?'; + if (hour < 18) return '下午好,今天想做点什么?'; + return '晚上好,今天想做点什么?'; + } + + // ---- AI 入口(阶段 B 接上,现在点进去是占位页)---- + Widget _aiEntry(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const AssistantPage()), + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFC8956E), width: 1.2), + boxShadow: [ + BoxShadow( + color: const Color(0xFF5D4037).withValues(alpha: 0.08), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + const Text('✨', style: TextStyle(fontSize: 18)), + const SizedBox(width: 10), + const Expanded( + child: Text( + '问点什么,或者说一句…', + style: TextStyle(fontSize: 14, color: Color(0xFFBCAAA4)), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFF5EFE6), + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + '建设中', + style: TextStyle(fontSize: 11, color: Color(0xFF8D6E63)), + ), + ), + ], + ), + ), + ); + } + + // ---- 模块卡片网格 ---- + // + // 两列。不用 GridView + childAspectRatio:卡片高度由内容(摘要可有可无) + // 决定,估错比例就会在开发时刷一屏溢出警告 —— GridView 卡片的经典坑。 + // 这里让每张卡片自己撑高,居上对齐。 + Widget _moduleGrid(BuildContext context) { + const gap = 12.0; + final rows = <Widget>[]; + + for (var i = 0; i < appModules.length; i += 2) { + final end = i + 2 <= appModules.length ? i + 2 : appModules.length; + final pair = appModules.sublist(i, end); + + if (rows.isNotEmpty) rows.add(const SizedBox(height: gap)); + rows.add( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var j = 0; j < pair.length; j++) ...[ + if (j > 0) const SizedBox(width: gap), + Expanded(child: _moduleCard(context, pair[j])), + ], + // 模块数是奇数时补一个空位,否则最后一张卡片会被拉成通栏 + if (pair.length == 1) ...[ + const SizedBox(width: gap), + const Expanded(child: SizedBox()), + ], + ], + ), + ); + } + + return Column(children: rows); + } + + Widget _moduleCard(BuildContext context, AppModule module) { + return GestureDetector( + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: module.open)), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: const Color(0xFF5D4037).withValues(alpha: 0.08), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(module.icon, style: const TextStyle(fontSize: 24)), + const SizedBox(height: 8), + Text( + module.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 3), + Text( + module.blurb, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 11.5, color: Color(0xFF8D6E63)), + ), + if (module.summary != null) ...[ + const SizedBox(height: 10), + module.summary!(context), + ], + ], + ), + ), + ); + } +} diff --git a/lib/pages/login/login_page.dart b/lib/pages/login/login_page.dart index 57f373f..bbd070c 100644 --- a/lib/pages/login/login_page.dart +++ b/lib/pages/login/login_page.dart @@ -251,7 +251,7 @@ class _LoginPageState extends ConsumerState<LoginPage> _decoLine(), const SizedBox(width: 10), const Text( - '强宝爱记账', + '强宝小助手', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, diff --git a/lib/pages/mine/mine_page.dart b/lib/pages/mine/mine_page.dart index 0d6a021..b256fa7 100644 --- a/lib/pages/mine/mine_page.dart +++ b/lib/pages/mine/mine_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/module/app_module.dart'; import '../../core/network/api_exception.dart'; import '../../core/network/auth_redirect.dart'; import '../../models/account.dart'; @@ -12,7 +13,6 @@ import '../../utils/amount_input_formatter.dart'; import '../../utils/amount_util.dart'; import '../../utils/toast.dart'; import '../../widgets/state_views.dart'; -import '../note/note_list_page.dart'; /// 「我的」页面 /// @@ -52,7 +52,7 @@ class MinePage extends ConsumerWidget { const SizedBox(height: 14), _budgetCard(context, ref, budgetAsync), const SizedBox(height: 14), - _notesEntry(context), + _moduleSection(context), const SizedBox(height: 24), _logoutButton(context), const SizedBox(height: 90), @@ -369,43 +369,73 @@ class MinePage extends ConsumerWidget { ); } - // ---------------- 笔记入口 ---------------- - Widget _notesEntry(BuildContext context) { - return GestureDetector( - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const NoteListPage()), - ), - child: Container( - padding: const EdgeInsets.fromLTRB(18, 16, 14, 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), + // ---------------- 功能模块 ---------------- + // + // 和首页仪表盘的卡片共用同一份注册表(core/module/app_module.dart), + // 所以新增模块时这个页面不用改。 + Widget _moduleSection(BuildContext context) { + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, // 让水波纹被圆角裁掉 + child: Padding( + padding: const EdgeInsets.fromLTRB(18, 16, 8, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '功能模块', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 2), + for (final module in appModules) _moduleRow(context, module), + ], ), + ), + ); + } + + Widget _moduleRow(BuildContext context, AppModule module) { + return InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: module.open), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ - const Text('📝', style: TextStyle(fontSize: 24)), - const SizedBox(width: 14), - const Expanded( + Text(module.icon, style: const TextStyle(fontSize: 20)), + const SizedBox(width: 12), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '我的笔记', - style: TextStyle( + module.name, + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), - SizedBox(height: 3), + const SizedBox(height: 2), Text( - '管理 Markdown 文档,随时查看和记录', - style: TextStyle(fontSize: 12.5, color: Color(0xFF8D6E63)), + module.blurb, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12, color: Color(0xFF8D6E63)), ), ], ), ), - const Icon(Icons.chevron_right, color: Color(0xFFBCAAA4)), + const Padding( + padding: EdgeInsets.only(right: 3), + child: Icon(Icons.chevron_right, color: Color(0xFFBCAAA4)), + ), ], ), ), diff --git a/lib/pages/note/widgets/note_module_summary.dart b/lib/pages/note/widgets/note_module_summary.dart new file mode 100644 index 0000000..a57fa94 --- /dev/null +++ b/lib/pages/note/widgets/note_module_summary.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../providers/note_provider.dart'; + +/// 首页仪表盘上「笔记」卡片的摘要:笔记总数 + 最近更新时间。 +/// +/// 数据来自 [noteOverviewProvider](只取 1 条),不碰列表页那套状态。 +class NoteModuleSummary extends ConsumerWidget { + const NoteModuleSummary({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final page = ref.watch(noteOverviewProvider).value; + final latest = (page == null || page.items.isEmpty) + ? null + : page.items.first.updateTime; + + return Row( + children: [ + Expanded(child: _item('共', page == null ? null : '${page.total} 篇')), + Expanded(child: _item('最近更新', latest == null ? null : _shortDate(latest))), + ], + ); + } + + /// 后端给的是 `yyyy-MM-dd HH:mm`,卡片上只要 `MM-dd` + String _shortDate(String value) => + value.length >= 10 ? value.substring(5, 10) : value; + + Widget _item(String label, String? value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF8D6E63))), + const SizedBox(height: 3), + Text( + value ?? '--', + style: const TextStyle( + fontSize: 14.5, + fontWeight: FontWeight.bold, + color: Color(0xFF5D4037), + ), + ), + ], + ); + } +} diff --git a/lib/pages/shell/main_shell.dart b/lib/pages/shell/main_shell.dart index 25eb1b7..74c1762 100644 --- a/lib/pages/shell/main_shell.dart +++ b/lib/pages/shell/main_shell.dart @@ -3,11 +3,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/auth_provider.dart'; import '../add/add_page.dart'; -import '../home/home_page.dart'; +import '../assistant/assistant_page.dart'; +import '../home/dashboard_page.dart'; import '../login/login_page.dart'; import '../mine/mine_page.dart'; -import '../statistics/statistics_page.dart'; +/// 底部导航的四个 Tab 是四个「角色」,不是四个功能: +/// +/// 首页 = 模块目录(卡片由 core/module/app_module.dart 生成) +/// 记录 = 全局录入入口 +/// 助手 = AI(阶段 B 接入,现在是占位页) +/// 我的 = 账号、模块管理、设置 +/// +/// **具体模块不进 Tab** —— 记账、笔记都从首页卡片进。 +/// 代价是打开 App 多一次点击才能看到账单,换来的是「以后加模块不用改这里」。 class MainShell extends ConsumerStatefulWidget { const MainShell({super.key}); @@ -38,12 +47,12 @@ class _MainShellState extends ConsumerState<MainShell> { @override Widget build(BuildContext context) { - // IndexedStack 保证四个页面常驻:写操作后首页/统计的数据失效能立即重取, + // IndexedStack 保证四个页面常驻:写操作后各页的数据失效能立即重取, // 用户切回时看到的就是最新数据。 const pages = [ - HomePage(), + DashboardPage(), AddPage(), - StatisticsPage(), + AssistantPage(), MinePage(), ]; return Scaffold( @@ -54,10 +63,26 @@ class _MainShellState extends ConsumerState<MainShell> { backgroundColor: Colors.white, indicatorColor: const Color(0x33C8956E), destinations: const [ - NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: '首页'), - NavigationDestination(icon: Icon(Icons.add_circle_outline), selectedIcon: Icon(Icons.add_circle), label: '记账'), - NavigationDestination(icon: Icon(Icons.bar_chart_outlined), selectedIcon: Icon(Icons.bar_chart), label: '统计'), - NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: '我的'), + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon(Icons.home), + label: '首页', + ), + NavigationDestination( + icon: Icon(Icons.add_circle_outline), + selectedIcon: Icon(Icons.add_circle), + label: '记录', + ), + NavigationDestination( + icon: Icon(Icons.auto_awesome_outlined), + selectedIcon: Icon(Icons.auto_awesome), + label: '助手', + ), + NavigationDestination( + icon: Icon(Icons.person_outline), + selectedIcon: Icon(Icons.person), + label: '我的', + ), ], ), ); diff --git a/lib/providers/note_provider.dart b/lib/providers/note_provider.dart index 8e60190..94f3043 100644 --- a/lib/providers/note_provider.dart +++ b/lib/providers/note_provider.dart @@ -229,3 +229,12 @@ final noteTagsProvider = FutureProvider.autoDispose<List<String>>((ref) { final noteFolderTreeProvider = FutureProvider.autoDispose<FolderTree>((ref) { return ref.watch(noteApiProvider).folderTree(); }); + +/// 首页仪表盘用:只想知道「一共几篇、最近什么时候更新」。 +/// +/// 故意不复用 [noteListProvider] —— 那个 Notifier 带着搜索、标签、分页和防抖, +/// 为了一个数字把它挂到首页上,既会多打请求也会引入一堆无关状态。 +/// 这里只取第 1 页第 1 条,用返回的 total 拿总数。 +final noteOverviewProvider = FutureProvider.autoDispose<NotePage>((ref) { + return ref.watch(noteApiProvider).list(page: 1, size: 1); +}); diff --git a/test/page_layout_test.dart b/test/page_layout_test.dart new file mode 100644 index 0000000..d891222 --- /dev/null +++ b/test/page_layout_test.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:qiangbao_accounting/models/bill.dart'; +import 'package:qiangbao_accounting/models/note.dart'; +import 'package:qiangbao_accounting/models/statistics_response.dart'; +import 'package:qiangbao_accounting/pages/bill/bill_home_page.dart'; +import 'package:qiangbao_accounting/pages/home/dashboard_page.dart'; +import 'package:qiangbao_accounting/providers/bill_provider.dart'; +import 'package:qiangbao_accounting/providers/note_provider.dart'; + +/// 临时校验:改造后的两个页面在窄屏 / 常见机 / 大屏下都不溢出。 +void main() { + // 故意用会撑宽的数字,探一探摘要位窄不窄 + final stats = StatisticsResponse( + totalIncome: 123456.78, + totalExpense: 98765.43, + balance: 24691.35, + categoryStatistics: const [], + dailyStatistics: const [], + ); + final page = NotePage( + items: const [ + NoteListItem( + id: 1, + folderId: null, + title: 'MySQL 主从复制搭建教程', + summary: '', + tags: [], + pinned: false, + updateTime: '2026-09-23 03:36', + ), + ], + total: 128, + hasMore: false, + ); + + const sizes = [ + ('iPhone SE 窄屏', Size(320, 568)), + ('常见安卓机', Size(393, 873)), + ('大屏', Size(480, 1000)), + ]; + + void useSize(WidgetTester tester, Size size) { + tester.view.physicalSize = size * 3; + tester.view.devicePixelRatio = 3.0; + addTearDown(tester.view.reset); + } + + for (final (label, size) in sizes) { + testWidgets('仪表盘 $label 不溢出', (tester) async { + useSize(tester, size); + await tester.pumpWidget( + ProviderScope( + overrides: [ + monthlyStatisticsProvider.overrideWith((ref, month) async => stats), + noteOverviewProvider.overrideWith((ref) async => page), + ], + child: const MaterialApp(home: DashboardPage()), + ), + ); + await tester.pumpAndSettle(); + + // 先确认假数据真渲染出来了 —— 否则这个测试会「因为卡片是空的」而假通过 + expect(find.text('128 篇'), findsOneWidget); + expect(find.textContaining('98765.43'), findsWidgets); + + expect(tester.takeException(), isNull); + }); + + testWidgets('记账首页(push 进来,带返回箭头)$label 不溢出', (tester) async { + useSize(tester, size); + await tester.pumpWidget( + ProviderScope( + overrides: [ + monthlyStatisticsProvider.overrideWith((ref, month) async => stats), + billsProvider.overrideWith((ref, month) async => <Bill>[]), + ], + child: MaterialApp( + home: Builder( + builder: (ctx) => Scaffold( + body: Center( + child: TextButton( + onPressed: () => Navigator.of(ctx).push( + MaterialPageRoute(builder: (_) => const BillHomePage()), + ), + child: const Text('进入'), + ), + ), + ), + ), + ), + ), + ); + + // 真的 push 一次,canPop() 才是 true,才能测到「返回箭头 + 标题 + + // 统计图标 + 月份切换」同时挤在一行的最坏情况 + await tester.tap(find.text('进入')); + await tester.pumpAndSettle(); + + expect(find.text('我的账单'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + } +} diff --git a/web/index.html b/web/index.html index 92d79d3..a515593 100644 --- a/web/index.html +++ b/web/index.html @@ -18,18 +18,18 @@ <meta charset="UTF-8"> <meta content="IE=Edge" http-equiv="X-UA-Compatible"> - <meta name="description" content="强宝爱记账 —— 智能记账,轻松管理"> + <meta name="description" content="强宝小助手 —— 智能记账,轻松管理"> <!-- iOS meta tags & icons --> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> - <meta name="apple-mobile-web-app-title" content="强宝爱记账"> + <meta name="apple-mobile-web-app-title" content="强宝小助手"> <link rel="apple-touch-icon" href="icons/Icon-192.png"> <!-- Favicon --> <link rel="icon" type="image/png" href="favicon.png"/> - <title>强宝爱记账 + 强宝小助手 diff --git a/web/manifest.json b/web/manifest.json index cbaa2db..3632193 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "强宝爱记账", - "short_name": "强宝记账", + "name": "强宝小助手", + "short_name": "强宝助手", "start_url": ".", "display": "standalone", "background_color": "#FFF6EC", "theme_color": "#C8956E", - "description": "强宝爱记账 —— 智能记账,轻松管理", + "description": "强宝小助手 —— 智能记账,轻松管理", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [