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` 的 `` 与 `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 @@
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` 的 `` | `强宝爱记账` |
-| 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` 的 `` | `强宝小助手` |
+| 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 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- 强宝爱记账
+ 强宝小助手
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
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 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 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 = [];
+
+ 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
_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 {
@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 {
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>((ref) {
final noteFolderTreeProvider = FutureProvider.autoDispose((ref) {
return ref.watch(noteApiProvider).folderTree();
});
+
+/// 首页仪表盘用:只想知道「一共几篇、最近什么时候更新」。
+///
+/// 故意不复用 [noteListProvider] —— 那个 Notifier 带着搜索、标签、分页和防抖,
+/// 为了一个数字把它挂到首页上,既会多打请求也会引入一堆无关状态。
+/// 这里只取第 1 页第 1 条,用返回的 total 拿总数。
+final noteOverviewProvider = FutureProvider.autoDispose((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 => []),
+ ],
+ 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 @@
-
+
-
+
- 强宝爱记账
+ 强宝小助手
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": [