97a1f46e3f
CI / flutter-gates (push) Successful in 1m31s
- 档案 Tab 替换为真实宠物列表(05 号规范 P1):PetsController 四态 驱动(loading/empty/error+重试/ready),空态插画 + 建档 CTA, 下拉刷新,虚线添加卡;页面一律走 T2-11 数据层、不直连 ApiClient - 新增 PetDetailPage(P2 档案信息部分):内存副本首屏 + getPet 刷新, 40401 防枚举 → 不存在态返回列表并刷新;仅 owner 显示编辑入口 (40300 语义);体重/疫苗/时间线区块留待 T2-13/14 接摘要与记录接口 - 新增 PetFormPage(建档/编辑):品种目录接口 + 自定义品种互斥 (目录失败回落自定义 + 重试)、性别契约必填、生日+估算、芯片号; 失焦+提交双校验、错误三层模型沿用登录纵切 - 40902 版本冲突:明确提示 + 自动拉取最新 version 重提路径(有测试); 40903 芯片号冲突字段级报错(有测试) - 埋点挂接:pet_create_started(表单首次输入、每次进入一次)/ succeeded / failed 三事件接线;建宠表单路由名 pet_form、详情页 pet_detail 进入既有 RouteObserver 采集;档案 Tab 页名 pet_archive 改报 pet_list(字典 v2);编辑表单不带路由名(漏斗到达段不掺编辑) - app.dart 装配:pet 服务 ApiClient(:8083)共享 TokenRefresher; 登出 reset 控制器防跨账号泄漏;PetsController.loadBreeds 按物种缓存 - AppState 清理:pets 页原 demo 消费方移除,vaccines 及其模型/持久化 删除;pet demo 仅存首页问候/创作页/主壳头像(后续工单收敛) - 头像按 ADR-010 本地占位,不做上传 - 测试 145 → 177 全绿(列表/详情/表单四态与冲突路径 widget 测试、 控制器 loadBreeds/reset、展示纯函数);analyze 0;format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
223 lines
7.5 KiB
Dart
223 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:patbond_flutter/analytics/analytics_page_name.dart';
|
||
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
|
||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||
import 'package:patbond_flutter/features/create/create_page.dart';
|
||
import 'package:patbond_flutter/features/home/home_page.dart';
|
||
import 'package:patbond_flutter/features/pets/pet_analytics.dart';
|
||
import 'package:patbond_flutter/features/pets/pets_controller.dart';
|
||
import 'package:patbond_flutter/features/pets/pets_page.dart';
|
||
import 'package:patbond_flutter/features/post/post_detail_page.dart';
|
||
import 'package:patbond_flutter/features/profile/profile_page.dart';
|
||
import 'package:patbond_flutter/features/services/services_page.dart';
|
||
import 'package:patbond_flutter/models/models.dart';
|
||
import 'package:patbond_flutter/state/app_state.dart';
|
||
import 'package:patbond_flutter/widgets/common.dart';
|
||
|
||
class MainShellPage extends StatefulWidget {
|
||
const MainShellPage({
|
||
required this.appState,
|
||
required this.petsController,
|
||
super.key,
|
||
this.petAnalytics,
|
||
this.pageViewTracker,
|
||
this.onLogout,
|
||
});
|
||
|
||
final AppState appState;
|
||
|
||
/// 宠物档案状态(T2-11 拆出的独立 pets feature;档案 Tab 数据源)。
|
||
final PetsController petsController;
|
||
|
||
/// pet 域埋点强类型封装(建宠漏斗三事件)。
|
||
final PetAnalytics? petAnalytics;
|
||
|
||
/// Tab 曝光补点(IndexedStack 切换不产生路由事件,03 号评估 §3.2)。
|
||
final PageViewTracker? pageViewTracker;
|
||
|
||
/// 退出登录:调 logout 接口并清会话,认证状态机自动回登录页。
|
||
final Future<void> Function()? onLogout;
|
||
|
||
@override
|
||
State<MainShellPage> createState() => _MainShellPageState();
|
||
}
|
||
|
||
class _MainShellPageState extends State<MainShellPage> {
|
||
int currentIndex = 0;
|
||
bool showPersonalServices = false;
|
||
|
||
static const titles = ['首页', '创作中心', '健康档案', '本地服务', '我的资料'];
|
||
|
||
/// Tab 索引 → pageName 枚举(03 号评估 §3.2 映射表;档案 Tab 自 T2-12
|
||
/// 起为真实宠物列表,页名由 pet_archive 改报 pet_list,06 §5.2 字典 v2)。
|
||
static const _tabPages = [
|
||
AnalyticsPageName.home,
|
||
AnalyticsPageName.create,
|
||
AnalyticsPageName.petList,
|
||
AnalyticsPageName.services,
|
||
AnalyticsPageName.profile,
|
||
];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// 初始 Tab 曝光补一次(03 号评估 §3.2 手动补点 1)。
|
||
widget.pageViewTracker?.reportTab(_tabPages[currentIndex]);
|
||
}
|
||
|
||
void selectTab(int index) {
|
||
setState(() => currentIndex = index);
|
||
widget.pageViewTracker?.reportTab(_tabPages[index]);
|
||
}
|
||
|
||
void openServices(bool personal) {
|
||
setState(() {
|
||
showPersonalServices = personal;
|
||
currentIndex = 3;
|
||
});
|
||
widget.pageViewTracker?.reportTab(_tabPages[3]);
|
||
}
|
||
|
||
void openPost(PostModel post) {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute<void>(
|
||
settings: RouteSettings(name: AnalyticsPageName.postDetail.pageName),
|
||
builder: (context) =>
|
||
PostDetailPage(appState: widget.appState, postId: post.id),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AnimatedBuilder(
|
||
animation: widget.appState,
|
||
builder: (context, _) {
|
||
if (!widget.appState.isReady) {
|
||
return const Scaffold(
|
||
body: Center(child: CircularProgressIndicator()),
|
||
);
|
||
}
|
||
|
||
final pages = [
|
||
HomePage(
|
||
appState: widget.appState,
|
||
onOpenPost: openPost,
|
||
onOpenServices: openServices,
|
||
onOpenCreate: () => selectTab(1),
|
||
),
|
||
CreatePage(
|
||
appState: widget.appState,
|
||
onPublished: (post) {
|
||
selectTab(0);
|
||
WidgetsBinding.instance.addPostFrameCallback(
|
||
(_) => openPost(post),
|
||
);
|
||
},
|
||
),
|
||
PetsPage(
|
||
controller: widget.petsController,
|
||
analytics: widget.petAnalytics,
|
||
),
|
||
ServicesPage(
|
||
showPersonal: showPersonalServices,
|
||
locationWeather: widget.appState.locationWeather,
|
||
),
|
||
ProfilePage(appState: widget.appState, onLogout: widget.onLogout),
|
||
];
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
backgroundColor: Colors.white.withAlpha(245),
|
||
surfaceTintColor: Colors.transparent,
|
||
scrolledUnderElevation: 1,
|
||
centerTitle: true,
|
||
title: Text(
|
||
titles[currentIndex],
|
||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
|
||
),
|
||
leadingWidth: 118,
|
||
leading: InkWell(
|
||
onTap: () => selectTab(2),
|
||
borderRadius: BorderRadius.circular(24),
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 14),
|
||
child: Row(
|
||
children: [
|
||
RemoteImage(
|
||
url: widget.appState.pet.avatarUrl,
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: BorderRadius.circular(18),
|
||
),
|
||
const SizedBox(width: 7),
|
||
const Expanded(
|
||
child: Text(
|
||
'Patbond',
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: AppColors.primary,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
actions: [
|
||
IconButton.filledTonal(
|
||
tooltip: '通知',
|
||
onPressed: () {
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(const SnackBar(content: Text('暂无新通知 🐾')));
|
||
},
|
||
icon: const Icon(Icons.notifications_outlined, size: 21),
|
||
),
|
||
const SizedBox(width: 10),
|
||
],
|
||
),
|
||
body: SafeArea(
|
||
top: false,
|
||
child: IndexedStack(index: currentIndex, children: pages),
|
||
),
|
||
bottomNavigationBar: NavigationBar(
|
||
selectedIndex: currentIndex,
|
||
onDestinationSelected: selectTab,
|
||
destinations: const [
|
||
NavigationDestination(
|
||
icon: Icon(Icons.home_outlined),
|
||
selectedIcon: Icon(Icons.home_rounded),
|
||
label: '首页',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.auto_awesome_outlined),
|
||
selectedIcon: Icon(Icons.auto_awesome),
|
||
label: '创作',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.pets_outlined),
|
||
selectedIcon: Icon(Icons.pets),
|
||
label: '档案',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.storefront_outlined),
|
||
selectedIcon: Icon(Icons.storefront),
|
||
label: '服务',
|
||
),
|
||
NavigationDestination(
|
||
icon: Icon(Icons.person_outline),
|
||
selectedIcon: Icon(Icons.person),
|
||
label: '我的',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|