Merge remote-tracking branch 'origin/feature/my' into feature/my

This commit is contained in:
2026-02-26 16:53:14 +08:00
5 changed files with 270 additions and 136 deletions

View File

@@ -3,6 +3,7 @@ import 'package:maibu_satabot_v2/core/router/route_paths.dart';
import 'package:maibu_satabot_v2/features/ai/presentation/routes/ai_routes.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/pages/login_page.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/routes/auth_routes.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
import 'package:maibu_satabot_v2/features/home/presentation/routes/home_routes.dart';
import 'package:maibu_satabot_v2/features/my/presentation/routes/my_routes.dart';
@@ -14,7 +15,6 @@ import 'go_router_refresh_stream.dart';
GoRouter createRouter(AuthCubit authCubit) {
return GoRouter(
initialLocation: RoutePaths.login,
refreshListenable: GoRouterRefreshStream(authCubit.stream),
redirect: (context, state) {
@@ -33,7 +33,14 @@ GoRouter createRouter(AuthCubit authCubit) {
// 注册 machineDetails 路由
GoRoute(
path: RoutePaths.machineDetails,
builder: (context, state) => const MachineDetailsPage(), // 替换为实际页面
name: 'machineDetails', // 建议加 name,方便跳转
builder: (context, state) {
// 从 extra 中取出设备数据(核心)
final DeviceEntity? device = state.extra as DeviceEntity?;
// 跳转到详情页组件,并传入设备数据
return MachineDetailsPage(device: device);
},
),
// 注册登录注册 路由

View File

@@ -355,7 +355,9 @@ class ImmersionHeader extends StatelessWidget {
),
const SizedBox(width: 4),
Text(
device.deviceAlias ?? '未知设备',
(device.deviceAlias?.trim() ?? '').isEmpty
? '未知设备'
: device.deviceAlias!,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
@@ -393,8 +395,11 @@ class ImmersionHeader extends StatelessWidget {
const SizedBox(height: 8),
CCPrimaryButton(
onPressed: () {
context.push(RoutePaths.machineDetails);
print("通过按钮进入详情");
context.push(
RoutePaths.machineDetails,
extra: device,
); // 把当前点击的这个设备对象传过去
print("通过按钮进入详情,设备名称:$device");
},
text: "详情",

View File

@@ -354,13 +354,20 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}
// ========== 新增:返回上一级页面的方法 ==========
void _navigateBack() {
// 关闭当前页面,返回上一级
Navigator.of(context).pop();
}
@override
void dispose() {
_positionSub?.cancel();
_traceManager.reset();
_mapController.dispose();
_sub?.cancel();
// 修复:_sub 是 non-null 类型,直接 cancel 无需 ?
_sub.cancel();
_plotNameController.dispose(); // 新增:释放文本控制器资源
super.dispose();
}
@@ -673,8 +680,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
/// ========== 新增:中心标与历史打点的虚线连线 ==========
///
/// if (_markedPoints.length >= 2)
PolylineLayer(
polylines: [
for (int i = 0; i < _markedPoints.length - 1; i++)
@@ -771,6 +776,53 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
],
),
// ========== 悬浮返回按钮(核心修改) ==========
Positioned(
top: 10,
left: 10,
child: GestureDetector(
// 自定义点击事件
onTap: _navigateBack,
// 自定义点击反馈(替代 IconButton 的高亮/水波纹)
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
// 核心:用 Stack 实现点击高亮层 + 图标层
child: Stack(
alignment: Alignment.center, // 所有子组件居中
children: [
// 1. 点击高亮层(默认隐藏,点击时显示)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.transparent, // 默认透明
borderRadius: BorderRadius.circular(20),
),
),
),
// 2. 图标层(绝对居中)
const Icon(
Icons.arrow_back_ios,
color: Colors.black87,
size: 20,
),
],
),
),
),
),
/// ========== 核心:地图正中心固定定位标 ==========
Positioned(
left: 0,
@@ -788,45 +840,37 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
),
),
//保存按钮
if (_saveBoxOpen)
Positioned(
left: 16,
right: 80,
top: maxTop > 10 ? 11 : maxTop,
child: // 改用小尺寸按钮容器,替代默认大尺寸FloatingActionButton
Container(
width: 40, // 按钮宽度(默认FAB是56,改为40更小巧)
height: 40, // 按钮高度
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF00C853), // 保留绿色主题
shape: BoxShape.circle, // 圆形
color: const Color(0xFF00C853),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4, // 对应elevation:4的阴影效果
offset: const Offset(0, 2), // 阴影偏移
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
onPressed: () {
_showSavePlotDialog();
//// 点击保存按钮的逻辑
//// 1. 可以直接调用保存方法
//_saveCurrentPath();
//// 2. 也可以弹出输入框让用户输入路径名称(推荐)
//_showSavePathDialog();
},
icon: const Icon(
Icons.save,
color: Colors.white,
size: 18, // 图标尺寸缩小(原20→18)
),
padding: EdgeInsets.zero, // 移除IconButton默认内边距
constraints: const BoxConstraints(), // 解除IconButton尺寸限制
icon: const Icon(Icons.save, color: Colors.white, size: 18),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
),
// 右侧悬浮菜单
Positioned(
height: 336,
@@ -852,7 +896,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
),
),
//开始作业面板
// 底部操作面板
if (_isPanelOpen)
@@ -879,13 +922,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
setState(() => _currentAreaMode = mode);
debugPrint('外部收到区域模式:$mode');
},
// ========== 绑定加号打点事件 ==========
onAddTap: () {
_addMarkedPoint(); // 点击加号打点
_addMarkedPoint();
debugPrint('外部处理加号按钮点击,已添加打点');
},
onDistanceTap: (distance) {
_workDistance = distance; // 更新距离状态
_workDistance = distance;
debugPrint(
'外部处理作业行距距离设置,当前距离:${distance.toStringAsFixed(1)}米',
);
@@ -920,7 +962,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
_angle = result['optimalHeading'] == true
? -1
: result['direction']; // 更新航线方向角状态
: result['direction'];
debugPrint('外部处理航线方向设置,当前角度:${_angle}');
setState(() => _directionBoxOpen = false);
},
@@ -929,14 +971,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
},
),
),
// 在build方法的Stack中添加抽屉组件
// 列表面板
if (_isListBoxOpen)
// 半透明背景 + 底部抽屉
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 1. 半透明遮罩(点击关闭抽屉)
Expanded(
child: GestureDetector(
onTap: () {
@@ -947,14 +988,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
child: Container(color: Colors.black.withOpacity(0.3)),
),
),
// 2. 底部抽屉主体
Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
top: Radius.circular(16), // 顶部圆角
top: Radius.circular(16),
),
boxShadow: [
BoxShadow(
@@ -964,14 +1003,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
],
),
// 限制抽屉高度(可自定义)
constraints: const BoxConstraints(
maxHeight: 600,
minHeight: 200,
),
child: Column(
children: [
// 顶部标题栏
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -980,7 +1017,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 标题:显示数据总数
Text(
'地块列表(共${_plotList.length}条)',
style: const TextStyle(
@@ -989,8 +1025,6 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
color: Colors.black87,
),
),
// 关闭按钮
IconButton(
onPressed: () {
setState(() {
@@ -1006,14 +1040,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
],
),
),
const Divider(height: 1, color: Colors.grey),
// 列表内容(可滚动)
Expanded(
child: _plotList.isEmpty
? // 空数据提示
const Center(
? const Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
@@ -1034,8 +1064,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
],
),
)
: // 列表项
ListView.builder(
: ListView.builder(
padding: const EdgeInsets.symmetric(
vertical: 8,
),

View File

@@ -117,13 +117,13 @@ class _WorkParamsCardState extends State<WorkParamsCard> {
final result = await useCase(params);
print("发送获取历史作业记录结束");
result.fold(
(failure) {
(failure) {
setState(() {
_errorMessage = failure.message;
_isLoading = false;
});
},
(data) {
(data) {
setState(() {
_statistics = data;
_isLoading = false;
@@ -141,9 +141,12 @@ class _WorkParamsCardState extends State<WorkParamsCard> {
}
// 弹窗展示详情
// 弹窗展示详情(从底部弹出)
// 弹窗展示详情(从底部弹出,覆盖底部导航栏)
void _showDetailsDialog(BuildContext context, List<DeviceRunStatisticsEntity> data) {
// 弹窗展示详情(从底部弹出)
// 弹窗展示详情(从底部弹出,覆盖底部导航栏)
void _showDetailsDialog(
BuildContext context,
List<DeviceRunStatisticsEntity> data,
) {
showModalBottomSheet(
context: context,
isScrollControlled: true, // 支持全屏高度
@@ -203,15 +206,31 @@ class _WorkParamsCardState extends State<WorkParamsCard> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(Icons.calendar_today, '开始时间', item.startTime),
_buildDetailRow(Icons.calendar_today_outlined, '结束时间', item.endTime),
_buildDetailRow(Icons.square_foot, '工作面积', '${item.workArea} 亩'),
_buildDetailRow(
Icons.calendar_today,
'开始时间',
item.startTime,
),
_buildDetailRow(
Icons.calendar_today_outlined,
'结束时间',
item.endTime,
),
_buildDetailRow(
Icons.square_foot,
'工作面积',
'${item.workArea} 亩',
),
_buildDetailRow(
Icons.directions_car,
'工作距离',
'${item.distance?.toStringAsFixed(2) ?? '未计算'} km',
),
_buildDetailRow(Icons.timer, '花费时间', '${item.time} 小时'),
_buildDetailRow(
Icons.timer,
'花费时间',
'${item.time} 小时',
),
],
),
);
@@ -244,9 +263,7 @@ class _WorkParamsCardState extends State<WorkParamsCard> {
);
}
// 构建详情行
// 构建详情行
Widget _buildDetailRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
@@ -265,10 +282,7 @@ class _WorkParamsCardState extends State<WorkParamsCard> {
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 14,
color: Colors.black54,
),
style: const TextStyle(fontSize: 14, color: Colors.black54),
),
),
],
@@ -276,9 +290,6 @@ class _WorkParamsCardState extends State<WorkParamsCard> {
);
}
// 构建单个参数项
Widget _buildParamItem(String label, String value, String unit) {
return Expanded(

View File

@@ -1,30 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
class MachineDetailsPage extends StatelessWidget {
const MachineDetailsPage({super.key});
final DeviceEntity? device; // 接收传过来的设备数据
const MachineDetailsPage({super.key, this.device});
@override
Widget build(BuildContext context) {
// 移除 MaterialApp,直接返回 Scaffold
return const DeviceDetailPage();
// 把 device 参数传递给 DeviceDetailPage
return DeviceDetailPage(device: device);
}
}
class DeviceDetailPage extends StatelessWidget {
const DeviceDetailPage({super.key});
final DeviceEntity? device; // 定义参数接收设备数据
// 构造函数接收 device 参数
const DeviceDetailPage({super.key, this.device});
@override
Widget build(BuildContext context) {
// 空值校验:无设备数据时显示提示
if (device == null) {
return const Scaffold(body: Center(child: Text("未获取到设备信息")));
}
// 非空断言:已经校验过 device 非空,放心使用
final currentDevice = device!;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
centerTitle: true,
title: const Text(
'设备详情',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
@@ -35,9 +47,9 @@ class DeviceDetailPage extends StatelessWidget {
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildDeviceStatusCard(),
_buildDeviceStatusCard(currentDevice), // 传递设备数据
const SizedBox(height: 16),
_buildBasicInfoCard(),
_buildBasicInfoCard(context, currentDevice), // 传递context和设备数据
const SizedBox(height: 16),
_buildResourceCenterCard(),
const SizedBox(height: 24),
@@ -48,8 +60,8 @@ class DeviceDetailPage extends StatelessWidget {
);
}
// 设备状态卡片
Widget _buildDeviceStatusCard() {
// 设备状态卡片(接收设备数据参数)
Widget _buildDeviceStatusCard(DeviceEntity device) {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
@@ -58,17 +70,16 @@ class DeviceDetailPage extends StatelessWidget {
),
child: Column(
children: [
Image.asset(
'assets/images/car.png',
width: 380,
fit: BoxFit.contain,
),
Image.asset('assets/images/car.png', width: 380, fit: BoxFit.contain),
const SizedBox(height: 12),
const Text(
'离线',
// 使用设备真实在线状态
Text(
device.isOnline ? '在线' : '离线',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
color: device.isOnline
? const Color(0xFF00C853)
: const Color(0xFF999999),
),
),
],
@@ -76,8 +87,17 @@ class DeviceDetailPage extends StatelessWidget {
);
}
// 基本信息卡片
Widget _buildBasicInfoCard() {
// 基本信息卡片(接收context和设备数据参数)
Widget _buildBasicInfoCard(BuildContext context, DeviceEntity device) {
// 处理设备名称(空值兜底)
final deviceName = (device.deviceAlias?.trim() ?? '').isEmpty
? '未知设备'
: device.deviceAlias!;
// 处理设备ID(空值兜底)
final deviceId = device.deviceName ?? '未知ID';
//print('激活时间:$device'); // 调试输出激活时间
//final activeTime = device.activeTime != null ? device.activeTime : '未知时间';
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -91,47 +111,114 @@ class DeviceDetailPage extends StatelessWidget {
padding: EdgeInsets.only(bottom: 16),
child: Text(
'基本信息',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
style: TextStyle(fontSize: 14, color: Color(0xFF999999)),
),
),
// 设备名称行 - 传递context参数
_buildInfoRow(
context: context, // 关键:补上必填的context参数
icon: Icons.info_outline,
label: '设备名称',
trailing: Row(
children: const [
Text(''),
SizedBox(width: 8),
Icon(Icons.edit, size: 16, color: Color(0xFF007AFF)),
mainAxisSize: MainAxisSize.min, // 防止Row撑满宽度
children: [
// 显示真实设备名称
Text(deviceName),
const SizedBox(width: 8),
const Icon(Icons.edit, size: 16, color: Color(0xFF007AFF)),
],
),
),
const Divider(height: 32, color: Color(0xFFF0F0F0)),
// 设备ID行 - 传递context参数
// 设备ID行 - 传递context参数
_buildInfoRow(
context: context,
icon: Icons.shield_outlined,
label: '设备ID',
trailing: Row(
children: const [
Text('undefined'),
SizedBox(width: 8),
Icon(Icons.copy, size: 16, color: Color(0xFF999999)),
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end, // 右对齐
mainAxisSize: MainAxisSize.min, // 只占必要高度
children: [
// 核心修改:去掉宽度限制,允许换行显示全部ID
SizedBox(
// 取消固定宽度,让文本自适应(也可设置最大宽度适配屏幕)
width:
MediaQuery.of(context).size.width - 180, // 适配屏幕宽度(避免超出)
child: GestureDetector(
onLongPress: () {
// 长按文本复制
Clipboard.setData(ClipboardData(text: deviceId));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('设备ID已复制'),
duration: Duration(seconds: 1),
),
);
}
},
child: Text(
deviceId,
textAlign: TextAlign.right, // 右对齐更美观
softWrap: true, // 允许换行
overflow: TextOverflow.visible, // 显示全部内容(无省略号)
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
),
const SizedBox(height: 4), // 文本和复制图标间距
// 复制图标单独一行
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: deviceId));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('设备ID已复制到剪贴板'),
duration: Duration(seconds: 1),
),
);
}
},
child: const Icon(
Icons.copy,
size: 16,
color: Color(0xFF999999),
),
),
//_buildInfoRow(
// context: context, // 关键:补上必填的context参数
// icon: Icons.info_outline,
// label: '激活时间',
// trailing: Row(
// mainAxisSize: MainAxisSize.min, // 防止Row撑满宽度
// children: [
// // 显示真实设备名称
// Text(activeTime),
// const SizedBox(width: 8),
// const Icon(
// Icons.edit,
// size: 16,
// color: Color(0xFF007AFF),
// ),
// ],
// ),
//),
],
),
),
const Divider(height: 32, color: Color(0xFFF0F0F0)),
_buildInfoRow(
icon: Icons.date_range_outlined,
label: '激活时间',
trailing: const Text(''),
),
],
),
);
}
// 资源中心卡片
// 资源中心卡片(无设备数据依赖)
Widget _buildResourceCenterCard() {
return Container(
padding: const EdgeInsets.all(16),
@@ -146,10 +233,7 @@ class DeviceDetailPage extends StatelessWidget {
padding: EdgeInsets.only(bottom: 16),
child: Text(
'资源中心',
style: TextStyle(
fontSize: 14,
color: Color(0xFF999999),
),
style: TextStyle(fontSize: 14, color: Color(0xFF999999)),
),
),
_buildResourceItem('使用说明', Icons.description_outlined),
@@ -160,8 +244,9 @@ class DeviceDetailPage extends StatelessWidget {
);
}
// 信息行组件
// 信息行组件(添加context必填参数)
Widget _buildInfoRow({
required BuildContext context, // 必填的context参数
required IconData icon,
required String label,
required Widget trailing,
@@ -170,10 +255,7 @@ class DeviceDetailPage extends StatelessWidget {
children: [
Icon(icon, size: 18, color: const Color(0xFF999999)),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(fontSize: 16),
),
Text(label, style: const TextStyle(fontSize: 16)),
const Spacer(),
trailing,
],
@@ -186,10 +268,7 @@ class DeviceDetailPage extends StatelessWidget {
children: [
Icon(icon, size: 18, color: const Color(0xFF007AFF)),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(fontSize: 16),
),
Text(title, style: const TextStyle(fontSize: 16)),
const Spacer(),
const Icon(Icons.arrow_forward_ios, size: 16, color: Color(0xFF999999)),
],
@@ -213,18 +292,19 @@ class DeviceDetailPage extends StatelessWidget {
// 实现解绑逻辑
showDialog(
context: context,
builder: (context) => AlertDialog(
builder: (dialogContext) => AlertDialog(
title: const Text('确认解绑'),
content: const Text('确定要解绑该设备吗?'),
content: Text('确定要解绑【${device?.deviceAlias ?? '该设备'}】吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () => Navigator.pop(dialogContext),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// 执行解绑操作
Navigator.pop(dialogContext);
// 执行解绑操作(传入设备ID)
// _unbindDevice(device?.deviceName ?? '');
},
child: const Text('确认'),
),
@@ -237,13 +317,15 @@ class DeviceDetailPage extends StatelessWidget {
children: [
Icon(Icons.unarchive_outlined, size: 18),
SizedBox(width: 12),
Text(
'解绑设备',
style: TextStyle(fontSize: 12),
),
Text('解绑设备', style: TextStyle(fontSize: 12)),
],
),
),
);
}
// 可选:解绑设备的方法(根据你的业务实现)
// void _unbindDevice(String deviceId) {
// // 调用解绑接口的逻辑
// }
}