9892b65a19
CI / flutter-gates (push) Successful in 3m9s
- 新增 PostComposePage(P3 发布页,push 路由 post_form):正文/类目 (general·help)/九宫格选图(组装 MediaUploader + UploadProgressOverlay, 删格按列表序重发 position)/发布 gating(正文非空 + 在场媒体全 ready) - 发布走「createPost(draft) → PATCH status=published」两步:迁移失败时草稿 已在服务端,UI 明确提示「草稿已保存」;40905 重置幂等键、42203 提示等图、 网络失败同键重放不重复建帖;40902 自动取新 version 重提一次 - 草稿:「存草稿」(manual) 与「取消 → 保留」(on_exit) 两路径 + 进页恢复最新 一条草稿(提示条/清空);「不保留」软删服务端草稿(post_deleted 触点) - 埋点:新增 post_analytics.dart(发布漏斗五事件 + 媒体三段,键集对齐字典 v3);MediaUploader 挂接 started/succeeded/failed(含 cancelled) 与 attemptSeq; pageName 枚举补 post_form - PostMediaEditGrid(同文件编辑态):3 列九宫格 + 虚线「+」格 + 删除角标 + 进度覆盖层 + 失败整格重试;页级上传汇总条 - create 页只余 AI 生成模拟(M4 原样保留),demo 发布流与 AppState.posts / publishPost / updatePost 及其持久化一并退役;首页 story「发布」与 Feed 空态 CTA 改为 push 真实发布页 - 测试 458 → 502(+44):发布页 widget 22、发布埋点 11、媒体三段 7、编辑态 九宫格 4、主壳发布闭环 1;另加桌面真链路 integration_test(env 门控) - flutter analyze 0 问题、dart format 无 diff;compose 六容器真链路实测通过 (选图上传 → 发布 → Feed 置顶 → 第二客户端可见) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
287 lines
10 KiB
Dart
287 lines
10 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/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_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,
|
||
super.key,
|
||
this.currentUserId,
|
||
this.petAnalytics,
|
||
this.healthRecordAnalytics,
|
||
this.feedAnalytics,
|
||
this.interactionAnalytics,
|
||
this.postAnalytics,
|
||
this.mediaUploaderFactory,
|
||
this.pageViewTracker,
|
||
this.onLogout,
|
||
});
|
||
|
||
final AppState appState;
|
||
|
||
/// 宠物档案状态(T2-11 拆出的独立 pets feature;档案 Tab 数据源)。
|
||
final PetsController petsController;
|
||
|
||
/// 社区状态(T3-12 数据层;首页 Feed segment 数据源,T3-14 接线)。
|
||
final CommunityController communityController;
|
||
|
||
/// 当前登录用户 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;
|
||
|
||
/// 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,
|
||
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,
|
||
),
|
||
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: '我的',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|