优化ai userid和markdown

This commit is contained in:
mmc
2026-03-17 10:54:33 +08:00
parent 14df0bc041
commit f952756f40
6 changed files with 215 additions and 227 deletions

View File

@@ -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<AiState> {
AiCubit() : super(const AiState()) {
final AppUserCubit appUserCubit;
AiCubit({required this.appUserCubit}) : super(const AiState()) {
createNewSession();
fetchSessions();
}
@@ -18,7 +21,8 @@ class AiCubit extends Cubit<AiState> {
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<void> fetchSessions() async {
try {
@@ -33,13 +37,15 @@ class AiCubit extends Cubit<AiState> {
}
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<void> loadSessionHistory(String sessionId) async {
@@ -61,7 +67,7 @@ class AiCubit extends Cubit<AiState> {
}
} catch (e) {
emit(state.copyWith(status: AiStatus.failure, error: "Failed to load session history: $e"));
}
}
}
Future<void> analyze({String? prompt, File? image}) async {
@@ -72,7 +78,7 @@ class AiCubit extends Cubit<AiState> {
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<AiState> {
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<AiState> {
}
final updatedMessages = List<ChatMessage>.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<AiState> {
fetchSessions();
},
onError: (error) {
final updatedMessages = List<ChatMessage>.from(state.messages);
updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false);
emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages));
final updatedMessages = List<ChatMessage>.from(state.messages);
updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false);
emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages));
},
);
} catch (e) {
final updatedMessages = List<ChatMessage>.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<ChatMessage>.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<AiState> {
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.
),
);
}
}

View File

@@ -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<AppUserCubit>(), // ✅ 这里传
),
child: const AiView(),
);
}
@@ -49,7 +53,7 @@ class _AiViewState extends State<AiView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AI智能体'),
title: const Text('新能源光伏智能体'),
actions: [
PopupMenuButton<String>(
onSelected: (value) {
@@ -103,19 +107,11 @@ class _AiViewState extends State<AiView> {
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<AiView> {
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<AiCubit>().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<AiView> {
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<AiView> {
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<AiView> {
children: <Widget>[
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Gallery'),
title: const Text('相册'),
onTap: () {
Navigator.of(context).pop();
context.read<AiCubit>().selectImage(ImageSource.gallery);
@@ -320,7 +380,7 @@ class _AiViewState extends State<AiView> {
),
ListTile(
leading: const Icon(Icons.photo_camera),
title: const Text('Camera'),
title: const Text('拍照'),
onTap: () {
Navigator.of(context).pop();
context.read<AiCubit>().selectImage(ImageSource.camera);
@@ -333,7 +393,6 @@ class _AiViewState extends State<AiView> {
);
}
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<AiView> {
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('History'),
title: const Text('历史记录'),
content: SizedBox(
width: double.maxFinite,
child: BlocBuilder<AiCubit, AiState>(
@@ -371,9 +430,7 @@ class _AiViewState extends State<AiView> {
},
),
),
actions: [
TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close')),
],
actions: [TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('关闭'))],
);
},
);

View File

@@ -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<int> bindDevice(String deviceId, String deviceAlias) async {
final logger = GetIt.I<ILoggerService>() 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<AuthCubit>().logout();
throw Exception('网络请求失败:${response.statusCode}');
}
final responseData = response.data;
if (responseData['code'] != 200) {
await sl<AuthCubit>().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'] ?? '业务异常');

View File

@@ -13,15 +13,9 @@ class DeviceRepositoryImpl implements DeviceRepository {
DeviceRepositoryImpl(this._deviceHttpDatasource);
@override
Future<Either<DeviceFailure, int>> bindDevice(
String deviceId,
String deviceAlias,
) async {
Future<Either<DeviceFailure, int>> 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<Either<DeviceFailure, List<DeviceEntity>>> getUserDevice(
String username,
) async {
Future<Either<DeviceFailure, List<DeviceEntity>>> 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<Either<DeviceFailure, int>> updateDeviceName(String deviceId, String deviceName) async {
try {
@@ -76,10 +69,7 @@ class DeviceRepositoryImpl implements DeviceRepository {
}
@override
Future<Either<DeviceFailure, int>> switchDevice(
String platform,
String deviceId,
) async {
Future<Either<DeviceFailure, int>> 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<Either<DeviceFailure, DeviceLocationEntity>>getDeviceLocation(String username) async {
Future<Either<DeviceFailure, DeviceLocationEntity>> getDeviceLocation(String username) async {
// TODO: implement getDeviceLocation
var result = await _deviceHttpDatasource.getDeviceLocation(username);
return Right(result);

View File

@@ -57,8 +57,9 @@ class DevicesCubit extends Cubit<DevicesState> {
this._generatePathUseCase,
this._routePlanningUseCase,
this._bindDeviceUseCase,
this._deviceStatusBloc,
this._tcpClient, this._pathPlanningService,
this._deviceStatusBloc,
this._tcpClient,
this._pathPlanningService,
) : super(const DevicesState());
Future<void> unbindDevice(String deviceId, String deviceName) async {
@@ -174,7 +175,6 @@ class DevicesCubit extends Cubit<DevicesState> {
isLoading: false,
),
);
});
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
@@ -207,55 +207,51 @@ class DevicesCubit extends Cubit<DevicesState> {
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<void> 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<DevicesState> {
// 开始路径规划
emit(state.copyWith(isLoading: true));
//传入全局的Service 中路径规划队列
Queue<DeviceAddPathPointModel> queue = _pathPlanningService.getQueue() ;
Queue<DeviceAddPathPointModel> queue = _pathPlanningService.getQueue();
print('📦 从 Service 获取的队列长度:${queue.length}');
final result = await _routePlanningUseCase.startRoutePlanning(queue);
result.fold(
@@ -372,7 +368,6 @@ class DevicesCubit extends Cubit<DevicesState> {
);
}
// 暂停
Future<void> pauseRoutePlanning() async {
emit(state.copyWith(isLoading: true));
@@ -409,6 +404,7 @@ class DevicesCubit extends Cubit<DevicesState> {
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');

View File

@@ -2362,7 +2362,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
_saveDataToLocal();
},
onDjShow: (bool isOpen) {
if (isOpen) context.go(RoutePaths.djMap);
if (isOpen) context.push(RoutePaths.djMap);
setState(() {
isDjMapShow = isOpen;
});