From f952756f4056ddd1c84e8aadb7bd241f566f33e7 Mon Sep 17 00:00:00 2001 From: mmc <1556375442@qq.com> Date: Tue, 17 Mar 2026 10:54:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ai=20=20userid=E5=92=8Cmarkdo?= =?UTF-8?q?wn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/presentation/bloc/ai_cubit.dart | 74 ++++---- .../ai/presentation/pages/ai_page.dart | 179 ++++++++++++------ .../impl/device_http_datasource_impl.dart | 98 +++------- .../repositories/device_repository_impl.dart | 25 +-- .../presentation/bloc/devices_cubit.dart | 64 +++---- .../widgets/map/testmap_pages.dart | 2 +- 6 files changed, 215 insertions(+), 227 deletions(-) diff --git a/lib/features/ai/presentation/bloc/ai_cubit.dart b/lib/features/ai/presentation/bloc/ai_cubit.dart index 5fb7e71f..2ad63ac8 100644 --- a/lib/features/ai/presentation/bloc/ai_cubit.dart +++ b/lib/features/ai/presentation/bloc/ai_cubit.dart @@ -4,13 +4,16 @@ import 'dart:io'; import 'package:bloc/bloc.dart'; import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'ai_state.dart'; import '../../data/models/chat_message.dart'; import '../../data/models/session.dart'; class AiCubit extends Cubit { - AiCubit() : super(const AiState()) { + final AppUserCubit appUserCubit; + + AiCubit({required this.appUserCubit}) : super(const AiState()) { createNewSession(); fetchSessions(); } @@ -18,7 +21,8 @@ class AiCubit extends Cubit { final Dio _dio = Dio(); final ImagePicker _picker = ImagePicker(); static const String BASE_URL = 'http://1.95.137.212:30009'; - final String _userId = '1'; + String get _userId => appUserCubit.state.user?.userId ?? '1'; + //final String _userId = '1'; Future fetchSessions() async { try { @@ -33,13 +37,15 @@ class AiCubit extends Cubit { } void createNewSession() { - emit(AiState( - sessionId: DateTime.now().millisecondsSinceEpoch.toString(), - messages: [ChatMessage(text: "快来和我一起探讨吧!", isUserMessage: false)], - status: AiStatus.initial, - selectedImageFile: null, // Explicitly set to null - sessions: state.sessions, // Preserve existing sessions - )); + emit( + AiState( + sessionId: DateTime.now().millisecondsSinceEpoch.toString(), + messages: [ChatMessage(text: "快来和我一起探讨吧!", isUserMessage: false)], + status: AiStatus.initial, + selectedImageFile: null, // Explicitly set to null + sessions: state.sessions, // Preserve existing sessions + ), + ); } Future loadSessionHistory(String sessionId) async { @@ -61,7 +67,7 @@ class AiCubit extends Cubit { } } catch (e) { emit(state.copyWith(status: AiStatus.failure, error: "Failed to load session history: $e")); - } + } } Future analyze({String? prompt, File? image}) async { @@ -72,7 +78,7 @@ class AiCubit extends Cubit { final bytes = await image.readAsBytes(); imageBase64 = 'data:image/png;base64,${base64Encode(bytes)}'; } - + final userMessage = ChatMessage(text: prompt, image: image, isUserMessage: true); final aiPlaceholder = ChatMessage(text: '', thinkingText: '', isUserMessage: false); @@ -90,12 +96,7 @@ class AiCubit extends Cubit { try { final response = await _dio.post( '$BASE_URL/analyze', - data: { - 'userId': _userId, - 'sessionId': state.sessionId, - 'userPrompt': prompt, - 'imageBase64': imageBase64, - }, + data: {'userId': _userId, 'sessionId': state.sessionId, 'userPrompt': prompt, 'imageBase64': imageBase64}, options: Options(responseType: ResponseType.stream), ); @@ -126,13 +127,8 @@ class AiCubit extends Cubit { } final updatedMessages = List.from(state.messages); - updatedMessages[0] = ChatMessage( - thinkingText: thinkingBuffer.toString(), - text: answerBuffer.toString(), - isUserMessage: false, - ); + updatedMessages[0] = ChatMessage(thinkingText: thinkingBuffer.toString(), text: answerBuffer.toString(), isUserMessage: false); emit(state.copyWith(messages: updatedMessages)); - } catch (e) { print("Error parsing stream data: $e"); } @@ -144,18 +140,18 @@ class AiCubit extends Cubit { fetchSessions(); }, onError: (error) { - final updatedMessages = List.from(state.messages); - updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false); - emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages)); + final updatedMessages = List.from(state.messages); + updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false); + emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages)); }, ); } catch (e) { - final updatedMessages = List.from(state.messages); - updatedMessages[0] = ChatMessage(text: "Failed to connect to the server: $e", isUserMessage: false); - emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages)); + final updatedMessages = List.from(state.messages); + updatedMessages[0] = ChatMessage(text: "Failed to connect to the server: $e", isUserMessage: false); + emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages)); } } - + void selectImage(ImageSource source) async { final pickedFile = await _picker.pickImage(source: source); if (pickedFile != null) { @@ -166,13 +162,15 @@ class AiCubit extends Cubit { void deselectImage() { // Manually create a new state to ensure selectedImageFile is set to null, // bypassing the limitation of the existing copyWith method. - emit(AiState( - status: state.status, - messages: state.messages, - sessions: state.sessions, - sessionId: state.sessionId, - error: state.error, - selectedImageFile: null, // This will now correctly clear the file. - )); + emit( + AiState( + status: state.status, + messages: state.messages, + sessions: state.sessions, + sessionId: state.sessionId, + error: state.error, + selectedImageFile: null, // This will now correctly clear the file. + ), + ); } } diff --git a/lib/features/ai/presentation/pages/ai_page.dart b/lib/features/ai/presentation/pages/ai_page.dart index a4fcdfa2..bc61dc8b 100644 --- a/lib/features/ai/presentation/pages/ai_page.dart +++ b/lib/features/ai/presentation/pages/ai_page.dart @@ -1,12 +1,13 @@ - import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import '../bloc/ai_cubit.dart'; import '../bloc/ai_state.dart'; @@ -21,7 +22,10 @@ class AiPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => AiCubit(), + create: (context) => AiCubit( + appUserCubit: context.read(), // ✅ 这里传 + ), + child: const AiView(), ); } @@ -49,7 +53,7 @@ class _AiViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('AI智能体'), + title: const Text('新能源光伏智能体'), actions: [ PopupMenuButton( onSelected: (value) { @@ -103,19 +107,11 @@ class _AiViewState extends State { mainAxisAlignment: isUserMessage ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!isUserMessage) - const CircleAvatar( - backgroundImage: AssetImage('assets/images/app_logo_black.png'), - radius: 16, - ), + if (!isUserMessage) const CircleAvatar(backgroundImage: AssetImage('assets/images/app_logo_black.png'), radius: 16), const SizedBox(width: 10), - Flexible( - child: isUserMessage ? _buildUserMessageContent(context, message) : _buildAiMessageContent(context, message), - ), - if (isUserMessage) - const SizedBox(width: 10), // <-- This was the missing part - if (isUserMessage) - const CircleAvatar(child: Icon(Icons.person, size: 20), radius: 16), + Flexible(child: isUserMessage ? _buildUserMessageContent(context, message) : _buildAiMessageContent(context, message)), + if (isUserMessage) const SizedBox(width: 10), // <-- This was the missing part + if (isUserMessage) const CircleAvatar(child: Icon(Icons.person, size: 20), radius: 16), ], ), ); @@ -123,44 +119,45 @@ class _AiViewState extends State { Widget _buildUserMessageContent(BuildContext context, ChatMessage message) { return Container( - padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0), - decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(18)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (message.image != null) - GestureDetector( - onTap: () => _showImagePreview(context, imageFile: message.image!), - child: ClipRRect( - borderRadius: BorderRadius.circular(12.0), - child: Image.file(message.image!, width: 200, height: 200, fit: BoxFit.cover), - ), - ) - else if (message.imageBase64 != null) - ClipRRect( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0), + decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(18)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.image != null) + GestureDetector( + onTap: () => _showImagePreview(context, imageFile: message.image!), + child: ClipRRect( borderRadius: BorderRadius.circular(12.0), - child: Image.memory(base64Decode(message.imageBase64!.split(',').last), width: 200, height: 200, fit: BoxFit.cover), + child: Image.file(message.image!, width: 200, height: 200, fit: BoxFit.cover), ), - if (message.text != null && message.text!.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text(message.text!, style: const TextStyle(color: Colors.white, fontSize: 16)), - ), - ], - )); + ) + else if (message.imageBase64 != null) + ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Image.memory(base64Decode(message.imageBase64!.split(',').last), width: 200, height: 200, fit: BoxFit.cover), + ), + if (message.text != null && message.text!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text(message.text!, style: const TextStyle(color: Colors.white, fontSize: 16)), + ), + ], + ), + ); } Widget _buildAiMessageContent(BuildContext context, ChatMessage message) { final state = context.watch().state; final hasThinking = message.thinkingText != null && message.thinkingText!.isNotEmpty; final hasAnswer = message.text != null && message.text!.isNotEmpty; - final showThinkingBlock = state.status == AiStatus.loading || hasThinking; + final showThinkingBlock = state.status == AiStatus.loading || !hasAnswer; if (!showThinkingBlock && !hasAnswer) return const SizedBox.shrink(); return Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: const Color(0xFFF5F7F9), // 更柔和的背景 borderRadius: BorderRadius.circular(18), ), child: Column( @@ -175,18 +172,76 @@ class _AiViewState extends State { iconColor: Colors.blue.shade700, showSpinner: state.status == AiStatus.loading && !hasThinking, ), + if (showThinkingBlock && hasAnswer) Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Divider(height: 1, color: Colors.grey.shade300), ), + + // ========== 最终结论(带美化 Markdown)========== if (hasAnswer) - _buildSection( - context, - icon: Icons.lightbulb_outline, - title: "最终结论", - content: message.text!, - iconColor: Colors.green.shade700, + Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.lightbulb_outline, size: 20, color: Colors.green.shade700), + const SizedBox(width: 8), + const Text( + "最终结论", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF2D7D46)), + ), + ], + ), + const SizedBox(height: 10), + + // ========== Markdown 渲染(超好看样式)========== + Markdown( + data: message.text!, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + styleSheet: MarkdownStyleSheet( + // 正文 + p: const TextStyle(fontSize: 15, height: 1.6, color: Color(0xFF262626)), + + // 标题 + h1: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, height: 1.5, color: Color(0xFF111111)), + h2: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold, height: 1.5, color: Color(0xFF111111)), + h3: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, height: 1.5, color: Color(0xFF111111)), + + // 加粗 + strong: const TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF000000)), + + // 斜体 + em: const TextStyle(fontStyle: FontStyle.italic, color: Color(0xFF444444)), + + // 列表 + listBullet: const TextStyle(fontSize: 15, height: 1.6, color: Color(0xFF262626)), + + // 引用 + blockquote: const TextStyle(fontSize: 15, color: Color(0xFF555555), height: 1.6), + blockquoteDecoration: BoxDecoration( + border: Border(left: BorderSide(color: Colors.green.shade300, width: 4)), + ), + + // 代码行 + code: TextStyle(fontSize: 14, color: const Color(0xFFD32F2F), backgroundColor: Colors.grey.shade200, height: 1.6), + + // 代码块 + codeblockDecoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), + codeblockPadding: const EdgeInsets.all(10), + + // 表格 + tableBorder: TableBorder.all(color: Colors.grey.shade300, width: 1), + tableHead: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + tableBody: const TextStyle(fontSize: 14, height: 1.5), + ), + ), + ], + ), ), ], ), @@ -199,15 +254,20 @@ class _AiViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Icon(icon, size: 20, color: iconColor), - const SizedBox(width: 8), - Text(title, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: iconColor)), - if (showSpinner) ...[ + Row( + children: [ + Icon(icon, size: 20, color: iconColor), const SizedBox(width: 8), - SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2.0, color: iconColor)), - ] - ]), + Text( + title, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: iconColor), + ), + if (showSpinner) ...[ + const SizedBox(width: 8), + SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2.0, color: iconColor)), + ], + ], + ), if (content != null && content.isNotEmpty) ...[ const SizedBox(height: 8), Text(content, style: const TextStyle(fontSize: 16, color: Colors.black87, height: 1.4)), @@ -312,7 +372,7 @@ class _AiViewState extends State { children: [ ListTile( leading: const Icon(Icons.photo_library), - title: const Text('Gallery'), + title: const Text('相册'), onTap: () { Navigator.of(context).pop(); context.read().selectImage(ImageSource.gallery); @@ -320,7 +380,7 @@ class _AiViewState extends State { ), ListTile( leading: const Icon(Icons.photo_camera), - title: const Text('Camera'), + title: const Text('拍照'), onTap: () { Navigator.of(context).pop(); context.read().selectImage(ImageSource.camera); @@ -333,7 +393,6 @@ class _AiViewState extends State { ); } - void _showImagePreview(BuildContext context, {File? imageFile, String? imageBase64}) { if (imageFile == null && imageBase64 == null) return; Navigator.of(context).push( @@ -349,7 +408,7 @@ class _AiViewState extends State { context: context, builder: (BuildContext dialogContext) { return AlertDialog( - title: const Text('History'), + title: const Text('历史记录'), content: SizedBox( width: double.maxFinite, child: BlocBuilder( @@ -371,9 +430,7 @@ class _AiViewState extends State { }, ), ), - actions: [ - TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close')), - ], + actions: [TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('关闭'))], ); }, ); 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 97615d5a..7cc0f066 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 @@ -2,6 +2,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart'; +import 'package:maibu_satabot_v2/core/di/injection.dart'; +import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart'; import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart'; @@ -28,41 +30,22 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { Future bindDevice(String deviceId, String deviceAlias) async { final logger = GetIt.I() as SentryLoggerImpl; logger.logWithLevel('用户点击bindDevice方法开始触发', level: 'INFO'); + logger.logWithLevel('用户点击bindDevice方法API 请求开始', level: 'INFO', data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'}); + var response = await dio.post(HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias}); logger.logWithLevel( - '用户点击bindDevice方法API 请求开始', - level: 'INFO', - data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'} - ); - var response = await dio.post( - HttpApiConsts.bindDevice, - data: {'deviceId': deviceId, 'deviceAlias': deviceAlias}, - ); - logger.logWithLevel( - '请求详细参数', - level: 'DEBUG', - data: { - 'url': 'https://serviceri.satabot.com/iot/device/bindDevice', - 'deviceId': deviceId, - 'deviceAlias': deviceAlias - } + '请求详细参数', + level: 'DEBUG', + data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice', 'deviceId': deviceId, 'deviceAlias': deviceAlias}, ); if (response.statusCode != 200) { - logger.logWithLevel( - 'API 响应失败', - level: 'ERROR', - data: {'statusCode': response.statusCode, 'data': response.data} - ); + logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data}); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; if (responseData['code'] != 200 || responseData['data'] != true) { - logger.logWithLevel( - 'API 响应失败', - level: 'ERROR', - data: {'statusCode': response.statusCode, 'data': response.data} - ); + logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data}); throw Exception(responseData['msg'] ?? '业务异常'); } @@ -85,12 +68,14 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { ), ); if (response.statusCode != 200) { + await sl().logout(); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; if (responseData['code'] != 200) { + await sl().logout(); throw Exception(responseData['msg'] ?? '业务异常'); } @@ -111,11 +96,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { final token = await _getToken(); print('用户点击switchDevice方法开始触发: $token'); logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG'); - logger.logWithLevel( - '用户点击switchDevice方法API 请求开始', - level: 'INFO', - data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'} - ); + logger.logWithLevel('用户点击switchDevice方法API 请求开始', level: 'INFO', data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'}); var response = await dio.post( HttpApiConsts.switchDevice, data: {'platform': platform, 'deviceId': deviceId}, @@ -127,35 +108,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { ), ); logger.logWithLevel( - '请求详细参数', - level: 'DEBUG', - data: { - 'url': 'https://serviceri.satabot.com/iot/device/switchDevice', - 'platform': platform, - 'deviceId': deviceId - } + '请求详细参数', + level: 'DEBUG', + data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice', 'platform': platform, 'deviceId': deviceId}, ); if (response.statusCode != 200) { - logger.logWithLevel( - 'API 请求失败', - level: 'ERROR', - data: {'statusCode': response.statusCode, 'data': response.data} - ); + logger.logWithLevel('API 请求失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data}); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; - logger.logWithLevel( - 'API 响应成功', - level: 'INFO', - data: {'statusCode': response.statusCode, 'data': response.data} - ); + logger.logWithLevel('API 响应成功', level: 'INFO', data: {'statusCode': response.statusCode, 'data': response.data}); if (responseData['code'] != 200 || responseData['data'] != true) { - logger.logWithLevel( - 'API 响应失败', - level: 'ERROR', - data: {'statusCode': response.statusCode, 'data': response.data} - ); + logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data}); throw Exception(responseData['msg'] ?? '业务异常'); } return 1; @@ -201,14 +166,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { try { // 发起 GET 请求 logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO'); - logger.logWithLevel( - '请求详细参数', - level: 'DEBUG', - data: { - 'url': 'https://serviceri.satabot.com/iot/device/userDevice', - 'tenantName': deviceame - } - ); + logger.logWithLevel('请求详细参数', level: 'DEBUG', data: {'url': 'https://serviceri.satabot.com/iot/device/userDevice', 'tenantName': deviceame}); final response = await dio.get( 'https://serviceri.satabot.com/iot/device/userDevice', queryParameters: {'tenantName': deviceame}, @@ -223,24 +181,12 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { // 检查响应状态码 if (response.statusCode == 200) { final data = response.data; - logger.logWithLevel( - 'API 响应接收', - level: 'DEBUG', - data: {'statusCode': response.statusCode, 'data': response.data} - ); + logger.logWithLevel('API 响应接收', level: 'DEBUG', data: {'statusCode': response.statusCode, 'data': response.data}); if (data['code'] == 200) { //return data['data']; // 返回设备数据 final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化 - logger.logWithLevel( - '成功获取设备位置', - level: 'INFO', - data: {'lat': locationData['latitude'], 'lng': locationData['longitude']} - ); - return DeviceLocationEntity( - deviceName: locationData['deviceName'], - latitude: locationData['latitude'], - longitude: locationData['longitude'], - ); + logger.logWithLevel('成功获取设备位置', level: 'INFO', data: {'lat': locationData['latitude'], 'lng': locationData['longitude']}); + return DeviceLocationEntity(deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude']); } else { logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR'); throw Exception(data['msg'] ?? '业务异常'); diff --git a/lib/features/devices/data/repositories/device_repository_impl.dart b/lib/features/devices/data/repositories/device_repository_impl.dart index 731a3c17..fa52ab6d 100644 --- a/lib/features/devices/data/repositories/device_repository_impl.dart +++ b/lib/features/devices/data/repositories/device_repository_impl.dart @@ -13,15 +13,9 @@ class DeviceRepositoryImpl implements DeviceRepository { DeviceRepositoryImpl(this._deviceHttpDatasource); @override - Future> bindDevice( - String deviceId, - String deviceAlias, - ) async { + Future> bindDevice(String deviceId, String deviceAlias) async { try { - var result = await _deviceHttpDatasource.bindDevice( - deviceId, - deviceAlias, - ); + var result = await _deviceHttpDatasource.bindDevice(deviceId, deviceAlias); return Right(result); } on DioException catch (e) { final String serverMessage = e.response?.data['msg'] ?? "网络连接异常"; @@ -33,9 +27,7 @@ class DeviceRepositoryImpl implements DeviceRepository { } @override - Future>> getUserDevice( - String username, - ) async { + Future>> getUserDevice(String username) async { try { var result = await _deviceHttpDatasource.getUserDevices(username); return Right(result); @@ -61,6 +53,7 @@ class DeviceRepositoryImpl implements DeviceRepository { return Left(DeviceFailure(cleanMessage)); } } + @override Future> updateDeviceName(String deviceId, String deviceName) async { try { @@ -76,10 +69,7 @@ class DeviceRepositoryImpl implements DeviceRepository { } @override - Future> switchDevice( - String platform, - String deviceId, - ) async { + Future> switchDevice(String platform, String deviceId) async { try { var result = await _deviceHttpDatasource.switchDevice(platform, deviceId); return Right(result); @@ -91,9 +81,10 @@ class DeviceRepositoryImpl implements DeviceRepository { return Left(DeviceFailure(cleanMessage)); } } - // TODO: implement switchDevice 实现获取设备的列表 + + // TODO: implement switchDevice 实现获取设备的列表 @override - Future>getDeviceLocation(String username) async { + Future> getDeviceLocation(String username) async { // TODO: implement getDeviceLocation var result = await _deviceHttpDatasource.getDeviceLocation(username); return Right(result); diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index a1ffe822..0ab17ef1 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -57,8 +57,9 @@ class DevicesCubit extends Cubit { this._generatePathUseCase, this._routePlanningUseCase, this._bindDeviceUseCase, - this._deviceStatusBloc, - this._tcpClient, this._pathPlanningService, + this._deviceStatusBloc, + this._tcpClient, + this._pathPlanningService, ) : super(const DevicesState()); Future unbindDevice(String deviceId, String deviceName) async { @@ -174,7 +175,6 @@ class DevicesCubit extends Cubit { isLoading: false, ), ); - }); } catch (e) { emit(state.copyWith(isLoading: false, errorMessage: e.toString())); @@ -207,55 +207,51 @@ class DevicesCubit extends Cubit { result.fold( // 失败处理 - (l) { + (l) { emit(state.copyWith(isLoading: false, errorMessage: l.message)); selectDevice(device); }, // 成功处理:改为 async 函数块 - (r) async { + (r) async { // 3. 最后更新 UI 状态 - try { - // 🔥 关键修复 1:先强制断开旧连接! - // 这一步会销毁旧 Socket,清除旧 Listener,防止旧数据继续推送 - if (_tcpClient.isConnected) { - debugPrint('🛑 检测到已连接,先断开旧 TCP 连接...'); - _tcpClient.disconnects(forSwitch: true); - // 稍微等待一下,确保底层 Socket 资源释放 (可选,但推荐) - // await Future.delayed(const Duration(milliseconds: 100)); - } + try { + // 🔥 关键修复 1:先强制断开旧连接! + // 这一步会销毁旧 Socket,清除旧 Listener,防止旧数据继续推送 + if (_tcpClient.isConnected) { + debugPrint('🛑 检测到已连接,先断开旧 TCP 连接...'); + _tcpClient.disconnects(forSwitch: true); + // 稍微等待一下,确保底层 Socket 资源释放 (可选,但推荐) + // await Future.delayed(const Duration(milliseconds: 100)); + } - // 🔥 关键修复 2:发起新连接 - // connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备 - debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}'); - await _tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: device.deviceName); - // 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据 - _deviceStatusBloc.add(DeviceStatusReset()); + // 🔥 关键修复 2:发起新连接 + // connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备 + debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}'); + await _tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: device.deviceName); + // 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据 + _deviceStatusBloc.add(DeviceStatusReset()); - debugPrint('✅ 新设备切换流程完成'); - } catch (e) { - debugPrint('⚠️ 设备切换成功,但 TCP 重连或状态重置失败:$e'); - // 即使 TCP 失败,也更新 UI 选中状态,让用户知道切换了,只是没数据 - } + debugPrint('✅ 新设备切换流程完成'); + } catch (e) { + debugPrint('⚠️ 设备切换成功,但 TCP 重连或状态重置失败:$e'); + // 即使 TCP 失败,也更新 UI 选中状态,让用户知道切换了,只是没数据 + } - emit(state.copyWith(isLoading: false, selectedDevice: device)); + emit(state.copyWith(isLoading: false, selectedDevice: device)); }, ); } /// 绑定设备 Future bindDevice(String deviceId, String deviceAlias) async { - emit(state.copyWith(isLoading: true, errorMessage: '')); try { final params = BindDeviceParams(deviceId, deviceAlias); final result = await _bindDeviceUseCase.call(params); result.fold( // 失败处理 - (failure) { - emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '绑定设备失败', - )); + (failure) { + emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '绑定设备失败')); // 抛出异常,携带后端返回的错误消息(如“设备不存在”) throw Exception(failure.message ?? '绑定设备失败'); }, @@ -363,7 +359,7 @@ class DevicesCubit extends Cubit { // 开始路径规划 emit(state.copyWith(isLoading: true)); //传入全局的Service 中路径规划队列 - Queue queue = _pathPlanningService.getQueue() ; + Queue queue = _pathPlanningService.getQueue(); print('📦 从 Service 获取的队列长度:${queue.length}'); final result = await _routePlanningUseCase.startRoutePlanning(queue); result.fold( @@ -372,7 +368,6 @@ class DevicesCubit extends Cubit { ); } - // 暂停 Future pauseRoutePlanning() async { emit(state.copyWith(isLoading: true)); @@ -409,6 +404,7 @@ class DevicesCubit extends Cubit { void resetWorkStatus() { emit(state.copyWith(isFinshWork: false)); } + void setArrivedLocation(double latitude, double longitude) { emit(state.copyWith(arriLatitude: latitude, arriLongitude: longitude)); print('✅ [DevicesCubit] 监听更新已完成到达位置:Lat=$latitude, Lng=$longitude'); diff --git a/lib/features/home/presentation/widgets/map/testmap_pages.dart b/lib/features/home/presentation/widgets/map/testmap_pages.dart index 1f6796c6..11feb47d 100644 --- a/lib/features/home/presentation/widgets/map/testmap_pages.dart +++ b/lib/features/home/presentation/widgets/map/testmap_pages.dart @@ -2362,7 +2362,7 @@ class _MapPageEnterpriseState extends State { _saveDataToLocal(); }, onDjShow: (bool isOpen) { - if (isOpen) context.go(RoutePaths.djMap); + if (isOpen) context.push(RoutePaths.djMap); setState(() { isDjMapShow = isOpen; });