a4a97c03c2
资料页头部三项(展示名 / 头像 / 数字)自此全部来自服务端,176 行硬编码 demo(「萌宠新手(豆豆家长)」/ 24 / 1.8k / 2)退役。 - `PatchField<T>` 承载契约 v1.4.0 的 PATCH 三态(absent 不落键 / clear 落 显式 null / value 落值)。三态必须由类型承载而非 `T?` 加约定:把「不改」 也编码成 null,用户只改昵称就会连头像一起被服务端清掉。 - `UserProfile` 补 nickname / avatarUrl,展示回退 `nickname ?? username` **做在客户端展示层**——服务端 /me 刻意返回 DB 原值,编辑页因此只用原值 预填,避免把展示约定固化成真实昵称。 - `ProfileController`:`/me` 主链路四态 + 两块统计独立三态(统计失败只降级 这一块,不为两个数字丢掉整页);登出 reset 防跨账号泄漏。 - 编辑页维护三态意图而非「当前值整体提交」:昵称 / 头像各有显式清除入口, 未改字段的键根本不进 JSON;空 patch 直接短路(服务端对空 patch 答 400)。 - 昵称校验按**码点**计 1~32 且先 btrim,与 `ck_users_nickname` 对齐; 纯空白是校验失败而非隐式清空。 - `MediaUploader` 的 purpose 参数化 + 复用其六态编排的 `AvatarUploadSheet` (单图、预览确认后才交付 ready assetId,孤儿防护不变)。 - 修复 `/api/v1/me` 端口线路:该端点由 user 服务(:8082)提供,`ApiAuthRepository` 此前只挂 auth(:8081)会得到 404。此前无人消费 me(),故这条错线一直没被 触发;本单是第一个真实消费者,桌面实测即暴露。 测试 526 → 588(+62)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
304 lines
11 KiB
Dart
304 lines
11 KiB
Dart
import 'dart:async';
|
||
|
||
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/core/widgets/avatar_upload_sheet.dart';
|
||
import 'package:patbond_flutter/features/community/community_controller.dart';
|
||
import 'package:patbond_flutter/features/community/community_interaction_analytics.dart';
|
||
import 'package:patbond_flutter/features/community/feed_analytics.dart';
|
||
import 'package:patbond_flutter/features/community/media_uploader.dart';
|
||
import 'package:patbond_flutter/features/community/post_analytics.dart';
|
||
import 'package:patbond_flutter/features/community/post_compose_page.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/health_record_analytics.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_controller.dart';
|
||
import 'package:patbond_flutter/features/profile/profile_page.dart';
|
||
import 'package:patbond_flutter/features/services/services_page.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,
|
||
required this.communityController,
|
||
required this.profileController,
|
||
super.key,
|
||
this.currentUserId,
|
||
this.petAnalytics,
|
||
this.healthRecordAnalytics,
|
||
this.feedAnalytics,
|
||
this.interactionAnalytics,
|
||
this.postAnalytics,
|
||
this.mediaUploaderFactory,
|
||
this.avatarUploaderBuilder,
|
||
this.pageViewTracker,
|
||
this.onLogout,
|
||
});
|
||
|
||
final AppState appState;
|
||
|
||
/// 宠物档案状态(T2-11 拆出的独立 pets feature;档案 Tab 数据源)。
|
||
final PetsController petsController;
|
||
|
||
/// 社区状态(T3-12 数据层;首页 Feed segment 数据源,T3-14 接线)。
|
||
final CommunityController communityController;
|
||
|
||
/// 本人资料与社区数字(T3.5-08);资料 Tab 与首页问候语共用。
|
||
final ProfileController profileController;
|
||
|
||
/// 当前登录用户 id(详情页评论删除入口 / 关注钮自见性的 UI 判定)。
|
||
final String? currentUserId;
|
||
|
||
/// pet 域埋点强类型封装(建宠漏斗三事件)。
|
||
final PetAnalytics? petAnalytics;
|
||
|
||
/// health_record 域埋点强类型封装(T2-13 创建漏斗三事件 + viewed)。
|
||
final HealthRecordAnalytics? healthRecordAnalytics;
|
||
|
||
/// feed 域埋点(T3-14 聚合曝光 + 加载失败)。
|
||
final FeedAnalytics? feedAnalytics;
|
||
|
||
/// 互动域埋点(T3-16 评论成败对 + 关注对;详情页消费)。
|
||
final CommunityInteractionAnalytics? interactionAnalytics;
|
||
|
||
/// post 域埋点(T3-17 发布漏斗五事件 + 媒体三段;发布页消费)。
|
||
final PostAnalytics? postAnalytics;
|
||
|
||
/// 发布页 [MediaUploader] 构造口(桌面实测 / 测试替换选图与压缩层)。
|
||
final MediaUploaderFactory? mediaUploaderFactory;
|
||
|
||
/// 头像上传构造口(T3.5-08/09:资料编辑页与宠物详情页)。
|
||
final AvatarUploaderBuilder? avatarUploaderBuilder;
|
||
|
||
/// 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]);
|
||
}
|
||
|
||
/// 帖子详情(T3-15:真实数据整页;Feed 卡片与详情共享
|
||
/// communityController,互动状态跨页一致)。
|
||
void openPost(String postId) {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute<void>(
|
||
settings: RouteSettings(name: AnalyticsPageName.postDetail.pageName),
|
||
builder: (context) => PostDetailPage(
|
||
controller: widget.communityController,
|
||
postId: postId,
|
||
currentUserId: widget.currentUserId,
|
||
analytics: widget.interactionAnalytics,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 发布页(T3-17:真实发布链路)。发布成功后回首页 Feed 并整体刷新,
|
||
/// 新帖按 `(published_at DESC, id DESC)` 落在首位(跨客户端同一序)。
|
||
Future<void> openCompose(PostEntryPoint entryPoint) async {
|
||
final published = await Navigator.of(context).push<bool>(
|
||
MaterialPageRoute<bool>(
|
||
settings: RouteSettings(name: AnalyticsPageName.postForm.pageName),
|
||
builder: (context) => PostComposePage(
|
||
controller: widget.communityController,
|
||
entryPoint: entryPoint,
|
||
analytics: widget.postAnalytics,
|
||
uploaderFactory: widget.mediaUploaderFactory,
|
||
),
|
||
),
|
||
);
|
||
if (!mounted || published != true) return;
|
||
selectTab(0);
|
||
unawaited(widget.communityController.refresh());
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(const SnackBar(content: Text('已发布,去首页看看吧 🐾')));
|
||
}
|
||
|
||
@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,
|
||
communityController: widget.communityController,
|
||
profileController: widget.profileController,
|
||
feedAnalytics: widget.feedAnalytics,
|
||
isActive: currentIndex == 0,
|
||
onOpenServices: openServices,
|
||
onOpenCompose: () => openCompose(PostEntryPoint.feed),
|
||
onOpenPost: openPost,
|
||
),
|
||
CreatePage(
|
||
appState: widget.appState,
|
||
// T3-17:发布半边已真实化(发布页 push),AI 生成模拟原样留 M4。
|
||
onOpenCompose: () => openCompose(PostEntryPoint.createTab),
|
||
),
|
||
PetsPage(
|
||
controller: widget.petsController,
|
||
analytics: widget.petAnalytics,
|
||
healthAnalytics: widget.healthRecordAnalytics,
|
||
avatarUploaderBuilder: widget.avatarUploaderBuilder,
|
||
),
|
||
ServicesPage(
|
||
showPersonal: showPersonalServices,
|
||
locationWeather: widget.appState.locationWeather,
|
||
),
|
||
ProfilePage(
|
||
appState: widget.appState,
|
||
controller: widget.profileController,
|
||
avatarUploaderBuilder: widget.avatarUploaderBuilder,
|
||
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: '我的',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|