优化ai userid和markdown
This commit is contained in:
@@ -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.
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('关闭'))],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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'] ?? '业务异常');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user