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:
崔宝新
2026-09-07 15:01:14 +08:00
parent 586c75f841
commit 7175bf3054
6 changed files with 659 additions and 287 deletions
+184
View File
@@ -0,0 +1,184 @@
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 换算)
}) {
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,
child: Container(
padding: EdgeInsets.symmetric(vertical: 30.w, horizontal: 15.w), // 60rpx 30rpx
constraints:
minWidth != null ? BoxConstraints(minWidth: minWidth!) : null,
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),
),
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),
),
],
),
),
],
),
),
);
}
/// 胶囊按钮(对标 default-button5 渐变确定键 / default-button6 白底取消键)
Widget _buildButton({
required String text,
required Color textColor,
required VoidCallback onTap,
Color? backgroundColor,
Gradient? gradient,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 46.w, // 92rpx
padding: EdgeInsets.symmetric(horizontal: 20.w),
constraints: BoxConstraints(minWidth: 135.w), // min-width 270rpx
alignment: Alignment.center,
decoration: BoxDecoration(
color: backgroundColor,
gradient: gradient,
borderRadius: BorderRadius.circular(100), // border-radius: 100px
boxShadow: gradient != null
? [
// CSS: 0px 15px 11.3px rgba(61,125,245,0.08)
// (inset 内发光 Flutter 不支持,省略)
BoxShadow(
color: const Color(0x143D7DF5),
offset: Offset(0, 15.w),
blurRadius: 11.w,
),
]
: null,
),
child: Text(
text,
style: TextStyle(fontSize: 16.sp, color: textColor), // 32rpx
),
),
);
}
}
+37 -34
View File
@@ -37,31 +37,32 @@ import 'package:talker_dio_logger/talker_dio_logger.dart';
/// 5. Android 真机请求网络必须加 INTERNET 权限(本工程 Manifest 已加)。
/// ============================================================
class DioClient {
/// 网络日志总开关:true = 控制台打印请求/响应数据,false = 静默
static const bool enableNetworkLog = false;
// ---------- 1. 单例:整个 App 只有一个 Dio ----------
DioClient._()
: _dio = Dio(
BaseOptions(
// 正式环境接口:统一从 ApiConfig 取,换环境只改 api_config.dart
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 10), // 连接超时
sendTimeout: const Duration(seconds: 10), // 发送超时
receiveTimeout: const Duration(seconds: 10), // 接收超时
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
),
) {
_dio.interceptors.add(
TalkerDioLogger(
talker: talker,
settings: const TalkerDioLoggerSettings(
printResponseTime: true,
hiddenHeaders: <String>{
'authorization',
},
: _dio = Dio(
BaseOptions(
// 正式环境接口:统一从 ApiConfig 取,换环境只改 api_config.dart
baseUrl: ApiConfig.baseUrl,
connectTimeout: const Duration(seconds: 10), // 连接超时
sendTimeout: const Duration(seconds: 10), // 发送超时
receiveTimeout: const Duration(seconds: 10), // 接收超时
headers: {'Content-Type': 'application/json; charset=utf-8'},
),
),
);
) {
if (enableNetworkLog) {
_dio.interceptors.add(
TalkerDioLogger(
talker: talker,
settings: const TalkerDioLoggerSettings(
printResponseTime: true,
hiddenHeaders: <String>{'authorization'},
),
),
);
}
_dio.interceptors.add(_buildInterceptors());
}
@@ -89,21 +90,26 @@ class DioClient {
onRequest: (options, handler) {
// OSS 直传会带自己的 authorization 头(OSS4-HMAC-SHA256 签名),
// 任何形式存在 authorization 都不再加 Bearer,否则会覆盖 OSS 签名
final hasAuth = options.headers['Authorization'] != null ||
final hasAuth =
options.headers['Authorization'] != null ||
options.headers['authorization'] != null;
if (accessToken != null && !hasAuth) {
options.headers['Authorization'] = 'Bearer $accessToken';
}
debugPrint('▶ 【请求】${options.method} ${options.uri}');
if (enableNetworkLog) {
debugPrint('▶ 【请求】${options.method} ${options.uri}');
}
handler.next(options); // 放行
},
// ② 收到响应后:打印日志
onResponse: (response, handler) {
debugPrint(
'◀ 【响应】${response.statusCode} '
'${response.requestOptions.method} ${response.requestOptions.uri}',
);
if (enableNetworkLog) {
debugPrint(
'◀ 【响应】${response.statusCode} '
'${response.requestOptions.method} ${response.requestOptions.uri}',
);
}
handler.next(response); // 放行
},
@@ -143,10 +149,8 @@ class DioClient {
final message = switch (e.type) {
DioExceptionType.connectionTimeout ||
DioExceptionType.sendTimeout ||
DioExceptionType.receiveTimeout =>
'网络连接超时,请稍后重试',
DioExceptionType.badResponse =>
'服务器开小差了(${e.response?.statusCode})',
DioExceptionType.receiveTimeout => '网络连接超时,请稍后重试',
DioExceptionType.badResponse => '服务器开小差了(${e.response?.statusCode})',
DioExceptionType.cancel => '请求已取消',
DioExceptionType.connectionError => '网络异常,请检查网络连接',
_ => '未知错误(${e.type.name})',
@@ -262,8 +266,7 @@ class DioClient {
DioExceptionType.connectionError ||
DioExceptionType.connectionTimeout ||
DioExceptionType.sendTimeout ||
DioExceptionType.receiveTimeout =>
true,
DioExceptionType.receiveTimeout => true,
_ => false,
};
}