From 9dc445eeb5c8d6c918a787168a12c775ddb337e6 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Mon, 10 Aug 2026 10:00:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96+=E6=91=84=E5=83=8F=E5=A4=B4+?= =?UTF-8?q?=E6=89=AB=E7=A0=81=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _fix_dup.py | 61 ++++ _fix_flow.py | 92 ++++++ _fix_impl.py | 51 +++ _fix_json_data.py | 43 +++ _fix_overflow.py | 43 +++ _fix_pause_stop.py | 74 +++++ _fix_startwork.py | 77 +++++ _fix_taskid.py | 75 +++++ _fix_taskid2.py | 99 ++++++ _fix_taskid3.py | 83 +++++ _patch_save.py | 81 +++++ fix_dup.py | 61 ++++ flutter_01.log | 107 +++++++ flutter_02.log | 106 +++++++ flutter_03.log | 107 +++++++ flutter_04.log | 106 +++++++ flutter_05.log | 106 +++++++ flutter_06.log | 106 +++++++ flutter_07.log | 106 +++++++ flutter_08.log | 106 +++++++ lib/core/app/app_user_cubit.dart | 4 +- lib/core/network/tcp/tcp_client.dart | 28 +- .../auth/presentation/bloc/auth_cubit.dart | 86 ++++- .../widgets/webrtc/webrtc_local_player.dart | 300 +++++++++++++++--- .../datasources/bind_device_datasource.dart | 4 +- .../datasources/device_remote_datasource.dart | 4 +- .../impl/bind_device_datasource_impl.dart | 15 +- .../data/models/robot_data_model.dart | 33 +- .../bind_device_repository_impl.dart | 4 +- .../repositories/bind_device_repository.dart | 4 +- .../domain/usecases/bind_device_usecases.dart | 4 +- .../presentation/cubit/bind_device_cubit.dart | 197 +++++++++--- .../presentation/cubit/bind_device_state.dart | 7 +- .../presentation/pages/bind_device_page.dart | 130 +++++--- .../pages/device_status_page.dart | 19 +- .../presentation/pages/robot_list_page.dart | 20 +- .../presentation/widgets/drone_osd_card.dart | 213 ++++--------- .../widgets/robot_header_card.dart | 244 ++++++++++---- .../presentation/widgets/robot_item_card.dart | 10 +- .../presentation/pages/alarm_center_page.dart | 20 ++ .../presentation/pages/workorder_page.dart | 20 ++ 41 files changed, 2618 insertions(+), 438 deletions(-) create mode 100644 _fix_dup.py create mode 100644 _fix_flow.py create mode 100644 _fix_impl.py create mode 100644 _fix_json_data.py create mode 100644 _fix_overflow.py create mode 100644 _fix_pause_stop.py create mode 100644 _fix_startwork.py create mode 100644 _fix_taskid.py create mode 100644 _fix_taskid2.py create mode 100644 _fix_taskid3.py create mode 100644 _patch_save.py create mode 100644 fix_dup.py create mode 100644 flutter_01.log create mode 100644 flutter_02.log create mode 100644 flutter_03.log create mode 100644 flutter_04.log create mode 100644 flutter_05.log create mode 100644 flutter_06.log create mode 100644 flutter_07.log create mode 100644 flutter_08.log diff --git a/_fix_dup.py b/_fix_dup.py new file mode 100644 index 00000000..159790c5 --- /dev/null +++ b/_fix_dup.py @@ -0,0 +1,61 @@ +import sys + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + +# 删除重复的 failMsg 声明和重复的检查逻辑 (行2402-2411,0索引为2401-2410) +# 但行号可能因之前的编辑有偏移,用内容查找 +del_start = None +del_end = None +for i, line in enumerate(lines): + if 'debugPrint' in line and '创建设备任务失败' in line and 'failure' in line: + # 检查下一行是否是重复的 final failMsg + if i+1 < len(lines) and 'final failMsg = failure.toString();' in lines[i+1]: + # 找到了重复的起点 + del_start = i # debugPrint 行 + # 找到这个重复块的结束:下一个 return; 之后的 }, + for j in range(i+2, len(lines)): + if '// 其他失败' in lines[j] or (lines[j].strip() == 'return;' and 'taskCubit.clearCurrentTask();' in lines[j-1]): + # 更精确:找到 " return;" 且前一行是 taskCubit.clearCurrentTask + if lines[j].strip() == 'return;': + del_end = j + 1 # 包含 return; 行 + break + if del_end is None: + # 备选:找到下一个 ' },' (fold 成功回调的开始) + for j in range(i+2, len(lines)): + if lines[j].strip().startswith('},') and 'taskId' in lines[j+1]: + del_end = j + break + break + +if del_start is not None and del_end is not None: + print(f'Deleting lines {del_start+1} to {del_end}') + for k in range(del_start, del_end): + print(f' DEL: L{k+1}: {lines[k].rstrip()}') + del lines[del_start:del_end] +else: + print('Could not find duplicate block, checking for alternate pattern...') + # 备选:直接搜索两个 final failMsg 行 + failmsg_lines = [] + for i, line in enumerate(lines): + if 'final failMsg = failure.toString();' in line: + failmsg_lines.append(i) + print(f'failMsg lines: {[l+1 for l in failmsg_lines]}') + if len(failmsg_lines) >= 2: + # 删除第二个 failMsg 及它所属的整个重复检查块 + second = failmsg_lines[1] + # 从第二个 failMsg 前一行 (debugPrint) 开始删 + start = second - 1 + # 找到这个 block 的结束 + end = second + 8 # 大约8行后是这个重复块的结束 + print(f'Alt: deleting lines {start+1} to {end}') + for k in range(start, min(end, len(lines))): + print(f' DEL: L{k+1}: {lines[k].rstrip()}') + del lines[start:end] + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.writelines(lines) + +print('DONE') diff --git a/_fix_flow.py b/_fix_flow.py new file mode 100644 index 00000000..584c6de6 --- /dev/null +++ b/_fix_flow.py @@ -0,0 +1,92 @@ +import re + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + content = f.read() + +# Fix 1: Add var failed = false and if(failed) return, plus else branch for needRequery +old_block = """ var needRequery = false; + result.fold( + (failure) { + final failMsg = failure.toString(); + if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + needRequery = true; + return; + } + _showPageToast(message: '作业启动失败', type: ToastType.error); + taskCubit.clearCurrentTask(); + return; + }, + (taskId) { + taskCubit.updateCurrentTaskId(taskId); + }, + ); + + if (needRequery) { + await taskCubit.fetchAndFilterTask(deviceId); + await Future.delayed(const Duration(milliseconds: 300)); + final existingTasks = taskCubit.state.activeTasks; + if (existingTasks.isNotEmpty) { + taskCubit.selectTask(existingTasks.first); + } + return; + }""" + +new_block = """ var needRequery = false; + var failed = false; + result.fold( + (failure) { + final failMsg = failure.toString(); + if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + needRequery = true; + return; + } + failed = true; + debugPrint('[开始作业] 创建失败: $failMsg'); + _showPageToast(message: '作业启动失败', type: ToastType.error); + taskCubit.clearCurrentTask(); + return; + }, + (taskId) { + taskCubit.updateCurrentTaskId(taskId); + }, + ); + + if (failed) return; + if (needRequery) { + await taskCubit.fetchAndFilterTask(deviceId); + await Future.delayed(const Duration(milliseconds: 300)); + final existingTasks = taskCubit.state.activeTasks; + if (existingTasks.isNotEmpty) { + taskCubit.selectTask(existingTasks.first); + } else { + debugPrint('[开始作业] needRequery 后仍无活跃任务'); + _showPageToast(message: '未找到活跃任务', type: ToastType.warn); + } + return; + }""" + +if old_block in content: + content = content.replace(old_block, new_block) + print("Block replaced successfully") +else: + print("Block NOT FOUND in file!") + # Try tab indentation + old_block_tabs = old_block.replace(' ', '\t\t\t\t') + if old_block_tabs in content: + content = content.replace(old_block_tabs, new_block.replace(' ', '\t\t\t\t')) + print("Block replaced with TABS") + else: + # Find the unique line + for i, line in enumerate(content.split('\n')): + if 'var needRequery = false;' in line and 'result.fold(' in content.split('\n')[i+1]: + print(f"Found at line {i+1}: {repr(line)}") + break + else: + print("Could not find needRequery line") + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.write(content) + +print("Done") diff --git a/_fix_impl.py b/_fix_impl.py new file mode 100644 index 00000000..f688d340 --- /dev/null +++ b/_fix_impl.py @@ -0,0 +1,51 @@ +import sys +filepath = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\devices\data\repositories\generate_path_repository_Impl.dart' +with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + +old = ''' if (response.statusCode != 200) { + throw Exception('创建设备任务失败: HTTP ${response.statusCode}'); + } + } catch (e) { + print('❌ [创建设备任务] 错误: $e'); + throw Exception('创建设备任务失败: $e'); + }''' + +new = ''' if (response.statusCode != 200) { + throw Exception('创建设备任务失败: HTTP ${response.statusCode}'); + } + + // 解析响应,提取 taskId + final data = jsonDecode(response.body) as Map; + if (data['code'] == 200) { + final taskData = data['data']; + if (taskData is int) { + print('✅ [创建设备任务] 任务ID: $taskData'); + return taskData; + } else if (taskData is Map) { + final taskId = taskData['id'] ?? taskData['taskId']; + if (taskId is int) { + print('✅ [创建设备任务] 任务ID: $taskId'); + return taskId; + } + } + throw Exception('创建设备任务成功但无法解析taskId: ${response.body}'); + } else { + throw Exception('创建设备任务失败: ${data['msg'] ?? response.body}'); + } + } catch (e) { + print('❌ [创建设备任务] 错误: $e'); + throw Exception('创建设备任务失败: $e'); + }''' + +if old in content: + content = content.replace(old, new) + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + print('SUCCESS') +else: + print('NOT FOUND') + # Debug: find the line + for i, line in enumerate(content.split('\n'), 1): + if '创建设备任务失败: HTTP' in line: + print(f'Line {i}: {repr(line)}') diff --git a/_fix_json_data.py b/_fix_json_data.py new file mode 100644 index 00000000..81618457 --- /dev/null +++ b/_fix_json_data.py @@ -0,0 +1,43 @@ +import codecs + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' +with codecs.open(path, 'r', 'utf-8') as f: + content = f.read() + +old = ''' return PlotData( + id: + record['id']?.toString() ?? + DateTime.now().microsecondsSinceEpoch.toString(), // \u552f\u4e00ID + plotName: record['workName'] ?? '\u672a\u547d\u540d\u5730\u5757', // \u5730\u5757\u540d\u79f0\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff09 + imageUrl: record['imgUrl'] ?? '', // \u56fe\u7247URL\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff0c\u65e0\u5219\u4e3a\u7a7a\uff09 + jsonData: record['jsonData'], // \u539f\u59cb\u6570\u636e\u7684JSON\u5b57\u7b26\u4e32\uff08\u53ef\u9009\uff0c\u4fbf\u4e8e\u8c03\u8bd5\u6216\u540e\u7eed\u4f7f\u7528\uff09 + );''' + +new = ''' // \u517c\u5bb9 jsonData \u4e3a Map \u6216 String\uff1a\u7edf\u4e00\u8f6c\u4e3a JSON \u5b57\u7b26\u4e32 + String? jsonDataStr; + final rawJsonData = record['jsonData']; + if (rawJsonData is String) { + jsonDataStr = rawJsonData; + } else if (rawJsonData is Map) { + jsonDataStr = jsonEncode(rawJsonData); + } + + return PlotData( + id: + record['id']?.toString() ?? + DateTime.now().microsecondsSinceEpoch.toString(), // \u552f\u4e00ID + plotName: record['workName'] ?? '\u672a\u547d\u540d\u5730\u5757', // \u5730\u5757\u540d\u79f0\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff09 + imageUrl: record['imgUrl'] ?? '', // \u56fe\u7247URL\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff0c\u65e0\u5219\u4e3a\u7a7a\uff09 + jsonData: jsonDataStr, // \u539f\u59cb\u6570\u636e\u7684JSON\u5b57\u7b26\u4e32\uff08\u53ef\u9009\uff0c\u4fbf\u4e8e\u8c03\u8bd5\u6216\u540e\u7eed\u4f7f\u7528\uff09 + );''' + +if old in content: + content = content.replace(old, new) + with codecs.open(path, 'w', 'utf-8') as f: + f.write(content) + print('SUCCESS: replaced') +else: + print('FAILED: old text not found') + lines = content.split('\n') + for i, line in enumerate(lines[3198:3208], start=3199): + print(f'{i}: {repr(line)}') diff --git a/_fix_overflow.py b/_fix_overflow.py new file mode 100644 index 00000000..02f4c842 --- /dev/null +++ b/_fix_overflow.py @@ -0,0 +1,43 @@ +filepath = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + +old = ''' const SizedBox(height: 4), + Row( + children: [ + Text( + '设备: ${currentTask.deviceId}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(width: 12), + Container(''' + +new = ''' const SizedBox(height: 4), + Text( + '设备: ${currentTask.deviceId}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + softWrap: true, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Container(''' + +if old in content: + content = content.replace(old, new) + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + print('SUCCESS - replaced device ID text with wrapping support') +else: + print('NOT FOUND') + # Debug: find the line + for i, line in enumerate(content.split('\n'), 1): + if "设备: ${currentTask.deviceId}" in line: + print(f'Line {i}: {repr(line)}') diff --git a/_fix_pause_stop.py b/_fix_pause_stop.py new file mode 100644 index 00000000..f074ae20 --- /dev/null +++ b/_fix_pause_stop.py @@ -0,0 +1,74 @@ +import sys + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + +print(f'Total lines: {len(lines)}') + +# Find lines with "无可用任务" +target_lines = [] +for i, line in enumerate(lines): + if '无可用任务' in line: + target_lines.append(i) + # Print surrounding context + start = max(0, i-5) + end = min(len(lines), i+3) + for j in range(start, end): + print(f' L{j+1}: {lines[j].rstrip()}') + +print(f'Found at lines: {[l+1 for l in target_lines]}') + +# For each occurrence, replace the block: +# lines[i-3]: " final taskId = taskCubit.state.currentTaskId;" +# lines[i-2]: " if (taskId == null) {" +# lines[i-1]: " _showPageToast(message: "无可用任务", type: ToastType.warn);" +# lines[i]: " return;" +# lines[i+1]: " }" + +for line_idx in target_lines: + # Find the start of the block + block_start = line_idx - 1 + block_end = line_idx + 2 + + indent = '' + for ch in lines[block_start]: + if ch in (' ', '\t'): + indent += ch + else: + break + + new_lines = [ + f'{indent}if (taskId == null) {{\n', + f'{indent} await taskCubit.fetchAndFilterTask(deviceId);\n', + f'{indent} final tasks = taskCubit.state.activeTasks;\n', + f'{indent} if (tasks.isNotEmpty) {{\n', + f'{indent} taskCubit.selectTask(tasks.first);\n', + f'{indent} taskId = tasks.first.id;\n', + f'{indent} }} else {{\n', + f'{indent} _showPageToast(message: "无可用任务", type: ToastType.warn);\n', + f'{indent} return;\n', + f'{indent} }}\n', + f'{indent}}}\n', + ] + + # Also change "final taskId" to "var taskId" + var_line_idx = line_idx - 2 + old_var_line = lines[var_line_idx] + lines[var_line_idx] = old_var_line.replace('final taskId', 'var taskId') + + # Replace the block + old_block = lines[block_start:block_end+1] + print(f'Replacing lines {block_start+1}-{block_end+1}:') + for l in old_block: + print(f' OLD: {l.rstrip()}') + for l in new_lines: + print(f' NEW: {l.rstrip()}') + + lines[block_start:block_end+1] = new_lines + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.writelines(lines) + +print('DONE') diff --git a/_fix_startwork.py b/_fix_startwork.py new file mode 100644 index 00000000..f277326c --- /dev/null +++ b/_fix_startwork.py @@ -0,0 +1,77 @@ +import sys + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + content = f.read() + +# Find the block to replace +old_block_start = content.find('result.fold(') +if old_block_start < 0: + print('ERROR: result.fold( not found') + sys.exit(1) + +# Find the end of the fold block: ');' after the fold call +# The fold ends with ');' on a line +old_block_end = content.find(' );', old_block_start) +if old_block_end < 0: + print('ERROR: fold end not found') + sys.exit(1) +old_block_end += len(' );') + +old_block = content[old_block_start:old_block_end] +print('Old block found:') +print(repr(old_block[:100])) +print('...') + +new_block = ''' final taskIdOrFlag = result.fold( + (failure) { + debugPrint('❌ [开始作业] 创建设备任务失败: $failure'); + final failMsg = failure.toString(); + if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + return null; + } + _showPageToast(message: "作业启动失败", type: ToastType.error); + return -1; + }, + (taskId) => taskId, + ); + + if (taskIdOrFlag == null) { + debugPrint('⚠️ [开始作业] 设备已有任务,重新查询任务池...'); + await taskCubit.fetchAndFilterTask(deviceId); + await Future.delayed(const Duration(milliseconds: 300)); + final existingTasks = taskCubit.state.activeTasks; + debugPrint('🔍 [开始作业] 重新查询到 ${existingTasks.length} 个活跃任务'); + if (existingTasks.isNotEmpty) { + final existingTask = existingTasks.first; + taskCubit.selectTask(existingTask); + debugPrint('✅ [开始作业] 复用已有任务 #${existingTask.id},taskId已保存'); + _showPageToast(message: "设备正在作业中", type: ToastType.success); + _saveDataToLocal(); + return; + } + debugPrint('⚠️ [开始作业] 重新查询后仍未找到活跃任务'); + _showPageToast(message: "设备正在作业中", type: ToastType.warn); + return; + } + + if (taskIdOrFlag == -1) { + taskCubit.clearCurrentTask(); + setState(() { + isStartWork = false; + _workStatus = WorkStatus.idle; + }); + return; + } + + final taskId = taskIdOrFlag as int; + debugPrint('✅ [开始作业] 创建设备任务成功,taskId: $taskId'); + taskCubit.updateCurrentTaskId(taskId);''' + +new_content = content[:old_block_start] + new_block + content[old_block_end:] + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.write(new_content) + +print('SUCCESS: File updated') diff --git a/_fix_taskid.py b/_fix_taskid.py new file mode 100644 index 00000000..62cec1e9 --- /dev/null +++ b/_fix_taskid.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +import sys + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + content = f.read() + +# Find the result.fold block +old_start = ' result.fold(' +pos = content.find(old_start) +if pos < 0: + print('ERROR: result.fold not found') + sys.exit(1) +print(f'Found result.fold at char {pos}') + +# Find the end of fold: " );" followed by " } catch" +# Search for the exact pattern +end_pattern = '\n );\n } catch' +end_pos = content.find(end_pattern, pos) +if end_pos < 0: + print(f'ERROR: fold end not found') + sys.exit(1) +end_pos += len('\n );') +print(f'Fold ends at char {end_pos}') + +old_block = content[pos:end_pos] +print('Old block length:', len(old_block)) +print('Old block preview:', repr(old_block[:80])) + +new_block = ''' var needRequery = false; + result.fold( + (failure) { + final failMsg = failure.toString(); + if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + needRequery = true; + return; + } + _showPageToast(message: "作业启动失败", type: ToastType.error); + taskCubit.clearCurrentTask(); + setState(() { + isStartWork = false; + _workStatus = WorkStatus.idle; + }); + return; + }, + (taskId) { + taskCubit.updateCurrentTaskId(taskId); + }, + ); + + if (needRequery) { + debugPrint('⚠️ [开始作业] 设备已有任务,重新查询任务池...'); + await taskCubit.fetchAndFilterTask(deviceId); + await Future.delayed(const Duration(milliseconds: 300)); + final existingTasks = taskCubit.state.activeTasks; + if (existingTasks.isNotEmpty) { + final existingTask = existingTasks.first; + taskCubit.selectTask(existingTask); + debugPrint('✅ [开始作业] 复用已有任务 #${existingTask.id},taskId已保存'); + _showPageToast(message: "设备正在作业中", type: ToastType.success); + _saveDataToLocal(); + return; + } + debugPrint('⚠️ [开始作业] 重新查询后仍未找到活跃任务'); + _showPageToast(message: "设备正在作业中", type: ToastType.warn); + return; + }''' + +new_content = content[:pos] + new_block + content[end_pos:] + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.write(new_content) + +print('SUCCESS - File updated') diff --git a/_fix_taskid2.py b/_fix_taskid2.py new file mode 100644 index 00000000..6ad8cd67 --- /dev/null +++ b/_fix_taskid2.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +import sys + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + content = f.read() + +# Find the "存在任务" branch that just returns without saving taskId +# We'll replace the "return;" in that branch with code that does async requery +# But fold callbacks can't be async... + +# Instead, let's find the fold end and add async handler after it +# Find: "\n );\n } catch (e) {" +old_pat = '\n );\n } catch (e) {' +idx = content.find(old_pat) +if idx < 0: + print('Pattern not found, trying variants...') + # Try with different whitespace + old_pat = ' );\n } catch (e) {' + idx = content.find(old_pat) + if idx < 0: + print('Still not found') + sys.exit(1) + +print(f'Found at index {idx}') + +# Insert code between the fold close and the catch +# We'll restructure to handle needRequery +insert_code = ''' + if (needRequery) { + debugPrint('设备已有任务,重新查询任务池...'); + await taskCubit.fetchAndFilterTask(deviceId); + await Future.delayed(const Duration(milliseconds: 300)); + final existingTasks = taskCubit.state.activeTasks; + if (existingTasks.isNotEmpty) { + final existingTask = existingTasks.first; + taskCubit.selectTask(existingTask); + debugPrint('复用已有任务 #${existingTask.id},taskId已保存'); + _showPageToast(message: "设备正在作业中", type: ToastType.success); + _saveDataToLocal(); + return; + } + _showPageToast(message: "设备正在作业中", type: ToastType.warn); + return; + } + } catch (e) {''' + +# Now find and fix the fold callback to set needRequery flag +# Replace the "存在任务" branch +old_branch = '''if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + debugPrint('⚠️ [开始作业] 设备已有任务,保留作业状态'); + _showPageToast(message: "设备正在作业中", type: ToastType.warn); + // 🔥 不重置 _workStatus,按钮保持可见 + return;''' + +new_branch = '''if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + needRequery = true; + return;''' + +branch_idx = content.find(old_branch) +if branch_idx < 0: + # Try without the debug print variation + old_branch = '''if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + debugPrint''' + branch_idx = content.find(old_branch) + if branch_idx < 0: + print('Branch pattern not found') + else: + print(f'Branch found at {branch_idx} (partial match)') +else: + print(f'Branch found at {branch_idx}') + # Do the branch replacement + content = content[:branch_idx] + new_branch + content[branch_idx + len(old_branch):] + +# Also add needRequery var declaration before fold +# Find " result.fold(" and add var before it +fold_marker = ' result.fold(' +fold_idx = content.find(fold_marker) +if fold_idx >= 0: + # Check if needRequery is already declared + check = content.find('needRequery', fold_idx - 50, fold_idx) + if check < 0: + content = content[:fold_idx] + ' var needRequery = false;\n' + content[fold_idx:] + print('Added needRequery declaration') + +# Now find and replace the fold end pattern again (after content modifications) +old_pat = '\n );\n } catch (e) {' +idx = content.find(old_pat) +if idx < 0: + print('Fold end pattern not found after modifications') + sys.exit(1) + +content = content[:idx + len('\n );')] + insert_code + content[idx + len(old_pat):] + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.write(content) + +print('DONE') diff --git a/_fix_taskid3.py b/_fix_taskid3.py new file mode 100644 index 00000000..2bf15750 --- /dev/null +++ b/_fix_taskid3.py @@ -0,0 +1,83 @@ +import sys + +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(path, 'r', encoding='utf-8') as f: + content = f.read() + +start_marker = ' try {\n final result = await sl().execute(' +start = content.find(start_marker) +if start < 0: + print('start not found') + sys.exit(1) +print(f'start={start}') + +end_marker = 'return;\n }\n }' +end = content.find(end_marker, start) +if end < 0: + print('end not found') + sys.exit(1) +end += len('return;\n }\n }') +print(f'end={end}') + +new_code = """ try { + final result = await sl().execute( + deviceId: deviceId, + routeId: routeId, + siteId: siteId, + orgId: orgId, + ); + + final taskId = result.fold( + (failure) { + final failMsg = failure.toString(); + if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { + return null; + } + _showPageToast(message: '作业启动失败', type: ToastType.error); + return -1; + }, + (id) => id, + ); + + if (taskId == null) { + await taskCubit.fetchAndFilterTask(deviceId); + await Future.delayed(const Duration(milliseconds: 300)); + final existingTasks = taskCubit.state.activeTasks; + if (existingTasks.isNotEmpty) { + taskCubit.selectTask(existingTasks.first); + _showPageToast(message: '设备正在作业中', type: ToastType.success); + _saveDataToLocal(); + return; + } + _showPageToast(message: '设备正在作业中', type: ToastType.warn); + return; + } + + if (taskId == -1) { + taskCubit.clearCurrentTask(); + setState(() { + isStartWork = false; + _workStatus = WorkStatus.idle; + }); + return; + } + + taskCubit.updateCurrentTaskId(taskId); + } catch (e) { + _showPageToast(message: '作业启动异常: $e', type: ToastType.error); + taskCubit.clearCurrentTask(); + setState(() { + isStartWork = false; + _workStatus = WorkStatus.idle; + }); + return; + } + }""" + +content = content[:start] + new_code + content[end:] + +with open(path, 'w', encoding='utf-8', newline='') as f: + f.write(content) + +print('DONE') diff --git a/_patch_save.py b/_patch_save.py new file mode 100644 index 00000000..bf074347 --- /dev/null +++ b/_patch_save.py @@ -0,0 +1,81 @@ +path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' +with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + +# Target: lines 915-954 (0-indexed: 914-953) are duplicate response blocks +# We want to replace them with a single clean response block +# Lines before 915 (0-914) = request params + _showPageToast (already good) +# Line 915-954 = duplicates to remove +# Lines 955+ = list refresh, _clearLocalData, setState (keep) + +# New response block (same indentation as original, 6 spaces) +clean_resp = [ + ' debugPrint(\'\U0001f4e5 [保存地块] 响应状态码: ${response.statusCode}\');\n', + ' debugPrint(\'\U0001f4e5 [保存地块] 响应体(完整): $responseBody\');\n', + '\n', + ' if (response.statusCode == 200) {\n', + ' // \U0001f525 解析响应体确认后端是否真的成功\n', + ' Map? respJson;\n', + ' try {\n', + ' respJson = jsonDecode(responseBody) as Map;\n', + ' } catch (_) {\n', + ' debugPrint(\'\u274c [保存地块] 响应体非JSON: $responseBody\');\n', + ' throw Exception(\'响应体非JSON: $responseBody\');\n', + ' }\n', + '\n', + ' final apiCode = respJson[\'code\'];\n', + ' if (apiCode != null && apiCode.toString() != \'200\') {\n', + ' final apiMsg = respJson[\'msg\'] ?? respJson[\'message\'] ?? \'未知错误\';\n', + ' debugPrint(\'\u274c [保存地块] 后端返回失败, code=$apiCode, msg=$apiMsg\');\n', + ' throw Exception(\'$apiMsg\');\n', + ' }\n', + '\n', + ' debugPrint(\'\u2705 [保存地块] 保存成功, plotName=$plotName, 响应: $respJson\');\n', + ' // \U0001f525 刷新地块列表\n', + ' debugPrint(\'\U0001f504 [保存地块] 刷新地块列表, siteId=$siteId\');\n', + ' context.read().loadWorkRecordsBySiteId(siteId);\n', +] + +# We also need to remove the duplicate list refresh code that comes after line 954 +# Line 955-957 is: debugPrint('🔄...'); context.read().loadWorkRecordsBySiteId(siteId); +# We'll include these in the new block above and remove them + +# Find the start of duplicate block +dup_start = None +dup_end = None +for i, line in enumerate(lines): + if 'debugPrint' in line and '响应状态码' in line and dup_start is None: + dup_start = i + if dup_start is not None and 'List _getPolygonPoints()' in line: + dup_end = i + break + +print(f"Duplicate block: lines {dup_start+1} to {dup_end+1}") + +if dup_start is not None and dup_end is not None: + # Keep: lines before dup_start + our clean block + lines after dup_end + before = lines[:dup_start] + after_dup_end = lines[dup_end:] + + # Remove the duplicate '🔄 刷新' and context.read lines that appear right after our block + # Those are at the start of after_dup_end + cleaned_after = [] + skip_dedup = False + for line in after_dup_end: + # Skip lines that duplicate what's already in our clean_resp block + if '🔄 [保存地块] 刷新地块列表' in line: + skip_dedup = True + continue + if skip_dedup and 'context.read().loadWorkRecordsBySiteId(siteId)' in line: + skip_dedup = False + continue + skip_dedup = False + cleaned_after.append(line) + + new_lines = before + clean_resp + cleaned_after + + with open(path, 'w', encoding='utf-8') as f: + f.writelines(new_lines) + print(f"OK: Replaced {len(lines) - len(new_lines)} lines") +else: + print(f"FAIL: dup_start={dup_start}, dup_end={dup_end}") \ No newline at end of file diff --git a/fix_dup.py b/fix_dup.py new file mode 100644 index 00000000..6944539c --- /dev/null +++ b/fix_dup.py @@ -0,0 +1,61 @@ +import re + +filepath = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart' + +with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + +# Pattern: find the second duplicate method (with debugPrint in catch) +# It starts with "/// 从 state.pathData 实时解析" and ends with "}\n\n Widget _buildWorkPanel" +pattern1 = r'\n /// 从 state\.pathData 实时解析 startWorkList\n /// 解决 BlocBuilder 因 state\.pathData 变化触发重建时,onTap 的异步赋值还未完成的时序问题\n List _parseStartWorkListFromPathData\(List>\? pathData\) \{\n if \(pathData == null \|\| pathData\.isEmpty\) return \[\];\n final firstRecord = pathData\.first;\n final nestedJsonRaw = firstRecord\['jsonData'\];\n if \(nestedJsonRaw is! String\) return \[\];\n try \{\n final parsedJson = jsonDecode\(nestedJsonRaw\);\n if \(parsedJson is! Map\) return \[\];\n final planModel = parsedJson\['planModel'\];\n final isBow = planModel == WorkMode\.bow\.value;\n\n List pathList = \[\];\n final rawPath = parsedJson\['path'\];\n if \(rawPath is String\) \{\n try \{ pathList = jsonDecode\(rawPath\) as List; \} catch \(_\) \{\}\n \} else if \(rawPath is List\) \{\n pathList = rawPath;\n \}\n\n List outerList = \[\];\n final rawOuter = parsedJson\['outer'\];\n if \(rawOuter is String\) \{\n try \{ outerList = jsonDecode\(rawOuter\) as List; \} catch \(_\) \{\}\n \} else if \(rawOuter is List\) \{\n outerList = rawOuter;\n \}\n\n if \(isBow\) \{\n return pathList\.isNotEmpty \? pathList : outerList;\n \} else \{\n return outerList\.isNotEmpty \? outerList : pathList;\n \}\n \} catch \(e\) \{\n debugPrint\('❌ \[_parseStartWorkListFromPathData\] 解析失败: \$e'\);\n return \[\];\n \}\n \}\n\n Widget _buildWorkPanel' + +# Try to find and replace the second duplicate method +match = re.search(pattern1, content) +if match: + print(f"Found duplicate method at position {match.start()}-{match.end()}") + content = content[:match.start()] + '\n Widget _buildWorkPanel' + content[match.end():] +else: + print("Pattern 1 not found, trying simpler approach...") + # Just find the second occurrence of _parseStartWorkListFromPathData + first_idx = content.find('_parseStartWorkListFromPathData') + second_idx = content.find('_parseStartWorkListFromPathData', first_idx + 1) + if second_idx != -1: + # Find the start of this method (go back to find " ///" or " List") + start = content.rfind(' /// 从 state.pathData', 0, second_idx) + if start == -1: + start = content.rfind(' List _parseStartWorkListFromPathData', 0, second_idx) + # Find the end of this method + end = content.find(' Widget _buildWorkPanel', second_idx) + if end == -1: + end = content.find('Widget _buildWorkPanel', second_idx) + if start != -1 and end != -1: + print(f"Found duplicate at {start}-{end}") + content = content[:start] + content[end:] + else: + print(f"Could not find boundaries: start={start}, end={end}") + else: + print("No duplicate method found") + +# Pattern 2: remove duplicate call in _buildWorkPanel +# Find the second "// 核心修复:" inside _buildWorkPanel builder +first_core = content.find('// 🔥 核心修复:从 state.pathData 实时计算 startWorkList') +second_core = content.find('// 核心修复:从 state.pathData 实时计算 startWorkList', first_core + 1) +if second_core != -1: + # Go back to find the blank line before this duplicate block + start_call = content.rfind('\n', 0, second_core) + start_call = content.rfind('\n', 0, start_call) # go back one more line + # Find the end after the duplicate block + # The block is: "// 核心修复...\n final parsedList...\n if...\n startWorkList...\n }\n" + end_call = content.find('\n return Positioned(', second_core) + if end_call != -1: + print(f"Found duplicate call at {start_call}-{end_call}") + content = content[:start_call] + content[end_call:] + else: + print(f"Could not find end of duplicate call") +else: + print("No duplicate call found") + +with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + +print("Done!") diff --git a/flutter_01.log b/flutter_01.log new file mode 100644 index 00000000..7ea4cc31 --- /dev/null +++ b/flutter_01.log @@ -0,0 +1,107 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8655], locale zh-CN) [1,125ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (6 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [5.0s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [10.4s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [31ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [26ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [5.7s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8655] + • Chrome (web) • chrome • web-javascript • Google Chrome 149.0.7827.116 + • Edge (web) • edge • web-javascript • Microsoft Edge 149.0.4022.98 + +[!] Network resources [203.6s] + ✗ A cryptographic error occurred while checking "https://github.com/": Connection terminated during handshake + You may be experiencing a man-in-the-middle attack, your network may be compromised, or you may have malware installed on your computer. + +! Doctor found issues in 2 categories. +``` diff --git a/flutter_02.log b/flutter_02.log new file mode 100644 index 00000000..d9e385d3 --- /dev/null +++ b/flutter_02.log @@ -0,0 +1,106 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8655], locale zh-CN) [744ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (6 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [3.7s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [12.7s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [13ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [11ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [6.1s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8655] + • Chrome (web) • chrome • web-javascript • Google Chrome 149.0.7827.116 + • Edge (web) • edge • web-javascript • Microsoft Edge 149.0.4022.98 + +[!] Network resources [22.1s] + ✗ An HTTP error occurred while checking "https://github.com/": 信号灯超时时间已到 + +! Doctor found issues in 2 categories. +``` diff --git a/flutter_03.log b/flutter_03.log new file mode 100644 index 00000000..c8d9aec3 --- /dev/null +++ b/flutter_03.log @@ -0,0 +1,107 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8655], locale zh-CN) [510ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (6 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [2.9s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [8.9s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [9ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [8ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [5.2s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8655] + • Chrome (web) • chrome • web-javascript • Google Chrome 149.0.7827.116 + • Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.48 + +[!] Network resources [251.0s] + ✗ A cryptographic error occurred while checking "https://github.com/": Connection terminated during handshake + You may be experiencing a man-in-the-middle attack, your network may be compromised, or you may have malware installed on your computer. + +! Doctor found issues in 2 categories. +``` diff --git a/flutter_04.log b/flutter_04.log new file mode 100644 index 00000000..6084c3bb --- /dev/null +++ b/flutter_04.log @@ -0,0 +1,106 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8655], locale zh-CN) [1,209ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (6 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [5.2s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [10.7s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [17ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [14ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [6.3s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8655] + • Chrome (web) • chrome • web-javascript • Google Chrome 149.0.7827.116 + • Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.48 + +[✓] Network resources [948ms] + • All expected network resources are available. + +! Doctor found issues in 1 category. +``` diff --git a/flutter_05.log b/flutter_05.log new file mode 100644 index 00000000..b9c47ae8 --- /dev/null +++ b/flutter_05.log @@ -0,0 +1,106 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN) [1,066ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (6 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [4.6s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [9.6s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [18ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [15ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [5.8s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8875] + • Chrome (web) • chrome • web-javascript • Google Chrome 149.0.7827.201 + • Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.65 + +[✓] Network resources [1,532ms] + • All expected network resources are available. + +! Doctor found issues in 1 category. +``` diff --git a/flutter_06.log b/flutter_06.log new file mode 100644 index 00000000..6747c607 --- /dev/null +++ b/flutter_06.log @@ -0,0 +1,106 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN) [1,220ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (7 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [5.5s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [155.0s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [13ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [10ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [6.3s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8875] + • Chrome (web) • chrome • web-javascript • Google Chrome 149.0.7827.201 + • Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.99 + +[✓] Network resources [2.5s] + • All expected network resources are available. + +! Doctor found issues in 1 category. +``` diff --git a/flutter_07.log b/flutter_07.log new file mode 100644 index 00000000..f86cc622 --- /dev/null +++ b/flutter_07.log @@ -0,0 +1,106 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN) [926ms] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (7 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [4.9s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [12.8s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [14ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [12ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [6.5s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8875] + • Chrome (web) • chrome • web-javascript • Google Chrome 150.0.7871.187 + • Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.99 + +[!] Network resources [22.2s] + ✗ An HTTP error occurred while checking "https://github.com/": 信号灯超时时间已到 + +! Doctor found issues in 2 categories. +``` diff --git a/flutter_08.log b/flutter_08.log new file mode 100644 index 00000000..3c867912 --- /dev/null +++ b/flutter_08.log @@ -0,0 +1,106 @@ +Flutter crash report. +Please report a bug at https://github.com/flutter/flutter/issues. + +## command + +flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true lib\main.dart + +## exception + +FormatException: FormatException: Unexpected character (at character 1) +Failed to init NativeSymbolResolver (SymInitialize 3221225476) +^ + + +``` +#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5) +#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9) +#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22) +#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13) +#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5) +#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#7 _rootRunUnary (dart:async/zone.dart:1538:47) +#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11) +#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11) +#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11) +#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7) +#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20) +#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5) +#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18) +#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24) +#20 _rootRunUnary (dart:async/zone.dart:1538:47) +#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#26 _StreamController._add (dart:async/stream_controller.dart:663:7) +#27 _StreamController.add (dart:async/stream_controller.dart:618:5) +#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41) +#29 _rootRunUnary (dart:async/zone.dart:1546:13) +#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19) +#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7) +#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11) +#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7) +#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19) +#35 _StreamController._add (dart:async/stream_controller.dart:663:7) +#36 _StreamController.add (dart:async/stream_controller.dart:618:5) +#37 new _RawSocket. (dart:io-patch/socket_patch.dart:2315:31) +#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14) +#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35) +#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) +#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13) +#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5) +``` + +## flutter doctor + +``` +[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN) [4.0s] + • Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter + • Upstream repository https://github.com/flutter/flutter.git + • Framework revision 3b62efc2a3 (7 months ago), 2026-01-13 13:47:42 -0800 + • Engine revision 78fc3012e4 + • Dart version 3.10.7 + • DevTools version 2.51.1 + • Pub download mirror https://pub.flutter-io.cn + • Flutter download mirror https://storage.flutter-io.cn + • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging + +[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [33.5s] + +[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [29.8s] + • Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk + • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A) + • Platform android-36, build-tools 36.1.0 + • ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk + • Java binary at: D:\devtools\anrdstod\jbr\bin\java + This is the JDK bundled with the latest Android Studio installation on this machine. + To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`. + • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72) + • All Android licenses accepted. + +[✓] Chrome - develop for the web [157ms] + • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe + +[✗] Visual Studio - develop Windows apps [138ms] + ✗ Visual Studio not installed; this is necessary to develop Windows apps. + Download at https://visualstudio.microsoft.com/downloads/. + Please install the "Desktop development with C++" workload, including all of its default components + +[✓] Connected device (4 available) [34.2s] + • PKB110 (mobile) • 4TRKWOEE4HR8AMEA • android-arm64 • Android 16 (API 36) + • Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8875] + • Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.75 + • Edge (web) • edge • web-javascript • Microsoft Edge 151.0.4129.59 + +[✓] Network resources [5.8s] + • All expected network resources are available. + +! Doctor found issues in 1 category. +``` diff --git a/lib/core/app/app_user_cubit.dart b/lib/core/app/app_user_cubit.dart index bfe1c68f..5e73c67d 100644 --- a/lib/core/app/app_user_cubit.dart +++ b/lib/core/app/app_user_cubit.dart @@ -7,9 +7,9 @@ class AppUserCubit extends Cubit { AppUserCubit() : super(const AppUserState()); void setAuth(UserEntity user) { - print('✅ [AppUserCubit] setAuth 被调用,用户: ${user.username}'); + print('✅ [AppUserCubit] setAuth 被调用,用户: ${user.username}, roleKey=${user.roleKey}, orgId=${user.orgId}, siteId=${user.siteId}'); emit(state.copyWith(user)); - print('✅ [AppUserCubit] 状态已更新,当前用户: ${state.user?.username}'); + print('✅ [AppUserCubit] 状态已更新,当前用户: ${state.user?.username}, roleKey=${state.user?.roleKey}'); } void clearAuth() { diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index ae81427f..4ad66793 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -44,6 +44,10 @@ class TcpClient { int _reconnectFailCount = 0; // 🔥 连续重连失败次数(含心跳超时),达到阈值后停止重连 /// 🔥 重连耗尽回调:连续4次重连失败(含心跳超时)后触发,通知上层弹窗提示用户 VoidCallback? onReconnectExhausted; + /// 🔥 重连开始回调:显示"TCP重连中"提示 + VoidCallback? onReconnectStarted; + /// 🔥 重连成功回调:显示"连接成功"提示 + VoidCallback? onReconnectSuccess; static const Duration _heartbeatTimeout = Duration(seconds: 15); // 心跳超时时间 String? _lastHost; @@ -418,11 +422,25 @@ class TcpClient { _logger.logWithLevel('✅ [TCP-重连定时器] 用户切换中,跳过', shouldLog: true); return; } - _logger.logWithLevel('⏰ 定时器触发,开始执行重连...', shouldLog: true); + // 🔥 显示"TCP重连中"提示 + onReconnectStarted?.call(); + final reconnectStart = DateTime.now(); + final timeStr = '${reconnectStart.hour.toString().padLeft(2,'0')}:${reconnectStart.minute.toString().padLeft(2,'0')}:${reconnectStart.second.toString().padLeft(2,'0')}.${reconnectStart.millisecond.toString().padLeft(3,'0')}'; + debugPrint('⏰ [TCP-重连] $timeStr 开始重连(第$_reconnectAttempt次,退避${delay}s)'); + _logger.logWithLevel('⏰ [TCP-重连] $timeStr 开始重连(第$_reconnectAttempt次,退避${delay}s)', shouldLog: true); try { await connect(host: _lastHost!, port: _lastPort!); + final connectDone = DateTime.now(); + final connectMs = connectDone.difference(reconnectStart).inMilliseconds; + debugPrint('✅ [TCP-重连] ${connectDone.hour.toString().padLeft(2,'0')}:${connectDone.minute.toString().padLeft(2,'0')}:${connectDone.second.toString().padLeft(2,'0')}.${connectDone.millisecond.toString().padLeft(3,'0')} connect() 完成,耗时 ${connectMs}ms'); _reconnectAttempt = 0; // 🔥 重连成功,重置退避计数 - // 🔥 重连成功后,补调 HTTP switchDevice 告知服务端推送目标设备 + // 🔥 关键修复:等待5秒再调 HTTP switchDevice,给服务端时间完成相关操作 + final delayStart = DateTime.now(); + debugPrint('⏳ [TCP-重连] ${delayStart.hour.toString().padLeft(2,'0')}:${delayStart.minute.toString().padLeft(2,'0')}:${delayStart.second.toString().padLeft(2,'0')}.${delayStart.millisecond.toString().padLeft(3,'0')} 开始等待5秒...'); + await Future.delayed(const Duration(milliseconds: 5000)); + final afterDelay = DateTime.now(); + final actualDelayMs = afterDelay.difference(delayStart).inMilliseconds; + debugPrint('⏳ [TCP-重连] ${afterDelay.hour.toString().padLeft(2,'0')}:${afterDelay.minute.toString().padLeft(2,'0')}:${afterDelay.second.toString().padLeft(2,'0')}.${afterDelay.millisecond.toString().padLeft(3,'0')} 等待结束,实际耗时 ${actualDelayMs}ms,开始调 HTTP switchDevice'); try { final remoteControlCubit = GetIt.I(); final targetDevice = remoteControlCubit.state.targetDevice; @@ -430,10 +448,14 @@ class TcpClient { debugPrint('🔄 [TCP-重连] 补调 HTTP switchDevice: ${targetDevice.deviceName}'); _logger.logWithLevel('🔄 [TCP-重连] 补调 HTTP switchDevice: ${targetDevice.deviceName}', shouldLog: true); await switchDeviceUseCase.deviceRepository.switchDevice('app', targetDevice.deviceName); - debugPrint('✅ [TCP-重连] HTTP switchDevice 成功'); + final switchDone = DateTime.now(); + final totalMs = switchDone.difference(reconnectStart).inMilliseconds; + debugPrint('✅ [TCP-重连] ${switchDone.hour.toString().padLeft(2,'0')}:${switchDone.minute.toString().padLeft(2,'0')}:${switchDone.second.toString().padLeft(2,'0')}.${switchDone.millisecond.toString().padLeft(3,'0')} HTTP switchDevice 成功,重连总耗时 ${totalMs}ms'); } else { debugPrint('⚠️ [TCP-重连] targetDevice 为 null,跳过 switchDevice'); } + // 🔥 显示"连接成功"提示 + onReconnectSuccess?.call(); } catch (e) { debugPrint('❌ [TCP-重连] HTTP switchDevice 失败: $e'); _logger.logWithLevel('❌ [TCP-重连] HTTP switchDevice 失败: $e', shouldLog: true); diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 08c885bf..f07dbd20 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -66,6 +66,9 @@ class AuthCubit extends Cubit { _listenToAuthResponse(); // 🔥 设置重连耗尽回调:连续4次重连失败后弹窗提示用户退出登录 tcp.onReconnectExhausted = _showReconnectFailedDialog; + // 🔥 设置重连提示回调:重连中/重连成功 SnackBar + tcp.onReconnectStarted = _showReconnectingToast; + tcp.onReconnectSuccess = _showReconnectSuccessToast; } /// App 启动时检查本地缓存 @@ -379,24 +382,42 @@ class AuthCubit extends Cubit { ], ), actions: [ - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.of(dialogContext).pop(); - logout(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF165DFF), - foregroundColor: Colors.white, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(dialogContext).pop(), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF86909C), + side: const BorderSide(color: Color(0xFFE5E6EB)), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('取消', style: TextStyle(fontSize: 15)), ), ), - child: const Text('退出登录', style: TextStyle(fontSize: 15)), - ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + logout(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF165DFF), + foregroundColor: Colors.white, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('退出登录', style: TextStyle(fontSize: 15)), + ), + ), + ], ), ], ); @@ -412,6 +433,39 @@ class AuthCubit extends Cubit { } } + /// 🔥 重连中提示 SnackBar + void _showReconnectingToast() { + try { + final context = navigatorKey.currentContext; + if (context != null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('TCP重连中...'), + backgroundColor: Color(0xFFFF7D00), + duration: Duration(seconds: 4), + ), + ); + } + } catch (_) {} + } + + /// 🔥 重连成功提示 SnackBar + void _showReconnectSuccessToast() { + try { + final context = navigatorKey.currentContext; + if (context != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('连接成功'), + backgroundColor: Color(0xFF00B42A), + duration: Duration(seconds: 2), + ), + ); + } + } catch (_) {} + } + /// 🔥 新增:清空所有业务状态,防止数据泄露到新账户 void _clearAllBusinessState() { try { diff --git a/lib/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart b/lib/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart index 292d9df6..52c43a77 100644 --- a/lib/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart +++ b/lib/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart @@ -1,9 +1,13 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:http/http.dart' as http; +/// 视频加载状态 +enum WebRTCLoadState { loading, playing, error } + class WebRTCLocalPlayer extends StatefulWidget { final String streamUrl; final bool showLeftPip; @@ -11,6 +15,16 @@ class WebRTCLocalPlayer extends StatefulWidget { final bool isFrontMain; final Alignment? mainViewAlignment; // 🔥 主视角对齐方式(指定后覆盖 isFrontMain) final VoidCallback? onDoubleTap; // 🔥 添加双击回调 + final VoidCallback? onTap; // 🔥 单击回调(播放中点击停止拉流) + + /// 🔥 视频加载状态回调(loading/playing/error) + /// - loading:正在建立连接 + /// - playing:已收到视频流,开始播放 + /// - error:连接失败或超时未收到画面 + final ValueChanged? onLoadingStateChanged; + + /// 🔥 等待视频流的最大时长,超时判定为失败(默认 8 秒) + final Duration connectTimeout; const WebRTCLocalPlayer({ super.key, @@ -20,6 +34,9 @@ class WebRTCLocalPlayer extends StatefulWidget { this.isFrontMain = true, this.mainViewAlignment, // 🔥 可选,指定要显示的象限 this.onDoubleTap, // 🔥 可选回调 + this.onTap, // 🔥 可选,播放中单击停止拉流 + this.onLoadingStateChanged, + this.connectTimeout = const Duration(seconds: 8), }); @override @@ -29,18 +46,132 @@ class WebRTCLocalPlayer extends StatefulWidget { class _WebRTCLocalPlayerState extends State { // 🔥 显示器:只初始化一次,终生使用 final RTCVideoRenderer _renderer = RTCVideoRenderer(); - + // 🔥 信号线:可以更换,但必须先拔后插 RTCPeerConnection? _peerConnection; - + // 🔥 URL 缓存:用于判断是否需要换线 String? _currentStreamUrl; // 画中画位置管理 - final ValueNotifier _leftPosNotifier = ValueNotifier(const Offset(20, 80)); - final ValueNotifier _rightPosNotifier = ValueNotifier(const Offset(200, 80)); + final ValueNotifier _leftPosNotifier = ValueNotifier( + const Offset(20, 80), + ); + final ValueNotifier _rightPosNotifier = ValueNotifier( + const Offset(200, 80), + ); bool _isPosInitialized = false; + // 🔥 加载状态相关 + WebRTCLoadState _loadState = WebRTCLoadState.loading; + Timer? _connectTimer; // 超时判定计时器 + Timer? _frameCheckTimer; // 🔥 真实视频帧检测计时器 + + /// 🔥 统一更新加载状态并通知外部 + void _updateLoadState(WebRTCLoadState state) { + if (_loadState == state) return; + _loadState = state; + widget.onLoadingStateChanged?.call(state); + } + + /// 🔥 启动超时计时器:超过 connectTimeout 仍未渲染出真实画面,判定为失败 + void _startConnectTimer() { + _connectTimer?.cancel(); + _connectTimer = Timer(widget.connectTimeout, () { + if (_loadState != WebRTCLoadState.playing) { + debugPrint('⏰ [WebRTC] 连接超时,未渲染出真实画面,判定为失败'); + _updateLoadState(WebRTCLoadState.error); + } + }); + } + + /// 🔥 取消超时计时器 + void _cancelConnectTimer() { + _connectTimer?.cancel(); + _connectTimer = null; + } + + /// 🔥 启动真实视频流检测:通过 WebRTC getStats 检查 inbound-rtp 的 bytesReceived 是否在增长 + /// 只有真实有数据包流入,才算真的有视频(避免 track 建立了但帧是黑的被误判为有视频) + /// - videoWidth/videoHeight 在 SDP 协商成功后就 > 0,无法区分黑屏流,不可靠 + /// - bytesReceived 持续增长 = 真实数据流;为 0 或停止增长 = 黑屏流 + void _startFrameCheck() { + _frameCheckTimer?.cancel(); + int lastBytesReceived = -1; + int stableCount = 0; // bytesReceived 未增长的连续次数 + + _frameCheckTimer = Timer.periodic(const Duration(milliseconds: 500), ( + _, + ) async { + if (!mounted || _peerConnection == null) { + _frameCheckTimer?.cancel(); + _frameCheckTimer = null; + return; + } + + try { + final stats = await _peerConnection!.getStats(); + // 查找视频入站 RTP 统计 + int currentBytes = 0; + for (final report in stats) { + if (report.type == 'inbound-rtp') { + final kind = report.values['kind']; + final bytes = report.values['bytesReceived']; + if (kind == 'video' && bytes is num) { + currentBytes = bytes.toInt(); + break; + } + } + } + + // 还没收到任何字节,继续等 + if (currentBytes == 0) { + debugPrint('⏳ [WebRTC] bytesReceived=0,等待数据流入...'); + return; + } + + // 字节数在增长 → 真实数据流 → 判定为 playing + if (currentBytes > lastBytesReceived && lastBytesReceived >= 0) { + debugPrint( + '✅ [WebRTC] 检测到真实视频流,bytesReceived: $currentBytes,判定为 playing', + ); + _frameCheckTimer?.cancel(); + _frameCheckTimer = null; + _cancelConnectTimer(); + _updateLoadState(WebRTCLoadState.playing); + return; + } + + // 字节数未增长 + if (currentBytes == lastBytesReceived) { + stableCount++; + debugPrint( + '⚠️ [WebRTC] bytesReceived 未增长 ($currentBytes),stableCount=$stableCount', + ); + // 连续 2 次未增长(约 1 秒)→ 判定为黑屏流 + if (stableCount >= 2) { + debugPrint('❌ [WebRTC] 字节流停止增长,判定为黑屏流'); + _frameCheckTimer?.cancel(); + _frameCheckTimer = null; + _cancelConnectTimer(); + _updateLoadState(WebRTCLoadState.error); + return; + } + } + + lastBytesReceived = currentBytes; + } catch (e) { + debugPrint('⚠️ [WebRTC] 获取 stats 失败: $e'); + } + }); + } + + /// 🔥 取消帧检测计时器 + void _cancelFrameCheck() { + _frameCheckTimer?.cancel(); + _frameCheckTimer = null; + } + @override void initState() { super.initState(); @@ -62,9 +193,10 @@ class _WebRTCLocalPlayerState extends State { @override void didUpdateWidget(WebRTCLocalPlayer oldWidget) { super.didUpdateWidget(oldWidget); - + // 🔥 只有 URL 真正变化时才换线 - if (oldWidget.streamUrl != widget.streamUrl && widget.streamUrl.isNotEmpty) { + if (oldWidget.streamUrl != widget.streamUrl && + widget.streamUrl.isNotEmpty) { debugPrint('🔄 [WebRTC] URL 变化,准备换线'); _connectSignal(); } @@ -75,22 +207,27 @@ class _WebRTCLocalPlayerState extends State { // 1️⃣ 如果 URL 为空,不插线 if (widget.streamUrl.isEmpty) { debugPrint('⚠️ [WebRTC] URL 为空,跳过连接'); + _updateLoadState(WebRTCLoadState.error); return; } - + // 2️⃣ 如果 URL 没变,不换线(防止重复连接) if (_currentStreamUrl == widget.streamUrl && _peerConnection != null) { debugPrint('✅ [WebRTC] URL 未变化,保持当前连接'); return; } - + // 3️⃣ 拔掉旧线(彻底释放旧连接) await _disconnectSignal(); - + + // 🔥 进入 loading 态,启动超时计时器 + _updateLoadState(WebRTCLoadState.loading); + _startConnectTimer(); + // 4️⃣ 插上新线(创建新连接) try { debugPrint('🔌 [WebRTC] 建立新连接...'); - + _peerConnection = await createPeerConnection({ "sdpSemantics": "unified-plan", "iceServers": [ @@ -106,48 +243,70 @@ class _WebRTCLocalPlayerState extends State { // 🔥 收到视频流时,直接连接到显示器 _peerConnection!.onTrack = (RTCTrackEvent event) { if (event.track.kind == 'video' && event.streams.isNotEmpty) { - debugPrint('✅ [WebRTC] 收到视频流,连接到显示器'); + debugPrint('✅ [WebRTC] 收到视频 track,连接到显示器,等待真实帧渲染'); if (mounted) { setState(() => _renderer.srcObject = event.streams[0]); + // 🔥 关键:收到 track 不等于有画面,必须等真实帧渲染才判定 playing + _startFrameCheck(); } } }; // 🔥 SDP 协商 - RTCSessionDescription offer = await _peerConnection!.createOffer({'offerToReceiveVideo': 1}); + RTCSessionDescription offer = await _peerConnection!.createOffer({ + 'offerToReceiveVideo': 1, + }); await _peerConnection!.setLocalDescription(offer); - Uri uri = Uri.parse(widget.streamUrl.replaceFirst('webrtc://', 'http://')); + Uri uri = Uri.parse( + widget.streamUrl.replaceFirst('webrtc://', 'http://'), + ); String apiUrl = "http://${uri.host}:1985/rtc/v1/play/"; final response = await http.post( Uri.parse(apiUrl), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ - "api": apiUrl, - "streamurl": widget.streamUrl, - "clientip": null, - "sdp": offer.sdp + "api": apiUrl, + "streamurl": widget.streamUrl, + "clientip": null, + "sdp": offer.sdp, }), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); final sdp = data['sdp']; - + if (sdp != null && sdp is String && sdp.isNotEmpty) { - await _peerConnection!.setRemoteDescription(RTCSessionDescription(sdp, 'answer')); + await _peerConnection!.setRemoteDescription( + RTCSessionDescription(sdp, 'answer'), + ); _currentStreamUrl = widget.streamUrl; // 🔥 记录当前 URL - debugPrint('✅ [WebRTC] 连接成功'); + debugPrint('✅ [WebRTC] SDP 协商成功'); + // 注意:此处仅代表协商成功,真正判定为 playing 须等 onTrack 收到画面 + // 超时定时器仍在运行,未收到画面会自动判定为 error + } else { + debugPrint('❌ [WebRTC] 返回的 sdp 为空,判定为失败'); + _cancelConnectTimer(); + _updateLoadState(WebRTCLoadState.error); } + } else { + debugPrint('❌ [WebRTC] HTTP 状态码: ${response.statusCode},判定为失败'); + _cancelConnectTimer(); + _updateLoadState(WebRTCLoadState.error); } } catch (e) { debugPrint("❌ [WebRTC] 连接失败: $e"); + _cancelConnectTimer(); + _updateLoadState(WebRTCLoadState.error); } } /// 🔥 拔线逻辑:彻底释放旧连接 Future _disconnectSignal() async { + _cancelConnectTimer(); // 🔥 拔线时一并取消超时计时器 + _cancelFrameCheck(); // 🔥 一并取消帧检测计时器 if (_peerConnection != null) { debugPrint('🗑️ [WebRTC] 释放旧连接...'); await _peerConnection?.close(); @@ -174,7 +333,11 @@ class _WebRTCLocalPlayerState extends State { // 🔥 俯视(center):显示完整视频帧,不做2倍裁剪 if (alignment == Alignment.center) { return RepaintBoundary( - child: RTCVideoView(_renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: false), + child: RTCVideoView( + _renderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + mirror: false, + ), ); } @@ -185,7 +348,11 @@ class _WebRTCLocalPlayerState extends State { widthFactor: 2.0, heightFactor: 2.0, alignment: alignment, - child: RTCVideoView(_renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: false), + child: RTCVideoView( + _renderer, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + mirror: false, + ), ), ), ); @@ -198,7 +365,12 @@ class _WebRTCLocalPlayerState extends State { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent], + colors: [ + Colors.transparent, + Colors.white, + Colors.white, + Colors.transparent, + ], stops: [0.0, 0.15, 0.85, 1.0], ).createShader(bounds); }, @@ -208,20 +380,30 @@ class _WebRTCLocalPlayerState extends State { return const LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, - colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent], + colors: [ + Colors.transparent, + Colors.white, + Colors.white, + Colors.transparent, + ], stops: [0.0, 0.05, 0.95, 1.0], ).createShader(bounds); }, blendMode: BlendMode.dstIn, - child: _buildQuadrantView(alignment: widget.mainViewAlignment ?? (widget.isFrontMain ? Alignment.topLeft : Alignment.topRight)), + child: _buildQuadrantView( + alignment: + widget.mainViewAlignment ?? + (widget.isFrontMain ? Alignment.topLeft : Alignment.topRight), + ), ), ); } @override Widget build(BuildContext context) { - // 🔥 如果正在初始化,显示加载指示器 - if (_peerConnection == null && _currentStreamUrl == null) { + // 🔥 在真正渲染出视频帧(playing 态)之前,始终显示加载指示器 + // 避免出现"track 已收到但画面是黑的"黑屏情况 + if (_loadState != WebRTCLoadState.playing) { return Container( color: Colors.black, child: Center( @@ -255,26 +437,47 @@ class _WebRTCLocalPlayerState extends State { if (!_isPosInitialized) { _leftPosNotifier.value = const Offset(20, 80); - _rightPosNotifier.value = Offset(constraints.maxWidth - pipW - 20, 80); + _rightPosNotifier.value = Offset( + constraints.maxWidth - pipW - 20, + 80, + ); _isPosInitialized = true; } return Stack( children: [ - // ② 主视频层(🔥 恢复双击切换前后视角) + // ② 主视频层(🔥 双击切换前后视角 / 单击停止拉流) Center( child: GestureDetector( + onTap: widget.onTap, // 🔥 播放中单击 → 停止拉流 onDoubleTap: () { debugPrint('👆 [WebRTC] 双击屏幕,切换前后视角'); widget.onDoubleTap?.call(); // 🔥 调用父组件回调 }, - child: AspectRatio(aspectRatio: 16 / 9, child: _buildMainViewWithFeathering()), + child: AspectRatio( + aspectRatio: 16 / 9, + child: _buildMainViewWithFeathering(), + ), ), ), // ③ 悬浮小窗 - 解决阻尼感的 1:1 稳定拖拽 - if (widget.showLeftPip) _buildFastPip(_leftPosNotifier, Alignment.bottomLeft, constraints, pipW, pipH), - if (widget.showRightPip) _buildFastPip(_rightPosNotifier, Alignment.bottomRight, constraints, pipW, pipH), + if (widget.showLeftPip) + _buildFastPip( + _leftPosNotifier, + Alignment.bottomLeft, + constraints, + pipW, + pipH, + ), + if (widget.showRightPip) + _buildFastPip( + _rightPosNotifier, + Alignment.bottomRight, + constraints, + pipW, + pipH, + ), ], ); }, @@ -282,7 +485,12 @@ class _WebRTCLocalPlayerState extends State { ); } - void _correctPosition(ValueNotifier notifier, BoxConstraints constraints, double w, double h) { + void _correctPosition( + ValueNotifier notifier, + BoxConstraints constraints, + double w, + double h, + ) { double maxX = (constraints.maxWidth - w).clamp(0.0, double.infinity); double maxY = (constraints.maxHeight - h).clamp(0.0, double.infinity); double newX = notifier.value.dx.clamp(0.0, maxX); @@ -292,7 +500,13 @@ class _WebRTCLocalPlayerState extends State { } } - Widget _buildFastPip(ValueNotifier notifier, Alignment align, BoxConstraints constraints, double w, double h) { + Widget _buildFastPip( + ValueNotifier notifier, + Alignment align, + BoxConstraints constraints, + double w, + double h, + ) { return ValueListenableBuilder( valueListenable: notifier, builder: (context, pos, child) { @@ -302,8 +516,14 @@ class _WebRTCLocalPlayerState extends State { child: GestureDetector( behavior: HitTestBehavior.opaque, onPanUpdate: (details) { - final double nextX = (notifier.value.dx + details.delta.dx).clamp(0.0, (constraints.maxWidth - w).clamp(0.0, double.infinity)); - final double nextY = (notifier.value.dy + details.delta.dy).clamp(0.0, (constraints.maxHeight - h).clamp(0.0, double.infinity)); + final double nextX = (notifier.value.dx + details.delta.dx).clamp( + 0.0, + (constraints.maxWidth - w).clamp(0.0, double.infinity), + ); + final double nextY = (notifier.value.dy + details.delta.dy).clamp( + 0.0, + (constraints.maxHeight - h).clamp(0.0, double.infinity), + ); notifier.value = Offset(nextX, nextY); }, child: child!, @@ -315,7 +535,13 @@ class _WebRTCLocalPlayerState extends State { height: h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(0, 8))], + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], ), clipBehavior: Clip.antiAlias, child: _buildQuadrantView(alignment: align), diff --git a/lib/features/v2/device_list/data/datasources/bind_device_datasource.dart b/lib/features/v2/device_list/data/datasources/bind_device_datasource.dart index b7848b11..1e77c893 100644 --- a/lib/features/v2/device_list/data/datasources/bind_device_datasource.dart +++ b/lib/features/v2/device_list/data/datasources/bind_device_datasource.dart @@ -8,7 +8,7 @@ abstract class BindDeviceDatasource { Future bindDevice({ required List deviceIds, required int orgId, - required int siteId, - required int userId, + int? siteId, + int? userId, }); } diff --git a/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart b/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart index e74db63f..6a3b4d6b 100644 --- a/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart +++ b/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart @@ -105,11 +105,13 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource { } DeviceDataModel _parseDeviceFromJson(Map json) { + // 在线状态:使用 onlineStatus 字段(1=在线, 0=离线) + final onlineStatus = json['onlineStatus'] ?? 0; return DeviceDataModel( deviceId: json['deviceId']?.toString() ?? '', name: json['deviceName'] ?? json['name'] ?? '', type: json['deviceTypeName'] ?? json['type'] ?? '未知设备', - status: _getStatusText(json['status'] ?? 0), + status: onlineStatus == 1 ? '在线' : '离线', power: (json['power'] as num?)?.toDouble() ?? 0, todayEnergy: (json['todayEnergy'] as num?)?.toDouble() ?? 0, temperature: (json['temperature'] as num?)?.toDouble(), diff --git a/lib/features/v2/device_list/data/datasources/impl/bind_device_datasource_impl.dart b/lib/features/v2/device_list/data/datasources/impl/bind_device_datasource_impl.dart index 0ba6d3cc..5762796a 100644 --- a/lib/features/v2/device_list/data/datasources/impl/bind_device_datasource_impl.dart +++ b/lib/features/v2/device_list/data/datasources/impl/bind_device_datasource_impl.dart @@ -127,18 +127,17 @@ class BindDeviceDatasourceImpl implements BindDeviceDatasource { Future bindDevice({ required List deviceIds, required int orgId, - required int siteId, - required int userId, + int? siteId, + int? userId, }) async { final token = await _getToken(); + final Map body = {'deviceIds': deviceIds, 'orgId': orgId}; + if (siteId != null) body['siteId'] = siteId; + if (userId != null) body['userId'] = userId; + final response = await _dio.post( HttpApiConsts.bindDevice, - data: { - 'deviceIds': deviceIds, - 'orgId': orgId, - 'siteId': siteId, - 'userId': userId, - }, + data: body, options: Options( headers: { 'Authorization': token != null ? 'Bearer $token' : '', diff --git a/lib/features/v2/device_list/data/models/robot_data_model.dart b/lib/features/v2/device_list/data/models/robot_data_model.dart index ae8b5bd7..ed19e06d 100644 --- a/lib/features/v2/device_list/data/models/robot_data_model.dart +++ b/lib/features/v2/device_list/data/models/robot_data_model.dart @@ -4,7 +4,9 @@ class RobotDataModel { final String id; final String? alias; // 设备别名 final String type; - final String status; + final String status; // '在线' / '离线',基于 onlineStatus 字段 + final bool isOnline; // 在线状态,基于接口 onlineStatus 字段 + final int statusCode; // 接口原始 status 状态码 final double battery; final String task; @@ -14,6 +16,8 @@ class RobotDataModel { this.alias, required this.type, required this.status, + this.isOnline = false, + this.statusCode = 0, required this.battery, required this.task, }); @@ -25,12 +29,21 @@ class RobotDataModel { (json['capacity_percent'] as num?)?.toDouble() ?? 100.0; + // 在线状态:优先使用 onlineStatus 字段(1=在线, 0=离线) + final onlineStatusValue = json['onlineStatus'] ?? 0; + final isOnline = onlineStatusValue == 1; + + // 设备状态码(原始 status 字段) + final statusCode = json['status'] ?? 0; + return RobotDataModel( name: json['deviceName'] ?? json['name'] ?? '', id: json['deviceId']?.toString() ?? json['id']?.toString() ?? '', alias: json['deviceAlias'] as String?, // 获取别名字段 type: _convertType(json['deviceTypeName'] ?? json['type'] ?? '未知类型'), - status: _getStatusText(json['status'] ?? 0), + status: isOnline ? '在线' : '离线', + isOnline: isOnline, + statusCode: statusCode is int ? statusCode : 0, battery: batteryValue, task: json['task'] ?? json['currentTask'] ?? '待机中', ); @@ -44,25 +57,13 @@ class RobotDataModel { 'alias': alias, 'type': type, 'status': status, + 'isOnline': isOnline, + 'statusCode': statusCode, 'battery': battery, 'task': task, }; } - /// 状态码转换 - static String _getStatusText(int status) { - switch (status) { - case 1: - return '在线'; - case 2: - return '离线'; - case 3: - return '异常'; - default: - return '未知'; - } - } - static String _convertType(String type) { if (type.contains('巡检')) { return 'inspection'; diff --git a/lib/features/v2/device_list/data/repositories/bind_device_repository_impl.dart b/lib/features/v2/device_list/data/repositories/bind_device_repository_impl.dart index 163cd694..9d695c1f 100644 --- a/lib/features/v2/device_list/data/repositories/bind_device_repository_impl.dart +++ b/lib/features/v2/device_list/data/repositories/bind_device_repository_impl.dart @@ -62,8 +62,8 @@ class BindDeviceRepositoryImpl implements BindDeviceRepository { Future> bindDevice({ required List deviceIds, required int orgId, - required int siteId, - required int userId, + int? siteId, + int? userId, }) async { try { await _datasource.bindDevice( diff --git a/lib/features/v2/device_list/domain/repositories/bind_device_repository.dart b/lib/features/v2/device_list/domain/repositories/bind_device_repository.dart index 1ff27434..49d0fb33 100644 --- a/lib/features/v2/device_list/domain/repositories/bind_device_repository.dart +++ b/lib/features/v2/device_list/domain/repositories/bind_device_repository.dart @@ -13,7 +13,7 @@ abstract class BindDeviceRepository { Future> bindDevice({ required List deviceIds, required int orgId, - required int siteId, - required int userId, + int? siteId, + int? userId, }); } diff --git a/lib/features/v2/device_list/domain/usecases/bind_device_usecases.dart b/lib/features/v2/device_list/domain/usecases/bind_device_usecases.dart index 4e564391..b07c98f3 100644 --- a/lib/features/v2/device_list/domain/usecases/bind_device_usecases.dart +++ b/lib/features/v2/device_list/domain/usecases/bind_device_usecases.dart @@ -30,8 +30,8 @@ class BindDeviceV2UseCase { Future> call({ required List deviceIds, required int orgId, - required int siteId, - required int userId, + int? siteId, + int? userId, }) => _repository.bindDevice( deviceIds: deviceIds, orgId: orgId, diff --git a/lib/features/v2/device_list/presentation/cubit/bind_device_cubit.dart b/lib/features/v2/device_list/presentation/cubit/bind_device_cubit.dart index 4b904f4b..b20e25b4 100644 --- a/lib/features/v2/device_list/presentation/cubit/bind_device_cubit.dart +++ b/lib/features/v2/device_list/presentation/cubit/bind_device_cubit.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart'; @@ -24,15 +25,51 @@ class BindDeviceCubit extends Cubit { ) : super(const BindDeviceState()); UserEntity? get _currentUser => _appUserCubit.state.user; - String get _roleKey => _currentUser?.roleKey ?? 'user'; + String get roleKey => state.roleKey; + bool get isOrgEnabled => roleKey == 'admin'; + bool get isSiteEnabled => roleKey == 'admin' || roleKey == 'manager'; + bool get isUserEnabled => + roleKey == 'admin' || roleKey == 'manager' || roleKey == 'siteManager'; + + // ── 去重辅助 ── + List _uniqueOrgs(List list) { + final seen = {}; + return list.where((e) => seen.add(e.id)).toList(); + } + + List _uniqueSites(List list) { + final seen = {}; + return list.where((e) => seen.add(e.id)).toList(); + } + + List _uniqueUsers(List list) { + final seen = {}; + return list.where((e) => seen.add(e.id)).toList(); + } + + /// 确保 selectedId 存在于列表中,否则置 null + int? _validSelected(int? id, List list) { + if (id == null) return null; + final ids = list.map((e) => e.id as int).toSet(); + return ids.contains(id) ? id : null; + } Future init() async { final user = _currentUser; - if (user == null) return; + if (user == null) { + debugPrint( + '>>> [BindDevice] ERROR: _currentUser is null! AppUserCubit.state.user is null. Cannot proceed.', + ); + return; + } - emit(state.copyWith(isLoading: true)); + debugPrint( + '>>> [BindDevice] roleKey=${user.roleKey}, orgId=${user.orgId}, siteId=${user.siteId}, userId=${user.userId}, username=${user.username}', + ); - switch (_roleKey) { + emit(state.copyWith(isLoading: true, roleKey: user.roleKey ?? 'user')); + + switch (roleKey) { case 'admin': await _loadAllOrgs(); break; @@ -57,7 +94,15 @@ class BindDeviceCubit extends Cubit { result.fold( (failure) => emit(state.copyWith(errorMessage: failure.message, isLoading: false)), - (orgs) => emit(state.copyWith(orgList: orgs)), + (orgs) { + final unique = _uniqueOrgs(orgs); + emit( + state.copyWith( + orgList: unique, + selectedOrgId: _validSelected(state.selectedOrgId, unique), + ), + ); + }, ); } @@ -67,8 +112,9 @@ class BindDeviceCubit extends Cubit { (failure) => emit(state.copyWith(errorMessage: failure.message, isLoading: false)), (orgs) async { - emit(state.copyWith(orgList: orgs, selectedOrgId: user.orgId)); - await _loadSitesByOrg(user.orgId); + final unique = _ensureOrgInList(_uniqueOrgs(orgs), user.orgId); + emit(state.copyWith(orgList: unique, selectedOrgId: user.orgId)); + await _loadSitesByOrgForManager(user.orgId, user.siteId); }, ); } @@ -79,7 +125,8 @@ class BindDeviceCubit extends Cubit { (failure) => emit(state.copyWith(errorMessage: failure.message, isLoading: false)), (orgs) async { - emit(state.copyWith(orgList: orgs, selectedOrgId: user.orgId)); + final unique = _ensureOrgInList(_uniqueOrgs(orgs), user.orgId); + emit(state.copyWith(orgList: unique, selectedOrgId: user.orgId)); await _loadSitesAndUsers(user); }, ); @@ -91,18 +138,65 @@ class BindDeviceCubit extends Cubit { (failure) => emit(state.copyWith(errorMessage: failure.message, isLoading: false)), (orgs) async { - emit(state.copyWith(orgList: orgs, selectedOrgId: user.orgId)); + final unique = _ensureOrgInList(_uniqueOrgs(orgs), user.orgId); + emit(state.copyWith(orgList: unique, selectedOrgId: user.orgId)); await _loadSitesAndUsers(user); }, ); } + List _ensureOrgInList(List orgs, int orgId) { + if (orgs.any((e) => e.id == orgId)) return orgs; + return [...orgs, OrgEntity(id: orgId, name: '组织 #$orgId')]; + } + + List _ensureSiteInList(List sites, int? siteId) { + if (siteId == null) return sites; + if (sites.any((e) => e.id == siteId)) return sites; + return [...sites, SiteEntity(id: siteId, name: '场站 #$siteId')]; + } + + List _ensureUserInList( + List users, + UserEntity user, + ) { + final uid = int.tryParse(user.userId) ?? 0; + if (users.any((e) => e.id == uid)) return users; + return [...users, UserSimpleEntity(id: uid, name: user.nickname)]; + } + Future _loadSitesByOrg(int orgId) async { final result = await _getSitesByOrgUseCase(orgId); result.fold( (failure) => emit(state.copyWith(errorMessage: failure.message, isLoading: false)), - (sites) => emit(state.copyWith(siteList: sites)), + (sites) { + final unique = _uniqueSites(sites); + emit( + state.copyWith( + siteList: unique, + selectedSiteId: _validSelected(state.selectedSiteId, unique), + ), + ); + }, + ); + } + + Future _loadSitesByOrgForManager(int orgId, int? siteId) async { + final result = await _getSitesByOrgUseCase(orgId); + result.fold( + (failure) => + emit(state.copyWith(errorMessage: failure.message, isLoading: false)), + (sites) { + final unique = _ensureSiteInList(_uniqueSites(sites), siteId); + emit( + state.copyWith( + siteList: unique, + selectedSiteId: + siteId ?? _validSelected(state.selectedSiteId, unique), + ), + ); + }, ); } @@ -112,7 +206,8 @@ class BindDeviceCubit extends Cubit { (failure) => emit(state.copyWith(errorMessage: failure.message, isLoading: false)), (sites) async { - emit(state.copyWith(siteList: sites, selectedSiteId: user.siteId)); + final unique = _ensureSiteInList(_uniqueSites(sites), user.siteId); + emit(state.copyWith(siteList: unique, selectedSiteId: user.siteId)); if (user.siteId != null) { await _loadUsers(user); } else { @@ -139,23 +234,21 @@ class BindDeviceCubit extends Cubit { name: user.nickname, ); result.fold( - (failure) => - emit(state.copyWith(errorMessage: failure.message, isLoading: false)), - (users) { - final allUsers = []; - final seenIds = {}; - for (final u in users) { - if (u.id != currentUser.id && !seenIds.contains(u.id)) { - allUsers.add(u); - seenIds.add(u.id); - } - } - if (!seenIds.contains(currentUser.id)) { - allUsers.add(currentUser); - } + (failure) { + final list = _ensureUserInList([], user); emit( state.copyWith( - userList: allUsers, + userList: list, + selectedUserId: currentUser.id, + isLoading: false, + ), + ); + }, + (users) { + final unique = _ensureUserInList(_uniqueUsers(users), user); + emit( + state.copyWith( + userList: unique, selectedUserId: currentUser.id, isLoading: false, ), @@ -179,7 +272,10 @@ class BindDeviceCubit extends Cubit { final result = await _getSitesByOrgUseCase(orgId); result.fold( (failure) => emit(state.copyWith(errorMessage: failure.message)), - (sites) => emit(state.copyWith(siteList: sites)), + (sites) { + final unique = _uniqueSites(sites); + emit(state.copyWith(siteList: unique)); + }, ); } @@ -196,7 +292,10 @@ class BindDeviceCubit extends Cubit { final result = await _getUsersBySiteUseCase(siteId); result.fold( (failure) => emit(state.copyWith(errorMessage: failure.message)), - (users) => emit(state.copyWith(userList: users)), + (users) { + final unique = _uniqueUsers(users); + emit(state.copyWith(userList: unique)); + }, ); } @@ -205,32 +304,39 @@ class BindDeviceCubit extends Cubit { } Future submitBind(String deviceId) async { - if (state.selectedOrgId == null || - state.selectedSiteId == null || - state.selectedUserId == null) { - emit(state.copyWith(errorMessage: '请选择完整的绑定信息')); + if (state.selectedOrgId == null) { + emit(state.copyWith(errorMessage: '请选择所属组织')); + return false; + } + + if (roleKey == 'user' && state.selectedUserId == null) { + emit(state.copyWith(errorMessage: '请选择负责人')); return false; } emit(state.copyWith(isSubmitting: true, errorMessage: null)); - final existResult = await _isDeviceAtSiteUseCase( - deviceId: deviceId, - siteId: state.selectedSiteId!, - ); + if (state.selectedSiteId != null) { + final existResult = await _isDeviceAtSiteUseCase( + deviceId: deviceId, + siteId: state.selectedSiteId!, + ); - final isAtSite = existResult.fold((failure) => false, (exists) => exists); + final isAtSite = existResult.fold((failure) => false, (exists) => exists); - if (isAtSite) { - emit(state.copyWith(isSubmitting: false, errorMessage: '该设备已在当前场站,无需绑定')); - return false; + if (isAtSite) { + emit( + state.copyWith(isSubmitting: false, errorMessage: '该设备已在当前场站,无需绑定'), + ); + return false; + } } final result = await _bindDeviceUseCase( deviceIds: [deviceId], orgId: state.selectedOrgId!, - siteId: state.selectedSiteId!, - userId: state.selectedUserId!, + siteId: state.selectedSiteId, + userId: state.selectedUserId, ); return result.fold( @@ -246,11 +352,4 @@ class BindDeviceCubit extends Cubit { }, ); } - - bool get isOrgEnabled => _roleKey == 'admin'; - bool get isSiteEnabled => _roleKey == 'admin' || _roleKey == 'manager'; - bool get isUserEnabled => - _roleKey == 'admin' || _roleKey == 'manager' || _roleKey == 'siteManager'; - - String get roleKey => _roleKey; } diff --git a/lib/features/v2/device_list/presentation/cubit/bind_device_state.dart b/lib/features/v2/device_list/presentation/cubit/bind_device_state.dart index 3852a2c0..2213f293 100644 --- a/lib/features/v2/device_list/presentation/cubit/bind_device_state.dart +++ b/lib/features/v2/device_list/presentation/cubit/bind_device_state.dart @@ -8,6 +8,7 @@ class BindDeviceState extends Equatable { final int? selectedOrgId; final int? selectedSiteId; final int? selectedUserId; + final String roleKey; final bool isLoading; final bool isSubmitting; final String? errorMessage; @@ -20,6 +21,7 @@ class BindDeviceState extends Equatable { this.selectedOrgId, this.selectedSiteId, this.selectedUserId, + this.roleKey = 'user', this.isLoading = false, this.isSubmitting = false, this.errorMessage, @@ -33,6 +35,7 @@ class BindDeviceState extends Equatable { int? selectedOrgId, int? selectedSiteId, int? selectedUserId, + String? roleKey, bool? isLoading, bool? isSubmitting, String? errorMessage, @@ -45,6 +48,7 @@ class BindDeviceState extends Equatable { selectedOrgId: selectedOrgId ?? this.selectedOrgId, selectedSiteId: selectedSiteId ?? this.selectedSiteId, selectedUserId: selectedUserId ?? this.selectedUserId, + roleKey: roleKey ?? this.roleKey, isLoading: isLoading ?? this.isLoading, isSubmitting: isSubmitting ?? this.isSubmitting, errorMessage: errorMessage, @@ -60,9 +64,10 @@ class BindDeviceState extends Equatable { selectedOrgId, selectedSiteId, selectedUserId, + roleKey, isLoading, isSubmitting, errorMessage, isSuccess, ]; -} \ No newline at end of file +} diff --git a/lib/features/v2/device_list/presentation/pages/bind_device_page.dart b/lib/features/v2/device_list/presentation/pages/bind_device_page.dart index a4303e0f..b286513b 100644 --- a/lib/features/v2/device_list/presentation/pages/bind_device_page.dart +++ b/lib/features/v2/device_list/presentation/pages/bind_device_page.dart @@ -92,6 +92,8 @@ class _BindDevicePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _buildRoleTag(state.roleKey), + const SizedBox(height: 16), _buildRequiredField( label: '已选装备 ID', child: Container( @@ -126,10 +128,15 @@ class _BindDevicePageState extends State { child: _buildSiteDropdown(state), ), const SizedBox(height: 16), - _buildField( - label: '负责人', - child: _buildUserDropdown(state), - ), + _cubit.roleKey == 'user' + ? _buildRequiredField( + label: '负责人', + child: _buildUserDropdown(state), + ) + : _buildField( + label: '负责人', + child: _buildUserDropdown(state), + ), const SizedBox(height: 32), Row( children: [ @@ -206,6 +213,12 @@ class _BindDevicePageState extends State { ); } + int? _validValue(List list, int? selectedId) { + if (selectedId == null) return null; + final count = list.where((e) => e.id == selectedId).length; + return count == 1 ? selectedId : null; + } + Widget _buildOrgDropdown(BindDeviceState state) { final enabled = _cubit.isOrgEnabled; final selectedName = state.selectedOrgId != null @@ -214,23 +227,22 @@ class _BindDevicePageState extends State { .fold(null, (_, org) => org.name) : null; - if (!enabled && selectedName != null) { - return _buildReadonlyField(selectedName); + if (!enabled) { + return _buildReadonlyField(selectedName ?? '未分配组织'); } + final validValue = _validValue(state.orgList, state.selectedOrgId); + return Container( decoration: BoxDecoration( - color: enabled ? Colors.white : const Color(0xFFF7F8FA), + color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE5E6EB)), ), child: DropdownButtonFormField( - value: state.selectedOrgId, - decoration: _dropdownDecoration(enabled), - icon: Icon( - Icons.expand_more, - color: enabled ? const Color(0xFF86909C) : const Color(0xFFC9CDD4), - ), + value: validValue, + decoration: _dropdownDecoration(enabled, '请选择'), + icon: const Icon(Icons.expand_more, color: Color(0xFF86909C)), items: state.orgList.map((org) { return DropdownMenuItem( value: org.id, @@ -241,11 +253,9 @@ class _BindDevicePageState extends State { ), ); }).toList(), - onChanged: enabled - ? (v) { - if (v != null) _cubit.onOrgChanged(v); - } - : null, + onChanged: (v) { + if (v != null) _cubit.onOrgChanged(v); + }, ), ); } @@ -258,23 +268,23 @@ class _BindDevicePageState extends State { .fold(null, (_, site) => site.name) : null; - if (!enabled && selectedName != null) { - return _buildReadonlyField(selectedName); + if (!enabled) { + return _buildReadonlyField(selectedName ?? '未分配场站'); } + final hintText = state.selectedOrgId == null ? '请先选择组织' : '请选择'; + final validValue = _validValue(state.siteList, state.selectedSiteId); + return Container( decoration: BoxDecoration( - color: enabled ? Colors.white : const Color(0xFFF7F8FA), + color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE5E6EB)), ), child: DropdownButtonFormField( - value: state.selectedSiteId, - decoration: _dropdownDecoration(enabled), - icon: Icon( - Icons.expand_more, - color: enabled ? const Color(0xFF86909C) : const Color(0xFFC9CDD4), - ), + value: validValue, + decoration: _dropdownDecoration(enabled, hintText), + icon: const Icon(Icons.expand_more, color: Color(0xFF86909C)), items: state.siteList.map((site) { return DropdownMenuItem( value: site.id, @@ -285,11 +295,9 @@ class _BindDevicePageState extends State { ), ); }).toList(), - onChanged: enabled - ? (v) { - if (v != null) _cubit.onSiteChanged(v); - } - : null, + onChanged: (v) { + if (v != null) _cubit.onSiteChanged(v); + }, ), ); } @@ -302,23 +310,23 @@ class _BindDevicePageState extends State { .fold(null, (_, user) => user.name) : null; - if (!enabled && selectedName != null) { - return _buildReadonlyField(selectedName); + if (!enabled) { + return _buildReadonlyField(selectedName ?? '未分配负责人'); } + final hintText = state.selectedSiteId == null ? '请先选择场站' : '请选择'; + final validValue = _validValue(state.userList, state.selectedUserId); + return Container( decoration: BoxDecoration( - color: enabled ? Colors.white : const Color(0xFFF7F8FA), + color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE5E6EB)), ), child: DropdownButtonFormField( - value: state.selectedUserId, - decoration: _dropdownDecoration(enabled), - icon: Icon( - Icons.expand_more, - color: enabled ? const Color(0xFF86909C) : const Color(0xFFC9CDD4), - ), + value: validValue, + decoration: _dropdownDecoration(enabled, hintText), + icon: const Icon(Icons.expand_more, color: Color(0xFF86909C)), items: state.userList.map((user) { return DropdownMenuItem( value: user.id, @@ -329,11 +337,37 @@ class _BindDevicePageState extends State { ), ); }).toList(), - onChanged: enabled - ? (v) { - _cubit.onUserChanged(v); - } - : null, + onChanged: (v) { + if (v != null) _cubit.onUserChanged(v); + }, + ), + ); + } + + Widget _buildRoleTag(String roleKey) { + final roleLabel = + const { + 'admin': '管理员', + 'manager': '组织管理员', + 'siteManager': '场站管理员', + 'user': '普通用户', + }[roleKey] ?? + '普通用户'; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFFE8F3FF), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFF165DFF), width: 0.5), + ), + child: Text( + '当前角色:$roleLabel($roleKey)', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w500, + ), ), ); } @@ -361,14 +395,14 @@ class _BindDevicePageState extends State { ); } - InputDecoration _dropdownDecoration(bool enabled) { + InputDecoration _dropdownDecoration(bool enabled, String hintText) { return InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, - hintText: enabled ? '请选择' : '请选择', + hintText: hintText, hintStyle: const TextStyle(color: Color(0xFF86909C), fontSize: 14), ); } diff --git a/lib/features/v2/device_list/presentation/pages/device_status_page.dart b/lib/features/v2/device_list/presentation/pages/device_status_page.dart index 49584b63..5fc863fc 100644 --- a/lib/features/v2/device_list/presentation/pages/device_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart @@ -618,8 +618,8 @@ class _DeviceStatusViewState extends State { productName: robot.type, tenantId: 0, tenantName: '', - status: robot.status == '在线' ? 1 : 0, - onlineStatus: robot.status == '在线' ? 1 : 0, + status: robot.isOnline ? 1 : 0, + onlineStatus: robot.isOnline ? 1 : 0, ); final remoteCubit = GetIt.I(); @@ -784,6 +784,13 @@ class _DeviceStatusViewState extends State { BuildContext context, DeviceListState.DeviceStatusLoaded state, ) { + // 从实际设备列表计算统计数据(不再使用硬编码的假数据) + final devices = state.devices; + final totalCount = devices.length; + final onlineCount = devices.where((d) => d.status == '在线').length; + final exceptionCount = devices.where((d) => d.status == '异常').length; + final offlineCount = totalCount - onlineCount - exceptionCount; + return Container( margin: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.all(16.0), @@ -801,7 +808,7 @@ class _DeviceStatusViewState extends State { child: Row( children: [ _buildStatItem( - value: state.deviceStatus.total.toString(), + value: totalCount.toString(), label: AppLocalizations.of( context, ).translate('device_list_v2.total_devices'), @@ -810,7 +817,7 @@ class _DeviceStatusViewState extends State { ), Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), _buildStatItem( - value: state.deviceStatus.online.toString(), + value: onlineCount.toString(), label: AppLocalizations.of( context, ).translate('device_list_v2.online'), @@ -819,7 +826,7 @@ class _DeviceStatusViewState extends State { ), Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), _buildStatItem( - value: state.deviceStatus.exception.toString(), + value: exceptionCount.toString(), label: AppLocalizations.of( context, ).translate('device_list_v2.alarm'), @@ -828,7 +835,7 @@ class _DeviceStatusViewState extends State { ), Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), _buildStatItem( - value: state.deviceStatus.offline.toString(), + value: offlineCount.toString(), label: AppLocalizations.of( context, ).translate('device_list_v2.offline'), diff --git a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart index c6890f14..5c5a9d35 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -109,11 +109,11 @@ class _RobotListViewState extends State { final weedingRobots = allRobots.where((r) => r.type == 'weeding').toList(); final onlineInspection = inspectionRobots - .where((r) => r.status == '在线') + .where((r) => r.isOnline) .length; - final onlineCleaning = cleaningRobots.where((r) => r.status == '在线').length; + final onlineCleaning = cleaningRobots.where((r) => r.isOnline).length; // 暂且所有设备都视为除草机器人,在线数使用总数 - final onlineWeeding = allRobots.where((r) => r.status == '在线').length; + final onlineWeeding = allRobots.where((r) => r.isOnline).length; return Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 12), padding: const EdgeInsets.symmetric(vertical: 16), @@ -385,14 +385,12 @@ class _RobotListViewState extends State { Widget _buildQuickActions(RobotListLoaded state) { // 计算统计数据 - final onlineCount = state.robots.where((r) => r.status == '在线').length; + final onlineCount = state.robots.where((r) => r.isOnline).length; final workingCount = state.robots .where((r) => r.task != '待机中' && r.task != '充电中') .length; final standbyCount = state.robots.where((r) => r.task == '待机中').length; - final faultCount = state.robots - .where((r) => r.status == '异常' || r.status == '离线') - .length; + final faultCount = state.robots.where((r) => !r.isOnline).length; return Container( margin: const EdgeInsets.fromLTRB(16, 0, 16, 12), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), @@ -429,7 +427,7 @@ class _RobotListViewState extends State { Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), _buildStatusItem( value: '$faultCount', - label: '故障数', + label: '离线数', valueColor: const Color(0xFFF53F3F), ), ], @@ -474,6 +472,8 @@ class _RobotListViewState extends State { id: '', type: '', status: '', + isOnline: false, + statusCode: 0, battery: 0, task: '无任务', ), @@ -641,8 +641,8 @@ class _RobotListViewState extends State { productName: robot.type, tenantId: 0, tenantName: '', - status: robot.status == '在线' ? 1 : 0, - onlineStatus: robot.status == '在线' ? 1 : 0, + status: robot.isOnline ? 1 : 0, + onlineStatus: robot.isOnline ? 1 : 0, ); debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...'); diff --git a/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart b/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart index d7af96f4..e3e831ef 100644 --- a/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart @@ -110,20 +110,20 @@ class _DroneOsdCardState extends State { void _initFields() { _osdFields.addAll([ + { + 'icon': Icons.battery_full_rounded, + 'label': '电量', + 'key': 'battery', + 'value': '未知', + 'color': const Color(0xFF00B42A), + }, { 'icon': Icons.height_rounded, - 'label': '飞行高度', + 'label': '高度', 'key': 'height', 'value': '未知', 'color': const Color(0xFF165DFF), }, - { - 'icon': Icons.speed_rounded, - 'label': '飞行速度', - 'key': 'speed', - 'value': '未知', - 'color': const Color(0xFF722ED1), - }, { 'icon': Icons.trending_flat_rounded, 'label': '水平速度', @@ -138,34 +138,6 @@ class _DroneOsdCardState extends State { 'value': '未知', 'color': const Color(0xFF00B42A), }, - { - 'icon': Icons.route_rounded, - 'label': '飞行距离', - 'key': 'distance', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.battery_full_rounded, - 'label': '电量', - 'key': 'battery', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.bolt_rounded, - 'label': '电池电压', - 'key': 'voltage', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.thermostat_rounded, - 'label': '电池温度', - 'key': 'batteryTemp', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, { 'icon': Icons.navigation_rounded, 'label': '航向角', @@ -173,48 +145,20 @@ class _DroneOsdCardState extends State { 'value': '未知', 'color': const Color(0xFF165DFF), }, - { - 'icon': Icons.rotate_90_degrees_cw_rounded, - 'label': '俯仰角', - 'key': 'pitch', - 'value': '未知', - 'color': const Color(0xFF722ED1), - }, - { - 'icon': Icons.rotate_right_rounded, - 'label': '横滚角', - 'key': 'roll', - 'value': '未知', - 'color': const Color(0xFF722ED1), - }, { 'icon': Icons.satellite_rounded, - 'label': 'GPS卫星', + 'label': 'GPS', 'key': 'gps', 'value': '未知', 'color': const Color(0xFF00B42A), }, { 'icon': Icons.satellite_alt_rounded, - 'label': 'RTK卫星', + 'label': 'RTK', 'key': 'rtk', 'value': '未知', 'color': const Color(0xFF00B42A), }, - { - 'icon': Icons.home_rounded, - 'label': '无人机状态', - 'key': 'droneState', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.flight_rounded, - 'label': '飞行模式', - 'key': 'flightMode', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, ]); } @@ -263,78 +207,45 @@ class _DroneOsdCardState extends State { (droneData['height'] as num?)?.toDouble() ?? (droneData['altitude'] as num?)?.toDouble(); - double? groundSpeed = (droneData['ground_speed'] as num?)?.toDouble(); - double? flightDistance = (droneData['flight_distance'] as num?)?.toDouble(); - int? flightTime = droneData['flight_time'] as int?; + double? horizontalSpeed = (droneData['horizontal_speed'] as num?) + ?.toDouble(); + double? verticalSpeed = (droneData['vertical_speed'] as num?)?.toDouble(); final batteryMap = droneData['battery'] as Map?; int? batteryPercent = droneData['battery_percent'] as int? ?? batteryMap?['capacity_percent'] as int?; - double? batteryVoltage = (droneData['battery_voltage'] as num?)?.toDouble(); - if (batteryVoltage == null && batteryMap != null) { - final batteries = batteryMap['batteries'] as List?; - if (batteries != null && batteries.isNotEmpty) { - final firstBattery = batteries[0] as Map?; - batteryVoltage = (firstBattery?['voltage'] as num?)?.toDouble(); - } - } - if (batteryVoltage != null && batteryVoltage > 1000) { - batteryVoltage = batteryVoltage / 1000; - } - - double? batteryTemp = (droneData['battery_temperature'] as num?) - ?.toDouble(); - if (batteryTemp == null && batteryMap != null) { - final batteries = batteryMap['batteries'] as List?; - if (batteries != null && batteries.isNotEmpty) { - final firstBattery = batteries[0] as Map?; - batteryTemp = (firstBattery?['temperature'] as num?)?.toDouble(); - } - } - double? heading = (droneData['heading'] as num?)?.toDouble() ?? (droneData['attitude_head'] as num?)?.toDouble(); - double? pitch = - (droneData['pitch'] as num?)?.toDouble() ?? - (droneData['attitude_pitch'] as num?)?.toDouble(); - double? roll = - (droneData['roll'] as num?)?.toDouble() ?? - (droneData['attitude_roll'] as num?)?.toDouble(); - int? gpsSatellites = droneData['gps_satellites'] as int?; - int? rtkSatellites = droneData['rtk_satellites'] as int?; - - double? horizontalSpeed = (droneData['horizontal_speed'] as num?) - ?.toDouble(); - double? verticalSpeed = (droneData['vertical_speed'] as num?)?.toDouble(); - - String? droneState = droneData['drone_state'] as String?; - if (droneState == null) { - final inHangar = droneData['in_hangar'] as int?; - if (inHangar != null) { - droneState = inHangar == 1 ? '在库' : '外出'; - } + // GPS/RTK:DJI Cloud API 中 GPS 卫星数和 RTK 定位状态均位于 position_state 对象内 + // - position_state.gps_number:GPS 卫星数 + // - position_state.is_fixed:RTK 定位状态(0=未定位, 1=浮点解, 2=固定解) + final positionState = droneData['position_state']; + int? gpsSatellites; + int? rtkFixed; + if (positionState is Map) { + gpsSatellites = positionState['gps_number'] as int?; + rtkFixed = positionState['is_fixed'] as int?; } - if (droneState == null) { - final droneInDock = droneData['drone_in_dock'] as int?; - if (droneInDock != null) { - droneState = droneInDock == 1 ? '在库' : '外出'; - } + // 兼容直接放在顶层的旧字段名 + gpsSatellites = gpsSatellites ?? droneData['gps_satellites'] as int?; + final int? rtkSatellites = droneData['rtk_satellites'] as int?; + + if (batteryPercent != null) { + parsedValues['battery'] = '$batteryPercent%'; + parsedColors['battery'] = batteryPercent > 50 + ? const Color(0xFF00B42A) + : batteryPercent > 20 + ? const Color(0xFFFF7D00) + : const Color(0xFFF53F3F); } - - String? flightMode = droneData['flight_mode'] as String?; - if (height != null) { parsedValues['height'] = '${height.toStringAsFixed(1)}m'; parsedColors['height'] = const Color(0xFF165DFF); } - if (groundSpeed != null) { - parsedValues['speed'] = '${groundSpeed.toStringAsFixed(1)}m/s'; - parsedColors['speed'] = const Color(0xFF722ED1); - } if (horizontalSpeed != null) { parsedValues['horizontalSpeed'] = '${horizontalSpeed.toStringAsFixed(1)}m/s'; @@ -345,58 +256,40 @@ class _DroneOsdCardState extends State { ? const Color(0xFFF53F3F) : const Color(0xFF00B42A); } - if (flightDistance != null) { - parsedValues['distance'] = '${flightDistance.toStringAsFixed(0)}m'; - } - if (batteryPercent != null) { - parsedValues['battery'] = '$batteryPercent%'; - parsedColors['battery'] = batteryPercent > 50 - ? const Color(0xFF00B42A) - : batteryPercent > 20 - ? const Color(0xFFFF7D00) - : const Color(0xFFF53F3F); - } - if (batteryVoltage != null) { - parsedValues['voltage'] = '${batteryVoltage.toStringAsFixed(1)}V'; - } - if (batteryTemp != null) { - parsedValues['batteryTemp'] = '${batteryTemp.toStringAsFixed(0)}°C'; - parsedColors['batteryTemp'] = batteryTemp > 50 - ? const Color(0xFFF53F3F) - : batteryTemp > 40 - ? const Color(0xFFFF7D00) - : const Color(0xFF165DFF); - } if (heading != null) { parsedValues['heading'] = '${heading.toStringAsFixed(0)}°'; } - if (pitch != null) { - parsedValues['pitch'] = '${pitch.toStringAsFixed(1)}°'; - } - if (roll != null) { - parsedValues['roll'] = '${roll.toStringAsFixed(1)}°'; - } if (gpsSatellites != null) { parsedValues['gps'] = '$gpsSatellites颗'; parsedColors['gps'] = gpsSatellites >= 6 ? const Color(0xFF00B42A) : const Color(0xFFFF7D00); } - if (rtkSatellites != null) { + // RTK:优先展示定位状态(is_fixed),其次回退到卫星数 + if (rtkFixed != null) { + String rtkStatus; + Color rtkColor; + switch (rtkFixed) { + case 2: + rtkStatus = '固定解'; + rtkColor = const Color(0xFF00B42A); + break; + case 1: + rtkStatus = '浮点解'; + rtkColor = const Color(0xFFFF7D00); + break; + default: + rtkStatus = '未定位'; + rtkColor = const Color(0xFF86909C); + } + parsedValues['rtk'] = rtkStatus; + parsedColors['rtk'] = rtkColor; + } else if (rtkSatellites != null) { parsedValues['rtk'] = '$rtkSatellites颗'; parsedColors['rtk'] = rtkSatellites >= 4 ? const Color(0xFF00B42A) : const Color(0xFFFF7D00); } - if (droneState != null) { - parsedValues['droneState'] = droneState; - parsedColors['droneState'] = droneState == '在库' - ? const Color(0xFF00B42A) - : const Color(0xFF165DFF); - } - if (flightMode != null) { - parsedValues['flightMode'] = flightMode; - } if (!mounted) return; setState(() { diff --git a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart index 946a2a08..60282ec5 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart @@ -5,10 +5,9 @@ import 'package:maibu_satabot_v2/core/app/app_user_state.dart'; // 🔥 导入 A import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart'; import '../../../device_run_param/presentation/pages/robot_param_settings_page.dart'; -import '../../../device_run_param/presentation/pages/robot_param_settings_page.dart'; /// 机器人顶部信息卡片 -class RobotHeaderCard extends StatefulWidget { +class RobotHeaderCard extends StatefulWidget { final Map robot; const RobotHeaderCard({super.key, required this.robot}); @@ -17,15 +16,31 @@ class RobotHeaderCard extends StatefulWidget { State createState() => _RobotHeaderCardState(); } +/// 视频展示状态 +/// - idle:未播放,显示播放按钮 +/// - loading:正在加载视频 +/// - playing:视频播放中 +/// - noStream:暂无视频(加载失败后也回到此态,并显示播放按钮供重试) +enum _VideoState { idle, loading, playing, noStream } + class _RobotHeaderCardState extends State { int _currentViewIndex = 0; // 0=前, 1=后, 2=左, 3=右, 4=上 + _VideoState _videoState = _VideoState.idle; // 视角配置 final List> _viewConfigs = [ {'name': '前视', 'alignment': Alignment.topLeft, 'icon': Icons.arrow_upward}, - {'name': '后视', 'alignment': Alignment.topRight, 'icon': Icons.arrow_downward}, + { + 'name': '后视', + 'alignment': Alignment.topRight, + 'icon': Icons.arrow_downward, + }, {'name': '左视', 'alignment': Alignment.bottomLeft, 'icon': Icons.arrow_back}, - {'name': '右视', 'alignment': Alignment.bottomRight, 'icon': Icons.arrow_forward}, + { + 'name': '右视', + 'alignment': Alignment.bottomRight, + 'icon': Icons.arrow_forward, + }, {'name': '俯视', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, ]; @@ -35,6 +50,42 @@ class _RobotHeaderCardState extends State { debugPrint('🎬 [RobotHeaderCard] 初始化 - robot: ${widget.robot}'); } + /// 🔥 构造视频流 URL(仅在用户点击播放时使用) + String _buildStreamUrl(String? token) { + final deviceId = widget.robot['name'] as String?; + if (deviceId == null || deviceId.isEmpty || token == null) { + return ''; + } + return "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=$token"; + } + + /// 🔥 点击播放按钮 + void _onPlayTapped(String? token) { + final url = _buildStreamUrl(token); + if (url.isEmpty) { + // 无法构造 URL(设备 ID 或 token 缺失)→ 直接显示暂无视频 + setState(() => _videoState = _VideoState.noStream); + return; + } + setState(() => _videoState = _VideoState.loading); + } + + /// 🔥 接收 WebRTCLocalPlayer 的加载状态回调 + void _onWebRTCStateChanged(WebRTCLoadState state) { + switch (state) { + case WebRTCLoadState.loading: + setState(() => _videoState = _VideoState.loading); + break; + case WebRTCLoadState.playing: + setState(() => _videoState = _VideoState.playing); + break; + case WebRTCLoadState.error: + // 打不开 → 显示暂无视频 → 视觉上仍展示播放按钮(可重试) + setState(() => _videoState = _VideoState.noStream); + break; + } + } + @override Widget build(BuildContext context) { return Container( @@ -128,9 +179,8 @@ class _RobotHeaderCardState extends State { Navigator.push( context, MaterialPageRoute( - builder: (_) => RobotParamSettingsPage( - robot: widget.robot, - ), + builder: (_) => + RobotParamSettingsPage(robot: widget.robot), ), ); }, @@ -143,67 +193,53 @@ class _RobotHeaderCardState extends State { ], ), const SizedBox(height: 12), - // 🔥 视频展示区域(替换原来的静态图片) + // 🔥 视频展示区域:默认显示播放按钮,点击后才加载视频 AspectRatio( aspectRatio: 16 / 9, child: Container( color: Colors.black, // 🔥 强制整个视频区域为黑色背景 child: BlocBuilder( builder: (context, userState) { - final deviceId = widget.robot['name'] as String?; // 🔥 使用 name 字段(长序列号) - String videoStreamUrl = ''; - - debugPrint('🎬 [RobotHeaderCard] BlocBuilder 重建'); - debugPrint(' - deviceId (name): $deviceId'); - debugPrint(' - user != null: ${userState.user != null}'); - debugPrint(' - token != null: ${userState.user?.token != null}'); - debugPrint(' - TCP_IP: ${TCPConsts.TCP_IP}'); - - if (deviceId != null && - deviceId.isNotEmpty && - userState.user != null && - userState.user!.token != null) { - videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}"; - debugPrint('✅ [RobotHeaderCard] 视频URL构建成功: $videoStreamUrl'); - } else { - debugPrint('❌ [RobotHeaderCard] 视频URL构建失败'); - if (deviceId == null || deviceId.isEmpty) { - debugPrint(' - 原因: deviceId 为空'); - } - if (userState.user == null) { - debugPrint(' - 原因: user 为 null'); - } - if (userState.user?.token == null) { - debugPrint(' - 原因: token 为 null'); - } + final token = userState.user?.token; + + // loading / playing 态:渲染 WebRTCLocalPlayer + // - loading:WebRTCLocalPlayer 内部自带“视频加载中...”加载屏,并建立连接 + // - playing:收到视频流后正常播放 + // 两种态都保持 WebRTCLocalPlayer 挂载,避免重建断流 + if (_videoState == _VideoState.loading || + _videoState == _VideoState.playing) { + final url = _buildStreamUrl(token); + return WebRTCLocalPlayer( + key: ValueKey('play_$url'), + streamUrl: url, + showLeftPip: false, + showRightPip: false, + mainViewAlignment: + _viewConfigs[_currentViewIndex]['alignment'] + as Alignment, + onLoadingStateChanged: _onWebRTCStateChanged, + onTap: () { + // 🔥 播放中单击 → 停止拉流,回到播放按钮态 + // 移除 WebRTCLocalPlayer 后其 dispose 会自动断连 + setState(() => _videoState = _VideoState.idle); + }, + ); } - - return videoStreamUrl.isNotEmpty - ? WebRTCLocalPlayer( - key: ValueKey(videoStreamUrl), - streamUrl: videoStreamUrl, - showLeftPip: false, - showRightPip: false, - mainViewAlignment: _viewConfigs[_currentViewIndex]['alignment'] as Alignment, - ) - : Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.videocam_off, size: 48, color: Colors.white54), - const SizedBox(height: 12), - Text( - '暂无视频', - style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500), - ), - ], - ), - ); + + // idle / noStream 态:显示播放按钮 + // - idle:进入页面默认态 + // - noStream:视频流打不开,显示“暂无视频”提示 + 播放按钮供重试 + final bool showNoStreamHint = + _videoState == _VideoState.noStream; + return _buildPlayOverlay( + showNoStreamHint: showNoStreamHint, + onTap: () => _onPlayTapped(token), + ); }, ), ), ), - + // 🔥 视角切换按钮 Padding( padding: const EdgeInsets.fromLTRB(0, 12, 0, 0), @@ -213,7 +249,7 @@ class _RobotHeaderCardState extends State { final index = entry.key; final config = entry.value; final isSelected = _currentViewIndex == index; - + return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 2), @@ -222,13 +258,22 @@ class _RobotHeaderCardState extends State { setState(() { _currentViewIndex = index; }); - debugPrint('🎬 [RobotHeaderCard] 切换视角: ${config['name']}'); + debugPrint( + '🎬 [RobotHeaderCard] 切换视角: ${config['name']}', + ); }, style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? const Color(0xFF165DFF) : Colors.grey[200], - foregroundColor: isSelected ? Colors.white : Colors.grey[700], + backgroundColor: isSelected + ? const Color(0xFF165DFF) + : Colors.grey[200], + foregroundColor: isSelected + ? Colors.white + : Colors.grey[700], elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), minimumSize: const Size(0, 36), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -257,6 +302,77 @@ class _RobotHeaderCardState extends State { ), ); } + + /// 🔥 播放按钮遮罩层 + /// - [showNoStreamHint]:true 时在按钮上方额外显示“暂无视频”提示(视频流打不开的回退态) + /// - [onTap]:点击播放按钮的回调 + Widget _buildPlayOverlay({ + required bool showNoStreamHint, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // 暂无视频提示(仅在 noStream 态显示) + if (showNoStreamHint) ...[ + const Icon(Icons.videocam_off, size: 36, color: Colors.white38), + const SizedBox(height: 8), + const Text( + '暂无视频', + style: TextStyle( + color: Colors.white54, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 12), + ], + // 播放按钮(圆角矩形,与卡片矩形风格呼应) + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: const Color(0xFF165DFF).withOpacity(0.9), + boxShadow: [ + // 主题色柔和光晕,与整体蓝色调呼应 + BoxShadow( + color: const Color(0xFF165DFF).withOpacity(0.35), + blurRadius: 16, + offset: const Offset(0, 4), + ), + // 细微黑色投影,与卡片阴影同色系更和谐 + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.play_arrow_rounded, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(height: 10), + // “播放”文字 + const Text( + '播放', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } } - - diff --git a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart index b3a9bc55..4e9283b7 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart @@ -196,17 +196,9 @@ class RobotItemCard extends StatelessWidget { bgColor = const Color(0xFFF0F9FF); textColor = const Color(0xFF00B42A); break; - case '离线': + default: bgColor = const Color(0xFFF5F0FF); textColor = const Color(0xFFF53F3F); - break; - case '异常': - bgColor = const Color(0xFFFFF7E6); - textColor = const Color(0xFFFF7D00); - break; - default: - bgColor = const Color(0xFFF5F5F5); - textColor = const Color(0xFF86909C); } return Container( diff --git a/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart b/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart index 4b8a5209..132db772 100644 --- a/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart +++ b/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart @@ -1,8 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; +import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart'; import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart'; import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart'; import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; @@ -24,6 +28,10 @@ class _AlarmCenterPageState extends State { late AlarmCubit _cubit; final ScrollController _scrollController = ScrollController(); + /// 监听场站切换 + StreamSubscription? _siteSub; + int? _currentSiteId; + @override void initState() { super.initState(); @@ -33,10 +41,22 @@ class _AlarmCenterPageState extends State { if (_cubit.state is AlarmInitial) { _cubit.loadAlarms(); } + + // 监听场站切换,自动重新加载告警数据 + _currentSiteId = GetIt.I().state.selectedSite?.id; + _siteSub = GetIt.I().stream.listen((siteState) { + if (!mounted) return; + final newSiteId = siteState.selectedSite?.id; + if (newSiteId != _currentSiteId) { + _currentSiteId = newSiteId; + _cubit.loadAlarms(); + } + }); } @override void dispose() { + _siteSub?.cancel(); _scrollController.dispose(); super.dispose(); } diff --git a/lib/features/v2/workorder/presentation/pages/workorder_page.dart b/lib/features/v2/workorder/presentation/pages/workorder_page.dart index 27c91aa4..b954b60e 100644 --- a/lib/features/v2/workorder/presentation/pages/workorder_page.dart +++ b/lib/features/v2/workorder/presentation/pages/workorder_page.dart @@ -1,9 +1,13 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/network/dio_client.dart'; +import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart'; import '../../../../../core/consts/workorder_consts.dart'; import '../../../site/presentation/widgets/site_selector_widget.dart'; import '../cubit/workorder_cubit.dart'; @@ -26,6 +30,10 @@ class _WorkOrderPageState extends State { final ScrollController _scrollController = ScrollController(); late final WorkOrderCubit _cubit; + /// 监听场站切换 + StreamSubscription? _siteSub; + int? _currentSiteId; + @override void initState() { super.initState(); @@ -41,10 +49,22 @@ class _WorkOrderPageState extends State { _scrollController.addListener(_onScroll); _cubit.loadInitialData(); + + // 监听场站切换,自动重新加载工单数据 + _currentSiteId = GetIt.I().state.selectedSite?.id; + _siteSub = GetIt.I().stream.listen((siteState) { + if (!mounted) return; + final newSiteId = siteState.selectedSite?.id; + if (newSiteId != _currentSiteId) { + _currentSiteId = newSiteId; + _cubit.loadInitialData(); + } + }); } @override void dispose() { + _siteSub?.cancel(); _scrollController.dispose(); _cubit.close(); super.dispose();