toast弹窗优化

This commit is contained in:
mmc
2026-03-03 10:28:50 +08:00
parent cf5415308a
commit 0a3a062c04
2 changed files with 226 additions and 30 deletions

173
lib/components/toast.dart Normal file
View File

@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
// 自定义Toast工具类 - 气泡式提示
class ToastUtils {
static OverlayEntry? _overlayEntry;
static bool _isShowing = false;
/// 显示气泡提示
/// [context] 上下文
/// [message] 提示文本
/// [backgroundColor] 背景色
/// [duration] 显示时长
/// [positionTop] 距离顶部的高度
static void showToast(
BuildContext context,
String message, {
Color backgroundColor = const Color(0xCC333333),
Duration duration = const Duration(seconds: 1),
double positionTop = 80.0,
}) {
// 先关闭正在显示的提示
if (_isShowing) {
_overlayEntry?.remove();
_isShowing = false;
}
// 创建OverlayEntry
_overlayEntry = OverlayEntry(
builder: (BuildContext context) => Positioned(
top: positionTop,
left: MediaQuery.of(context).size.width * 0.1,
right: MediaQuery.of(context).size.width * 0.1,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(18),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, 2))],
),
child: Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14, height: 1.2),
),
),
),
),
);
// 显示提示
SchedulerBinding.instance.addPostFrameCallback((_) {
Overlay.of(context).insert(_overlayEntry!);
_isShowing = true;
// 定时关闭
Future.delayed(duration, () {
if (_isShowing) {
_overlayEntry?.remove();
_isShowing = false;
}
});
});
}
/// 显示成功提示(绿色背景 + 85%不透明度)
static void showSuccess(BuildContext context, String message) {
showToast(
context,
message,
// 绿色:0xFF00C853 → 调整为 0xD900C853(D9=85%不透明度)
backgroundColor: const Color(0xD900C853),
duration: const Duration(seconds: 1),
);
}
/// 显示错误提示(红色背景 + 85%不透明度)
static void showError(BuildContext context, String message) {
showToast(
context,
message,
// 红色:0xFFFF5252 → 调整为 0xD9FF5252(D9=85%不透明度)
backgroundColor: const Color(0xD9FF5252),
duration: const Duration(seconds: 1),
);
}
/// 显示普通提示(灰色背景 + 80%不透明度)
static void showInfo(BuildContext context, String message) {
showToast(
context,
message,
// 灰色:0xCC333333 → 调整为 0xCC333333(保持80%,也可改为 0xB3333333=70%)
backgroundColor: const Color(0xCC333333),
duration: const Duration(seconds: 1),
);
}
/// 显示警告提示(黄色背景 + 85%不透明度)
static void showWarn(BuildContext context, String message) {
showToast(
context,
message,
// 黄色:0xFFFFC107 → 调整为 0xD9FFC107(D9=85%不透明度)
backgroundColor: const Color(0xD9FFC107),
duration: const Duration(seconds: 1),
);
}
/// 显示加载提示(蓝色背景 + 85%不透明度)
static void showLoading(BuildContext context, String message) {
showToast(
context,
message,
// 蓝色:0xFF0688D8 → 调整为 0xD90688D8(D9=85%不透明度)
backgroundColor: const Color(0xD90688D8),
duration: const Duration(seconds: 1),
);
}
// 扩展:支持自定义文字颜色
static void showToastWithTextColor(
BuildContext context,
String message, {
Color backgroundColor = const Color(0xCC333333),
Color textColor = Colors.white,
Duration duration = const Duration(seconds: 2),
double positionTop = 80.0,
}) {
if (_isShowing) {
_overlayEntry?.remove();
_isShowing = false;
}
_overlayEntry = OverlayEntry(
builder: (BuildContext context) => Positioned(
top: positionTop,
left: MediaQuery.of(context).size.width * 0.1,
right: MediaQuery.of(context).size.width * 0.1,
child: Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(24),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, 2))],
),
child: Text(
message,
textAlign: TextAlign.center,
style: TextStyle(color: textColor, fontSize: 14, height: 1.2),
),
),
),
),
);
SchedulerBinding.instance.addPostFrameCallback((_) {
Overlay.of(context).insert(_overlayEntry!);
_isShowing = true;
Future.delayed(duration, () {
if (_isShowing) {
_overlayEntry?.remove();
_isShowing = false;
}
});
});
}
}

View File

@@ -12,6 +12,7 @@ import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
import 'package:maibu_satabot_v2/components/confrim_dialog.dart';
import 'package:maibu_satabot_v2/components/toast.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
import 'package:maibu_satabot_v2/core/di/injection.dart';
import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
@@ -260,7 +261,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
} else {
// 截图失败时,允许空图片保存(可根据业务调整为强制失败)
_savePlotData(plotName, null);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块保存成功,但地图截图生成失败!'), backgroundColor: Colors.amber));
ToastUtils.showWarn(context, '地块保存成功,但地图截图生成失败!');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块保存成功,但地图截图生成失败!'), backgroundColor: Colors.amber));
}
}
},
@@ -323,7 +325,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 1. 获取用户ID
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
if (userId.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('用户ID为空,无法保存'), backgroundColor: Colors.red));
ToastUtils.showError(context, '用户ID为空,无法保存!');
return;
}
@@ -389,15 +391,16 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 7. 发送请求
try {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正在保存地块...'), backgroundColor: Colors.blue));
ToastUtils.showLoading(context, '正在保存地块「$plotName」...');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正在保存地块...'), backgroundColor: Colors.blue));
final http.StreamedResponse response = await request.send();
final String responseBody = await response.stream.bytesToString();
debugPrint('保存地块响应状态: ${response.statusCode}');
debugPrint('保存地块响应体: $responseBody');
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块「$plotName」保存成功!'), backgroundColor: Colors.green));
ToastUtils.showSuccess(context, '地块「$plotName」保存成功!');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块「$plotName」保存成功!'), backgroundColor: Colors.green));
// 保存成功
setState(() {
_markedPoints.clear(); // 清空打点
@@ -412,7 +415,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}
} catch (e) {
debugPrint('保存失败: $e');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'), backgroundColor: Colors.red));
ToastUtils.showError(context, '保存失败: ${e.toString()}');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'), backgroundColor: Colors.red));
}
}
@@ -448,9 +452,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (_isWorkAreaCompleted && _markedPoints.length >= 3) {
_generatePath(showTips: false);
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回作业区域最后一个点,剩余${_markedPoints.length}个点')));
ToastUtils.showInfo(context, '已撤回作业区域最后一个点,剩余${_markedPoints.length}个点');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回作业区域最后一个点,剩余${_markedPoints.length}个点')));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可撤回')));
ToastUtils.showInfo(context, '暂无作业区域点可撤回');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可撤回')));
}
return;
}
@@ -463,8 +469,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (_currentObstaclePoints.isEmpty) {
_isObstacleEditing = false;
}
ToastUtils.showInfo(context, '已撤回当前空洞最后一个点,剩余${_currentObstaclePoints.length}个点');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回当前空洞最后1个点,剩余${_currentObstaclePoints.length}个点')));
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回当前空洞最后1个点,剩余${_currentObstaclePoints.length}个点')));
return; // 撤完点就结束,不执行后面的逻辑
}
@@ -475,12 +482,15 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞')));
ToastUtils.showInfo(context, '已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞')));
return;
}
// 第三步:无任何可撤内容
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无空洞可撤回')));
ToastUtils.showInfo(context, '暂无空洞可撤回');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无空洞可撤回')));
}
});
}
@@ -495,7 +505,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 1. 作业区域模式:清空作业点 + 重置路径
if (_currentAreaMode == AreaMode.work) {
if (_markedPoints.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可删除')));
ToastUtils.showInfo(context, '暂无作业区域点可删除');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可删除')));
return;
}
_markedPoints.clear();
@@ -504,12 +516,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
_isWorkAreaCompleted = false;
_saveBoxOpen = false;
typedPathList.clear();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有作业区域点和路径')));
ToastUtils.showSuccess(context, '已清空所有作业区域点和路径');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有作业区域点和路径')));
}
// 2. 空洞模式:清空所有空洞 + 恢复UI + 重新生成路径
else if (_currentAreaMode == AreaMode.obstacle) {
if (_obstacleHoles.isEmpty && _currentObstaclePoints.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无空洞可删除')));
ToastUtils.showInfo(context, '暂无空洞可删除');
return;
}
_obstacleHoles.clear();
@@ -519,7 +533,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (_isWorkAreaCompleted) {
_generatePath(showTips: false);
}
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有空洞')));
ToastUtils.showSuccess(context, '已清空所有空洞');
}
});
}
@@ -639,15 +653,17 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 刷新成功提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('页面已刷新完成'), backgroundColor: Colors.green, duration: Duration(seconds: 2)));
ToastUtils.showSuccess(context, '页面已刷新完成');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('页面已刷新完成'), backgroundColor: Colors.green, duration: Duration(seconds: 2)));
}
} catch (e) {
// 错误处理
debugPrint('刷新失败:$e');
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('刷新失败:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red, duration: Duration(seconds: 3)));
ToastUtils.showError(context, '刷新失败:${e.toString().substring(0, 50)}');
//ScaffoldMessenger.of(
// context,
//).showSnackBar(SnackBar(content: Text('刷新失败:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red, duration: Duration(seconds: 3)));
}
} finally {
// 标记刷新结束
@@ -682,7 +698,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// ========== 障碍物模式:完成当前障碍物绘制 ==========
void _completeObstacle() {
if (_currentObstaclePoints.length < 3) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('障碍物区域至少需要3个打点!'), backgroundColor: Colors.red));
ToastUtils.showError(context, '障碍物区域至少需要3个打点!');
return;
}
@@ -693,7 +709,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
});
debugPrint('完成障碍物绘制,当前障碍物组数:${_obstacleHoles.length}');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已添加第${_obstacleHoles.length}个障碍物区域'), backgroundColor: Colors.green));
ToastUtils.showSuccess(context, '已添加第${_obstacleHoles.length}个障碍物区域');
}
//void _undoObstaclePoint() {
@@ -723,7 +740,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// ========== 核心方法:打点逻辑 ==========
void _addMarkedPoint() {
if (_currentAreaMode == AreaMode.work && _isWorkAreaCompleted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业区域已完成,无法继续添加打点!'), backgroundColor: Colors.red));
ToastUtils.showError(context, '作业区域已完成,无法继续添加打点!');
return;
}
setState(() {
@@ -1072,7 +1089,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
debugPrint('开始作业:${_selectedPlot!.plotName},路径数据:$gcjPathPoints');
// 可选:显示作业提示
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green));
ToastUtils.showSuccess(context, '作业已开始');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green));
}
/// 暂停作业
@@ -1094,7 +1112,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 模拟停止逻辑(替换为你的实际停止代码)
debugPrint('停止作业:${_selectedPlot!.plotName}');
// 可选:显示停止提示
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已停止'), backgroundColor: Colors.redAccent));
ToastUtils.showError(context, '作业已停止');
//ScaffoldMessenger.of(co ntext).showSnackBar(const SnackBar(content: Text('作业已停止'), backgroundColor: Colors.redAccent));
}
/// 继续作业
@@ -1395,19 +1414,21 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 1. 校验必要条件
if (_currentAreaMode == AreaMode.work && _markedPoints.isEmpty) {
if (showTips) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先添加作业区域打点!'), backgroundColor: Colors.red));
ToastUtils.showError(context, '请先添加作业区域打点!');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先添加作业区域打点!'), backgroundColor: Colors.red));
}
return;
}
if (_currentAreaMode == AreaMode.work && _markedPoints.length < 3) {
if (showTips) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业区域至少需要3个打点才能生成路径!'), backgroundColor: Colors.red));
ToastUtils.showError(context, '作业区域至少需要3个打点才能生成路径!');
}
return;
}
if (_currentWorkMode == null) {
if (showTips) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择作业模式!'), backgroundColor: Colors.red));
ToastUtils.showError(context, '请先选择作业模式!');
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择作业模式!'), backgroundColor: Colors.red));
}
return;
}
@@ -1473,11 +1494,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
if (cubitState.errorMessage != null) {
if (showTips) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成失败:${cubitState.errorMessage}'), backgroundColor: Colors.red));
ToastUtils.showError(context, '路径生成失败:${cubitState.errorMessage}');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成失败:${cubitState.errorMessage}'), backgroundColor: Colors.red));
}
} else if (cubitState.generatedPath != null) {
if (showTips) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('路径生成成功!'), backgroundColor: Colors.green));
ToastUtils.showSuccess(context, '路径生成成功!');
}
debugPrint('生成的路径数据:${cubitState.generatedPath}');
@@ -1506,7 +1528,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
} catch (e) {
debugPrint('路径生成异常:$e');
if (showTips) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成异常:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red));
ToastUtils.showError(context, '路径生成异常:${e.toString().substring(0, 50)}');
//ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成异常:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red));
}
}
}