import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:new_learn/core/api/class/api.dart'; import 'package:new_learn/core/api/class/model.dart'; import 'package:new_learn/core/components/user_circle_avatar.dart'; import 'package:new_learn/core/providers/user_provider.dart'; import 'package:new_learn/core/utils/img_url.dart'; import 'package:new_learn/features/class/widgets/class_update_widgets.dart'; /// ============================================================ /// 【MyUpdateDetailPage】「动态详情」页 —— 对标 Vue pages/classPackage/myInteract/myUpdateDetail/index.vue /// /// 一、数据:从列表卡片带入初始 UserPostDto,进入后若无 id 停止; /// 有 id 则 getUserPostDetail 增量更新评论/点赞头像/点赞状态/计数。 /// 二、展示:复用 PostCard(showCommentComposer=false,隐藏内嵌输入/预览), /// 下方接 CommentDetail:点赞人头像 + 「全部留言」列表。 /// 三、交互: /// - 点赞 → addUserLike(乐观更新,失败回滚) /// - 长按留言 → 仅本人或发帖人可删 → confirm → deleteUserCommon(乐观移除) /// ============================================================ class MyUpdateDetailPage extends ConsumerStatefulWidget { const MyUpdateDetailPage({super.key, required this.initialPost}); final UserPostDto initialPost; @override ConsumerState createState() => _MyUpdateDetailPageState(); } class _MyUpdateDetailPageState extends ConsumerState { late UserPostDto _post; @override void initState() { super.initState(); _post = widget.initialPost; if (_post.id != 0) _fetchDetail(); } /// 用详情接口增量更新评论相关字段(对标 Vue fetchDetail) /// /// 关键:只有当 detail 确实带增量内容时才更新,且单个字段非空才覆盖。 /// 否则后端未返回评论/计数(空壳)时,会把列表带入的正确数据“清空” /// —— 表现为初次闪现对的数据后瞬间空白。 Future _fetchDetail() async { try { final res = await ClassApi.instance.getUserPostDetail(_post.id); if (!mounted) return; // 空壳判定:detail 未携带任何有效增量 → 完全信任列表带入的数据,不更新 final hasDetail = res.commons.isNotEmpty || res.userLikeAvatars.isNotEmpty || res.replyCount > 0 || res.likeCount > 0 || res.userLikeStatus; if (!hasDetail) return; setState(() { _post = _post.copyWith( commons: res.commons.isNotEmpty ? res.commons : _post.commons, userLikeAvatars: res.userLikeAvatars.isNotEmpty ? res.userLikeAvatars : _post.userLikeAvatars, userLikeStatus: res.userLikeStatus, replyCount: res.replyCount > 0 ? res.replyCount : _post.replyCount, likeCount: res.likeCount > 0 ? res.likeCount : _post.likeCount, ); }); } catch (_) { // 详情拉取失败:用列表带入的数据兜底展示 } } Future _toggleLike() async { final liked = !_post.userLikeStatus; final newCount = _post.likeCount + (liked ? 1 : -1); final old = _post; setState(() { _post = _post.copyWith( userLikeStatus: liked, likeCount: newCount < 0 ? 0 : newCount, ); }); try { await ClassApi.instance.addUserLike( userPostId: _post.id, likeStatus: liked, ); } catch (_) { if (!mounted) return; setState(() => _post = old); } } Future _deleteComment(UserPostCommonDto c) async { final old = _post; // 本地新增无 id 的评论直接删,不发接口 if (c.id == 0) { setState(() { _post = _post.copyWith( commons: _post.commons.where((x) => x != c).toList(), replyCount: _post.replyCount > 0 ? _post.replyCount - 1 : 0, ); }); return; } setState(() { _post = _post.copyWith( commons: _post.commons.where((x) => x.id != c.id).toList(), replyCount: _post.replyCount > 0 ? _post.replyCount - 1 : 0, ); }); try { await ClassApi.instance.deleteUserCommon(c.id); } catch (_) { if (!mounted) return; setState(() => _post = old); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F8FA), appBar: AppBar( backgroundColor: const Color(0xFFF5F8FA), elevation: 0, title: const Text( '动态详情', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w400, color: Color(0xFF1D2B47), ), ), centerTitle: true, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2B47)), onPressed: () => context.canPop() ? context.pop() : context.go('/class'), ), ), body: ListView( padding: EdgeInsets.fromLTRB(14.w, 12.h, 14.w, 24.h), children: [ PostCard( post: _post, showCommentComposer: false, onLike: _toggleLike, onComment: (_) {}, ), SizedBox(height: 10.h), _DetailComments(post: _post, onDelete: _deleteComment), ], ), ); } } /// 详情版留言区:点赞人头像 + 全部留言(对标 LikeAndCommentDetail) class _DetailComments extends ConsumerStatefulWidget { const _DetailComments({required this.post, required this.onDelete}); final UserPostDto post; final Future Function(UserPostCommonDto) onDelete; @override ConsumerState<_DetailComments> createState() => _DetailCommentsState(); } class _DetailCommentsState extends ConsumerState<_DetailComments> { /// 当前长按中(背景高亮 + 显示「删除」浮层)的评论索引 int? _active; /// 二次确认弹窗期间保持背景高亮(对标 Vue pendingDeleteIndex) int? _pending; UserPostDto get _post => widget.post; /// 点浮层「删除」:先关闭浮层再弹确认框;确认弹窗期间用 _pending 保持高亮 Future _handleDelete(int index, UserPostCommonDto c) async { setState(() { _active = null; _pending = index; }); final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('是否删除评论'), content: const Text('删除评论后将不可恢复,请慎重选择!'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('确定'), ), ], ), ); if (!mounted) return; // 弹窗关闭后无论确认/取消都移除高亮 setState(() => _pending = null); if (ok != true) return; await widget.onDelete(c); } @override Widget build(BuildContext context) { final me = ref.watch(currentUserProvider).value; final myId = me?.userId ?? 0; return Container( width: double.infinity, // 横向内边距改由各分区自行包裹,评论列表本身无横向内边距即可铺满卡片宽度 padding: EdgeInsets.only(top: 18.h), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.r), ), // 外层捕获非评论区域的点击 → 关闭「删除」浮层(对标 Vue 容器 @tap="closeDeletePopover") child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (_active != null) setState(() => _active = null); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ---------- 点赞人头像 ---------- if (_post.userLikeAvatars.isNotEmpty) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only(top: 5.h), child: Image.asset( 'assets/img/class/like-icon.png', width: 16.w, height: 16.w, errorBuilder: (_, _, _) => const Icon( Icons.favorite, size: 16, color: Color(0xFF3D7DF5), ), ), ), SizedBox(width: 10.w), Expanded( child: Wrap( spacing: 9.w, runSpacing: 9.h, children: [ for (final u in _post.userLikeAvatars) UserCircleAvatar(url: _avatar(u), radius: 13.w), ], ), ), ], ), ), SizedBox(height: 16.h), ], // ---------- 全部留言 ---------- Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: const Text( '全部留言', style: TextStyle(fontSize: 15, color: Color(0xFF1D2B47)), ), ), SizedBox(height: 8.h), if (_post.commons.isEmpty) Padding( padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 12.h), child: const Text( '暂无留言', style: TextStyle(fontSize: 13, color: Color(0xFFB4C2E0)), ), ) else for (int i = 0; i < _post.commons.length; i++) _commentItem(_post.commons[i], i, myId), SizedBox(height: 16.h), ], ), ), ); } Widget _commentItem(UserPostCommonDto c, int index, int myId) { final active = _active == index; final pending = _pending == index; final canDelete = (myId == c.userId) || (myId == _post.userId); return GestureDetector( behavior: HitTestBehavior.opaque, // 长按(仅可删时)→ 高亮该条 + 弹出「删除」浮层(对标 Vue @longpress) onLongPress: canDelete ? () => setState(() => _active = index) : null, // 单击评论本身:吸收点击、保持浮层与高亮(对标 Vue @tap.stop,避免事件冒泡到外层关闭浮层) onTap: active ? () {} : null, child: Container( width: double.infinity, padding: EdgeInsets.symmetric(vertical: 10.h), color: (active || pending) ? const Color(0xFFF5F8FA) : Colors.transparent, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ UserCircleAvatar(url: _avatar(c.avatar), radius: 20.w), SizedBox(width: 15.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( c.userName, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xFF1D2B47), ), ), Text( _time(c.createTime), style: const TextStyle( fontSize: 12, color: Color(0xFF687FAD), ), ), ], ), SizedBox(height: 6.h), // 文本 + 长按浮层(覆盖在文本上方居中) Stack( clipBehavior: Clip.none, children: [ Text( c.content, style: const TextStyle( fontSize: 14, color: Color(0xFF495A7A), height: 1.5, ), ), if (active) Positioned( top: -46.h, left: 0, right: 0, child: Center(child: _deletePopover(index, c)), ), ], ), ], ), ), ], ), ), ); } /// 长按后弹出的「删除」黑色浮层(对标 Vue delete-popover) Widget _deletePopover(int index, UserPostCommonDto c) { const bg = Color(0xBF000000); // rgba(0,0,0,0.75) return GestureDetector( onTap: () => _handleDelete(index, c), child: Container( width: 60.w, padding: EdgeInsets.fromLTRB(0, 8.h, 0, 0), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(8.r), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( '删除', style: TextStyle(fontSize: 14, color: Colors.white), ), Icon(Icons.arrow_drop_down, color: bg, size: 18), ], ), ), ); } String _time(String t) { if (t.isEmpty) return ''; final s = t.replaceFirst('T', ' ').replaceFirst('Z', ''); return s.length > 16 ? s.substring(0, 16) : s; } static String _avatar(String? raw) { final url = ImgUrl.build(raw); return url.isEmpty ? ImgUrl.build('/StaticResource/2026-04-03/fake-avatar.png') : url; } }