refactor(class): 优化接口请求与页面细节
1. 统一处理后端统一包结构,自动解析data字段 2. 新增点赞评论栏显示控制参数,详情页隐藏该栏 3. 简化详情页数据更新逻辑,对齐Vue版本实现 4. 重构确认弹窗组件,优化样式与弹窗尺寸 5. 为评论项添加水平边距,优化列表排版
This commit is contained in:
@@ -113,11 +113,16 @@ class ClassApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 动态详情(含全量留言 / 点赞头像)—— GET /api/v1/UserPost/GetUserPostDetail/{id}
|
/// 动态详情(含全量留言 / 点赞头像)—— GET /api/v1/UserPost/GetUserPostDetail/{id}
|
||||||
|
///
|
||||||
|
/// 后端统一包 {code, data} 结构,需取 data 内层再解析(对标 Vue request 拦截器自动解包)。
|
||||||
Future<UserPostDto> getUserPostDetail(int id) async {
|
Future<UserPostDto> getUserPostDetail(int id) async {
|
||||||
final data = await DioClient.instance.get(
|
final raw = await DioClient.instance.get(
|
||||||
'/api/v1/UserPost/GetUserPostDetail/$id',
|
'/api/v1/UserPost/GetUserPostDetail/$id',
|
||||||
);
|
);
|
||||||
return UserPostDto.fromJson(data as Map<String, dynamic>);
|
final data = raw is Map<String, dynamic> && raw['data'] is Map<String, dynamic>
|
||||||
|
? raw['data'] as Map<String, dynamic>
|
||||||
|
: raw as Map<String, dynamic>;
|
||||||
|
return UserPostDto.fromJson(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除评论 —— DELETE /api/v1/UserPost/DeleteUserCommon/{id}
|
/// 删除评论 —— DELETE /api/v1/UserPost/DeleteUserCommon/{id}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ Future<bool> showConfirmBox(
|
|||||||
bool showCancel = true,
|
bool showCancel = true,
|
||||||
double? minWidth, // 弹窗最小宽度(逻辑像素,调用方自行 .w 换算)
|
double? minWidth, // 弹窗最小宽度(逻辑像素,调用方自行 .w 换算)
|
||||||
}) {
|
}) {
|
||||||
|
// 默认弹窗固定宽度 310.w(按钮 135.w × 2 + 间距 10.w + 左右 15.w×2 = 310.w)
|
||||||
|
minWidth ??= 310.w;
|
||||||
return showDialog<bool>(
|
return showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true, // 点遮罩关闭 → false(对标 onPopupClose)
|
barrierDismissible: true, // 点遮罩关闭 → false(对标 onPopupClose)
|
||||||
@@ -66,15 +68,18 @@ class ConfirmBox extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
|
// Dialog 默认左右 insetPadding: 40 太宽,310.w 在手机上放不下
|
||||||
|
// 这里减到 20,给 310.w 固定宽度留足空间(375-40=335 > 310)
|
||||||
|
insetPadding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(vertical: 30.w, horizontal: 15.w), // 60rpx 30rpx
|
width: minWidth, // 固定宽度(minWidth 语义变了:这里就是固定宽)
|
||||||
constraints:
|
padding: EdgeInsets.symmetric(vertical: 30.w, horizontal: 15.w),
|
||||||
minWidth != null ? BoxConstraints(minWidth: minWidth!) : null,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20.w), // 40rpx
|
borderRadius: BorderRadius.circular(20.w), // 40rpx
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: NetworkImage(
|
image: NetworkImage(
|
||||||
ImgUrl.build('/StaticResource/2026-03-31/header-bac.png')),
|
ImgUrl.build('/StaticResource/2026-03-31/header-bac.png'),
|
||||||
|
),
|
||||||
fit: BoxFit.fill, // background-size: 100% 100%
|
fit: BoxFit.fill, // background-size: 100% 100%
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -94,7 +99,9 @@ class ConfirmBox extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 18.w), // 36rpx
|
padding: EdgeInsets.only(top: 18.w), // 36rpx
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: 263.w), // max-width 526rpx
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 263.w,
|
||||||
|
), // max-width 526rpx
|
||||||
child: Text(
|
child: Text(
|
||||||
content,
|
content,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -115,6 +122,7 @@ class ConfirmBox extends StatelessWidget {
|
|||||||
backgroundColor: const Color(0xFFFDFFFF),
|
backgroundColor: const Color(0xFFFDFFFF),
|
||||||
textColor: const Color(0xFF6195F6),
|
textColor: const Color(0xFF6195F6),
|
||||||
onTap: () => Navigator.of(context).pop(false),
|
onTap: () => Navigator.of(context).pop(false),
|
||||||
|
isConfirm: false,
|
||||||
),
|
),
|
||||||
SizedBox(width: 10.w), // 20rpx
|
SizedBox(width: 10.w), // 20rpx
|
||||||
],
|
],
|
||||||
@@ -133,6 +141,7 @@ class ConfirmBox extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
onTap: () => Navigator.of(context).pop(true),
|
onTap: () => Navigator.of(context).pop(true),
|
||||||
|
isConfirm: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -143,40 +152,45 @@ class ConfirmBox extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 胶囊按钮(对标 default-button5 渐变确定键 / default-button6 白底取消键)
|
/// 胶囊按钮:取消键(白底+蓝字+边框+阴影)/ 确定键(渐变+边框+阴影)
|
||||||
Widget _buildButton({
|
Widget _buildButton({
|
||||||
required String text,
|
required String text,
|
||||||
required Color textColor,
|
required Color textColor,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
|
required bool isConfirm,
|
||||||
Color? backgroundColor,
|
Color? backgroundColor,
|
||||||
Gradient? gradient,
|
Gradient? gradient,
|
||||||
}) {
|
}) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 46.w, // 92rpx
|
width: 135.w,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
height: 49.w,
|
||||||
constraints: BoxConstraints(minWidth: 135.w), // min-width 270rpx
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
gradient: gradient,
|
gradient: gradient,
|
||||||
borderRadius: BorderRadius.circular(100), // border-radius: 100px
|
borderRadius: BorderRadius.circular(50.w),
|
||||||
boxShadow: gradient != null
|
boxShadow: [
|
||||||
? [
|
if (isConfirm)
|
||||||
// CSS: 0px 15px 11.3px rgba(61,125,245,0.08)
|
|
||||||
// (inset 内发光 Flutter 不支持,省略)
|
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0x143D7DF5),
|
color: Colors.black.withValues(alpha: 0.25),
|
||||||
offset: Offset(0, 15.w),
|
offset: Offset(0, 1.w),
|
||||||
blurRadius: 11.w,
|
blurRadius: 9.2.w,
|
||||||
|
spreadRadius: 3.w,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.25),
|
||||||
|
offset: Offset(0, 2.w),
|
||||||
|
blurRadius: 12.2.w,
|
||||||
|
spreadRadius: 0.5.w,
|
||||||
),
|
),
|
||||||
]
|
],
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(fontSize: 16.sp, color: textColor), // 32rpx
|
style: TextStyle(fontSize: 16.sp, color: textColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:new_learn/core/api/class/api.dart';
|
import 'package:new_learn/core/api/class/api.dart';
|
||||||
import 'package:new_learn/core/api/class/model.dart';
|
import 'package:new_learn/core/api/class/model.dart';
|
||||||
|
import 'package:new_learn/core/components/confirm_box.dart';
|
||||||
import 'package:new_learn/core/components/user_circle_avatar.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/providers/user_provider.dart';
|
||||||
import 'package:new_learn/core/utils/img_url.dart';
|
import 'package:new_learn/core/utils/img_url.dart';
|
||||||
@@ -41,30 +42,19 @@ class _MyUpdateDetailPageState extends ConsumerState<MyUpdateDetailPage> {
|
|||||||
|
|
||||||
/// 用详情接口增量更新评论相关字段(对标 Vue fetchDetail)
|
/// 用详情接口增量更新评论相关字段(对标 Vue fetchDetail)
|
||||||
///
|
///
|
||||||
/// 关键:只有当 detail 确实带增量内容时才更新,且单个字段非空才覆盖。
|
/// 简化逻辑:接口成功返回就直接用详情数据覆盖评论相关字段,
|
||||||
/// 否则后端未返回评论/计数(空壳)时,会把列表带入的正确数据"清空"
|
/// 和 Vue 版一致(Vue 里 res.commons !== undefined 就直接覆盖)。
|
||||||
/// —— 表现为初次闪现对的数据后瞬间空白。
|
|
||||||
Future<void> _fetchDetail() async {
|
Future<void> _fetchDetail() async {
|
||||||
try {
|
try {
|
||||||
final res = await ClassApi.instance.getUserPostDetail(_post.id);
|
final res = await ClassApi.instance.getUserPostDetail(_post.id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// 空壳判定:detail 未携带任何有效增量 → 完全信任列表带入的数据,不更新
|
|
||||||
final hasDetail =
|
|
||||||
res.commons.isNotEmpty ||
|
|
||||||
res.userLikeAvatars.isNotEmpty ||
|
|
||||||
res.replyCount > 0 ||
|
|
||||||
res.likeCount > 0 ||
|
|
||||||
res.userLikeStatus;
|
|
||||||
if (!hasDetail) return;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_post = _post.copyWith(
|
_post = _post.copyWith(
|
||||||
commons: res.commons.isNotEmpty ? res.commons : _post.commons,
|
commons: res.commons,
|
||||||
userLikeAvatars: res.userLikeAvatars.isNotEmpty
|
userLikeAvatars: res.userLikeAvatars,
|
||||||
? res.userLikeAvatars
|
|
||||||
: _post.userLikeAvatars,
|
|
||||||
userLikeStatus: res.userLikeStatus,
|
userLikeStatus: res.userLikeStatus,
|
||||||
replyCount: res.replyCount > 0 ? res.replyCount : _post.replyCount,
|
replyCount: res.replyCount,
|
||||||
likeCount: res.likeCount > 0 ? res.likeCount : _post.likeCount,
|
likeCount: res.likeCount,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -158,6 +148,7 @@ class _MyUpdateDetailPageState extends ConsumerState<MyUpdateDetailPage> {
|
|||||||
onLike: _toggleLike,
|
onLike: _toggleLike,
|
||||||
onComment: (_) {},
|
onComment: (_) {},
|
||||||
hasCardBackground: false,
|
hasCardBackground: false,
|
||||||
|
showLikeCommentBar: false,
|
||||||
),
|
),
|
||||||
_DetailComments(post: _post, onDelete: _deleteComment),
|
_DetailComments(post: _post, onDelete: _deleteComment),
|
||||||
],
|
],
|
||||||
@@ -199,22 +190,10 @@ class _DetailCommentsState extends ConsumerState<_DetailComments> {
|
|||||||
_active = null;
|
_active = null;
|
||||||
_pending = index;
|
_pending = index;
|
||||||
});
|
});
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showConfirmBox(
|
||||||
context: context,
|
context,
|
||||||
builder: (ctx) => AlertDialog(
|
title: '是否删除评论',
|
||||||
title: const Text('是否删除评论'),
|
content: '删除评论后将不可恢复,请慎重选择!',
|
||||||
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;
|
if (!mounted) return;
|
||||||
setState(() => _pending = null);
|
setState(() => _pending = null);
|
||||||
@@ -287,7 +266,12 @@ class _DetailCommentsState extends ConsumerState<_DetailComments> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < _post.commons.length; i++) {
|
for (int i = 0; i < _post.commons.length; i++) {
|
||||||
items.add(_commentItem(_post.commons[i], i, myId));
|
items.add(
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||||
|
child: _commentItem(_post.commons[i], i, myId),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items.add(SizedBox(height: 16.h));
|
items.add(SizedBox(height: 16.h));
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class PostCard extends StatelessWidget {
|
|||||||
required this.onComment,
|
required this.onComment,
|
||||||
this.showCommentComposer = true,
|
this.showCommentComposer = true,
|
||||||
this.hasCardBackground = true,
|
this.hasCardBackground = true,
|
||||||
|
this.showLikeCommentBar = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final UserPostDto post;
|
final UserPostDto post;
|
||||||
@@ -40,6 +41,9 @@ class PostCard extends StatelessWidget {
|
|||||||
/// 是否显示白色卡片背景(列表页 true;详情页与留言区共用一张大卡片 → false)
|
/// 是否显示白色卡片背景(列表页 true;详情页与留言区共用一张大卡片 → false)
|
||||||
final bool hasCardBackground;
|
final bool hasCardBackground;
|
||||||
|
|
||||||
|
/// 是否显示点赞/评论标记栏(列表页 true;详情页不需要 → false)
|
||||||
|
final bool showLikeCommentBar;
|
||||||
|
|
||||||
String get _avatar {
|
String get _avatar {
|
||||||
final url = ImgUrl.build(post.avatar);
|
final url = ImgUrl.build(post.avatar);
|
||||||
return url.isEmpty
|
return url.isEmpty
|
||||||
@@ -96,6 +100,7 @@ class PostCard extends StatelessWidget {
|
|||||||
AlbumGrid(urls: post.urls.map(ImgUrl.build).toList()),
|
AlbumGrid(urls: post.urls.map(ImgUrl.build).toList()),
|
||||||
],
|
],
|
||||||
SizedBox(height: 10.h),
|
SizedBox(height: 10.h),
|
||||||
|
if (showLikeCommentBar)
|
||||||
LikeCommentBar(
|
LikeCommentBar(
|
||||||
post: post,
|
post: post,
|
||||||
onLike: onLike,
|
onLike: onLike,
|
||||||
|
|||||||
Reference in New Issue
Block a user