9.9 KiB
9.9 KiB
项目长期记忆:强宝小助手
项目概况
- 名称:
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/,未配置桌面端。
技术栈与约定
- 状态管理:flutter_riverpod ^3.0.0(
Notifier/NotifierProvider,非旧版 StateNotifier)。 - 网络:dio ^5.8.0,API 层按模块拆分在
lib/api/,每个模块导出xxxApiProvider = Provider<XxxApi>(...)。 - 本地存储:
shared_preferences,统一封装在lib/core/storage/local_storage.dart。 - 其他依赖:
image_picker(OCR 选图)、intl、cupertino_icons。 - UI:Material 3(Flutter 3.16+ 默认启用),未显式设置
useMaterial3。
架构与关键文件
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 是角色不是功能 —— 首页(仪表盘)/记录(全局录入)/助手(AI)/我的,用IndexedStack保留各页状态,NavigationBar(M3)承载导航,默认_index = 0(首页)。initState里有登录态兜底跳转。lib/pages/login/login_page.dart:登录/注册页,含毛玻璃卡片(BackdropFilter)、动画背景圆球(AnimationController+Stack+Positioned+Transform.translate)、记住密码(utils/crypto.dart加解密)。- 首页、统计、我的都已是真实实现(不是占位页)。记账录入页在
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)。
AI 助手(2026-09-23 阶段 B)
- 链路:
lib/api/ai_api.dart(SSE 客户端)→lib/providers/ai_provider.dart(assistantProvider+chatSessionsProvider+aiStatusProvider)→lib/pages/assistant/assistant_page.dart(真对话页)。 - SSE 是手动解析的:dio 没有内置 SSE 支持,用
ResponseType.stream拿字节流,再cast<List<int>>().transform(utf8.decoder).transform(LineSplitter())按event:/data:/ 空行切。cast不能省 —— dio 给的是Stream<Uint8List>,Dart 泛型不允许它直接喂给StreamTransformer<List<int>, String>。 sessionId传 null 开新会话,后端在done事件里才把新 ID 返回,AssistantNotifier.send必须把它记下来,否则下一条消息又会开一个新会话。- 历史会话存后端(
chat_session/chat_message两张表),App 只发sessionId + content。工具调用的中间过程不入库,所以 App 不用理解 tool 消息格式。 - 助手气泡用
MarkdownBody渲染(回答里常有列表/代码块),用户气泡用SelectableText。 - 后端未配 key 时不报错:
/api/ai/status返回enabled:false,/chat推一个 error 事件,页面顶部显示红色提示条。 - AI 写库后必须让 App 侧的数据失效(2026-09-24 修的真实 bug)。
AssistantNotifier.send里跟踪 SSE 的tool事件,命中_mutatingTools(create_bill/create_note)就在对话结束后调invalidateBillData(ref)或失效那四个笔记 provider。 为什么必须显式做:IndexedStack让首页仪表盘和「我的」页常驻,它们 watch 的monthlyStatisticsProvider/budgetProvider/accountProvider永远不会被 autoDispose 回收,只靠「写操作后显式失效」更新。AI 是在服务端直接写库的,App 不知情 → 这三个一直是旧数。而billsProvider只被 push 出来的记账首页 watch,会被回收重取 —— 于是现象是**「账单列表对了、汇总和预算环没对」**,很有迷惑性。 回归测试在test/ai_refresh_test.dart(用假 AiApi 吐 SSE 事件,断言 provider 的重建次数)。加新的写工具时记得同步_mutatingTools,否则会静默失效。
设计规范(配色)
- 品牌主色:
#C8956E(驼色/焦糖色),在app.dart中作为ColorScheme.fromSeed的 seedColor。 - 深棕文字色:
#5D4037;辅助文字:#8D6E63;页面底色:#F8F8F8。 - 登录页另用暖橙渐变
#FFE5CC → #FFD4A8 → #FFC08A,点缀金色#FFD700、粉色#FFB6C1。 - 注意:项目中颜色多为硬编码字面量,尚未统一走
Theme.of(context).colorScheme(有优化空间)。
网络环境配置(重要)
- 环境切换在
lib/core/network/api_env.dart:baseUrl三级优先级 =--dart-define=API_BASE_URL>!kDebugMode取kProdBaseUrl>kLocalBaseUrl。 - 消费链:
dio_client.dart的dioProvider→BaseOptions.baseUrl;API 层使用相对路径(如/auth/login)。 - 生产地址当前为
http://103.36.220.231:12345/api(明文 HTTP + 裸 IP)。与注释及docs/Flutter打包与真机安装指南.md中记录的https://accounting.aqroid.cn/api不一致。 ios/Runner/Info.plist无任何 ATS 例外配置 → iOS 包请求 http 会被拦截。android/app/src/main/AndroidManifest.xml已设android:usesCleartextTraffic="true"。- 已知风险:①
--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或域例外)。 MainShell未做响应式:宽屏(≥600dp/840dp)应切换NavigationRail/NavigationDrawer。MainShell.initState的addPostFrameCallback回调里可加if (!mounted) return;。- Flutter 3.47 起 Material/Cupertino 已解耦为
material_ui/cupertino_ui包,package:flutter/material.dart仍可用,暂无需迁移。
应用名称与图标(已落地)
- 显示名统一为 强宝小助手:
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,不会进包。 assets/images/logo.png(1120×928 贴纸海报)不可用作图标:非正方形、带虚化底纹、含大字,48px 下不可读。- 改图标 = 重跑上面两条命令;改名字后需卸载重装才刷新桌面。
用户画像与偏好
- 用户正在系统学习 Flutter 基础概念,常就单个文件/代码片段提问(如
GlobalKey、ConsumerWidget、ref、Stack)。 - 沟通偏好:希望用中文解释,讲清「是什么 + 为什么这么写 + 在本项目中的上下文」,并配合结构化的表格/分层说明。
- 用户对 Material 3、多端适配、官方文档出处感兴趣,回答时应附官方文档链接。