1. 统一处理后端统一包结构,自动解析data字段 2. 新增点赞评论栏显示控制参数,详情页隐藏该栏 3. 简化详情页数据更新逻辑,对齐Vue版本实现 4. 重构确认弹窗组件,优化样式与弹窗尺寸 5. 为评论项添加水平边距,优化列表排版
199 lines
6.6 KiB
Dart
199 lines
6.6 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
|
||
import '../utils/img_url.dart';
|
||
|
||
/// ============================================================
|
||
/// 【ConfirmBox】全局二次确认弹窗 —— 对标小程序 components/ConfirmBox
|
||
///
|
||
/// 一、与 Vue3 版的架构差异:
|
||
/// 小程序不支持动态插入 DOM,所以 Vue 版需要每个页面手动放置
|
||
/// `<ConfirmBox />`,并在 service.js 里按路由注册 controller。
|
||
/// Flutter 的 showDialog 可以从任意 context 直接弹出(等价于 Web
|
||
/// 动态挂载),因此这里无需注册机制,单文件即可全局使用。
|
||
///
|
||
/// 二、用法(对标 Vue 的 `await proxy.$confirm({...})`):
|
||
/// final ok = await showConfirmBox(
|
||
/// context,
|
||
/// title: '提示',
|
||
/// content: '确定要删除吗?',
|
||
/// );
|
||
/// 点"确定"返回 true;点"取消"、点遮罩、按返回键返回 false。
|
||
/// ============================================================
|
||
Future<bool> showConfirmBox(
|
||
BuildContext context, {
|
||
String title = '提示',
|
||
String content = '',
|
||
String cancelText = '取消',
|
||
String confirmText = '确定',
|
||
bool showCancel = true,
|
||
double? minWidth, // 弹窗最小宽度(逻辑像素,调用方自行 .w 换算)
|
||
}) {
|
||
// 默认弹窗固定宽度 310.w(按钮 135.w × 2 + 间距 10.w + 左右 15.w×2 = 310.w)
|
||
minWidth ??= 310.w;
|
||
return showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: true, // 点遮罩关闭 → false(对标 onPopupClose)
|
||
barrierColor: Colors.black54,
|
||
builder: (_) => ConfirmBox(
|
||
title: title,
|
||
content: content,
|
||
cancelText: cancelText,
|
||
confirmText: confirmText,
|
||
showCancel: showCancel,
|
||
minWidth: minWidth,
|
||
),
|
||
).then((result) => result ?? false);
|
||
}
|
||
|
||
class ConfirmBox extends StatelessWidget {
|
||
const ConfirmBox({
|
||
super.key,
|
||
required this.title,
|
||
required this.content,
|
||
required this.cancelText,
|
||
required this.confirmText,
|
||
required this.showCancel,
|
||
this.minWidth,
|
||
});
|
||
|
||
final String title;
|
||
final String content;
|
||
final String cancelText;
|
||
final String confirmText;
|
||
final bool showCancel;
|
||
final double? minWidth;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Dialog(
|
||
backgroundColor: Colors.transparent,
|
||
// Dialog 默认左右 insetPadding: 40 太宽,310.w 在手机上放不下
|
||
// 这里减到 20,给 310.w 固定宽度留足空间(375-40=335 > 310)
|
||
insetPadding: EdgeInsets.symmetric(horizontal: 20.w),
|
||
child: Container(
|
||
width: minWidth, // 固定宽度(minWidth 语义变了:这里就是固定宽)
|
||
padding: EdgeInsets.symmetric(vertical: 30.w, horizontal: 15.w),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(20.w), // 40rpx
|
||
image: DecorationImage(
|
||
image: NetworkImage(
|
||
ImgUrl.build('/StaticResource/2026-03-31/header-bac.png'),
|
||
),
|
||
fit: BoxFit.fill, // background-size: 100% 100%
|
||
),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (title.isNotEmpty)
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontSize: 16.sp, // 32rpx
|
||
fontWeight: FontWeight.w600,
|
||
color: const Color(0xFF1D2B47),
|
||
),
|
||
),
|
||
if (content.isNotEmpty)
|
||
Padding(
|
||
padding: EdgeInsets.only(top: 18.w), // 36rpx
|
||
child: ConstrainedBox(
|
||
constraints: BoxConstraints(
|
||
maxWidth: 263.w,
|
||
), // max-width 526rpx
|
||
child: Text(
|
||
content,
|
||
style: TextStyle(
|
||
fontSize: 14.sp, // 28rpx
|
||
color: const Color(0xFF1D2B47),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: EdgeInsets.only(top: 23.w), // 46rpx
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (showCancel) ...[
|
||
_buildButton(
|
||
text: cancelText,
|
||
backgroundColor: const Color(0xFFFDFFFF),
|
||
textColor: const Color(0xFF6195F6),
|
||
onTap: () => Navigator.of(context).pop(false),
|
||
isConfirm: false,
|
||
),
|
||
SizedBox(width: 10.w), // 20rpx
|
||
],
|
||
_buildButton(
|
||
text: confirmText,
|
||
gradient: const LinearGradient(
|
||
// CSS: linear-gradient(360deg, #8AD1FD -11.22%, #3C84FF 60.2%, #3D7DF5 100%)
|
||
begin: Alignment.bottomCenter,
|
||
end: Alignment.topCenter,
|
||
colors: [
|
||
Color(0xFF8AD1FD),
|
||
Color(0xFF3C84FF),
|
||
Color(0xFF3D7DF5),
|
||
],
|
||
stops: [0, 0.602, 1],
|
||
),
|
||
textColor: Colors.white,
|
||
onTap: () => Navigator.of(context).pop(true),
|
||
isConfirm: true,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 胶囊按钮:取消键(白底+蓝字+边框+阴影)/ 确定键(渐变+边框+阴影)
|
||
Widget _buildButton({
|
||
required String text,
|
||
required Color textColor,
|
||
required VoidCallback onTap,
|
||
required bool isConfirm,
|
||
Color? backgroundColor,
|
||
Gradient? gradient,
|
||
}) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: 135.w,
|
||
height: 49.w,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: backgroundColor,
|
||
gradient: gradient,
|
||
borderRadius: BorderRadius.circular(50.w),
|
||
boxShadow: [
|
||
if (isConfirm)
|
||
BoxShadow(
|
||
color: Colors.black.withValues(alpha: 0.25),
|
||
offset: Offset(0, 1.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,
|
||
),
|
||
],
|
||
),
|
||
child: Text(
|
||
text,
|
||
style: TextStyle(fontSize: 16.sp, color: textColor),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|