新增:帖子详情页真实数据整页替换——四态/真九宫格大图/评论区/关注双态,Feed 导航与互动全接线,demo 详情退役(T3-15/16)
CI / flutter-gates (push) Failing after 20s
CI / flutter-gates (push) Failing after 20s
- post_detail_page 整页重写:内存副本先渲染 + getPost 拉新、四态 (loading/error+重试/ready/40403 提示后返回 Feed 并刷新);媒体全量 ——单图原比例(高度钳制 [0.75w,1.33w])、多图真九宫格(D11 轮播 弃用,本单裁定)+ InteractiveViewer 全屏大图(03 号拍板 E) - 评论区:游标列表触底补页 + 底部输入条创建(幂等键在数据层)+ 仅本人评论渲染删除入口(40301/40404 服务端兜底);成败埋点对 (durationMs 取输入会话起点、attemptSeq 会话内递增);CommentTile 升共享组件(@ 回复前缀呈现、降级作者「宠友」占位) - 关注双态钮(tonal +关注 / Outlined 已关注 + 取关确认)乐观翻转、 回滚零动画 + SnackBar,user_followed/unfollowed(post_detail) 挂接; 本人帖不渲染 - Feed 接通:整卡与评论钮导航详情(T3-14 占位提示移除)、点赞/收藏 接共享 ToggleSync(详情与卡片同一实例跨页一致)、toggleError 一次性 SnackBar 消费 - 主壳/装配:openPost 按 postId push;create demo 流不再导航已退役的 demo 详情(发布页真实化留 T3-17);AppState 在 post_detail/Feed 面 消费清零 - compose 冒烟(env 门控默认跳过):真链路发帖/点赞幂等/收藏/评论/ 删评一轮 + 断网点赞回滚(生产 ApiClient 异常链) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+8
-1
@@ -13,6 +13,7 @@ import 'package:patbond_flutter/features/auth/login_page.dart';
|
||||
import 'package:patbond_flutter/features/auth/session_manager.dart';
|
||||
import 'package:patbond_flutter/features/auth/splash_page.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/community_repository.dart';
|
||||
import 'package:patbond_flutter/features/community/feed_analytics.dart';
|
||||
import 'package:patbond_flutter/features/main/main_shell_page.dart';
|
||||
@@ -50,6 +51,7 @@ class _AppState extends State<App> {
|
||||
late final PetAnalytics petAnalytics;
|
||||
late final HealthRecordAnalytics healthRecordAnalytics;
|
||||
late final FeedAnalytics feedAnalytics;
|
||||
late final CommunityInteractionAnalytics interactionAnalytics;
|
||||
late final SessionTracker _sessionTracker;
|
||||
late final AnalyticsService _analytics;
|
||||
late final PageViewTracker _pageViewTracker;
|
||||
@@ -95,9 +97,12 @@ class _AppState extends State<App> {
|
||||
petsController = PetsController(
|
||||
repository: widget.petsRepository ?? _buildPetsRepository(),
|
||||
);
|
||||
// T3-14:Feed segment 接线主壳(数据层 T3-12 就位)。
|
||||
// T3-14:Feed segment 接线主壳(数据层 T3-12 就位);T3-16 起
|
||||
// 点赞/收藏成功埋点经 interactionAnalytics 在 controller 内上报。
|
||||
interactionAnalytics = CommunityInteractionAnalytics(_analytics.trackEvent);
|
||||
communityController = CommunityController(
|
||||
repository: widget.communityRepository ?? _buildCommunityRepository(),
|
||||
interactionAnalytics: interactionAnalytics,
|
||||
);
|
||||
petAnalytics = PetAnalytics(_analytics.trackEvent);
|
||||
healthRecordAnalytics = HealthRecordAnalytics(_analytics.trackEvent);
|
||||
@@ -235,9 +240,11 @@ class _AppState extends State<App> {
|
||||
appState: appState,
|
||||
petsController: petsController,
|
||||
communityController: communityController,
|
||||
currentUserId: sessionManager.userId,
|
||||
petAnalytics: petAnalytics,
|
||||
healthRecordAnalytics: healthRecordAnalytics,
|
||||
feedAnalytics: feedAnalytics,
|
||||
interactionAnalytics: interactionAnalytics,
|
||||
pageViewTracker: _pageViewTracker,
|
||||
onLogout: authRepository.logout,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/core/widgets/pet_avatar.dart';
|
||||
import 'package:patbond_flutter/features/community/community_display.dart';
|
||||
import 'package:patbond_flutter/features/community/community_models.dart';
|
||||
|
||||
/// 评论条目(05 号规范 §3.4,demo 气泡形态升共享):
|
||||
/// `PetAvatar sm32` + 10 + 气泡(`surface` 底、`border` 1px、圆角 16、
|
||||
/// padding 12)——作者名 13/w700 → 4 → 内容 bodyMedium → 6 → 底行
|
||||
/// (时间 11 `inkSoft` + 仅本人评论的「删除」入口)。
|
||||
///
|
||||
/// - @ 回复(单层平铺,契约 replyToUser)以「回复 @昵称:」前缀呈现;
|
||||
/// 降级作者统一「宠友」占位(isDegraded 一个判定口)。
|
||||
/// - [onDelete] 非 null 才渲染删除入口——**权限判定在调用方**(17 号
|
||||
/// 后端语义:仅评论作者可删,他人可见评论 403/40301);[deleting]
|
||||
/// 期间入口替换为 14 转圈防重复提交。
|
||||
/// - 评论点赞(§3.4 底行右端)无契约端点,M3 不渲染。
|
||||
class CommentTile extends StatelessWidget {
|
||||
const CommentTile({
|
||||
required this.comment,
|
||||
super.key,
|
||||
this.onDelete,
|
||||
this.deleting = false,
|
||||
this.now,
|
||||
});
|
||||
|
||||
final PostComment comment;
|
||||
|
||||
/// 删除回调;null = 非本人评论,不渲染删除入口。
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
/// 删除请求在途(入口转圈锁定)。
|
||||
final bool deleting;
|
||||
|
||||
/// 相对时间的参考时钟(测试注入;缺省取当前时间)。
|
||||
final DateTime? now;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final replyTo = comment.replyToUser;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PetAvatar(size: PetAvatarSize.sm, url: comment.author.avatarUrl),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border.all(color: AppColors.border),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
authorDisplayName(comment.author),
|
||||
style: const TextStyle(
|
||||
color: AppColors.ink,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (replyTo != null)
|
||||
TextSpan(
|
||||
text: '回复 @${authorDisplayName(replyTo)}:',
|
||||
style: const TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
TextSpan(text: comment.content),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
feedRelativeTime(comment.createdAt, now: now),
|
||||
style: const TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (onDelete != null)
|
||||
deleting
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.inkSoft,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Semantics(
|
||||
label: '删除评论',
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: onDelete,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppRadius.pill,
|
||||
),
|
||||
// 视觉 11 字,触控由 padding 撑到 ≥32
|
||||
//(气泡内行高受限,不足 44 以热区扩展补偿)。
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
'删除',
|
||||
style: TextStyle(
|
||||
color: AppColors.inkSoft,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class HomePage extends StatefulWidget {
|
||||
required this.onOpenServices,
|
||||
required this.onOpenCreate,
|
||||
super.key,
|
||||
this.onOpenPost,
|
||||
this.feedAnalytics,
|
||||
this.isActive = true,
|
||||
});
|
||||
@@ -38,6 +39,9 @@ class HomePage extends StatefulWidget {
|
||||
final ValueChanged<bool> onOpenServices;
|
||||
final VoidCallback onOpenCreate;
|
||||
|
||||
/// 帖子详情导航(T3-15 接通;主壳 push PostDetailPage)。
|
||||
final ValueChanged<String>? onOpenPost;
|
||||
|
||||
/// feed 域埋点(feed_viewed 聚合曝光 + feed_load_failed)。
|
||||
final FeedAnalytics? feedAnalytics;
|
||||
|
||||
@@ -231,14 +235,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/// T3-14 取舍:详情页数据层重写属 T3-15,demo 详情页无法按服务端
|
||||
/// postId 渲染真实帖,故整卡点按先提示、互动按钮为纯展示禁用态。
|
||||
void _showDetailPending() {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(const SnackBar(content: Text('帖子详情正在接入真实数据,敬请期待')));
|
||||
}
|
||||
|
||||
/// 搜索词过滤(demo 交互保留:只过滤已加载的多页缓存,不发检索请求;
|
||||
/// 契约 v1.3.0 无搜索端点)。
|
||||
List<FeedCard> get _visibleCards {
|
||||
@@ -309,6 +305,17 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
return ListenableBuilder(
|
||||
listenable: _feed,
|
||||
builder: (context, _) {
|
||||
// 点赞/收藏对账失败的一次性 SnackBar(§3.5 回滚提示;与详情页
|
||||
// 共用 controller 的 toggleError 消费口,先消费者清空)。
|
||||
if (_feed.toggleError != null) {
|
||||
_feed.clearToggleError();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('操作失败,请重试')));
|
||||
});
|
||||
}
|
||||
// ready 后(含翻页追加)补一次可见性扫描:无滚动也能记首屏曝光。
|
||||
if (_viewSegment != null && _feed.phase == FeedPhase.ready) {
|
||||
_scheduleVisibilityScan();
|
||||
@@ -467,7 +474,15 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: KeyedSubtree(
|
||||
key: _cardKeys.putIfAbsent(card.id, GlobalKey.new),
|
||||
child: PostCard(card: card, onTap: _showDetailPending),
|
||||
// 点赞/收藏经共享 ToggleSync(source=feed 缺省);整卡与
|
||||
// 评论钮进详情(T3-15 导航接通,T3-14 的占位提示移除)。
|
||||
child: PostCard(
|
||||
card: card,
|
||||
onTap: () => widget.onOpenPost?.call(card.id),
|
||||
onCommentTap: () => widget.onOpenPost?.call(card.id),
|
||||
onLikeTap: () => _feed.toggleLike(card.id),
|
||||
onBookmarkTap: () => _feed.toggleBookmark(card.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 搜索过滤中不渲染尾部(过滤只作用于已加载页,翻页语义混淆)。
|
||||
|
||||
@@ -3,6 +3,7 @@ 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/create/create_page.dart';
|
||||
import 'package:patbond_flutter/features/home/home_page.dart';
|
||||
@@ -13,7 +14,6 @@ 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';
|
||||
|
||||
@@ -23,9 +23,11 @@ class MainShellPage extends StatefulWidget {
|
||||
required this.petsController,
|
||||
required this.communityController,
|
||||
super.key,
|
||||
this.currentUserId,
|
||||
this.petAnalytics,
|
||||
this.healthRecordAnalytics,
|
||||
this.feedAnalytics,
|
||||
this.interactionAnalytics,
|
||||
this.pageViewTracker,
|
||||
this.onLogout,
|
||||
});
|
||||
@@ -38,6 +40,9 @@ class MainShellPage extends StatefulWidget {
|
||||
/// 社区状态(T3-12 数据层;首页 Feed segment 数据源,T3-14 接线)。
|
||||
final CommunityController communityController;
|
||||
|
||||
/// 当前登录用户 id(详情页评论删除入口 / 关注钮自见性的 UI 判定)。
|
||||
final String? currentUserId;
|
||||
|
||||
/// pet 域埋点强类型封装(建宠漏斗三事件)。
|
||||
final PetAnalytics? petAnalytics;
|
||||
|
||||
@@ -47,6 +52,9 @@ class MainShellPage extends StatefulWidget {
|
||||
/// feed 域埋点(T3-14 聚合曝光 + 加载失败)。
|
||||
final FeedAnalytics? feedAnalytics;
|
||||
|
||||
/// 互动域埋点(T3-16 评论成败对 + 关注对;详情页消费)。
|
||||
final CommunityInteractionAnalytics? interactionAnalytics;
|
||||
|
||||
/// Tab 曝光补点(IndexedStack 切换不产生路由事件,03 号评估 §3.2)。
|
||||
final PageViewTracker? pageViewTracker;
|
||||
|
||||
@@ -93,12 +101,18 @@ class _MainShellPageState extends State<MainShellPage> {
|
||||
widget.pageViewTracker?.reportTab(_tabPages[3]);
|
||||
}
|
||||
|
||||
void openPost(PostModel post) {
|
||||
/// 帖子详情(T3-15:真实数据整页;Feed 卡片与详情共享
|
||||
/// communityController,互动状态跨页一致)。
|
||||
void openPost(String postId) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
settings: RouteSettings(name: AnalyticsPageName.postDetail.pageName),
|
||||
builder: (context) =>
|
||||
PostDetailPage(appState: widget.appState, postId: post.id),
|
||||
builder: (context) => PostDetailPage(
|
||||
controller: widget.communityController,
|
||||
postId: postId,
|
||||
currentUserId: widget.currentUserId,
|
||||
analytics: widget.interactionAnalytics,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -122,15 +136,13 @@ class _MainShellPageState extends State<MainShellPage> {
|
||||
isActive: currentIndex == 0,
|
||||
onOpenServices: openServices,
|
||||
onOpenCreate: () => selectTab(1),
|
||||
onOpenPost: openPost,
|
||||
),
|
||||
CreatePage(
|
||||
appState: widget.appState,
|
||||
onPublished: (post) {
|
||||
selectTab(0);
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => openPost(post),
|
||||
);
|
||||
},
|
||||
// demo 详情页已退役(T3-15):demo 发布流不再导航详情,
|
||||
// 回首页 Feed;发布页真实化随 T3-17 收编。
|
||||
onPublished: (_) => selectTab(0),
|
||||
),
|
||||
PetsPage(
|
||||
controller: widget.petsController,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user