From 952bab03f5092640d46968a1cee3f93ce17bd1c0 Mon Sep 17 00:00:00 2001 From: mmc <1556375442@qq.com> Date: Fri, 27 Feb 2026 21:04:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AE=BE=E5=A4=87=E5=88=AB?= =?UTF-8?q?=E5=90=8D=E6=8E=A5=E5=8F=A3+UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/di/injection.dart | 4 +- .../datasources/device_http_datasource.dart | 1 + .../impl/device_http_datasource_impl.dart | 22 +- .../repositories/device_repository_impl.dart | 13 + .../repositories/device_repository.dart | 4 + .../usecases/update_devicename_usecase.dart | 26 + .../presentation/bloc/devices_cubit.dart | 77 +- .../presentation/bloc/devices_state.dart | 12 +- .../presentation/widgets/ImmersionHeader.dart | 1 + .../pages/machine_details_page.dart | 682 ++++++++++++------ 10 files changed, 633 insertions(+), 209 deletions(-) create mode 100644 lib/features/devices/domain/usecases/update_devicename_usecase.dart diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index fbea7137..1e55e4b0 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -7,6 +7,7 @@ import 'package:maibu_satabot_v2/features/devices/data/datasources/impl/device_h import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_user_device_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicename_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart'; import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart'; import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart'; @@ -103,12 +104,13 @@ Future init() async { sl.registerLazySingleton(() => DiffSteerUseCase()); sl.registerLazySingleton(() => DeviceWorkHostrirty(sl())); sl.registerLazySingleton(() => UnbindDeviceUseCase(sl())); + sl.registerLazySingleton(() => UpdateDevicenameUsecase(sl())); /// 5. 状态管理 (Cubit/Bloc) sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册 sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl())); sl.registerLazySingleton( - () => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl()), + () => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl()), ); sl.registerFactory(() => RemoteControlCubit(sl())); diff --git a/lib/features/devices/data/datasources/device_http_datasource.dart b/lib/features/devices/data/datasources/device_http_datasource.dart index 2103afd9..174c7b5b 100644 --- a/lib/features/devices/data/datasources/device_http_datasource.dart +++ b/lib/features/devices/data/datasources/device_http_datasource.dart @@ -4,6 +4,7 @@ abstract class DeviceHttpDatasource { Future> getUserDevices(String username); Future bindDevice(String deviceId, String deviceAlias); Future unbindDevice(String deviceId, String deviceName); + Future updateDeviceName(String deviceId, String deviceName); Future switchDevice(String platform, String deviceId); Future getDeviceLocation(String username) async {} diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index 3cda047b..f9decb89 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -92,7 +92,27 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { throw Exception(data['msg']); } - return data['data']; + return data['data'] == true ? 1 : 0; // 根据接口返回的布尔值转换为 int + } + + Future updateDeviceName(String deviceId, String deviceName) async { + final response = await dio.post( + '/forward/device/updateDeviceAlias', + data: {"deviceId": deviceId, "deviceAlias": deviceName}, + ); + print('修改别名响应: ${response}'); // 调试输出响应数据 + + final data = response.data; + + if (response.statusCode != 200) { + throw Exception('网络错误'); + } + + if (data['code'] != 200) { + throw Exception(data['msg']); + } + + return data['data'] == true ? 1 : 0; // 根据接口返回的布尔值转换为 int } @override diff --git a/lib/features/devices/data/repositories/device_repository_impl.dart b/lib/features/devices/data/repositories/device_repository_impl.dart index af8daf38..731a3c17 100644 --- a/lib/features/devices/data/repositories/device_repository_impl.dart +++ b/lib/features/devices/data/repositories/device_repository_impl.dart @@ -61,6 +61,19 @@ class DeviceRepositoryImpl implements DeviceRepository { return Left(DeviceFailure(cleanMessage)); } } + @override + Future> updateDeviceName(String deviceId, String deviceName) async { + try { + var result = await _deviceHttpDatasource.updateDeviceName(deviceId, deviceName); + return Right(result); + } on DioException catch (e) { + final String serverMessage = e.response?.data['msg'] ?? "网络连接异常"; + return Left(DeviceFailure(serverMessage)); + } catch (e) { + final cleanMessage = e.toString().replaceFirst('Exception: ', ''); + return Left(DeviceFailure(cleanMessage)); + } + } @override Future> switchDevice( diff --git a/lib/features/devices/domain/repositories/device_repository.dart b/lib/features/devices/domain/repositories/device_repository.dart index 07e2aafb..30075fe3 100644 --- a/lib/features/devices/domain/repositories/device_repository.dart +++ b/lib/features/devices/domain/repositories/device_repository.dart @@ -15,6 +15,10 @@ abstract class DeviceRepository { String deviceId, String deviceName, ); + Future> updateDeviceName( + String deviceId, + String deviceAlias, + ); Future> switchDevice( String platform, String deviceId, diff --git a/lib/features/devices/domain/usecases/update_devicename_usecase.dart b/lib/features/devices/domain/usecases/update_devicename_usecase.dart new file mode 100644 index 00000000..f2470990 --- /dev/null +++ b/lib/features/devices/domain/usecases/update_devicename_usecase.dart @@ -0,0 +1,26 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/domain/usecases/base_usecase.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart'; + +class UpdateDevicenameUsecase + implements BaseUseCase { + final DeviceRepository repository; + + UpdateDevicenameUsecase(this.repository); + + @override + Future> call(UpdateDevicenameParams params) async { + return await repository.updateDeviceName( + params.deviceId, + params.deviceAlias, + ); + } +} + +class UpdateDevicenameParams { + final String deviceId; + final String deviceAlias; + + UpdateDevicenameParams(this.deviceId, this.deviceAlias); +} diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index c7875bde..40fe037d 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -3,6 +3,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity. import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_user_device_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicename_usecase.dart'; import '../../domain/usecases/delete_work_record_usecase.dart'; import '../../domain/usecases/get_device_location_usecase.dart'; @@ -16,6 +17,7 @@ class DevicesCubit extends Cubit { final GetWorkRecordUseCase _getWorkRecordUseCase; final DeleteWorkRecordUseCase _deleteWorkRecordUseCase; final UnbindDeviceUseCase _unbindDeviceUseCase; + final UpdateDevicenameUsecase _updateDevicename; DevicesCubit( this.repository, @@ -24,10 +26,17 @@ class DevicesCubit extends Cubit { this._getWorkRecordUseCase, this._deleteWorkRecordUseCase, this._unbindDeviceUseCase, + this._updateDevicename, ) : super(const DevicesState()); Future unbindDevice(String deviceId, String deviceName) async { - emit(state.copyWith(isLoading: true, errorMessage: '')); + emit( + state.copyWith( + isLoading: true, + errorMessage: '', + operationType: DeviceOperationType.unbind, + ), + ); try { final params = UnbindDeviceParams(deviceId, deviceName); @@ -38,6 +47,7 @@ class DevicesCubit extends Cubit { state.copyWith( isLoading: false, errorMessage: failure.message ?? '解绑设备失败', + operationType: DeviceOperationType.none, // 操作结束重置 ), ), (successCode) { @@ -56,6 +66,7 @@ class DevicesCubit extends Cubit { ? null : state.selectedDevice, errorMessage: '', + operationType: DeviceOperationType.none, ), ); } else { @@ -63,6 +74,7 @@ class DevicesCubit extends Cubit { state.copyWith( isLoading: false, errorMessage: '解绑失败:状态码 $successCode', + operationType: DeviceOperationType.none, ), ); } @@ -70,7 +82,68 @@ class DevicesCubit extends Cubit { ); } catch (e) { emit( - state.copyWith(isLoading: false, errorMessage: '解绑异常:${e.toString()}'), + state.copyWith(isLoading: false, errorMessage: '解绑异常:${e.toString()}', operationType: DeviceOperationType.none), + ); + } + } + + Future updateDeviceName(String deviceId, String deviceName) async { + emit( + state.copyWith( + isLoading: true, + errorMessage: '', + operationType: DeviceOperationType.updateName, + ), + ); + + try { + final params = UpdateDevicenameParams(deviceId, deviceName); + final result = await _updateDevicename.call(params); + + result.fold( + (failure) => emit( + state.copyWith( + isLoading: false, + errorMessage: failure.message ?? '更新设备名称失败', + operationType: DeviceOperationType.none, + ), + ), + (successCode) { + // 核心修复:int 转 bool 条件判断 + print('更新设备名称结果代码: $successCode'); // 调试输出结果代码 + final isSuccess = successCode == 1; // 显式转为 bool + if (isSuccess) { + //final updatedDevices = state.devices?.map((device) { + // return device.deviceName == deviceId ? device.copyWith(deviceName: deviceName) : device; + //}).toList(); + + emit( + state.copyWith( + isLoading: false, + //devices: updatedDevices, + //selectedDevice: state.selectedDevice?.deviceName == deviceId ? state.selectedDevice?.copyWith(deviceName: deviceName) : state.selectedDevice, + errorMessage: '', + operationType: DeviceOperationType.none, + ), + ); + } else { + emit( + state.copyWith( + isLoading: false, + errorMessage: '更新设备名称失败:状态码 $successCode', + operationType: DeviceOperationType.none, + ), + ); + } + }, + ); + } catch (e) { + emit( + state.copyWith( + isLoading: false, + errorMessage: '更新设备名称异常:${e.toString()}', + operationType: DeviceOperationType.none, + ), ); } } diff --git a/lib/features/devices/presentation/bloc/devices_state.dart b/lib/features/devices/presentation/bloc/devices_state.dart index a7fb1703..0e4d4795 100644 --- a/lib/features/devices/presentation/bloc/devices_state.dart +++ b/lib/features/devices/presentation/bloc/devices_state.dart @@ -1,5 +1,11 @@ import 'package:equatable/equatable.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart'; +enum DeviceOperationType { + none, // 无操作 + unbind, // 解绑设备 + updateName, // 修改设备名称 +} + class DevicesState extends Equatable { final List devices; // 名下所有设备列表 @@ -9,6 +15,7 @@ class DevicesState extends Equatable { final double? deviceLatitude; // 新增字段 final double? deviceLongitude; // 新增字段 final List>? workRecords; + final DeviceOperationType operationType; // 标记当前操作类型 const DevicesState({ this.devices = const [], this.selectedDevice, @@ -16,7 +23,8 @@ class DevicesState extends Equatable { this.errorMessage, this.deviceLatitude, this.deviceLongitude, - this.workRecords + this.workRecords, + this.operationType = DeviceOperationType.none, // 默认无操作 }); // 使用 copyWith 方便局部更新状态 @@ -28,6 +36,7 @@ class DevicesState extends Equatable { double? deviceLatitude, double? deviceLongitude, List>? workRecords, + DeviceOperationType? operationType, }) { return DevicesState( devices: devices ?? this.devices, @@ -37,6 +46,7 @@ class DevicesState extends Equatable { deviceLatitude: deviceLatitude ?? this.deviceLatitude, deviceLongitude: deviceLongitude ?? this.deviceLongitude, workRecords: workRecords ?? this.workRecords, + operationType: operationType ?? this.operationType, // 更新操作类型 ); } diff --git a/lib/features/home/presentation/widgets/ImmersionHeader.dart b/lib/features/home/presentation/widgets/ImmersionHeader.dart index dc81a786..e0c54f99 100644 --- a/lib/features/home/presentation/widgets/ImmersionHeader.dart +++ b/lib/features/home/presentation/widgets/ImmersionHeader.dart @@ -374,6 +374,7 @@ class ImmersionHeader extends StatelessWidget { ), ], ), + const SizedBox(height: 6), const Text( "点击查看详情", style: TextStyle(color: Colors.grey, fontSize: 12), diff --git a/lib/features/machine_details/presentation/pages/machine_details_page.dart b/lib/features/machine_details/presentation/pages/machine_details_page.dart index 8390f79f..19846f60 100644 --- a/lib/features/machine_details/presentation/pages/machine_details_page.dart +++ b/lib/features/machine_details/presentation/pages/machine_details_page.dart @@ -22,36 +22,54 @@ class _MachineDetailsPageState extends State { late String _deviceName; late String _deviceId; BuildContext? _dialogContext; + BuildContext? _editNameSheetContext; + BuildContext? _loadingDialogContext; // 解绑加载弹窗 + BuildContext? _updateLoadingContext; // 修改名称加载弹窗 + bool _isUpdatingName = false; @override void initState() { super.initState(); - _deviceName = (widget.device?.deviceAlias?.trim().isEmpty ?? true) ? '未知设备' : widget.device!.deviceAlias!; _deviceId = widget.device?.deviceName ?? ''; - - _nameController = TextEditingController(); + _nameController = TextEditingController(text: _deviceName); } @override void dispose() { _nameController.dispose(); + // 兜底关闭所有弹窗 + if (_loadingDialogContext != null && + Navigator.canPop(_loadingDialogContext!)) { + Navigator.pop(_loadingDialogContext!); + } + if (_updateLoadingContext != null && + Navigator.canPop(_updateLoadingContext!)) { + Navigator.pop(_updateLoadingContext!); + } + if (_dialogContext != null && Navigator.canPop(_dialogContext!)) { + Navigator.pop(_dialogContext!); + } + if (_editNameSheetContext != null && + Navigator.canPop(_editNameSheetContext!)) { + Navigator.pop(_editNameSheetContext!); + } super.dispose(); } - /// 弹出修改设备名称底部抽屉 + /// 弹出修改设备名称底部抽屉(保持不变) void _showEditNameSheet() { _nameController.text = _deviceName; - - showModalBottomSheet( + Future bottomSheetFuture = showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (ctx) { + _editNameSheetContext = ctx; return Padding( padding: EdgeInsets.only( left: 20, @@ -74,6 +92,7 @@ class _MachineDetailsPageState extends State { decoration: const InputDecoration( hintText: "请输入设备名称", border: OutlineInputBorder(), + counterText: "", ), ), const SizedBox(height: 10), @@ -81,34 +100,45 @@ class _MachineDetailsPageState extends State { children: [ Expanded( child: OutlinedButton( - onPressed: () => Navigator.pop(ctx), + onPressed: () { + _isUpdatingName = false; + Navigator.pop(ctx); + }, child: const Text("取消"), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton( - onPressed: () { - final text = _nameController.text.trim(); - - if (text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("名称不能为空")), - ); - return; - } - - setState(() { - _deviceName = text; - }); - - Navigator.pop(ctx); - - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("已修改为:$text"))); - }, - child: const Text("确认"), + onPressed: _isUpdatingName + ? null + : () async { + final text = _nameController.text.trim(); + if (text.isEmpty) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("名称不能为空")), + ); + } + return; + } + if (text == _deviceName) { + Navigator.pop(ctx); + return; + } + setState(() => _isUpdatingName = true); + await _executeUpdateDeviceName(text); + }, + child: _isUpdatingName + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text("确认修改"), ), ), ], @@ -118,21 +148,49 @@ class _MachineDetailsPageState extends State { ); }, ); + bottomSheetFuture.whenComplete(() { + setState(() { + _isUpdatingName = false; + _editNameSheetContext = null; + }); + }); } @override Widget build(BuildContext context) { if (widget.device == null) { - return const Scaffold(body: Center(child: Text("未获取到设备信息"))); + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: AppBar( + title: const Text('设备详情'), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + // 优化1:先刷新列表,再返回(保证刷新逻辑执行) + + // 核心:返回上一页 + Navigator.pop(context); + if (context.mounted) { + final username = + context.read().state.user?.username ?? ""; + context.read().fetchAllDevices(username); + } + }, + ), + ), + body: const Center(child: Text("未获取到设备信息")), + ); } final currentDevice = widget.device!; return BlocListener( listener: (context, state) { - // 只有解绑操作触发的状态变化才处理 - if (state.isLoading || state.errorMessage != null) { + if (state.operationType == DeviceOperationType.unbind) { _handleUnbindResult(state); } + if (state.operationType == DeviceOperationType.updateName) { + _handleUpdateNameResult(state); + } }, child: Scaffold( backgroundColor: const Color(0xFFF5F5F5), @@ -144,8 +202,18 @@ class _MachineDetailsPageState extends State { ), leading: IconButton( icon: const Icon(Icons.arrow_back_ios), - onPressed: () => Navigator.pop(context), + onPressed: () { + Navigator.pop(context); + if (context.mounted) { + final username = + context.read().state.user?.username ?? ""; + context.read().fetchAllDevices(username); + } + }, ), + elevation: 1, + backgroundColor: Colors.white, + foregroundColor: Colors.black, ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), @@ -153,7 +221,7 @@ class _MachineDetailsPageState extends State { children: [ _buildDeviceStatusCard(currentDevice), const SizedBox(height: 16), - _buildBasicInfoCard(context, currentDevice), // 传递context和设备数据 + _buildBasicInfoCard(context, currentDevice), const SizedBox(height: 16), _buildResourceCenterCard(), const SizedBox(height: 24), @@ -165,25 +233,52 @@ class _MachineDetailsPageState extends State { ); } - /// 设备状态卡片 + /// 设备状态卡片(保持不变) Widget _buildDeviceStatusCard(DeviceEntity device) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: Column( children: [ - Image.asset('assets/images/car.png', width: 300, fit: BoxFit.contain), + Image.asset( + 'assets/images/car.png', + width: 300, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.device_hub, + size: 100, + color: Colors.grey, + ); + }, + ), const SizedBox(height: 12), - Text( - device.isOnline ? '在线' : '离线', - style: TextStyle( - fontSize: 14, + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( color: device.isOnline - ? const Color(0xFF00C853) - : const Color(0xFF999999), + ? const Color(0xFF00C853).withOpacity(0.1) + : const Color(0xFF999999).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + device.isOnline ? '在线' : '离线', + style: TextStyle( + fontSize: 14, + color: device.isOnline + ? const Color(0xFF00C853) + : const Color(0xFF999999), + ), ), ), ], @@ -191,20 +286,21 @@ class _MachineDetailsPageState extends State { ); } - // 基本信息卡片(接收context和设备数据参数) + /// 基本信息卡片(保持不变) Widget _buildBasicInfoCard(BuildContext context, DeviceEntity device) { - // 处理设备名称(空值兜底) - final deviceName = (device.deviceAlias?.trim() ?? '').isEmpty - ? '未知设备' - : device.deviceAlias!; - // 处理设备ID(空值兜底) final deviceId = device.deviceName ?? '未知ID'; - return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -216,17 +312,16 @@ class _MachineDetailsPageState extends State { style: TextStyle(fontSize: 14, color: Color(0xFF999999)), ), ), - // 设备名称行 - 传递context参数 _buildInfoRow( - context: context, // 关键:补上必填的context参数 + context: context, icon: Icons.info_outline, label: '设备名称', trailing: GestureDetector( - onTap: _showEditNameSheet, + onTap: _isUpdatingName ? null : _showEditNameSheet, child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(_deviceName), + Text(_deviceName, style: const TextStyle(fontSize: 16)), const SizedBox(width: 8), const Icon(Icons.edit, size: 16, color: Color(0xFF007AFF)), ], @@ -234,40 +329,23 @@ class _MachineDetailsPageState extends State { ), ), const Divider(height: 32, color: Color(0xFFF0F0F0)), - // 设备ID行 - 传递context参数 - // 设备ID行 - 传递context参数 _buildInfoRow( context: context, icon: Icons.shield_outlined, label: '设备ID', trailing: Column( - crossAxisAlignment: CrossAxisAlignment.end, // 右对齐 - mainAxisSize: MainAxisSize.min, // 只占必要高度 + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, children: [ - // 核心修改:去掉宽度限制,允许换行显示全部ID SizedBox( - // 取消固定宽度,让文本自适应(也可设置最大宽度适配屏幕) - width: - MediaQuery.of(context).size.width - 180, // 适配屏幕宽度(避免超出) + 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), - ), - ); - } - }, + onLongPress: () => _copyToClipboard(deviceId, '设备ID已复制'), child: Text( deviceId, - textAlign: TextAlign.right, // 右对齐更美观 - softWrap: true, // 允许换行 - overflow: TextOverflow.visible, // 显示全部内容(无省略号) - + textAlign: TextAlign.right, + softWrap: true, + overflow: TextOverflow.visible, style: const TextStyle( fontSize: 14, color: Colors.black87, @@ -275,45 +353,15 @@ class _MachineDetailsPageState extends State { ), ), ), - const SizedBox(height: 4), // 文本和复制图标间距 - // 复制图标单独一行 + 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), - ), - ); - } - }, + onTap: () => _copyToClipboard(deviceId, '设备ID已复制到剪贴板'), 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), - // ), - // ], - // ), - //), ], ), ), @@ -322,13 +370,20 @@ class _MachineDetailsPageState extends State { ); } - // 资源中心卡片(无设备数据依赖) + /// 资源中心卡片(保持不变) Widget _buildResourceCenterCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -348,9 +403,9 @@ class _MachineDetailsPageState extends State { ); } - // 信息行组件(添加context必填参数) + /// 通用信息行组件(保持不变) Widget _buildInfoRow({ - required BuildContext context, // 必填的context参数 + required BuildContext context, required IconData icon, required String label, required Widget trailing, @@ -366,24 +421,35 @@ class _MachineDetailsPageState extends State { ); } - // 资源项组件 + /// 资源项组件(保持不变) Widget _buildResourceItem(String title, IconData icon) { - return Row( - children: [ - Icon(icon, size: 18, color: const Color(0xFF007AFF)), - const SizedBox(width: 12), - Text(title, style: const TextStyle(fontSize: 16)), - const Spacer(), - const Icon(Icons.arrow_forward_ios, size: 16, color: Color(0xFF999999)), - ], + return InkWell( + onTap: () { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('暂未开放$title功能'))); + }, + child: Row( + children: [ + Icon(icon, size: 18, color: const Color(0xFF007AFF)), + const SizedBox(width: 12), + Text(title, style: const TextStyle(fontSize: 16)), + const Spacer(), + const Icon( + Icons.arrow_forward_ios, + size: 16, + color: Color(0xFF999999), + ), + ], + ), ); } - // 解绑设备按钮 + /// 解绑设备按钮(保持不变) Widget _buildUnbindButton(BuildContext context) { return SizedBox( width: double.infinity, - height: 48, + height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.white, @@ -391,49 +457,58 @@ class _MachineDetailsPageState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), + side: const BorderSide(color: Color(0xFFFF3B30), width: 1), + padding: const EdgeInsets.symmetric(vertical: 8), + textStyle: const TextStyle(height: 1.2), ), - onPressed: () { - // 实现解绑逻辑 - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Text('确认解绑'), - content: Text('确定要解绑【${_deviceName ?? '该设备'}】吗?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('取消'), - ), - TextButton( - onPressed: _executeUnbind, // 执行解绑 - //onPressed: () { - - // //Navigator.pop(dialogContext); - // //context.read().unbindDevice( - // // _deviceId ?? '', - // // _deviceName ?? '', - // //); - // // 执行解绑操作(传入设备ID) - // // _unbindDevice(device?.deviceName ?? ''); - //}, - child: const Text('确认'), - ), - ], - ), - ); - }, + onPressed: _isUpdatingName ? null : () => _showUnbindConfirmDialog(), child: const Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.unarchive_outlined, size: 18), - SizedBox(width: 12), - Text('解绑设备', style: TextStyle(fontSize: 12)), + SizedBox(width: 8), + Text( + '解绑设备', + style: TextStyle(fontSize: 16, height: 1.0), + overflow: TextOverflow.visible, + ), ], ), ), ); } + /// 显示解绑确认弹窗(保持不变) + void _showUnbindConfirmDialog() { + showDialog( + context: context, + builder: (dialogContext) { + _dialogContext = dialogContext; + return AlertDialog( + title: const Text('确认解绑'), + content: Text('确定要解绑【$_deviceName】吗?解绑后将无法管理该设备'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('取消'), + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFFF3B30), + ), + onPressed: _executeUnbind, + child: const Text('确认'), + ), + ], + ); + }, + ); + } + Future _executeUnbind() async { if (_deviceId.isEmpty || _deviceName.isEmpty) { if (context.mounted) { @@ -447,70 +522,269 @@ class _MachineDetailsPageState extends State { return; } - // 调用 Cubit 解绑方法 - await context.read().unbindDevice(_deviceId, _deviceName); - } - - /// 处理解绑结果(成功/失败) - void _handleUnbindResult(DevicesState state) { - // 1. 关闭确认弹窗 + // 关闭确认弹窗 if (_dialogContext != null && Navigator.canPop(_dialogContext!)) { Navigator.pop(_dialogContext!); - _dialogContext = null; } - // 2. 处理加载状态(隐藏加载弹窗/按钮禁用等) - if (state.isLoading) { - // 可选:显示加载弹窗 - showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => const AlertDialog( - content: Row( - children: [ - CircularProgressIndicator(), - SizedBox(width: 16), - Text("正在解绑设备..."), - ], + try { + // 显示加载弹窗并记录上下文 + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + _loadingDialogContext = ctx; + return const AlertDialog( + content: Row( + children: [ + CircularProgressIndicator(strokeWidth: 2), + SizedBox(width: 16), + Text("正在解绑设备..."), + ], + ), + ); + }, + ); + } + + // 调用Cubit解绑方法 + await context.read().unbindDevice(_deviceId, _deviceName); + + // 主动关闭加载弹窗 + if (_loadingDialogContext != null && + Navigator.canPop(_loadingDialogContext!)) { + Navigator.pop(_loadingDialogContext!); + _loadingDialogContext = null; + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("解绑异常:${e.toString()}"), + backgroundColor: Colors.red, ), - ), - ); + ); + // 关闭加载弹窗 + if (_loadingDialogContext != null && + Navigator.canPop(_loadingDialogContext!)) { + Navigator.pop(_loadingDialogContext!); + _loadingDialogContext = null; + } + } + } + } + + /// 执行修改设备名称(核心修改:新增兜底逻辑) + Future _executeUpdateDeviceName(String newDeviceName) async { + // 基础校验 + if (_deviceId.isEmpty || newDeviceName.isEmpty) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("设备信息异常,无法修改名称"), + backgroundColor: Colors.red, + ), + ); + } + setState(() => _isUpdatingName = false); return; } - // 3. 处理结果 - if (state.errorMessage?.isEmpty == true) { - // 解绑成功 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("设备「$_deviceName」解绑成功"), - backgroundColor: Colors.green, - duration: const Duration(seconds: 2), - ), + try { + // 显示加载弹窗并记录专属上下文 + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + _updateLoadingContext = ctx; + return const AlertDialog( + content: Row( + children: [ + CircularProgressIndicator(strokeWidth: 2), + SizedBox(width: 16), + Text("正在修改名称..."), + ], + ), + ); + }, + ); + } + + print( + "调用updateDeviceName,deviceId: $_deviceId, newDeviceName: $newDeviceName", + ); + // 调用Cubit修改方法 + await context.read().updateDeviceName( + _deviceId, + newDeviceName, ); - // 延迟返回上一页(让用户看到提示) - Future.delayed(const Duration(seconds: 1), () { - if (context.mounted) { - Navigator.pop(context); // 返回设备列表页 + // 主动关闭修改名称加载弹窗 + if (_updateLoadingContext != null && + Navigator.canPop(_updateLoadingContext!)) { + Navigator.pop(_updateLoadingContext!); + _updateLoadingContext = null; + } + // 主动重置更新状态 + setState(() => _isUpdatingName = false); + + // 兜底:强制将Cubit的isLoading置为false + if (context.mounted) { + context.read().emit( + context.read().state.copyWith( + isLoading: false, + operationType: DeviceOperationType.none, + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("修改异常:${e.toString()}"), + backgroundColor: Colors.red, + ), + ); + // 关闭修改名称加载弹窗 + if (_updateLoadingContext != null && + Navigator.canPop(_updateLoadingContext!)) { + Navigator.pop(_updateLoadingContext!); + _updateLoadingContext = null; } - }); - } else if (state.errorMessage?.isNotEmpty == true) { - // 解绑失败 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("解绑失败:${state.errorMessage ?? '未知错误'}"), - backgroundColor: Colors.red, - ), - ); + } + setState(() => _isUpdatingName = false); - // 关闭加载弹窗(如果存在) - if (Navigator.canPop(context)) { - Navigator.pop(context); + // 兜底:强制重置Cubit的loading状态 + if (context.mounted) { + context.read().emit( + context.read().state.copyWith( + isLoading: false, + operationType: DeviceOperationType.none, + ), + ); + } + } + } + + /// 处理解绑结果(保持不变) + void _handleUnbindResult(DevicesState state) { + // 强制关闭加载弹窗 + if (_loadingDialogContext != null && + Navigator.canPop(_loadingDialogContext!)) { + Navigator.pop(_loadingDialogContext!); + _loadingDialogContext = null; + } else if (!state.isLoading && + context.mounted && + Navigator.canPop(context)) { + Navigator.pop(context); + } + + if (state.errorMessage?.isEmpty == true && !state.isLoading) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("设备「$_deviceName」解绑成功"), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + + final username = + context.read().state.user?.username ?? ""; + context.read().fetchAllDevices(username); + } + } else if (state.errorMessage?.isNotEmpty == true && !state.isLoading) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("解绑失败:${state.errorMessage ?? '未知错误'}"), + backgroundColor: Colors.red, + ), + ); } } - final username = context.read().state.user?.username ?? ""; - context.read().fetchAllDevices(username); context.go(RoutePaths.home); } + + /// 处理修改名称结果(核心修改:放宽判断条件) + void _handleUpdateNameResult(DevicesState state) { + // 强制关闭修改名称加载弹窗(优先用专属上下文) + if (_updateLoadingContext != null && + Navigator.canPop(_updateLoadingContext!)) { + Navigator.pop(_updateLoadingContext!); + _updateLoadingContext = null; + } + // 兜底:兼容旧逻辑 + else if (context.mounted && Navigator.canPop(context)) { + // 不管loading状态,只要弹窗存在就关闭 + Navigator.pop(context); + } + + // 处理结果:放宽条件,只要无错误信息就认为成功(不管loading) + print( + "处理修改名称结果,operationType: ${state.operationType}, isLoading: ${state.isLoading}, errorMessage: ${state.errorMessage},state.errorMessage?.isEmpty: ${state.errorMessage?.isEmpty}", + ); + + // 核心修改:去掉!state.isLoading的判断 + if (state.errorMessage?.isEmpty == true) { + setState(() { + _deviceName = _nameController.text.trim(); + _isUpdatingName = false; // 双重保障重置状态 + }); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("设备名称已修改为:$_deviceName"), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + + // 关闭修改名称的底部抽屉(如果还在) + if (_editNameSheetContext != null && + Navigator.canPop(_editNameSheetContext!)) { + Navigator.pop(_editNameSheetContext!); + _editNameSheetContext = null; + } + + // 强制重置Cubit的loading状态 + context.read().emit( + state.copyWith( + isLoading: false, + operationType: DeviceOperationType.none, + ), + ); + } + } else if (state.errorMessage?.isNotEmpty == true) { + setState(() => _isUpdatingName = false); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("修改失败:${state.errorMessage ?? '未知错误'}"), + backgroundColor: Colors.red, + ), + ); + // 强制重置Cubit的loading状态 + context.read().emit( + state.copyWith( + isLoading: false, + operationType: DeviceOperationType.none, + ), + ); + } + } + } + + /// 复制到剪贴板(保持不变) + void _copyToClipboard(String text, String tip) { + Clipboard.setData(ClipboardData(text: text)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(tip), duration: const Duration(seconds: 1)), + ); + } + } }