feat: 新增全局确认弹窗、多页面UI优化与网络日志开关
1. 新增全局二次确认弹窗组件confirm_box,支持自定义参数并对标小程序实现
2. 优化班级板块多处UI:
- 修复point_rank_view中Web端字体溢出问题,使用FittedBox自适应缩放
- 重构class_update_widgets的PostCard,新增卡片背景控制开关并替换光晕实现
- 优化class_update_view的在线同学栏与头像组布局,支持动态计算展示数量
- 调整my_update_detail_page的评论区结构,修复评论高亮与删除浮层定位问题
3. 为dio_client新增网络日志总开关,可按需开启请求响应打印
4. 修复多处代码格式与注释细节问题
This commit is contained in:
@@ -42,7 +42,7 @@ class _MyUpdateDetailPageState extends ConsumerState<MyUpdateDetailPage> {
|
||||
/// 用详情接口增量更新评论相关字段(对标 Vue fetchDetail)
|
||||
///
|
||||
/// 关键:只有当 detail 确实带增量内容时才更新,且单个字段非空才覆盖。
|
||||
/// 否则后端未返回评论/计数(空壳)时,会把列表带入的正确数据“清空”
|
||||
/// 否则后端未返回评论/计数(空壳)时,会把列表带入的正确数据"清空"
|
||||
/// —— 表现为初次闪现对的数据后瞬间空白。
|
||||
Future<void> _fetchDetail() async {
|
||||
try {
|
||||
@@ -144,14 +144,25 @@ class _MyUpdateDetailPageState extends ConsumerState<MyUpdateDetailPage> {
|
||||
body: ListView(
|
||||
padding: EdgeInsets.fromLTRB(14.w, 12.h, 14.w, 24.h),
|
||||
children: [
|
||||
PostCard(
|
||||
post: _post,
|
||||
showCommentComposer: false,
|
||||
onLike: _toggleLike,
|
||||
onComment: (_) {},
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15.r),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
PostCard(
|
||||
post: _post,
|
||||
showCommentComposer: false,
|
||||
onLike: _toggleLike,
|
||||
onComment: (_) {},
|
||||
hasCardBackground: false,
|
||||
),
|
||||
_DetailComments(post: _post, onDelete: _deleteComment),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
_DetailComments(post: _post, onDelete: _deleteComment),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -176,9 +187,13 @@ class _DetailCommentsState extends ConsumerState<_DetailComments> {
|
||||
/// 二次确认弹窗期间保持背景高亮(对标 Vue pendingDeleteIndex)
|
||||
int? _pending;
|
||||
|
||||
/// 每条评论的定位锚点(懒加载,始终创建)
|
||||
final Map<int, LayerLink> _links = {};
|
||||
LayerLink _linkFor(int index) => _links.putIfAbsent(index, () => LayerLink());
|
||||
|
||||
UserPostDto get _post => widget.post;
|
||||
|
||||
/// 点浮层「删除」:先关闭浮层再弹确认框;确认弹窗期间用 _pending 保持高亮
|
||||
/// 点浮层「删除」→ 关浮层 + 弹确认框
|
||||
Future<void> _handleDelete(int index, UserPostCommonDto c) async {
|
||||
setState(() {
|
||||
_active = null;
|
||||
@@ -202,7 +217,6 @@ class _DetailCommentsState extends ConsumerState<_DetailComments> {
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
// 弹窗关闭后无论确认/取消都移除高亮
|
||||
setState(() => _pending = null);
|
||||
if (ok != true) return;
|
||||
await widget.onDelete(c);
|
||||
@@ -212,96 +226,135 @@ class _DetailCommentsState extends ConsumerState<_DetailComments> {
|
||||
Widget build(BuildContext context) {
|
||||
final me = ref.watch(currentUserProvider).value;
|
||||
final myId = me?.userId ?? 0;
|
||||
// 构建评论项时顺手收集所有 LayerLink(确保全部创建好)
|
||||
final items = <Widget>[];
|
||||
if (_post.userLikeAvatars.isNotEmpty) {
|
||||
items.add(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
items.add(SizedBox(height: 16.h));
|
||||
}
|
||||
items.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: const Text(
|
||||
'全部留言',
|
||||
style: TextStyle(fontSize: 15, color: Color(0xFF1D2B47)),
|
||||
),
|
||||
),
|
||||
);
|
||||
items.add(SizedBox(height: 8.h));
|
||||
if (_post.commons.isEmpty) {
|
||||
items.add(
|
||||
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++) {
|
||||
items.add(_commentItem(_post.commons[i], i, myId));
|
||||
}
|
||||
}
|
||||
items.add(SizedBox(height: 16.h));
|
||||
|
||||
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)),
|
||||
),
|
||||
// Stack 包裹 Column + 悬浮 Follower
|
||||
// Follower 放在最后绘制 → 浮层在最顶层,既能显示也能接收到点击
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none, // 允许浮层溢出 Stack 边界(向上悬浮在评论上方)
|
||||
children: [
|
||||
// 主内容
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (_active != null) setState(() => _active = null);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Follower 悬浮层:active 评论的「删除」浮层(最后绘制 → 顶层)
|
||||
_buildPopoverFollower(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 悬浮层:CompositedTransformFollower 定位到 active 评论文本上方
|
||||
///
|
||||
/// 关键:Follower 直接放在 Stack 里(不是 OverlayPortal),
|
||||
/// 和 Target 在同一 widget 树 → link 一定能连上,坐标计算无跨 Overlay 问题。
|
||||
Widget _buildPopoverFollower() {
|
||||
final index = _active;
|
||||
if (index == null || index >= _post.commons.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return CompositedTransformFollower(
|
||||
link: _linkFor(index),
|
||||
targetAnchor: Alignment.topCenter,
|
||||
followerAnchor: Alignment.bottomCenter,
|
||||
showWhenUnlinked: false,
|
||||
child: _deletePopover(index, _post.commons[index]),
|
||||
);
|
||||
}
|
||||
|
||||
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,避免事件冒泡到外层关闭浮层)
|
||||
onLongPress: canDelete
|
||||
? () {
|
||||
// 确保 link 已创建
|
||||
_linkFor(index);
|
||||
setState(() => _active = index);
|
||||
}
|
||||
: null,
|
||||
// active 时吸收点击 → 不冒泡到外层关闭浮层
|
||||
onTap: active ? () {} : null,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
@@ -309,88 +362,83 @@ class _DetailCommentsState extends ConsumerState<_DetailComments> {
|
||||
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),
|
||||
// 包整个 Row 作为锚点 → 浮层对准整条评论 item 的水平中心
|
||||
child: CompositedTransformTarget(
|
||||
link: _linkFor(index),
|
||||
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),
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
_commentText(c.content),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 长按后弹出的「删除」黑色浮层(对标 Vue delete-popover)
|
||||
Widget _commentText(String content) => Text(
|
||||
content,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF495A7A), height: 1.5),
|
||||
);
|
||||
|
||||
/// 「删除」黑色浮层:黑框在上 + 三角在下,三角紧贴黑框底部指向下方文本
|
||||
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(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 黑框在上
|
||||
Container(
|
||||
width: 60.w,
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: const Text(
|
||||
'删除',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
Icon(Icons.arrow_drop_down, color: bg, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 三角:往上拉 4px 紧贴黑框(Icons.arrow_drop_down 有内置留白)
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -9.5),
|
||||
child: Icon(Icons.arrow_drop_down, color: bg, size: 22),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user