Compare commits
2 Commits
feature-te
...
feature/my
| Author | SHA1 | Date | |
|---|---|---|---|
| ec4653b8d3 | |||
| ad68952731 |
61
_fix_dup.py
61
_fix_dup.py
@@ -1,61 +0,0 @@
|
||||
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')
|
||||
92
_fix_flow.py
92
_fix_flow.py
@@ -1,92 +0,0 @@
|
||||
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")
|
||||
51
_fix_impl.py
51
_fix_impl.py
@@ -1,51 +0,0 @@
|
||||
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<String, dynamic>;
|
||||
if (data['code'] == 200) {
|
||||
final taskData = data['data'];
|
||||
if (taskData is int) {
|
||||
print('✅ [创建设备任务] 任务ID: $taskData');
|
||||
return taskData;
|
||||
} else if (taskData is Map<String, dynamic>) {
|
||||
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)}')
|
||||
@@ -1,43 +0,0 @@
|
||||
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)}')
|
||||
@@ -1,43 +0,0 @@
|
||||
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)}')
|
||||
@@ -1,74 +0,0 @@
|
||||
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')
|
||||
@@ -1,77 +0,0 @@
|
||||
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<int?>(
|
||||
(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')
|
||||
@@ -1,75 +0,0 @@
|
||||
# -*- 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')
|
||||
@@ -1,99 +0,0 @@
|
||||
# -*- 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')
|
||||
@@ -1,83 +0,0 @@
|
||||
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<CreateDeviceTaskUseCase>().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<CreateDeviceTaskUseCase>().execute(
|
||||
deviceId: deviceId,
|
||||
routeId: routeId,
|
||||
siteId: siteId,
|
||||
orgId: orgId,
|
||||
);
|
||||
|
||||
final taskId = result.fold<int?>(
|
||||
(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')
|
||||
@@ -1,81 +0,0 @@
|
||||
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<String, dynamic>? respJson;\n',
|
||||
' try {\n',
|
||||
' respJson = jsonDecode(responseBody) as Map<String, dynamic>;\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<DevicesCubit>().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<DevicesCubit>().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<LatLng> _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<DevicesCubit>().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}")
|
||||
@@ -1,45 +0,0 @@
|
||||
kotlin version: 2.2.20
|
||||
error message: Failed connecting to the daemon in 4 retries
|
||||
|
||||
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
|
||||
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
|
||||
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
|
||||
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
|
||||
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
|
||||
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
|
||||
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
|
||||
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
|
||||
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
|
||||
at org.gradle.internal.Factories$1.create(Factories.java:31)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
|
||||
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
|
||||
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||
at java.base/java.lang.Thread.run(Unknown Source)
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -8,13 +5,6 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// 🔥 加载签名配置
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.maibu.maibu_satabot_v2"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -40,20 +30,11 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
android/app/proguard-rules.pro
vendored
65
android/app/proguard-rules.pro
vendored
@@ -6,68 +6,3 @@
|
||||
# 如果你用 dio + json_serializable / gson
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# ===== 火山引擎 RTC SDK 保留规则 =====
|
||||
# 保留所有火山引擎相关类
|
||||
-dontwarn com.ss.bytertc.**
|
||||
-keep class com.ss.bytertc.** { *; }
|
||||
|
||||
# 荣耀音频相关类
|
||||
-dontwarn com.hihonor.android.magicx.media.audio.interfaces.**
|
||||
-keep class com.hihonor.android.magicx.media.audio.interfaces.** { *; }
|
||||
|
||||
# ===== 声网 Agora RTC SDK 保留规则 =====
|
||||
# 保留所有声网相关类
|
||||
-dontwarn io.agora.**
|
||||
-keep class io.agora.** { *; }
|
||||
|
||||
# 保留native方法
|
||||
-keepclasseswithmembernames class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# ===== FastJSON 保留规则 =====
|
||||
# AWT 相关
|
||||
-dontwarn java.awt.**
|
||||
-keep class java.awt.Color { *; }
|
||||
-keep class java.awt.Font { *; }
|
||||
-keep class java.awt.Point { *; }
|
||||
-keep class java.awt.Rectangle { *; }
|
||||
|
||||
# Javax Money
|
||||
-dontwarn javax.money.**
|
||||
-keep class javax.money.CurrencyUnit { *; }
|
||||
-dontwarn org.javamoney.moneta.**
|
||||
-keep class org.javamoney.moneta.Money { *; }
|
||||
|
||||
# JAX-RS
|
||||
-dontwarn javax.ws.rs.**
|
||||
-keep class javax.ws.rs.Consumes { *; }
|
||||
-keep class javax.ws.rs.Produces { *; }
|
||||
-keep class javax.ws.rs.core.Response { *; }
|
||||
-keep class javax.ws.rs.core.StreamingOutput { *; }
|
||||
-keep class javax.ws.rs.ext.MessageBodyReader { *; }
|
||||
-keep class javax.ws.rs.ext.MessageBodyWriter { *; }
|
||||
-keep class javax.ws.rs.ext.Provider { *; }
|
||||
|
||||
# Jersey
|
||||
-dontwarn org.glassfish.jersey.internal.spi.**
|
||||
-keep class org.glassfish.jersey.internal.spi.AutoDiscoverable { *; }
|
||||
|
||||
# Joda Time
|
||||
-dontwarn org.joda.time.**
|
||||
-keep class org.joda.time.DateTime { *; }
|
||||
-keep class org.joda.time.DateTimeZone { *; }
|
||||
-keep class org.joda.time.Duration { *; }
|
||||
-keep class org.joda.time.Instant { *; }
|
||||
-keep class org.joda.time.LocalDate { *; }
|
||||
-keep class org.joda.time.LocalDateTime { *; }
|
||||
-keep class org.joda.time.LocalTime { *; }
|
||||
-keep class org.joda.time.Period { *; }
|
||||
-keep class org.joda.time.ReadablePartial { *; }
|
||||
-keep class org.joda.time.format.DateTimeFormat { *; }
|
||||
-keep class org.joda.time.format.DateTimeFormatter { *; }
|
||||
|
||||
# Springfox
|
||||
-dontwarn springfox.documentation.spring.web.json.**
|
||||
-keep class springfox.documentation.spring.web.json.Json { *; }
|
||||
|
||||
@@ -1,42 +1,85 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.maibu.maibu_satabot_v2">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 关键! -->
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<!-- 如果还需要扫描闪光灯控制,可选添加 -->
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
|
||||
<application
|
||||
android:label="maibu_satabot_v2"
|
||||
android:label="飒沓机器人"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:largeHeap="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2"/>
|
||||
android:value="2" />
|
||||
|
||||
<!-- 高德 Key -->
|
||||
<meta-data
|
||||
android:name="com.amap.api.v2.apikey"
|
||||
android:value="bbb1f0f20eed6bf679eddf2625630aba" />
|
||||
|
||||
|
||||
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
// 使用阿里云镜像加速(优先)
|
||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/central") }
|
||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
// 火山引擎 RTC 仓库
|
||||
maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") }
|
||||
// BytePlus 公共仓库(包含火山引擎依赖)
|
||||
maven { url = uri("https://artifact.byteplus.com/repository/public/") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.3,TLSv1.2,TLSv1.1 -Djava.net.preferIPv4Stack=true -Djavax.net.ssl.trustStoreType=JKS
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
|
||||
# 使用阿里云镜像加速
|
||||
android.enableJetifier=true
|
||||
|
||||
# 禁用代理(覆盖全局配置)
|
||||
systemProp.http.proxyHost=
|
||||
systemProp.http.proxyPort=
|
||||
systemProp.https.proxyHost=
|
||||
systemProp.https.proxyPort=
|
||||
|
||||
# 放行SSL证书与协议,解决TLS握手异常
|
||||
systemProp.http.ssl.insecure=true
|
||||
systemProp.http.ssl.allowall=true
|
||||
systemProp.http.ssl.ignore.validity.dates=true
|
||||
|
||||
# Android SDK 镜像配置(使用清华大学镜像)
|
||||
systemProp.android.sdkmanager.channel=stable
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 521 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB |
@@ -19,11 +19,10 @@
|
||||
"password": "Password",
|
||||
"login_button": "Login",
|
||||
"logout": "Logout",
|
||||
"confirm_logout": "Are you sure to logout?",
|
||||
"username_hint": "Enter username",
|
||||
"password_hint": "Enter password",
|
||||
"login_success": "Login Success",
|
||||
"login_failed": "Login Failed"
|
||||
"login_success": "Login successful",
|
||||
"login_failed": "Login failed"
|
||||
},
|
||||
|
||||
"home": {
|
||||
@@ -99,9 +98,7 @@
|
||||
"value": "Value",
|
||||
"initialized": "Initialized",
|
||||
"no_data": "No Data",
|
||||
"voltage": "Voltage",
|
||||
"tcp_reconnected": "TCP connection restored",
|
||||
"tcp_reconnect_failed": "TCP reconnection failed, please check network"
|
||||
"voltage": "Voltage"
|
||||
},
|
||||
|
||||
"machine_details": {
|
||||
@@ -311,185 +308,6 @@
|
||||
"refresh_failed": "Refresh failed"
|
||||
},
|
||||
|
||||
"home_v2": {
|
||||
"select_site": "Select Site",
|
||||
"no_sites": "No available sites",
|
||||
"site_code": "Code: %s",
|
||||
"weather": "Cloudy 28°C",
|
||||
"emergency_alarm": "Emergency Alarm",
|
||||
"real_time_power": "Real-time Power",
|
||||
"today_generation": "Today's Generation",
|
||||
"equivalent_hours": "Equivalent Hours",
|
||||
"alarm_count": "Alarm Count",
|
||||
"pending_work_order": "Pending Orders",
|
||||
"quick_entry": "Quick Entry",
|
||||
"device_report": "Device Report",
|
||||
"scan_qr": "Scan QR",
|
||||
"work_order": "Work Order",
|
||||
"alarm_center": "Alarm Center",
|
||||
"power_plant_map": "Plant Map",
|
||||
"device_list": "Device List",
|
||||
"power_trend": "Power Trend",
|
||||
"day": "Day",
|
||||
"month": "Month",
|
||||
"year": "Year",
|
||||
"plant_overview": "Plant Overview",
|
||||
"installed_capacity": "Installed Capacity",
|
||||
"commission_date": "Commission Date",
|
||||
"work_order_stats": "Work Order Stats",
|
||||
"work_order_pending": "Pending",
|
||||
"work_order_processing": "Processing",
|
||||
"work_order_pending_acceptance": "Pending Acceptance",
|
||||
"power_trend": "Power Trend (Today)",
|
||||
"day": "Day",
|
||||
"month": "Month"
|
||||
},
|
||||
|
||||
"device_list_v2": {
|
||||
"title": "Device Status",
|
||||
"search_hint": "Search device name/ID",
|
||||
"search": "Search",
|
||||
"all": "All",
|
||||
"robot": "Robot",
|
||||
"drone_station": "Drone Station",
|
||||
"inverter": "Inverter",
|
||||
"combiner_box": "Combiner Box",
|
||||
"module": "Module",
|
||||
"monitor": "Monitor",
|
||||
"total_devices": "All Devices",
|
||||
"online": "Online",
|
||||
"alarm": "Alarm",
|
||||
"offline": "Offline",
|
||||
"no_data": "No data",
|
||||
"select_site_first": "Please select a site first",
|
||||
"no_drone_stations": "No drone stations at this site",
|
||||
"view_details_from_tab": "Please view details from the 'Drone Station' tab",
|
||||
"device_type_not_supported": "Device type not supported: %s",
|
||||
"battery_level": "Battery",
|
||||
"current_task": "Current Task",
|
||||
"task": "Task",
|
||||
"power": "Power",
|
||||
"today_energy": "Today Energy",
|
||||
"current": "Current",
|
||||
"temperature": "Temperature",
|
||||
"radiation": "Radiation",
|
||||
"data": "Data",
|
||||
"inspection_robot": "Inspection Robot",
|
||||
"cleaning_robot": "Cleaning Robot",
|
||||
"weeding_robot": "Weeding Robot"
|
||||
},
|
||||
|
||||
"alarm_center": {
|
||||
"title": "Alarm Center",
|
||||
"retry": "Retry",
|
||||
"filter_title": "Filter",
|
||||
"alarm_level": "Alarm Level",
|
||||
"time_range": "Time Range",
|
||||
"area": "Area",
|
||||
"device": "Device",
|
||||
"high_risk": "High Risk",
|
||||
"medium_risk": "Medium Risk",
|
||||
"low_risk": "Low Risk",
|
||||
"hint": "Hint",
|
||||
"today": "Today",
|
||||
"this_week": "This Week",
|
||||
"this_month": "This Month",
|
||||
"all": "All",
|
||||
"apply_filter": "Apply Filter",
|
||||
"ai_diagnosis": "AI Diagnosis",
|
||||
"view_details": "View Details",
|
||||
"unprocessed": "Unprocessed",
|
||||
"processing": "Processing",
|
||||
"confirmed": "Confirmed",
|
||||
"recovered": "Recovered",
|
||||
"unconfirmed": "Unconfirmed",
|
||||
"today_new": "Today's New"
|
||||
},
|
||||
|
||||
"work_order_v2": {
|
||||
"title": "Work Orders",
|
||||
"retry": "Retry",
|
||||
"add": "Add",
|
||||
"pending": "Pending",
|
||||
"executing": "Executing",
|
||||
"completed": "Completed",
|
||||
"all": "All",
|
||||
"today_completed": "Today Completed",
|
||||
"order_no": "Order No.",
|
||||
"priority": "Priority",
|
||||
"create_time": "Create Time",
|
||||
"complete_time": "Complete Time",
|
||||
"location": "Location",
|
||||
"executor": "Executor",
|
||||
"high": "High",
|
||||
"medium": "Medium",
|
||||
"low": "Low",
|
||||
"progress": "Progress"
|
||||
},
|
||||
|
||||
"my_v2": {
|
||||
"title": "Profile",
|
||||
"clear_cache": "Clear Cache",
|
||||
"confirm_clear_cache": "Are you sure to clear all cache data?",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"night_mode": "Night Mode",
|
||||
"system_settings": "System Settings",
|
||||
"personal_info": "Personal Info",
|
||||
"offline_cache": "Offline Cache",
|
||||
"message": "My Messages",
|
||||
"message_settings": "Message Settings",
|
||||
"favorite": "My Favorites",
|
||||
"work_order": "My Orders",
|
||||
"help_feedback": "Help & Feedback",
|
||||
"about_us": "About Us",
|
||||
"suspend_bar": "Float Bar",
|
||||
"suspend_bar_desc": "Show device status float bar at the bottom",
|
||||
"tab_settings": "Tab Settings"
|
||||
},
|
||||
|
||||
"report": {
|
||||
"title": "Field Report",
|
||||
"submit": "Submit",
|
||||
"select_location": "Select Location",
|
||||
"report_type": "Report Type",
|
||||
"device_fault": "Device Fault",
|
||||
"patrol_record": "Patrol Record",
|
||||
"hidden_danger": "Hidden Danger",
|
||||
"defect_report": "Defect Report",
|
||||
"select_device": "Select Device",
|
||||
"please_select_device": "Please select device",
|
||||
"problem_description": "Problem Description",
|
||||
"description_hint": "Please describe the problem in detail...",
|
||||
"upload_media": "Upload Image/Video",
|
||||
"take_photo": "Take Photo",
|
||||
"record_video": "Record Video",
|
||||
"problem_level": "Problem Level",
|
||||
"normal": "Normal",
|
||||
"important": "Important",
|
||||
"urgent": "Urgent",
|
||||
"cancel": "Cancel",
|
||||
"select": "Select",
|
||||
"submit_success": "Submitted",
|
||||
"submit_success_message": "Your report has been submitted successfully",
|
||||
"submit_failed": "Submission Failed",
|
||||
"confirm": "OK",
|
||||
"main_building_a": "Main Building A",
|
||||
"main_building_b": "Main Building B",
|
||||
"boiler_room": "Boiler Room",
|
||||
"turbine_room": "Turbine Room",
|
||||
"control_room": "Control Room",
|
||||
"power_distribution_room": "Power Distribution Room",
|
||||
"boiler_1": "Boiler #1",
|
||||
"boiler_2": "Boiler #2",
|
||||
"turbine_1": "Turbine #1",
|
||||
"generator_1": "Generator #1",
|
||||
"transformer_1": "Transformer #1",
|
||||
"water_pump_1": "Water Pump #1",
|
||||
"select_image": "Select Image",
|
||||
"select_video": "Select Video"
|
||||
},
|
||||
|
||||
"product": {
|
||||
"intro": "Product Introduction",
|
||||
"highlights": "Product Highlights",
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"password": "密码",
|
||||
"login_button": "登录",
|
||||
"logout": "退出登录",
|
||||
"confirm_logout": "确定要退出登录吗?",
|
||||
"username_hint": "请输入用户名",
|
||||
"password_hint": "请输入密码",
|
||||
"login_success": "登录成功",
|
||||
@@ -99,9 +98,7 @@
|
||||
"value": "数值",
|
||||
"initialized": "已初始化",
|
||||
"no_data": "暂无数据",
|
||||
"voltage": "电压",
|
||||
"tcp_reconnected": "TCP连接已恢复",
|
||||
"tcp_reconnect_failed": "TCP重连失败,请检查网络"
|
||||
"voltage": "电压"
|
||||
},
|
||||
|
||||
"machine_details": {
|
||||
@@ -324,186 +321,6 @@
|
||||
"clear_cache_success": "已清除所有缓存,恢复初始状态",
|
||||
"refresh_failed": "刷新失败"
|
||||
},
|
||||
"home_v2": {
|
||||
"select_site": "选择电站",
|
||||
"no_sites": "暂无可用电站",
|
||||
"site_code": "编号: %s",
|
||||
"weather": "多云 28°C",
|
||||
"emergency_alarm": "紧急告警",
|
||||
"real_time_power": "实时发电功率",
|
||||
"today_generation": "今日发电量",
|
||||
"equivalent_hours": "等效小时数",
|
||||
"alarm_count": "告警数",
|
||||
"pending_work_order": "待办工单",
|
||||
"quick_entry": "快捷入口",
|
||||
"device_report": "设备上报",
|
||||
"scan_qr": "扫一扫",
|
||||
"work_order": "工单任务",
|
||||
"alarm_center": "告警中心",
|
||||
"power_plant_map": "电厂地图",
|
||||
"device_list": "设备列表",
|
||||
"power_trend": "发电趋势",
|
||||
"day": "日",
|
||||
"month": "月",
|
||||
"year": "年",
|
||||
"plant_overview": "电站概览",
|
||||
"installed_capacity": "装机容量",
|
||||
"commission_date": "投运日期",
|
||||
"work_order_stats": "工单统计",
|
||||
"work_order_pending": "待处理",
|
||||
"work_order_processing": "处理中",
|
||||
"work_order_pending_acceptance": "待验收",
|
||||
"power_trend": "发电趋势(今日)",
|
||||
"day": "日",
|
||||
"month": "月"
|
||||
},
|
||||
|
||||
"device_list_v2": {
|
||||
"title": "设备状态",
|
||||
"search_hint": "搜索设备名称/编号",
|
||||
"search": "搜索",
|
||||
"all": "全部",
|
||||
"robot": "机器人",
|
||||
"drone_station": "无人机机场",
|
||||
"inverter": "逆变器",
|
||||
"combiner_box": "汇流箱",
|
||||
"module": "组件",
|
||||
"monitor": "监控",
|
||||
"total_devices": "全部设备",
|
||||
"online": "在线",
|
||||
"alarm": "告警",
|
||||
"offline": "离线",
|
||||
"no_data": "暂无数据",
|
||||
"select_site_first": "请先选择场站",
|
||||
"no_drone_stations": "该场站暂无无人机机场",
|
||||
"view_details_from_tab": "请从\"无人机机场\"标签页查看详情",
|
||||
"device_type_not_supported": "该设备类型暂不支持查看详情: %s",
|
||||
"battery_level": "电量",
|
||||
"current_task": "当前任务",
|
||||
"task": "任务",
|
||||
"power": "功率",
|
||||
"today_energy": "今日发电",
|
||||
"current": "电流",
|
||||
"temperature": "温度",
|
||||
"radiation": "辐照度",
|
||||
"data": "数据",
|
||||
"inspection_robot": "巡检机器人",
|
||||
"cleaning_robot": "清洗机器人",
|
||||
"weeding_robot": "除草机器人"
|
||||
},
|
||||
|
||||
"alarm_center": {
|
||||
"title": "告警中心",
|
||||
"retry": "重试",
|
||||
"filter_title": "筛选条件",
|
||||
"alarm_level": "告警等级",
|
||||
"time_range": "时间范围",
|
||||
"area": "区域",
|
||||
"device": "设备",
|
||||
"high_risk": "高危",
|
||||
"medium_risk": "中危",
|
||||
"low_risk": "低危",
|
||||
"hint": "提示",
|
||||
"today": "今天",
|
||||
"this_week": "本周",
|
||||
"this_month": "本月",
|
||||
"all": "全部",
|
||||
"apply_filter": "应用筛选",
|
||||
"ai_diagnosis": "AI 诊断建议",
|
||||
"view_details": "查看详情",
|
||||
"unprocessed": "未处理",
|
||||
"processing": "处理中",
|
||||
"confirmed": "已确认",
|
||||
"recovered": "已恢复",
|
||||
"unconfirmed": "未确认",
|
||||
"today_new": "今日新增"
|
||||
},
|
||||
|
||||
"work_order_v2": {
|
||||
"title": "工单任务",
|
||||
"retry": "重试",
|
||||
"add": "添加",
|
||||
"pending": "待处理",
|
||||
"executing": "执行中",
|
||||
"completed": "已完成",
|
||||
"all": "全部",
|
||||
"today_completed": "今日完成",
|
||||
"order_no": "工单号",
|
||||
"priority": "优先级",
|
||||
"create_time": "创建时间",
|
||||
"complete_time": "完成时间",
|
||||
"location": "地点",
|
||||
"executor": "执行人",
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低",
|
||||
"progress": "进度",
|
||||
"no_data": "暂无数据"
|
||||
},
|
||||
|
||||
"my_v2": {
|
||||
"title": "我的",
|
||||
"clear_cache": "清除缓存",
|
||||
"confirm_clear_cache": "确定要清除所有缓存数据吗?",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"night_mode": "夜间模式",
|
||||
"system_settings": "系统设置",
|
||||
"personal_info": "个人信息",
|
||||
"offline_cache": "离线缓存",
|
||||
"message": "我的消息",
|
||||
"message_settings": "消息设置",
|
||||
"favorite": "我的收藏",
|
||||
"work_order": "我的工单",
|
||||
"help_feedback": "帮助与反馈",
|
||||
"about_us": "关于我们",
|
||||
"suspend_bar": "悬浮条",
|
||||
"suspend_bar_desc": "在页面底部显示设备状态悬浮条",
|
||||
"tab_settings": "Tab 设置"
|
||||
},
|
||||
|
||||
"report": {
|
||||
"title": "现场上报",
|
||||
"submit": "提交",
|
||||
"select_location": "选择位置",
|
||||
"report_type": "上报类型",
|
||||
"device_fault": "设备故障",
|
||||
"patrol_record": "巡检记录",
|
||||
"hidden_danger": "隐患上报",
|
||||
"defect_report": "缺陷上报",
|
||||
"select_device": "选择设备",
|
||||
"please_select_device": "请选择设备",
|
||||
"problem_description": "问题描述",
|
||||
"description_hint": "请详细描述问题现象...",
|
||||
"upload_media": "上传图片/视频",
|
||||
"take_photo": "拍照",
|
||||
"record_video": "录像",
|
||||
"problem_level": "问题等级",
|
||||
"normal": "一般",
|
||||
"important": "重要",
|
||||
"urgent": "紧急",
|
||||
"cancel": "取消",
|
||||
"select": "选择",
|
||||
"submit_success": "提交成功",
|
||||
"submit_success_message": "您的上报已成功提交",
|
||||
"submit_failed": "提交失败",
|
||||
"confirm": "确定",
|
||||
"main_building_a": "主厂房A区",
|
||||
"main_building_b": "主厂房B区",
|
||||
"boiler_room": "锅炉房",
|
||||
"turbine_room": "汽机房",
|
||||
"control_room": "控制室",
|
||||
"power_distribution_room": "配电室",
|
||||
"boiler_1": "锅炉1号",
|
||||
"boiler_2": "锅炉2号",
|
||||
"turbine_1": "汽轮机1号",
|
||||
"generator_1": "发电机1号",
|
||||
"transformer_1": "变压器1号",
|
||||
"water_pump_1": "水泵1号",
|
||||
"select_image": "选择图片",
|
||||
"select_video": "选择视频"
|
||||
},
|
||||
|
||||
"product": {
|
||||
"intro": "产品介绍",
|
||||
"highlights": "产品亮点",
|
||||
|
||||
@@ -2,5 +2,4 @@ description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- get_it: true
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
- provider: true
|
||||
3
dist/download/.gitkeep
vendored
3
dist/download/.gitkeep
vendored
@@ -1,3 +0,0 @@
|
||||
# 完整安装包目录
|
||||
|
||||
将 app-release.apk 放在此目录下用于测试
|
||||
6
dist/manifest.json
vendored
6
dist/manifest.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"version": "1.0.13",
|
||||
"md5": "181176ea51ec26ac97f34c2a550661f0",
|
||||
"targetVersionCode": 1,
|
||||
"abi": "arm64-v8a"
|
||||
}
|
||||
3
dist/patch/.gitkeep
vendored
3
dist/patch/.gitkeep
vendored
@@ -1,3 +0,0 @@
|
||||
# 差量更新包目录
|
||||
|
||||
将生成的 .patch 文件放在此目录下
|
||||
9
dist/version/check.json
vendored
9
dist/version/check.json
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"hasUpdate": true,
|
||||
"patch": {
|
||||
"version": "1.0.2",
|
||||
"patchUrl": "http://127.0.0.1:8080/libapp.so",
|
||||
"md5": "",
|
||||
"targetVersionCode": 1
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
# DeviceTaskCubit 使用指南
|
||||
|
||||
## 功能概述
|
||||
|
||||
DeviceTaskCubit 提供了设备任务管理的完整功能:
|
||||
- 获取任务池并过滤出当前设备的任务
|
||||
- 取消任务
|
||||
- 暂停任务
|
||||
- 恢复任务
|
||||
- 全局管理 taskId
|
||||
|
||||
## 在页面中集成
|
||||
|
||||
### 1. 在 BlocProvider 中注册
|
||||
|
||||
```dart
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../presentation/bloc/device_task_cubit.dart';
|
||||
|
||||
class YourPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => GetIt.I<DeviceTaskCubit>(),
|
||||
child: YourPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取任务池(选择航线时调用)
|
||||
|
||||
```dart
|
||||
// 当用户选择航线后,获取该设备的任务
|
||||
final deviceId = targetDevice?.deviceId ?? '';
|
||||
if (deviceId.isNotEmpty) {
|
||||
context.read<DeviceTaskCubit>().fetchAndFilterTask(deviceId);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 监听状态变化(自动显示错误弹窗)
|
||||
|
||||
```dart
|
||||
BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
|
||||
listener: (context, state) {
|
||||
// 🔥 自动显示错误弹窗(2秒后自动消失)
|
||||
if (state.shouldShowError && state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage!),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 操作成功提示
|
||||
if (state.operationType == DeviceTaskOperationType.cancel &&
|
||||
!state.isLoading) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('取消任务成功')),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// 显示加载状态
|
||||
if (state.isLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
// 显示当前任务ID
|
||||
final taskId = state.currentTaskId;
|
||||
return Text('当前任务ID: $taskId');
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
- `shouldShowError` 为 true 时,表示发生了错误,需要显示弹窗
|
||||
- 弹窗会自动在 2 秒后消失
|
||||
- **不会影响页面展示**,页面继续正常运行
|
||||
|
||||
### 4. 执行任务操作
|
||||
|
||||
#### 取消任务
|
||||
```dart
|
||||
final deviceId = targetDevice?.deviceId ?? '';
|
||||
context.read<DeviceTaskCubit>().cancelTask(deviceId);
|
||||
```
|
||||
|
||||
#### 暂停任务
|
||||
```dart
|
||||
final deviceId = targetDevice?.deviceId ?? '';
|
||||
context.read<DeviceTaskCubit>().pauseTask(deviceId);
|
||||
```
|
||||
|
||||
#### 恢复任务
|
||||
```dart
|
||||
final deviceId = targetDevice?.deviceId ?? '';
|
||||
context.read<DeviceTaskCubit>().recoveryTask(deviceId);
|
||||
```
|
||||
|
||||
### 5. 手动更新 taskId(选择新航线时)
|
||||
|
||||
```dart
|
||||
// 如果需要在选择航线时手动设置 taskId
|
||||
context.read<DeviceTaskCubit>().updateCurrentTaskId(newTaskId);
|
||||
```
|
||||
|
||||
### 6. 清除当前任务
|
||||
|
||||
```dart
|
||||
// 退出页面或切换设备时清除
|
||||
context.read<DeviceTaskCubit>().clearCurrentTask();
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **自动获取用户信息和场站ID**
|
||||
- Cubit 内部会自动从 AppUserCubit 和 SiteCubit 获取所需参数
|
||||
- 无需手动传递 userId、orgId、siteId
|
||||
|
||||
2. **统一错误处理**
|
||||
- 所有错误都会通过 ErrorHandler 转换为友好提示
|
||||
- 不会显示原始错误信息(如 HTTP 404、连接超时等)
|
||||
|
||||
3. **taskId 全局管理**
|
||||
- fetchAndFilterTask 会自动过滤并保存当前设备的 taskId
|
||||
- 所有操作接口都会使用这个全局保存的 taskId
|
||||
|
||||
4. **状态监听**
|
||||
- operationType 可以区分当前正在进行的操作类型
|
||||
- isLoading 表示是否正在执行网络请求
|
||||
|
||||
## 完整示例
|
||||
|
||||
```dart
|
||||
class RoutePlanningPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => GetIt.I<DeviceTaskCubit>(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: const Text('路径规划')),
|
||||
body: BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
|
||||
listener: (context, state) {
|
||||
if (state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage!)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
// 显示当前任务ID
|
||||
Text('当前任务ID: ${state.currentTaskId ?? "无"}'),
|
||||
|
||||
// 操作按钮
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final deviceId = 'YOUR_DEVICE_ID';
|
||||
context.read<DeviceTaskCubit>().pauseTask(deviceId);
|
||||
},
|
||||
child: const Text('暂停'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final deviceId = 'YOUR_DEVICE_ID';
|
||||
context.read<DeviceTaskCubit>().recoveryTask(deviceId);
|
||||
},
|
||||
child: const Text('恢复'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final deviceId = 'YOUR_DEVICE_ID';
|
||||
context.read<DeviceTaskCubit>().cancelTask(deviceId);
|
||||
},
|
||||
child: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 加载指示器
|
||||
if (state.isLoading)
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理优化
|
||||
|
||||
项目中已实现统一的错误处理机制:
|
||||
- 所有接口错误都会转换为友好的中文提示
|
||||
- 不会显示原始的错误信息(如 "HTTP 404"、"Connection timeout" 等)
|
||||
- 提示会自动消失(2秒后)
|
||||
- 页面不会崩溃
|
||||
|
||||
示例错误提示:
|
||||
- "网络连接超时,请检查网络设置"
|
||||
- "登录已过期,请重新登录"
|
||||
- "操作失败,请稍后重试"
|
||||
- "未知错误,请稍后重试"
|
||||
@@ -1,296 +0,0 @@
|
||||
# 无人机机场 OSD 实时数据展示功能
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
在无人机机场详情页面添加了 **OSD 实时数据卡片**,通过 MQTT 订阅 `thing/product/${gatewaySn}/osd` topic,实时展示机场的遥测数据。
|
||||
|
||||
## 🎯 核心特性
|
||||
|
||||
### 1. 滑动窗口设计
|
||||
- **左右滑动切换**:用户可以左右滑动查看不同的 OSD 数据字段
|
||||
- **底部指示器**:显示当前页码和总页数
|
||||
- **滑动提示**:左右箭头提示用户可以滑动
|
||||
|
||||
### 2. 智能状态管理
|
||||
- **在线时**:显示实时 OSD 数据(电量、温度、风速等)
|
||||
- **离线时**:显示"机场离线,暂无实时数据"提示
|
||||
- **加载中**:显示加载动画
|
||||
|
||||
### 3. 数据字段展示
|
||||
|
||||
目前展示的 OSD 字段包括:
|
||||
|
||||
| 字段 | 图标 | 颜色规则 | 示例值 |
|
||||
|------|------|---------|--------|
|
||||
| 电量 | 🔋 battery_full | >50% 绿色, 20-50% 橙色, <20% 红色 | 85% |
|
||||
| 温度 | 🌡️ thermostat | 蓝色 | 25°C |
|
||||
| 风速 | 💨 air | 绿色 | 3.5 m/s |
|
||||
| 降雨量 | 💧 water_drop | 蓝色 | 小雨 / 12 mm |
|
||||
| 网络状态 | 📶 network_check | 灰色 | 等级 4 |
|
||||
| GPS | 🛰️ gps_fixed | 紫色 | 12 颗 |
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
lib/features/v2/device_list/
|
||||
├── presentation/
|
||||
│ ├── pages/
|
||||
│ │ └── drone_station_detail_page.dart # 详情页(集成 OSD 卡片)
|
||||
│ └── widgets/
|
||||
│ └── drone_station_osd_card.dart # OSD 实时数据卡片组件
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 1. MQTT 数据订阅
|
||||
|
||||
```dart
|
||||
// 订阅机场 OSD topic
|
||||
final stationTopic = 'thing/product/$gatewaySn/osd';
|
||||
|
||||
// 监听数据流
|
||||
_subscription = _dataSource.stationOsdStream.listen((osd) {
|
||||
setState(() {
|
||||
_currentOsd = osd;
|
||||
_parseOsdFields(osd); // 解析并更新 UI
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 数据解析
|
||||
|
||||
```dart
|
||||
void _parseOsdFields(DroneOsdEntity osd) {
|
||||
final data = osd.rawData;
|
||||
|
||||
_osdFields = [
|
||||
{
|
||||
'icon': Icons.battery_full_rounded,
|
||||
'label': '电量',
|
||||
'value': data['battery'] != null ? '${data['battery']}%' : '未知',
|
||||
'color': _getBatteryColor(data['battery']),
|
||||
},
|
||||
// ... 其他字段
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 滑动卡片实现
|
||||
|
||||
使用 `PageView.builder` 实现左右滑动:
|
||||
|
||||
```dart
|
||||
PageView.builder(
|
||||
itemCount: _osdFields.length,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final field = _osdFields[index];
|
||||
return _buildOsdPage(field, index);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## 🎨 UI 设计
|
||||
|
||||
### 卡片布局
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ [图标] 电量 ◀ ▶ │
|
||||
│ │
|
||||
│ 85% │
|
||||
│ │
|
||||
│ 1 / 6 │
|
||||
│ │
|
||||
│ ● ○ ○ ○ ○ ○ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 状态指示器
|
||||
- **当前页**:长条蓝色圆点(16px)
|
||||
- **其他页**:短条灰色圆点(6px)
|
||||
|
||||
## 📊 数据映射规则
|
||||
|
||||
### 降雨量格式化
|
||||
|
||||
```dart
|
||||
String _formatRainfall(dynamic rainfall) {
|
||||
if (rainfall is String) {
|
||||
switch (rainfall.toLowerCase()) {
|
||||
case 'no_rain': return '无降雨';
|
||||
case 'light_rain': return '小雨';
|
||||
case 'moderate_rain': return '中雨';
|
||||
case 'heavy_rain': return '大雨';
|
||||
default: return '$rainfall mm';
|
||||
}
|
||||
}
|
||||
return '$rainfall mm';
|
||||
}
|
||||
```
|
||||
|
||||
### 电量颜色规则
|
||||
|
||||
```dart
|
||||
Color _getBatteryColor(dynamic battery) {
|
||||
final value = battery is num ? battery.toDouble() : 0.0;
|
||||
if (value > 50) return Color(0xFF00B42A); // 绿色
|
||||
if (value > 20) return Color(0xFFFF7D00); // 橙色
|
||||
return Color(0xFFF53F3F); // 红色
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 生命周期管理
|
||||
|
||||
### 启动监听
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startListening(); // 开始监听 MQTT
|
||||
}
|
||||
```
|
||||
|
||||
### 停止监听
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
_stopListening(); // 取消订阅,释放资源
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### 动态更新
|
||||
```dart
|
||||
@override
|
||||
void didUpdateWidget(DroneStationOsdCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// gatewaySn 或 isOnline 变化时重新订阅
|
||||
if (oldWidget.gatewaySn != widget.gatewaySn ||
|
||||
oldWidget.isOnline != widget.isOnline) {
|
||||
_stopListening();
|
||||
_startListening();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 在详情页中集成
|
||||
|
||||
```dart
|
||||
DroneStationOsdCard(
|
||||
gatewaySn: widget.station.gatewaySn,
|
||||
isOnline: detail.isOnline,
|
||||
)
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `gatewaySn` | String | 机场网关序列号(用于 MQTT topic) |
|
||||
| `isOnline` | bool | 机场是否在线(决定是否显示实时数据) |
|
||||
|
||||
## 📝 扩展建议
|
||||
|
||||
### 1. 添加更多 OSD 字段
|
||||
|
||||
在 `_parseOsdFields` 方法中添加新字段:
|
||||
|
||||
```dart
|
||||
{
|
||||
'icon': Icons.new_icon,
|
||||
'label': '新字段名称',
|
||||
'value': data['newField'] != null ? '${data['newField']}' : '未知',
|
||||
'color': const Color(0xFF165DFF),
|
||||
},
|
||||
```
|
||||
|
||||
### 2. 自定义滑动方向
|
||||
|
||||
修改 `PageView` 的 `scrollDirection`:
|
||||
|
||||
```dart
|
||||
PageView.builder(
|
||||
scrollDirection: Axis.vertical, // 改为上下滑动
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 自动轮播
|
||||
|
||||
添加定时器自动切换卡片:
|
||||
|
||||
```dart
|
||||
Timer.periodic(Duration(seconds: 3), (timer) {
|
||||
if (_osdFields.isNotEmpty) {
|
||||
setState(() {
|
||||
_currentIndex = (_currentIndex + 1) % _osdFields.length;
|
||||
});
|
||||
_pageController.animateToPage(_currentIndex, ...);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 手势优化
|
||||
|
||||
添加双击放大、长按复制等功能:
|
||||
|
||||
```dart
|
||||
GestureDetector(
|
||||
onDoubleTap: () {
|
||||
// 双击放大
|
||||
},
|
||||
onLongPress: () {
|
||||
// 长按复制数值
|
||||
Clipboard.setData(ClipboardData(text: field['value']));
|
||||
},
|
||||
child: _buildOsdPage(field, index),
|
||||
)
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **MQTT 连接**:确保应用已连接到 MQTT 服务器
|
||||
2. **topic 格式**:必须为 `thing/product/${gatewaySn}/osd`
|
||||
3. **数据格式**:MQTT 消息 payload 必须是 JSON 格式
|
||||
4. **内存管理**:页面销毁时必须取消订阅,避免内存泄漏
|
||||
5. **在线判断**:只有机场在线时才订阅 MQTT,离线时不订阅
|
||||
|
||||
## 🐛 调试技巧
|
||||
|
||||
### 查看 MQTT 日志
|
||||
|
||||
```dart
|
||||
debugPrint('🔊 [DroneStationOsdCard] 开始监听机场 OSD: ${widget.gatewaySn}');
|
||||
debugPrint('✅ [DroneStationOsdCard] 收到机场 OSD 数据');
|
||||
```
|
||||
|
||||
### 检查数据解析
|
||||
|
||||
```dart
|
||||
debugPrint('📊 [DroneStationOsdCard] OSD 原始数据: ${osd.rawData}');
|
||||
debugPrint('📊 [DroneStationOsdCard] 解析后字段数: ${_osdFields.length}');
|
||||
```
|
||||
|
||||
## 📦 依赖项
|
||||
|
||||
- `flutter_bloc`: 状态管理
|
||||
- `mqtt_client`: MQTT 通信
|
||||
- `equatable`: 实体类比较
|
||||
- `get_it`: 依赖注入
|
||||
|
||||
## ✅ 完成清单
|
||||
|
||||
- [x] 创建 `DroneStationOsdCard` 组件
|
||||
- [x] 实现 MQTT 数据订阅
|
||||
- [x] 实现滑动窗口 UI
|
||||
- [x] 实现数据解析和格式化
|
||||
- [x] 集成到详情页
|
||||
- [x] 添加状态管理(在线/离线/加载中)
|
||||
- [x] 添加生命周期管理
|
||||
- [x] 添加调试日志
|
||||
@@ -1,303 +0,0 @@
|
||||
# Flutter 项目统一错误处理规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
**所有接口异常都不应该影响页面展示,只显示友好提示弹窗(2秒后自动消失)**
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. Cubit/Bloc 层处理
|
||||
|
||||
#### ✅ 正确做法
|
||||
|
||||
```dart
|
||||
class MyCubit extends Cubit<MyState> {
|
||||
Future<void> fetchData() async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
final result = await useCase.call(params);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
// 🔥 设置错误信息 + 标记需要显示弹窗
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 关键!
|
||||
));
|
||||
},
|
||||
(data) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
data: data,
|
||||
));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// 🔥 捕获所有异常
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 关键!
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ❌ 错误做法
|
||||
|
||||
```dart
|
||||
// ❌ 不要抛出异常到页面层
|
||||
throw Exception('网络错误');
|
||||
|
||||
// ❌ 不要显示原始错误信息
|
||||
emit(state.copyWith(errorMessage: e.toString()));
|
||||
|
||||
// ❌ 不要让页面崩溃
|
||||
if (error) throw error;
|
||||
```
|
||||
|
||||
### 2. State 设计
|
||||
|
||||
```dart
|
||||
class MyState extends Equatable {
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final bool shouldShowError; // 🔥 关键字段
|
||||
|
||||
const MyState({
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
MyState copyWith({
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
bool? shouldShowError,
|
||||
}) {
|
||||
return MyState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage,
|
||||
shouldShowError: shouldShowError ?? false, // 🔥 默认重置为 false
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
- `shouldShowError` 默认为 `false`
|
||||
- 每次 emit 时如果不传,会自动重置为 `false`
|
||||
- 这样可以确保错误弹窗只显示一次
|
||||
|
||||
### 3. 页面层监听
|
||||
|
||||
```dart
|
||||
BlocConsumer<MyCubit, MyState>(
|
||||
listener: (context, state) {
|
||||
// 🔥 自动显示错误弹窗
|
||||
if (state.shouldShowError && state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage!),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// 页面正常渲染,不受错误影响
|
||||
if (state.isLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
return YourContent();
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### 4. ErrorHandler 工具类
|
||||
|
||||
```dart
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ErrorHandler {
|
||||
/// 获取友好的错误消息
|
||||
static String getErrorMessage(Object error) {
|
||||
if (error is DioException) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return '网络连接超时,请检查网络设置';
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return '网络连接失败,请检查网络';
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode;
|
||||
if (statusCode == 401) {
|
||||
return '登录已过期,请重新登录';
|
||||
} else if (statusCode == 403) {
|
||||
return '没有权限执行此操作';
|
||||
} else if (statusCode == 404) {
|
||||
return '请求的资源不存在';
|
||||
} else if (statusCode == 500) {
|
||||
return '服务器异常,请稍后重试';
|
||||
} else {
|
||||
return '服务器响应异常';
|
||||
}
|
||||
|
||||
default:
|
||||
return '请求失败,请稍后重试';
|
||||
}
|
||||
} else {
|
||||
return '操作失败,请稍后重试';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### DeviceTaskCubit 示例
|
||||
|
||||
```dart
|
||||
class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
Future<void> cancelTask(String deviceId) async {
|
||||
final taskId = state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
emit(state.copyWith(errorMessage: '无可用任务'));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.cancel,
|
||||
));
|
||||
|
||||
try {
|
||||
final result = await _cancelTaskUseCase.call(params);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('❌ 取消任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
));
|
||||
},
|
||||
(success) {
|
||||
_logger.logWithLevel('✅ 取消任务成功');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 取消任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 页面使用示例
|
||||
|
||||
```dart
|
||||
class TaskPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => GetIt.I<DeviceTaskCubit>(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: const Text('任务管理')),
|
||||
body: BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
|
||||
listener: (context, state) {
|
||||
// 🔥 自动显示错误弹窗
|
||||
if (state.shouldShowError && state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage!),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
// 页面内容不受错误影响
|
||||
Text('当前任务ID: ${state.currentTaskId ?? "无"}'),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DeviceTaskCubit>().cancelTask(deviceId);
|
||||
},
|
||||
child: const Text('取消任务'),
|
||||
),
|
||||
|
||||
if (state.isLoading)
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误提示文案规范
|
||||
|
||||
| 错误类型 | 提示文案 |
|
||||
|---------|---------|
|
||||
| 网络超时 | "网络连接超时,请检查网络设置" |
|
||||
| 连接失败 | "网络连接失败,请检查网络" |
|
||||
| 401 未授权 | "登录已过期,请重新登录" |
|
||||
| 403 禁止访问 | "没有权限执行此操作" |
|
||||
| 404 资源不存在 | "请求的资源不存在" |
|
||||
| 500 服务器错误 | "服务器异常,请稍后重试" |
|
||||
| 其他业务错误 | "操作失败,请稍后重试" |
|
||||
| 未知错误 | "未知错误,请稍后重试" |
|
||||
|
||||
**注意:**
|
||||
- ✅ 使用友好的中文提示
|
||||
- ❌ 不要显示 HTTP 状态码
|
||||
- ❌ 不要显示技术术语(如 "DioException"、"timeout")
|
||||
- ❌ 不要显示完整的错误堆栈
|
||||
|
||||
## 优势
|
||||
|
||||
1. **页面不崩溃** - 所有异常都被捕获
|
||||
2. **用户体验好** - 友好的中文提示
|
||||
3. **自动消失** - 2秒后弹窗自动关闭
|
||||
4. **不影响操作** - 用户可以继续使用页面
|
||||
5. **统一规范** - 全项目统一的错误处理方式
|
||||
|
||||
## 检查清单
|
||||
|
||||
在开发新功能时,确保:
|
||||
|
||||
- [ ] Cubit 中所有 try-catch 都使用了 `ErrorHandler.getErrorMessage()`
|
||||
- [ ] State 中有 `shouldShowError` 字段
|
||||
- [ ] 错误时设置 `shouldShowError: true`
|
||||
- [ ] copyWith 中 `shouldShowError` 默认为 `false`
|
||||
- [ ] 页面 BlocConsumer listener 中监听 `shouldShowError`
|
||||
- [ ] 使用 SnackBar 显示错误(2秒自动消失)
|
||||
- [ ] 不显示原始错误信息
|
||||
@@ -1,388 +0,0 @@
|
||||
# 统一错误处理修复记录
|
||||
|
||||
## 问题描述
|
||||
|
||||
之前项目中存在多个地方直接将原始错误信息(如 `DioException [connection error]...`)显示在页面上,严重影响用户体验。这些错误包括:
|
||||
- 网络连接失败
|
||||
- HTTP 状态码错误
|
||||
- 超时错误
|
||||
- 其他技术异常
|
||||
|
||||
**问题表现:**
|
||||
- 全屏显示错误页面
|
||||
- 直接展示技术错误信息(如 `DioException`、`HTTP 500` 等)
|
||||
- 用户无法继续操作
|
||||
- 错误信息不友好,普通用户看不懂
|
||||
|
||||
## 解决方案
|
||||
|
||||
采用统一的错误处理机制,确保:
|
||||
1. ✅ **所有错误都被拦截**,不会直接显示原始错误
|
||||
2. ✅ **友好的中文提示**,使用 SnackBar 浮动显示 2 秒后自动消失
|
||||
3. ✅ **不影响页面渲染**,错误发生时页面保持不变,用户可以继续操作
|
||||
4. ✅ **统一的错误处理模式**,所有 BLoC/Cubit 都遵循相同规范
|
||||
|
||||
## 修复的文件列表
|
||||
|
||||
### 1. DeviceStatusBloc (设备状态)
|
||||
**文件路径:** `lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart`
|
||||
|
||||
**修改内容:**
|
||||
- ✅ 添加 `ErrorHandler` 导入
|
||||
- ✅ 将 `emit(DeviceStatusError(e.toString()))` 改为使用 `ErrorHandler.getErrorMessage(e)`
|
||||
- ✅ 设置 `shouldShowError: true` 标记
|
||||
|
||||
**State 更新:**
|
||||
```dart
|
||||
class DeviceStatusError extends DeviceStatusState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 新增
|
||||
|
||||
const DeviceStatusError({
|
||||
required this.message,
|
||||
this.shouldShowError = false,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**页面更新:**
|
||||
```dart
|
||||
// device_status_page.dart
|
||||
BlocConsumer<DeviceStatusBloc, DeviceStatusState>(
|
||||
listener: (context, state) {
|
||||
if (state is DeviceStatusError && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DeviceStatusError) {
|
||||
return const SizedBox.shrink(); // 🔥 不再显示全屏错误
|
||||
}
|
||||
// ... 正常内容
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. DroneStationBloc (无人机机场)
|
||||
**文件路径:** `lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart`
|
||||
|
||||
**修改内容:**
|
||||
- ✅ 添加 `ErrorHandler` 导入
|
||||
- ✅ 更新所有错误状态:
|
||||
- `DroneStationError`
|
||||
- `UAVDetailError`
|
||||
- `VideoStreamError`
|
||||
- `UavVideoStreamError`
|
||||
- ✅ 所有错误都设置 `shouldShowError: true`
|
||||
|
||||
**State 更新:**
|
||||
```dart
|
||||
// lib/features/v2/device_list/presentation/bloc/drone_station_state.dart
|
||||
class DroneStationError extends DroneStationState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 新增
|
||||
|
||||
const DroneStationError({
|
||||
required this.message,
|
||||
this.shouldShowError = false,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**页面更新:**
|
||||
```dart
|
||||
// device_status_page.dart - _buildDroneStationList
|
||||
BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
if (state is DroneStationError && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(...);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DroneStationError) {
|
||||
return const SizedBox.shrink(); // 🔥 保持页面
|
||||
}
|
||||
// ... 正常内容
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. RobotListBloc (机器人列表)
|
||||
**文件路径:** `lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart`
|
||||
|
||||
**修改内容:**
|
||||
- ✅ 添加 `ErrorHandler` 导入
|
||||
- ✅ 将 `emit(RobotListError(e.toString()))` 改为使用 `ErrorHandler`
|
||||
- ✅ 设置 `shouldShowError: true`
|
||||
|
||||
**State 更新:**
|
||||
```dart
|
||||
class RobotListError extends RobotListState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 新增
|
||||
|
||||
const RobotListError({
|
||||
required this.message,
|
||||
this.shouldShowError = false,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**页面更新:**
|
||||
```dart
|
||||
// robot_list_page.dart
|
||||
BlocConsumer<RobotListBloc, RobotListState>(
|
||||
listener: (context, state) {
|
||||
if (state is RobotListError && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(...);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is RobotListError) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// ... 正常内容
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. HomeV2Bloc (首页 V2)
|
||||
**文件路径:** `lib/features/v2/home/presentation/bloc/home_v2_bloc.dart`
|
||||
|
||||
**修改内容:**
|
||||
- ✅ 添加 `ErrorHandler` 导入
|
||||
- ✅ 将所有 `emit(HomeV2Error(failure.message))` 改为使用 `ErrorHandler`
|
||||
- ✅ 设置 `shouldShowError: true`
|
||||
|
||||
**State 更新:**
|
||||
```dart
|
||||
class HomeV2Error extends HomeV2State {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 新增
|
||||
|
||||
const HomeV2Error({
|
||||
required this.message,
|
||||
this.shouldShowError = false,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**页面更新:**
|
||||
```dart
|
||||
// home_v2_page.dart
|
||||
BlocConsumer<HomeV2Bloc, HomeV2State>(
|
||||
listener: (context, state) {
|
||||
if (state is HomeV2Error && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(...);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is HomeV2Error) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// ... 正常内容
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 统一错误处理流程
|
||||
|
||||
### 1. BLoC/Cubit 层
|
||||
```dart
|
||||
try {
|
||||
// 业务逻辑
|
||||
final result = await useCase.call(params);
|
||||
result.fold(
|
||||
(failure) => emit(ErrorState(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 关键:标记需要显示弹窗
|
||||
)),
|
||||
(data) => emit(SuccessState(data)),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(ErrorState(
|
||||
message: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 关键:标记需要显示弹窗
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
### 2. State 层
|
||||
```dart
|
||||
class ErrorState extends SomeState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 关键:用于标记是否显示弹窗
|
||||
|
||||
const ErrorState({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认不显示
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Page 层
|
||||
```dart
|
||||
BlocConsumer<SomeBloc, SomeState>(
|
||||
listener: (context, state) {
|
||||
// 🔥 监听错误并显示友好提示
|
||||
if (state is ErrorState && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is ErrorState) {
|
||||
// 🔥 返回空容器,保持当前页面不变
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// ... 正常内容
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ErrorHandler 工具类
|
||||
|
||||
**文件路径:** `lib/core/network/error_handler.dart`
|
||||
|
||||
**功能:**
|
||||
- 将所有 Dio 异常转换为友好的中文提示
|
||||
- 支持多种错误类型:
|
||||
- 连接超时 → "网络连接超时,请检查网络设置"
|
||||
- 连接失败 → "网络连接失败,请检查网络"
|
||||
- 401 → "登录已过期,请重新登录"
|
||||
- 403 → "没有权限执行此操作"
|
||||
- 404 → "请求的资源不存在"
|
||||
- 500 → "服务器异常,请稍后重试"
|
||||
- 其他 → "操作失败,请稍后重试"
|
||||
|
||||
**使用方法:**
|
||||
```dart
|
||||
// 在 BLoC/Cubit 中
|
||||
String friendlyMessage = ErrorHandler.getErrorMessage(error);
|
||||
|
||||
// 或在页面中直接显示
|
||||
ErrorHandler.handleError(context, error);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 修复效果对比
|
||||
|
||||
### 修复前 ❌
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ │
|
||||
│ ⚠️ Error Icon │
|
||||
│ │
|
||||
│ DioException [connection │
|
||||
│ error]: The connection │
|
||||
│ errored: Failed host │
|
||||
│ lookup... │
|
||||
│ │
|
||||
│ [ 重试 ] │
|
||||
│ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
- 全屏错误页面
|
||||
- 显示原始技术错误
|
||||
- 用户无法继续操作
|
||||
- 体验极差
|
||||
|
||||
### 修复后 ✅
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ │
|
||||
│ [正常页面内容] │
|
||||
│ │
|
||||
│ 用户可以看到页面 │
|
||||
│ 可以继续操作 │
|
||||
│ │
|
||||
└─────────────────────────────┘
|
||||
↓
|
||||
┌──────────────┐
|
||||
│ 网络连接失败 │ ← SnackBar 浮动显示
|
||||
│ 请检查网络 │ 2秒后自动消失
|
||||
└──────────────┘
|
||||
```
|
||||
- 页面保持不变
|
||||
- 友好的中文提示
|
||||
- SnackBar 浮动显示
|
||||
- 2秒自动消失
|
||||
- 用户可以继续操作
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### ✅ DO - 应该这样做
|
||||
1. 所有 BLoC/Cubit 的错误处理都使用 `ErrorHandler.getErrorMessage()`
|
||||
2. 所有错误状态都设置 `shouldShowError: true`
|
||||
3. 页面使用 `BlocConsumer` 监听错误
|
||||
4. 错误时使用 `SizedBox.shrink()` 保持页面
|
||||
5. SnackBar 设置为 `floating` 和 `orange` 背景
|
||||
|
||||
### ❌ DON'T - 不要这样做
|
||||
1. ❌ 直接使用 `e.toString()` 作为错误消息
|
||||
2. ❌ 在页面中显示全屏错误页面
|
||||
3. ❌ 直接展示原始技术错误(DioException、HTTP 状态码等)
|
||||
4. ❌ 让用户点击"重试"按钮才能恢复
|
||||
5. ❌ 不同页面使用不同的错误处理方式
|
||||
|
||||
---
|
||||
|
||||
## 后续工作
|
||||
|
||||
### 待迁移的 BLoC/Cubit
|
||||
以下 BLoC/Cubit 可能还需要迁移到统一的错误处理模式:
|
||||
- [ ] 其他未检查的 BLoC
|
||||
- [ ] 第三方库相关的错误处理
|
||||
- [ ] WebSocket 连接的错误处理
|
||||
|
||||
### 优化建议
|
||||
1. 考虑为不同类型的错误设置不同的 SnackBar 颜色
|
||||
2. 可以添加错误日志上报功能
|
||||
3. 可以考虑添加错误重试机制(在后台自动重试)
|
||||
4. 可以为特定场景定制错误提示文案
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
通过本次修复,我们实现了:
|
||||
✅ 统一的错误处理机制
|
||||
✅ 友好的用户提示
|
||||
✅ 不影响页面渲染
|
||||
✅ 提升用户体验
|
||||
✅ 代码规范统一
|
||||
|
||||
**核心原则:**
|
||||
> 任何接口异常都不应该影响页面的展示,给个几秒弹窗就行!
|
||||
|
||||
---
|
||||
|
||||
**修复日期:** 2026-06-15
|
||||
**修复人员:** AI Assistant
|
||||
**影响范围:** 设备管理、首页相关的所有 BLoC/Cubit
|
||||
@@ -1,197 +0,0 @@
|
||||
# Tab 配置系统开发规范
|
||||
|
||||
## 📋 功能说明
|
||||
|
||||
Tab 配置系统支持动态开关控制底部导航栏的 Tab 项显示/隐藏,配置自动保存到 SharedPreferences。
|
||||
|
||||
## 🔧 添加新 Tab 项的步骤
|
||||
|
||||
当需要添加新的 Tab 项时,必须按以下顺序修改 **3 个文件**:
|
||||
|
||||
### 1️⃣ 修改 `tab_config.dart` - 添加默认配置
|
||||
|
||||
**文件路径:** `lib/features/main_container/domain/tab_config.dart`
|
||||
|
||||
在 `TabConfig.defaultConfig()` 方法的 `items` 列表中添加新的 `TabConfigItem`:
|
||||
|
||||
```dart
|
||||
TabConfigItem(
|
||||
id: 'your_tab_id', // 唯一标识符(小写+下划线)
|
||||
name: '显示名称', // 中文名称(简洁,建议2-4个字)
|
||||
nameEn: 'Display Name', // 英文名称
|
||||
icon: 'icon_name_rounded', // 图标名称(Material Icons)
|
||||
isEnabled: true, // 默认是否启用(true/false)
|
||||
order: 7, // 排序序号(从1开始递增)
|
||||
),
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- `id` 必须唯一,建议使用有意义的英文标识
|
||||
- `name` 要简洁,符合 TAB 命名规范
|
||||
- `order` 决定 Tab 在导航栏中的显示顺序
|
||||
- `isEnabled` 控制默认是否显示该 Tab
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ 修改 `custom_main_container.dart` - 添加页面映射
|
||||
|
||||
**文件路径:** `lib/features/main_container/presentation/pages/custom_main_container.dart`
|
||||
|
||||
#### 2.1 导入新页面
|
||||
|
||||
在文件顶部添加新页面的 import:
|
||||
|
||||
```dart
|
||||
import 'package:maibu_satabot_v2/features/xxx/presentation/pages/your_page.dart';
|
||||
```
|
||||
|
||||
#### 2.2 在 `_buildPages` 方法中添加 case
|
||||
|
||||
```dart
|
||||
List<Widget> _buildPages(List<dynamic> enabledTabs) {
|
||||
return enabledTabs.map((tab) {
|
||||
switch (tab.id) {
|
||||
case 'home_v2':
|
||||
return const HomeV2Page();
|
||||
case 'home':
|
||||
return const HomePage();
|
||||
case 'device':
|
||||
return const DeviceStatusPage();
|
||||
case 'ai':
|
||||
return const AiPage();
|
||||
case 'warning':
|
||||
return const WarningCenterPage();
|
||||
case 'my':
|
||||
return const MyPage();
|
||||
case 'your_tab_id': // 👈 新增这个 case
|
||||
return const YourPage();
|
||||
default:
|
||||
return const HomePage();
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ 修改 `tab_settings_page.dart` - 添加图标映射
|
||||
|
||||
**文件路径:** `lib/features/main_container/presentation/pages/tab_settings_page.dart`
|
||||
|
||||
在 `_getIconData` 方法中添加新图标的映射:
|
||||
|
||||
```dart
|
||||
IconData _getIconData(String iconName) {
|
||||
switch (iconName) {
|
||||
case 'home_rounded':
|
||||
return Icons.home_rounded;
|
||||
case 'grid_view_rounded':
|
||||
return Icons.grid_view_rounded;
|
||||
case 'devices_rounded':
|
||||
return Icons.devices_rounded;
|
||||
case 'auto_awesome_rounded':
|
||||
return Icons.auto_awesome_rounded;
|
||||
case 'warning_amber_rounded':
|
||||
return Icons.warning_amber_rounded;
|
||||
case 'person_rounded':
|
||||
return Icons.person_rounded;
|
||||
case 'your_icon_name': // 👈 新增这个 case
|
||||
return Icons.your_icon_name;
|
||||
default:
|
||||
return Icons.home_rounded;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意:** 如果使用的是 Material Icons 标准图标,通常不需要修改此文件,除非使用了特殊图标。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 检查清单
|
||||
|
||||
添加新 Tab 后,请确认:
|
||||
|
||||
- [ ] 已在 `tab_config.dart` 中添加 `TabConfigItem` 配置
|
||||
- [ ] 已在 `custom_main_container.dart` 中导入新页面
|
||||
- [ ] 已在 `custom_main_container.dart` 的 `switch` 中添加 case 分支
|
||||
- [ ] 已在 `tab_settings_page.dart` 中添加图标映射(如需要)
|
||||
- [ ] `id`、`order` 没有与其他 Tab 冲突
|
||||
- [ ] 测试开关功能是否正常
|
||||
- [ ] 测试切换 Tab 是否正常显示对应页面
|
||||
|
||||
---
|
||||
|
||||
## 🚀 完整示例:添加"消息"Tab
|
||||
|
||||
假设要添加一个"消息"Tab,显示消息中心页面:
|
||||
|
||||
### 步骤 1:修改 `tab_config.dart`
|
||||
|
||||
```dart
|
||||
TabConfigItem(
|
||||
id: 'message',
|
||||
name: '消息',
|
||||
nameEn: 'Messages',
|
||||
icon: 'notifications_rounded',
|
||||
isEnabled: true,
|
||||
order: 7,
|
||||
),
|
||||
```
|
||||
|
||||
### 步骤 2:修改 `custom_main_container.dart`
|
||||
|
||||
```dart
|
||||
// 顶部添加导入
|
||||
import 'package:maibu_satabot_v2/features/message/presentation/pages/message_page.dart';
|
||||
|
||||
// 在 _buildPages 方法中添加
|
||||
case 'message':
|
||||
return const MessagePage();
|
||||
```
|
||||
|
||||
### 步骤 3:修改 `tab_settings_page.dart`
|
||||
|
||||
```dart
|
||||
case 'notifications_rounded':
|
||||
return Icons.notifications_rounded;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 常见问题
|
||||
|
||||
### Q1: 添加新 Tab 后看不到?
|
||||
**A:** 点击 Tab 配置页面的"重置为默认"按钮,或清除应用数据重新运行。
|
||||
|
||||
### Q2: 点击 Tab 后页面不显示?
|
||||
**A:** 检查 `custom_main_container.dart` 中的 `switch-case` 是否正确添加了新 Tab 的映射。
|
||||
|
||||
### Q3: 图标显示不正确?
|
||||
**A:** 检查 `tab_settings_page.dart` 中的 `_getIconData` 方法是否添加了对应的图标映射。
|
||||
|
||||
### Q4: Tab 顺序不对?
|
||||
**A:** 检查 `tab_config.dart` 中各 Tab 的 `order` 值,确保按期望顺序排列。
|
||||
|
||||
---
|
||||
|
||||
## 📝 核心原理
|
||||
|
||||
1. **配置管理:** `TabConfigCubit` 管理所有 Tab 的配置和选中状态
|
||||
2. **持久化:** 配置自动保存到 SharedPreferences,重启应用后保持
|
||||
3. **动态渲染:** 底部导航栏根据 `enabledItems` 动态渲染启用的 Tab
|
||||
4. **页面切换:** 使用 `IndexedStack` 保持各页面状态,通过 `selectedIndex` 切换
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
1. **Tab 命名:** 保持简洁,中文 2-4 个字,避免过长
|
||||
2. **默认状态:** 新功能建议设置 `isEnabled: false`,通过灰度发布逐步开放
|
||||
3. **排序规划:** 预留 order 间隔(如 10, 20, 30),方便后续插入新 Tab
|
||||
4. **图标选择:** 优先使用 Material Icons 的 `_rounded` 版本,风格统一
|
||||
5. **测试验证:** 每次修改后测试开关、切换、持久化等功能
|
||||
|
||||
---
|
||||
|
||||
**最后更新:** 2026-05-20
|
||||
**维护者:** 开发团队
|
||||
@@ -1,195 +0,0 @@
|
||||
# 无人机实时视频接口对接完成总结
|
||||
|
||||
## ✅ 已完成工作
|
||||
|
||||
### 1. API 层
|
||||
- [x] 添加 API 常量 `changeUAVLens` 到 `HttpApiConsts`
|
||||
|
||||
### 2. 实体层 (Domain/Entities)
|
||||
- [x] 创建 `UavVideoStreamEntity` 实体类
|
||||
- [x] 定义 `UavLensType` 枚举(广角、变焦、红外)
|
||||
- [x] 定义 `VideoQualityType` 枚举(自适应、低、中、高清晰度)
|
||||
- [x] 实现 RTC 参数解析方法(appId、roomId、token、userId)
|
||||
|
||||
### 3. 数据源层 (Data/Datasources)
|
||||
- [x] 扩展 `DroneStationDataSource` 接口,添加 `getUavVideoStream` 方法
|
||||
- [x] 实现 `DroneStationDataSourceImpl.getUavVideoStream`
|
||||
- [x] 支持可选的镜头类型参数
|
||||
- [x] 支持自定义清晰度和 Token 有效期
|
||||
- [x] 添加详细的日志输出用于调试
|
||||
|
||||
### 4. 仓库层 (Data/Repositories)
|
||||
- [x] 扩展 `DroneStationRepository` 接口
|
||||
- [x] 实现 `DroneStationRepositoryImpl.getUavVideoStream`
|
||||
- [x] 使用 `Either<Failure, T>` 模式处理错误
|
||||
|
||||
### 5. 用例层 (Domain/UseCases)
|
||||
- [x] 创建 `GetUavVideoStreamUseCase`
|
||||
- [x] 封装业务逻辑,供 BLoC 调用
|
||||
- [x] 提供合理的默认值
|
||||
|
||||
### 6. 状态管理层 (Presentation/BLoC)
|
||||
- [x] 添加 `UavVideoStreamLoad` 事件
|
||||
- [x] 添加 `UavVideoStreamLoading` 状态
|
||||
- [x] 添加 `UavVideoStreamLoaded` 状态
|
||||
- [x] 添加 `UavVideoStreamError` 状态
|
||||
- [x] 在 `DroneStationBloc` 中实现事件处理逻辑
|
||||
|
||||
### 7. 依赖注入 (DI)
|
||||
- [x] 导入 `GetUavVideoStreamUseCase`
|
||||
- [x] 注册 `GetUavVideoStreamUseCase` 为单例
|
||||
- [x] 更新 `DroneStationBloc` 工厂,注入新的 UseCase
|
||||
|
||||
### 8. UI 层 (Presentation/Pages)
|
||||
- [x] 创建示例页面 `UavLiveVideoPage`
|
||||
- [x] 实现镜头切换功能(PopupMenuButton)
|
||||
- [x] 实现加载状态显示
|
||||
- [x] 实现错误处理和重试机制
|
||||
- [x] 预留 RTC SDK 集成位置
|
||||
|
||||
### 9. 测试
|
||||
- [x] 编写单元测试 `get_uav_video_stream_usecase_test.dart`
|
||||
- [x] 测试成功场景
|
||||
- [x] 测试失败场景
|
||||
- [x] 测试默认参数
|
||||
|
||||
### 10. 文档
|
||||
- [x] 创建集成指南 `UAV_VIDEO_INTEGRATION_GUIDE.md`
|
||||
- [x] 创建使用示例 `UAV_VIDEO_USAGE_EXAMPLES.md`
|
||||
|
||||
## 📁 文件清单
|
||||
|
||||
### 新增文件(7个)
|
||||
1. `lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart` - 实体类
|
||||
2. `lib/features/v2/device_list/domain/usecases/get_uav_video_stream_usecase.dart` - 用例
|
||||
3. `lib/features/v2/device_list/presentation/pages/uav_live_video_page.dart` - 示例页面
|
||||
4. `test/features/v2/device_list/domain/usecases/get_uav_video_stream_usecase_test.dart` - 单元测试
|
||||
5. `docs/UAV_VIDEO_INTEGRATION_GUIDE.md` - 集成指南
|
||||
6. `docs/UAV_VIDEO_USAGE_EXAMPLES.md` - 使用示例
|
||||
7. `docs/UAV_VIDEO_COMPLETION_SUMMARY.md` - 本文件
|
||||
|
||||
### 修改文件(9个)
|
||||
1. `lib/core/consts/http_api_consts.dart` - 添加 API 常量
|
||||
2. `lib/features/v2/device_list/data/datasources/drone_station_datasource.dart` - 添加接口方法
|
||||
3. `lib/features/v2/device_list/data/datasources/drone_station_datasource_impl.dart` - 实现接口
|
||||
4. `lib/features/v2/device_list/data/repositories/drone_station_repository_impl.dart` - 实现仓库
|
||||
5. `lib/features/v2/device_list/domain/repositories/drone_station_repository.dart` - 添加仓库接口
|
||||
6. `lib/features/v2/device_list/presentation/bloc/drone_station_event.dart` - 添加事件
|
||||
7. `lib/features/v2/device_list/presentation/bloc/drone_station_state.dart` - 添加状态
|
||||
8. `lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart` - 添加事件处理
|
||||
9. `lib/core/di/injection.dart` - 注册依赖
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 清洁架构设计
|
||||
- **分层清晰**:Entity → UseCase → Repository → DataSource → BLoC → UI
|
||||
- **依赖倒置**:高层模块不依赖低层模块的具体实现
|
||||
- **可测试性**:每层都可以独立测试
|
||||
|
||||
### 可扩展性
|
||||
- **多镜头支持**:通过枚举轻松扩展更多镜头类型
|
||||
- **多清晰度支持**:支持自适应、低、中、高四种清晰度
|
||||
- **多 RTC SDK 支持**:兼容火山引擎和声网两种 RTC SDK
|
||||
- **可复用**:可在任何页面中使用,无需重复实现
|
||||
|
||||
### 健壮性
|
||||
- **错误处理**:使用 `Either<Failure, T>` 统一处理错误
|
||||
- **默认值**:提供合理的默认参数,简化调用
|
||||
- **日志记录**:详细的日志输出,方便调试
|
||||
- **重试机制**:支持手动重试和自动刷新
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方式一:直接使用示例页面
|
||||
```dart
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UavLiveVideoPage(
|
||||
droneSn: '1581F8HGX253U00A063U',
|
||||
cameraIndex: '176-0-0',
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 方式二:在自定义页面中集成
|
||||
参考 `docs/UAV_VIDEO_USAGE_EXAMPLES.md` 中的详细示例。
|
||||
|
||||
## 📋 API 说明
|
||||
|
||||
### 请求参数
|
||||
```json
|
||||
{
|
||||
"sn": "1581F8HGX253U00A063U", // 必填:设备序列号
|
||||
"lensType": "", // 可选:wide/zoom/ir
|
||||
"cameraIndex": "176-0-0", // 必填:摄像头编号
|
||||
"qualityType": "adaptive", // 可选:adaptive/low/medium/high
|
||||
"videoExpire": 720000000 // 可选:Token有效期(毫秒)
|
||||
}
|
||||
```
|
||||
|
||||
### 返回数据
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 200,
|
||||
"data": {
|
||||
"sn": "1581F8HGX253U00A063U",
|
||||
"camera_index": "176-0-0",
|
||||
"url": "app_id=xxx&room_id=xxx&token=xxx&user_id=xxx",
|
||||
"expire_ts": 1781155234,
|
||||
"url_type": "volc" // volc 或 agora
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 后续工作
|
||||
|
||||
1. [ ] 集成 RTC SDK 显示实际视频画面
|
||||
- 根据 `urlType` 选择火山引擎或声网 SDK
|
||||
- 初始化 RTC 引擎并加入房间
|
||||
- 渲染远程视频流
|
||||
|
||||
2. [ ] 添加视频录制功能
|
||||
- 开始/停止录制
|
||||
- 保存录制文件
|
||||
|
||||
3. [ ] 添加截图功能
|
||||
- 截取当前帧
|
||||
- 保存到相册
|
||||
|
||||
4. [ ] 优化视频加载超时处理
|
||||
- 设置合理的超时时间
|
||||
- 提供友好的超时提示
|
||||
|
||||
5. [ ] 支持多路视频同时观看
|
||||
- 分屏显示多个摄像头
|
||||
- 切换主视图
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [完整集成指南](./UAV_VIDEO_INTEGRATION_GUIDE.md)
|
||||
- [使用示例](./UAV_VIDEO_USAGE_EXAMPLES.md)
|
||||
- [API 接口定义](../lib/core/consts/http_api_consts.dart)
|
||||
- [实体类定义](../lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart)
|
||||
- [BLoC 状态管理](../lib/features/v2/device_list/presentation/bloc/)
|
||||
|
||||
## ✅ 验证结果
|
||||
|
||||
- [x] 所有文件编译通过,无错误
|
||||
- [x] 单元测试编写完成
|
||||
- [x] 依赖注入配置正确
|
||||
- [x] 文档齐全
|
||||
|
||||
## 💡 注意事项
|
||||
|
||||
1. **不要在页面上直接调用 API**:必须通过 BLoC 管理状态
|
||||
2. **及时释放资源**:在 `dispose()` 中关闭 BLoC
|
||||
3. **合理设置 Token 有效期**:建议设置为较长的时间,避免频繁刷新
|
||||
4. **检查 RTC 参数**:确保 AppId、RoomId、Token、UserId 正确解析
|
||||
5. **根据 urlType 选择 SDK**:volc 使用火山引擎,agora 使用声网
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
无人机实时视频接口已成功对接,采用清洁架构设计,具有良好的可扩展性和可维护性。所有代码已通过编译检查,单元测试覆盖主要场景,文档齐全。下一步只需集成 RTC SDK 即可显示实际视频画面。
|
||||
@@ -1,323 +0,0 @@
|
||||
# 无人机实时视频接口对接说明
|
||||
|
||||
## 概述
|
||||
|
||||
本模块实现了无人机实时视频流的获取功能,采用清洁架构设计,支持多种镜头类型切换(广角、变焦、红外),并兼容火山引擎和声网两种RTC SDK。
|
||||
|
||||
## API 接口
|
||||
|
||||
### 接口地址
|
||||
```
|
||||
POST http://1.95.137.212:59015/iot/UAV/changeLens
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
```json
|
||||
{
|
||||
"sn": "1581F8HGX253U00A063U", // 无人机设备序列号(必填)
|
||||
"lensType": "", // 镜头类型:wide(广角)、zoom(变焦)、ir(红外),可为空
|
||||
"cameraIndex": "176-0-0", // 摄像头编号(必填)
|
||||
"qualityType": "adaptive", // 清晰度:adaptive(自适应)、low、medium、high(默认adaptive)
|
||||
"videoExpire": 720000000 // Token有效期(毫秒,默认720000000)
|
||||
}
|
||||
```
|
||||
|
||||
### 返回数据
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 200,
|
||||
"data": {
|
||||
"sn": "1581F8HGX253U00A063U",
|
||||
"camera_index": "176-0-0",
|
||||
"url": "app_id=xxx&expire_time=xxx&room_id=xxx&token=xxx&user_id=xxx",
|
||||
"expire_ts": 1781155234,
|
||||
"url_type": "volc" // volc(火山引擎) 或 agora(声网)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
lib/features/v2/device_list/
|
||||
├── domain/
|
||||
│ ├── entities/
|
||||
│ │ └── uav_video_stream_entity.dart # 实体类
|
||||
│ ├── repositories/
|
||||
│ │ ── drone_station_repository.dart # 仓库接口
|
||||
│ └── usecases/
|
||||
│ └── get_uav_video_stream_usecase.dart # 用例
|
||||
├── data/
|
||||
│ ├── datasources/
|
||||
│ │ ├── drone_station_datasource.dart # 数据源接口
|
||||
│ │ └── drone_station_datasource_impl.dart # 数据源实现
|
||||
│ └── repositories/
|
||||
│ └── drone_station_repository_impl.dart # 仓库实现
|
||||
── presentation/
|
||||
├── bloc/
|
||||
│ ├── drone_station_bloc.dart # BLoC状态管理
|
||||
│ ├── drone_station_event.dart # 事件定义
|
||||
│ └── drone_station_state.dart # 状态定义
|
||||
└── pages/
|
||||
── uav_live_video_page.dart # 示例页面
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 1. 实体类 (Entity)
|
||||
**文件**: `domain/entities/uav_video_stream_entity.dart`
|
||||
|
||||
定义了枚举类型和实体类:
|
||||
- `UavLensType`: 镜头类型枚举(wide、zoom、ir)
|
||||
- `VideoQualityType`: 视频质量枚举(adaptive、low、medium、high)
|
||||
- `UavVideoStreamEntity`: 视频流实体,包含解析RTC参数的方法
|
||||
|
||||
#### 2. 数据源 (DataSource)
|
||||
**文件**: `data/datasources/drone_station_datasource_impl.dart`
|
||||
|
||||
实现了API调用逻辑:
|
||||
- 发送POST请求到 `/iot/UAV/changeLens`
|
||||
- 处理响应并转换为实体对象
|
||||
- 包含详细的日志输出用于调试
|
||||
|
||||
#### 3. 仓库 (Repository)
|
||||
**文件**: `data/repositories/drone_station_repository_impl.dart`
|
||||
|
||||
使用 `fpdart` 的 `Either` 类型处理错误:
|
||||
- 成功时返回 `Right(UavVideoStreamEntity)`
|
||||
- 失败时返回 `Left(Failure)`
|
||||
|
||||
#### 4. 用例 (UseCase)
|
||||
**文件**: `domain/usecases/get_uav_video_stream_usecase.dart`
|
||||
|
||||
封装业务逻辑,供BLoC调用。
|
||||
|
||||
#### 5. BLoC 状态管理
|
||||
**文件**: `presentation/bloc/drone_station_bloc.dart`
|
||||
|
||||
新增事件和状态:
|
||||
- **事件**: `UavVideoStreamLoad` - 加载视频流
|
||||
- **状态**:
|
||||
- `UavVideoStreamLoading` - 加载中
|
||||
- `UavVideoStreamLoaded` - 加载成功
|
||||
- `UavVideoStreamError` - 加载失败
|
||||
|
||||
#### 6. 依赖注入
|
||||
**文件**: `core/di/injection.dart`
|
||||
|
||||
已注册以下单例:
|
||||
```dart
|
||||
sl.registerLazySingleton<GetUavVideoStreamUseCase>(
|
||||
() => GetUavVideoStreamUseCase(sl()),
|
||||
);
|
||||
sl.registerFactory<DroneStationBloc>(
|
||||
() => DroneStationBloc(sl(), sl(), sl(), sl()),
|
||||
);
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:直接使用示例页面
|
||||
|
||||
```dart
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/uav_live_video_page.dart';
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UavLiveVideoPage(
|
||||
droneSn: '1581F8HGX253U00A063U',
|
||||
cameraIndex: '176-0-0',
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### 方式二:在现有页面中使用
|
||||
|
||||
#### 1. 导入必要的类
|
||||
```dart
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||
import '../bloc/drone_station_bloc.dart';
|
||||
import '../bloc/drone_station_event.dart';
|
||||
import '../bloc/drone_station_state.dart';
|
||||
```
|
||||
|
||||
#### 2. 初始化 BLoC
|
||||
```dart
|
||||
late DroneStationBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
// 加载视频流
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 加载视频流
|
||||
```dart
|
||||
void _loadVideoStream(UavLensType lensType) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndex,
|
||||
lensType: lensType,
|
||||
qualityType: VideoQualityType.adaptive,
|
||||
videoExpire: 720000000,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 监听状态变化
|
||||
```dart
|
||||
BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
final videoStream = state.videoStream;
|
||||
debugPrint('URL Type: ${videoStream.urlType}');
|
||||
debugPrint('AppId: ${videoStream.appId}');
|
||||
debugPrint('RoomId: ${videoStream.roomId}');
|
||||
debugPrint('UserId: ${videoStream.userId}');
|
||||
|
||||
// TODO: 根据 urlType 初始化对应的 RTC 引擎
|
||||
if (videoStream.urlType.toLowerCase() == 'volc') {
|
||||
// 使用火山引擎 RTC SDK
|
||||
} else if (videoStream.urlType.toLowerCase() == 'agora') {
|
||||
// 使用声网 RTC SDK
|
||||
}
|
||||
} else if (state is UavVideoStreamError) {
|
||||
setState(() {
|
||||
_errorMessage = state.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// 根据状态渲染UI
|
||||
if (state is UavVideoStreamLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
// 显示视频画面
|
||||
return Container();
|
||||
}
|
||||
|
||||
if (state is UavVideoStreamError) {
|
||||
return Text('错误: ${state.message}');
|
||||
}
|
||||
|
||||
return Container();
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### 5. 切换镜头类型
|
||||
```dart
|
||||
// 切换到广角镜头
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
|
||||
// 切换到变焦镜头
|
||||
_loadVideoStream(UavLensType.zoom);
|
||||
|
||||
// 切换到红外镜头
|
||||
_loadVideoStream(UavLensType.ir);
|
||||
```
|
||||
|
||||
## RTC 集成
|
||||
|
||||
获取到视频流后,需要根据 `urlType` 选择对应的 RTC SDK:
|
||||
|
||||
### 火山引擎 (volc)
|
||||
```dart
|
||||
final appId = videoStream.appId;
|
||||
final roomId = videoStream.roomId;
|
||||
final token = videoStream.token;
|
||||
final userId = videoStream.userId;
|
||||
|
||||
// 使用 volc_engine_rtc SDK
|
||||
final engine = await RTCEngine.createRTCEngine(
|
||||
RTCVideoContext(appId: appId, eventHandler: handler),
|
||||
);
|
||||
final room = await engine.createRTCRoom(roomId);
|
||||
await room.joinRoom(token: token, userId: userId);
|
||||
```
|
||||
|
||||
### 声网 (agora)
|
||||
```dart
|
||||
final appId = videoStream.appId;
|
||||
final channelId = videoStream.roomId;
|
||||
final token = videoStream.token;
|
||||
final uid = int.tryParse(videoStream.userId) ?? 0;
|
||||
|
||||
// 使用 agora_rtc_engine SDK
|
||||
final engine = createAgoraRtcEngine();
|
||||
await engine.initialize(RtcEngineContext(appId: appId));
|
||||
await engine.joinChannel(
|
||||
token: token,
|
||||
channelId: channelId,
|
||||
uid: uid,
|
||||
options: ChannelMediaOptions(...),
|
||||
);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **可扩展性**: 本模块采用清洁架构设计,所有业务逻辑与UI分离,便于在其他页面复用。
|
||||
|
||||
2. **错误处理**: 使用 `Either<Failure, T>` 模式统一处理错误,确保异常不会直接抛出。
|
||||
|
||||
3. **日志记录**: 数据源层包含详细的日志输出,方便调试和问题排查。
|
||||
|
||||
4. **默认值**:
|
||||
- `lensType` 可为空,后端会根据实际情况选择默认镜头
|
||||
- `qualityType` 默认为 `adaptive`(自适应)
|
||||
- `videoExpire` 默认为 `720000000` 毫秒
|
||||
|
||||
5. **Token 有效期**: `videoExpire` 参数单位为毫秒,建议设置较长的有效期以避免频繁刷新。
|
||||
|
||||
6. **多镜头支持**: 通过 `UavLensType` 枚举可以轻松扩展更多镜头类型。
|
||||
|
||||
## 后续工作
|
||||
|
||||
1. [ ] 集成 RTC SDK 显示实际视频画面
|
||||
2. [ ] 添加视频录制功能
|
||||
3. [ ] 添加截图功能
|
||||
4. [ ] 优化视频加载超时处理
|
||||
5. [ ] 添加视频质量切换功能
|
||||
6. [ ] 支持多路视频同时观看
|
||||
|
||||
## 相关文件清单
|
||||
|
||||
### 新增文件
|
||||
- `lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart`
|
||||
- `lib/features/v2/device_list/domain/usecases/get_uav_video_stream_usecase.dart`
|
||||
- `lib/features/v2/device_list/presentation/pages/uav_live_video_page.dart`
|
||||
|
||||
### 修改文件
|
||||
- `lib/core/consts/http_api_consts.dart` - 添加 API 常量
|
||||
- `lib/features/v2/device_list/data/datasources/drone_station_datasource.dart` - 添加接口方法
|
||||
- `lib/features/v2/device_list/data/datasources/drone_station_datasource_impl.dart` - 实现接口
|
||||
- `lib/features/v2/device_list/data/repositories/drone_station_repository_impl.dart` - 实现仓库
|
||||
- `lib/features/v2/device_list/domain/repositories/drone_station_repository.dart` - 添加仓库接口
|
||||
- `lib/features/v2/device_list/presentation/bloc/drone_station_event.dart` - 添加事件
|
||||
- `lib/features/v2/device_list/presentation/bloc/drone_station_state.dart` - 添加状态
|
||||
- `lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart` - 添加事件处理
|
||||
- `lib/core/di/injection.dart` - 注册依赖
|
||||
@@ -1,298 +0,0 @@
|
||||
# 无人机视频页面集成完成
|
||||
|
||||
## 📋 更新内容
|
||||
|
||||
已将新的无人机实时视频接口集成到现有的**无人机视频回传/远程控制**页面中。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 集成位置
|
||||
|
||||
### 1. 无人机详情页面 → 无人机状态卡片
|
||||
|
||||
**文件**: `lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart`
|
||||
|
||||
**修改内容**:
|
||||
```dart
|
||||
// 点击"无人机状态"卡片时,传递设备序列号和摄像头索引
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneVideoControlPage(
|
||||
droneSn: detail.deviceSn, // ✅ 传递设备序列号
|
||||
cameraIndex: detail.gatewayCameraList != null &&
|
||||
detail.gatewayCameraList!.isNotEmpty
|
||||
? detail.gatewayCameraList!.first.cameraIndex
|
||||
: '176-0-0', // 默认值
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 无人机视频控制页面
|
||||
|
||||
**文件**: `lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart`
|
||||
|
||||
**新增功能**:
|
||||
|
||||
#### ✅ 接收参数
|
||||
```dart
|
||||
class DroneVideoControlPage extends StatefulWidget {
|
||||
final String droneSn; // 无人机设备序列号
|
||||
final String cameraIndex; // 摄像头编号
|
||||
|
||||
const DroneVideoControlPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.cameraIndex,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 自动加载视频流
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
// 默认加载广角镜头的视频流
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 镜头切换功能
|
||||
在视频播放器右下角添加了镜头类型选择器:
|
||||
- **广角** (wide)
|
||||
- **变焦** (zoom)
|
||||
- **红外** (ir)
|
||||
|
||||
点击可快速切换不同镜头的视频流。
|
||||
|
||||
#### ✅ 加载状态显示
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ │
|
||||
│ ⏳ 正在加载... │
|
||||
│ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
#### ✅ 错误处理和重试
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ │
|
||||
│ ❌ 加载失败 │
|
||||
│ 错误信息: xxxxx │
|
||||
│ │
|
||||
│ [ 重试 ] │
|
||||
│ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[无人机详情页] --> B[点击无人机状态卡片]
|
||||
B --> C[打开视频控制页面]
|
||||
C --> D[自动调用changeLens接口]
|
||||
D --> E{加载结果}
|
||||
E -->|成功| F[显示视频画面]
|
||||
E -->|失败| G[显示错误+重试按钮]
|
||||
F --> H[用户切换镜头]
|
||||
H --> D
|
||||
G -->|点击重试| D
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 UI 展示
|
||||
|
||||
### 视频播放器区域
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ ● REC 00:12:36 │
|
||||
│ │
|
||||
│ │
|
||||
│ [视频画面/加载状态] │
|
||||
│ │
|
||||
│ │
|
||||
│ ┌────────┐ │
|
||||
│ │广角 ▼│ │ ← 镜头切换
|
||||
│ └────────┘ │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 镜头切换菜单
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ 广角 │ ← 当前选中
|
||||
│ 变焦 │
|
||||
│ 红外 │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 1. BLoC 状态管理
|
||||
```dart
|
||||
// 监听视频流状态
|
||||
BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
// 获取视频流数据
|
||||
_videoStream = state.videoStream;
|
||||
|
||||
// TODO: 初始化 RTC 引擎并显示视频
|
||||
debugPrint('AppId: ${state.videoStream.appId}');
|
||||
debugPrint('RoomId: ${state.videoStream.roomId}');
|
||||
debugPrint('UserId: ${state.videoStream.userId}');
|
||||
} else if (state is UavVideoStreamError) {
|
||||
// 显示错误信息
|
||||
_errorMessage = state.message;
|
||||
}
|
||||
},
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 视频流加载
|
||||
```dart
|
||||
void _loadVideoStream(UavLensType lensType) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_currentLensType = lensType;
|
||||
});
|
||||
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndex,
|
||||
lensType: lensType,
|
||||
qualityType: VideoQualityType.adaptive,
|
||||
videoExpire: 720000000,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 镜头切换
|
||||
```dart
|
||||
PopupMenuButton<UavLensType>(
|
||||
icon: Text(_getLensTypeName(_currentLensType)),
|
||||
onSelected: (UavLensType lensType) {
|
||||
if (lensType != _currentLensType) {
|
||||
_loadVideoStream(lensType); // 重新加载视频流
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: UavLensType.wide, child: Text('广角')),
|
||||
PopupMenuItem(value: UavLensType.zoom, child: Text('变焦')),
|
||||
PopupMenuItem(value: UavLensType.ir, child: Text('红外')),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成的功能
|
||||
|
||||
- ✅ 从无人机详情页传递设备序列号和摄像头索引
|
||||
- ✅ 自动调用 `changeLens` 接口获取视频流
|
||||
- ✅ 支持三种镜头类型切换(广角/变焦/红外)
|
||||
- ✅ 加载状态显示
|
||||
- ✅ 错误处理和重试机制
|
||||
- ✅ 打印视频流参数(AppId、RoomId、UserId等)
|
||||
- ✅ 清洁架构设计,易于扩展和维护
|
||||
|
||||
---
|
||||
|
||||
## 🔜 下一步工作(TODO)
|
||||
|
||||
### 1. 集成 RTC SDK 显示实际视频
|
||||
|
||||
在 `_buildVideoPlayer()` 方法中,当 `_videoStream != null` 时:
|
||||
|
||||
```dart
|
||||
if (_videoStream != null) {
|
||||
// 根据 urlType 选择对应的 RTC SDK
|
||||
if (_videoStream!.urlType == 'volc') {
|
||||
// 使用火山引擎 RTC SDK
|
||||
return VolcEngineRtcView(
|
||||
appId: _videoStream!.appId,
|
||||
roomId: _videoStream!.roomId,
|
||||
token: _videoStream!.token,
|
||||
userId: _videoStream!.userId,
|
||||
);
|
||||
} else if (_videoStream!.urlType == 'agora') {
|
||||
// 使用声网 RTC SDK
|
||||
return AgoraRtcView(
|
||||
appId: _videoStream!.appId,
|
||||
channelId: _videoStream!.roomId,
|
||||
token: _videoStream!.token,
|
||||
uid: int.parse(_videoStream!.userId),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 参考现有实现
|
||||
|
||||
可以参考 `drone_station_detail_page.dart` 中的以下方法:
|
||||
- `_initFloatingVolcEngine()` - 火山引擎初始化
|
||||
- `_initFloatingAgoraEngine()` - 声网引擎初始化
|
||||
- `_disposeVolcEngine()` / `_disposeAgoraEngine()` - 资源释放
|
||||
|
||||
### 3. 添加更多控制功能
|
||||
|
||||
- 云台控制(上下左右)
|
||||
- 变焦倍数调节
|
||||
- 拍照/录像
|
||||
- AI识别结果展示
|
||||
|
||||
---
|
||||
|
||||
## 📝 测试步骤
|
||||
|
||||
1. **进入无人机详情页**
|
||||
- 确保无人机在线且有摄像头列表
|
||||
|
||||
2. **点击"无人机状态"卡片**
|
||||
- 应该跳转到视频控制页面
|
||||
|
||||
3. **观察加载过程**
|
||||
- 看到 "正在加载视频流..." 提示
|
||||
- 成功后显示视频画面(当前为占位图)
|
||||
|
||||
4. **测试镜头切换**
|
||||
- 点击右下角的镜头类型按钮
|
||||
- 选择"广角"、"变焦"或"红外"
|
||||
- 观察是否重新加载并切换成功
|
||||
|
||||
5. **测试错误处理**
|
||||
- 断开网络连接
|
||||
- 应该显示错误信息和重试按钮
|
||||
- 点击重试应该重新加载
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
✅ **集成完成!**
|
||||
|
||||
现在无人机视频控制页面已经:
|
||||
- 正确接收设备参数
|
||||
- 自动调用新的视频流接口
|
||||
- 支持多镜头切换
|
||||
- 完善的加载和错误处理
|
||||
|
||||
只需最后一步:**集成 RTC SDK 显示实际视频画面**,即可完整实现无人机实时视频监控功能!
|
||||
@@ -1,642 +0,0 @@
|
||||
# 无人机实时视频接口使用示例
|
||||
|
||||
## 快速开始
|
||||
|
||||
本指南展示如何在你的项目中快速集成无人机实时视频功能。
|
||||
|
||||
## 1. 基础用法 - 直接导航到视频页面
|
||||
|
||||
最简单的方式是直接导航到 `UavLiveVideoPage`:
|
||||
|
||||
```dart
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/uav_live_video_page.dart';
|
||||
|
||||
// 在任意地方调用
|
||||
void _openVideo() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UavLiveVideoPage(
|
||||
droneSn: '1581F8HGX253U00A063U', // 无人机序列号
|
||||
cameraIndex: '176-0-0', // 摄像头编号
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 高级用法 - 在自定义页面中集成
|
||||
|
||||
如果你需要在自己的页面中集成视频功能,可以按照以下步骤操作:
|
||||
|
||||
### 步骤 1: 创建页面并初始化 BLoC
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||
import '../bloc/drone_station_bloc.dart';
|
||||
import '../bloc/drone_station_event.dart';
|
||||
import '../bloc/drone_station_state.dart';
|
||||
|
||||
class MyCustomVideoPage extends StatefulWidget {
|
||||
final String droneSn;
|
||||
final String cameraIndex;
|
||||
|
||||
const MyCustomVideoPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.cameraIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MyCustomVideoPage> createState() => _MyCustomVideoPageState();
|
||||
}
|
||||
|
||||
class _MyCustomVideoPageState extends State<MyCustomVideoPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
UavVideoStreamEntity? _videoStream;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
UavLensType? _currentLensType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
// 默认加载广角镜头
|
||||
_loadVideoStream(UavLensType.wide);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2: 实现加载视频流方法
|
||||
|
||||
```dart
|
||||
/// 加载视频流
|
||||
void _loadVideoStream(UavLensType lensType) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_currentLensType = lensType;
|
||||
});
|
||||
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndex,
|
||||
lensType: lensType,
|
||||
qualityType: VideoQualityType.adaptive, // 自适应清晰度
|
||||
videoExpire: 720000000, // Token有效期(毫秒)
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3: 构建 UI 并监听状态变化
|
||||
|
||||
```dart
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'无人机实时视频',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
// 镜头切换按钮
|
||||
PopupMenuButton<UavLensType>(
|
||||
icon: const Icon(Icons.videocam, color: Colors.white),
|
||||
tooltip: '切换镜头',
|
||||
onSelected: (lensType) {
|
||||
_loadVideoStream(lensType);
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: UavLensType.wide,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.videocam, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('广角镜头'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: UavLensType.zoom,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.zoom_in, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('变焦镜头'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: UavLensType.ir,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.thermostat, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('红外镜头'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
// 监听状态变化
|
||||
if (state is UavVideoStreamLoaded) {
|
||||
setState(() {
|
||||
_videoStream = state.videoStream;
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
debugPrint('=== 视频流加载成功 ===');
|
||||
debugPrint('URL Type: ${state.videoStream.urlType}');
|
||||
debugPrint('AppId: ${state.videoStream.appId}');
|
||||
debugPrint('RoomId: ${state.videoStream.roomId}');
|
||||
debugPrint('UserId: ${state.videoStream.userId}');
|
||||
|
||||
// TODO: 这里可以初始化 RTC 引擎并显示视频
|
||||
_initRtcEngine(state.videoStream);
|
||||
} else if (state is UavVideoStreamError) {
|
||||
setState(() {
|
||||
_errorMessage = state.message;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// 根据状态渲染不同的 UI
|
||||
|
||||
// 加载中状态
|
||||
if (_isLoading || state is UavVideoStreamLoading) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: Colors.white),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'正在加载视频流...',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (_errorMessage != null || state is UavVideoStreamError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage ?? (state as UavVideoStreamError).message,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_currentLensType != null) {
|
||||
_loadVideoStream(_currentLensType!);
|
||||
}
|
||||
},
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 无视频信号
|
||||
if (_videoStream == null) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'暂无视频信号',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 视频已加载,显示视频画面
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.black,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.videocam_off,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'视频流已获取\nURL Type: ${_videoStream!.urlType}\nCamera: $_currentLensType',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'请集成 RTC SDK 后在此处显示视频画面',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 4: 初始化 RTC 引擎(可选)
|
||||
|
||||
```dart
|
||||
/// 初始化 RTC 引擎
|
||||
Future<void> _initRtcEngine(UavVideoStreamEntity videoStream) async {
|
||||
final appId = videoStream.appId;
|
||||
final roomId = videoStream.roomId;
|
||||
final token = videoStream.token;
|
||||
final userId = videoStream.userId.isNotEmpty
|
||||
? videoStream.userId
|
||||
: 'user_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
if (appId.isEmpty || roomId.isEmpty || token.isEmpty) {
|
||||
debugPrint('RTC 参数缺失');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('=== RTC 初始化 ===');
|
||||
debugPrint('AppId: $appId');
|
||||
debugPrint('RoomId: $roomId');
|
||||
debugPrint('UserId: $userId');
|
||||
debugPrint('URL Type: ${videoStream.urlType}');
|
||||
|
||||
// 根据 urlType 选择不同的 RTC SDK
|
||||
final sdkType = videoStream.urlType.toLowerCase() == 'agora'
|
||||
? RtcSdkType.agora
|
||||
: RtcSdkType.volcengine;
|
||||
|
||||
if (sdkType == RtcSdkType.agora) {
|
||||
await _initAgoraEngine(appId, roomId, token, userId);
|
||||
} else {
|
||||
await _initVolcEngine(appId, roomId, token, userId);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 实现具体的 RTC 引擎初始化逻辑
|
||||
// 参考 drone_station_detail_page.dart 中的实现
|
||||
```
|
||||
|
||||
## 3. 常见场景示例
|
||||
|
||||
### 场景 1: 从列表页跳转到视频页
|
||||
|
||||
```dart
|
||||
// 在设备列表中点击某个设备
|
||||
void _onDeviceTap(Device device) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => UavLiveVideoPage(
|
||||
droneSn: device.sn,
|
||||
cameraIndex: device.cameraIndex,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 2: 支持多个摄像头切换
|
||||
|
||||
```dart
|
||||
class MultiCameraVideoPage extends StatefulWidget {
|
||||
final String droneSn;
|
||||
final List<String> cameraIndices;
|
||||
|
||||
const MultiCameraVideoPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.cameraIndices,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MultiCameraVideoPage> createState() => _MultiCameraVideoPageState();
|
||||
}
|
||||
|
||||
class _MultiCameraVideoPageState extends State<MultiCameraVideoPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
int _currentCameraIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
_loadCurrentCamera();
|
||||
}
|
||||
|
||||
void _loadCurrentCamera() {
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndices[_currentCameraIndex],
|
||||
lensType: UavLensType.wide,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _switchCamera(int index) {
|
||||
setState(() {
|
||||
_currentCameraIndex = index;
|
||||
});
|
||||
_loadCurrentCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('摄像头 ${_currentCameraIndex + 1}/${widget.cameraIndices.length}'),
|
||||
actions: [
|
||||
// 切换摄像头按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.switch_camera),
|
||||
onPressed: () {
|
||||
final nextIndex = (_currentCameraIndex + 1) % widget.cameraIndices.length;
|
||||
_switchCamera(nextIndex);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<DroneStationBloc, DroneStationState>(
|
||||
builder: (context, state) {
|
||||
// ... 根据状态渲染UI
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 3: 自动刷新视频流(Token 过期时)
|
||||
|
||||
```dart
|
||||
class AutoRefreshVideoPage extends StatefulWidget {
|
||||
final String droneSn;
|
||||
final String cameraIndex;
|
||||
|
||||
const AutoRefreshVideoPage({
|
||||
super.key,
|
||||
required this.droneSn,
|
||||
required this.cameraIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AutoRefreshVideoPage> createState() => _AutoRefreshVideoPageState();
|
||||
}
|
||||
|
||||
class _AutoRefreshVideoPageState extends State<AutoRefreshVideoPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
Timer? _refreshTimer;
|
||||
static const _tokenRefreshInterval = Duration(minutes: 55); // 每55分钟刷新一次(Token有效期约1小时)
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = sl<DroneStationBloc>();
|
||||
_loadVideoStream();
|
||||
|
||||
// 启动定时刷新
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
void _startAutoRefresh() {
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = Timer.periodic(_tokenRefreshInterval, (timer) {
|
||||
debugPrint('⏰ 自动刷新视频流 Token');
|
||||
_loadVideoStream();
|
||||
});
|
||||
}
|
||||
|
||||
void _loadVideoStream() {
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndex,
|
||||
lensType: UavLensType.wide,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 最佳实践
|
||||
|
||||
### ✅ 推荐做法
|
||||
|
||||
1. **始终检查错误状态**
|
||||
```dart
|
||||
if (state is UavVideoStreamError) {
|
||||
// 显示友好的错误提示
|
||||
showSnackBar(context, '视频加载失败: ${state.message}');
|
||||
}
|
||||
```
|
||||
|
||||
2. **提供重试机制**
|
||||
```dart
|
||||
ElevatedButton(
|
||||
onPressed: () => _loadVideoStream(_currentLensType!),
|
||||
child: const Text('重试'),
|
||||
)
|
||||
```
|
||||
|
||||
3. **记录关键日志**
|
||||
```dart
|
||||
debugPrint('视频流加载成功: URL Type=${videoStream.urlType}');
|
||||
```
|
||||
|
||||
4. **合理设置 Token 有效期**
|
||||
```dart
|
||||
videoExpire: 720000000, // 约8天,避免频繁刷新
|
||||
```
|
||||
|
||||
5. **及时释放资源**
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ 避免的做法
|
||||
|
||||
1. **不要在页面外直接调用 API**
|
||||
```dart
|
||||
// 错误:绕过 BLoC 直接调用
|
||||
final result = await repository.getUavVideoStream(...);
|
||||
|
||||
// ✅ 正确:通过 BLoC 管理状态
|
||||
_bloc.add(UavVideoStreamLoad(...));
|
||||
```
|
||||
|
||||
2. **不要忘记处理加载状态**
|
||||
```dart
|
||||
// ❌ 错误:没有加载指示器
|
||||
if (state is UavVideoStreamLoaded) { ... }
|
||||
|
||||
// ✅ 正确:显示加载状态
|
||||
if (state is UavVideoStreamLoading) {
|
||||
return CircularProgressIndicator();
|
||||
}
|
||||
```
|
||||
|
||||
3. **不要硬编码参数**
|
||||
```dart
|
||||
// 错误:硬编码
|
||||
sn: '1581F8HGX253U00A063U',
|
||||
|
||||
// ✅ 正确:使用变量
|
||||
sn: widget.droneSn,
|
||||
```
|
||||
|
||||
## 5. 故障排查
|
||||
|
||||
### 问题 1: 视频流加载失败
|
||||
|
||||
**可能原因:**
|
||||
- 网络问题
|
||||
- 设备序列号错误
|
||||
- 摄像头编号错误
|
||||
- Token 过期
|
||||
|
||||
**解决方案:**
|
||||
1. 检查网络连接
|
||||
2. 验证 `droneSn` 和 `cameraIndex` 是否正确
|
||||
3. 查看控制台日志输出
|
||||
4. 尝试重新加载
|
||||
|
||||
### 问题 2: 视频画面不显示
|
||||
|
||||
**可能原因:**
|
||||
- RTC SDK 未正确初始化
|
||||
- RTC 参数解析失败
|
||||
- SDK 版本不兼容
|
||||
|
||||
**解决方案:**
|
||||
1. 检查 `urlType` 字段,确认使用正确的 SDK
|
||||
2. 验证 AppId、RoomId、Token、UserId 是否正确解析
|
||||
3. 查看 RTC SDK 的日志输出
|
||||
4. 参考 `drone_station_detail_page.dart` 中的实现
|
||||
|
||||
### 问题 3: 镜头切换无效
|
||||
|
||||
**可能原因:**
|
||||
- 后端不支持该镜头类型
|
||||
- 摄像头不支持指定的镜头
|
||||
|
||||
**解决方案:**
|
||||
1. 检查后端返回的错误信息
|
||||
2. 尝试其他镜头类型
|
||||
3. 联系后端确认支持的镜头类型
|
||||
|
||||
## 6. 扩展功能
|
||||
|
||||
### 添加视频录制功能
|
||||
|
||||
```dart
|
||||
// TODO: 集成 RTC SDK 的录制功能
|
||||
Future<void> _startRecording() async {
|
||||
// 根据使用的 RTC SDK 调用相应的录制 API
|
||||
}
|
||||
|
||||
Future<void> _stopRecording() async {
|
||||
// 停止录制并保存文件
|
||||
}
|
||||
```
|
||||
|
||||
### 添加截图功能
|
||||
|
||||
```dart
|
||||
// TODO: 集成 RTC SDK 的截图功能
|
||||
Future<void> _takeScreenshot() async {
|
||||
// 根据使用的 RTC SDK 调用相应的截图 API
|
||||
// 保存截图到相册
|
||||
}
|
||||
```
|
||||
|
||||
### 添加视频质量切换
|
||||
|
||||
```dart
|
||||
void _changeQuality(VideoQualityType quality) {
|
||||
_bloc.add(
|
||||
UavVideoStreamLoad(
|
||||
sn: widget.droneSn,
|
||||
cameraIndex: widget.cameraIndex,
|
||||
lensType: _currentLensType,
|
||||
qualityType: quality, // 切换清晰度
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 相关文档
|
||||
|
||||
- [完整集成指南](./UAV_VIDEO_INTEGRATION_GUIDE.md)
|
||||
- [API 接口文档](../lib/core/consts/http_api_consts.dart)
|
||||
- [实体类定义](../lib/features/v2/device_list/domain/entities/uav_video_stream_entity.dart)
|
||||
- [BLoC 状态管理](../lib/features/v2/device_list/presentation/bloc/)
|
||||
|
||||
## 8. 技术支持
|
||||
|
||||
如遇到问题,请:
|
||||
1. 查看控制台日志输出
|
||||
2. 参考示例代码 `uav_live_video_page.dart`
|
||||
3. 查阅集成指南文档
|
||||
4. 联系开发团队
|
||||
61
fix_dup.py
61
fix_dup.py
@@ -1,61 +0,0 @@
|
||||
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<dynamic> _parseStartWorkListFromPathData\(List<Map<String, dynamic>>\? 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<String, dynamic>\) return \[\];\n final planModel = parsedJson\['planModel'\];\n final isBow = planModel == WorkMode\.bow\.value;\n\n List<dynamic> 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<dynamic> 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<dynamic>")
|
||||
start = content.rfind(' /// 从 state.pathData', 0, second_idx)
|
||||
if start == -1:
|
||||
start = content.rfind(' List<dynamic> _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!")
|
||||
107
flutter_01.log
107
flutter_01.log
@@ -1,107 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
106
flutter_02.log
106
flutter_02.log
@@ -1,106 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
107
flutter_03.log
107
flutter_03.log
@@ -1,107 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
106
flutter_04.log
106
flutter_04.log
@@ -1,106 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
106
flutter_05.log
106
flutter_05.log
@@ -1,106 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
106
flutter_06.log
106
flutter_06.log
@@ -1,106 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
106
flutter_07.log
106
flutter_07.log
@@ -1,106 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
106
flutter_08.log
106
flutter_08.log
@@ -1,106 +0,0 @@
|
||||
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.<anonymous closure> (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.
|
||||
```
|
||||
@@ -1,6 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
source 'https://github.com/volcengine/volcengine-specs.git'error: Classes can only extend other classes. (extends_non_class at [maibu_satabot_v2] lib\features\v2\device_list\domain\entities\video_stream_entity.dart:2)error: Classes can only extend other classes. (extends_non_class at [maibu_satabot_v2] lib\features\v2\device_list\domain\entities\video_stream_entity.dart:2)you
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -66,32 +66,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<!-- 相机权限 -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要使用相机拍照和录像</string>
|
||||
<!-- 相册权限 -->
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问相册以选择图片和视频</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要保存拍摄的照片和视频到相册</string>
|
||||
<!-- 麦克风权限(录像需要) -->
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>录像时需要使用麦克风录制声音</string>
|
||||
<!-- 允许 HTTP 连接(火山引擎视频流) -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要使用相机</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要使用麦克风</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,130 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/tcp/tcp_status_cubit.dart';
|
||||
|
||||
class TcpStatusIndicator extends StatefulWidget {
|
||||
final double size;
|
||||
final bool showDisconnected; // 是否显示未连接状态
|
||||
final VoidCallback? onTap; // 点击回调
|
||||
|
||||
const TcpStatusIndicator({
|
||||
super.key,
|
||||
this.size = 12.0,
|
||||
this.showDisconnected = false, // 默认不显示未连接状态
|
||||
this.onTap, // 点击回调
|
||||
});
|
||||
|
||||
@override
|
||||
State<TcpStatusIndicator> createState() => _TcpStatusIndicatorState();
|
||||
}
|
||||
|
||||
class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TcpStatusCubit _tcpStatusCubit;
|
||||
bool _hasActivity = false;
|
||||
AnimationController? _controller;
|
||||
Animation<double>? _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tcpStatusCubit = GetIt.I<TcpStatusCubit>();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: 0.6,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(parent: _controller!, curve: Curves.easeInOut));
|
||||
|
||||
_controller!.repeat(reverse: true);
|
||||
|
||||
_tcpStatusCubit.stream.listen((state) {
|
||||
if (state.status != TcpConnectionStatus.disconnected) {
|
||||
_hasActivity = true;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = _tcpStatusCubit.state;
|
||||
|
||||
if (!widget.showDisconnected &&
|
||||
!_hasActivity &&
|
||||
state.status == TcpConnectionStatus.disconnected) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Color color;
|
||||
String tooltip;
|
||||
bool shouldAnimate;
|
||||
|
||||
switch (state.status) {
|
||||
case TcpConnectionStatus.connected:
|
||||
color = Colors.green;
|
||||
tooltip = 'TCP已连接';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
case TcpConnectionStatus.connecting:
|
||||
color = Colors.yellow;
|
||||
tooltip = 'TCP连接中...';
|
||||
shouldAnimate = true;
|
||||
break;
|
||||
case TcpConnectionStatus.error:
|
||||
color = Colors.red;
|
||||
tooltip = state.errorMessage ?? 'TCP连接错误';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
case TcpConnectionStatus.disconnected:
|
||||
default:
|
||||
color = Colors.grey;
|
||||
tooltip = 'TCP未连接';
|
||||
shouldAnimate = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(widget.size / 2),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation!,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation!.value : 1.0;
|
||||
return Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,7 @@ class AppUserCubit extends Cubit<AppUserState> {
|
||||
AppUserCubit() : super(const AppUserState());
|
||||
|
||||
void setAuth(UserEntity user) {
|
||||
print('✅ [AppUserCubit] setAuth 被调用,用户: ${user.username}, roleKey=${user.roleKey}, orgId=${user.orgId}, siteId=${user.siteId}');
|
||||
emit(state.copyWith(user));
|
||||
print('✅ [AppUserCubit] 状态已更新,当前用户: ${state.user?.username}, roleKey=${state.user?.roleKey}');
|
||||
}
|
||||
|
||||
void clearAuth() {
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'bytes_util.dart';
|
||||
import 'protocol_parser.dart';
|
||||
|
||||
class BleManager {
|
||||
static final BleManager instance = BleManager._internal();
|
||||
BleManager._internal();
|
||||
|
||||
BluetoothDevice? _connectedDevice;
|
||||
BluetoothDevice? _connectingDevice;
|
||||
BluetoothCharacteristic? _writeCharacteristic;
|
||||
BluetoothCharacteristic? _readCharacteristic;
|
||||
|
||||
/// 有状态的协议解析器(支持 BLE 分片)
|
||||
final ProtocolParser _parser = ProtocolParser();
|
||||
|
||||
final Map<DeviceIdentifier, ScanResult> _scanResults = {};
|
||||
final StreamController<List<ScanResult>> _scanController =
|
||||
StreamController.broadcast();
|
||||
final StreamController<BlePacket> _packetController =
|
||||
StreamController.broadcast();
|
||||
final StreamController<BluetoothDevice?> _connectionController =
|
||||
StreamController.broadcast();
|
||||
final StreamController<BluetoothDevice?> _connectingController =
|
||||
StreamController.broadcast();
|
||||
|
||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||
StreamSubscription<List<int>>? _readSubscription;
|
||||
StreamSubscription<BluetoothAdapterState>? _adapterStateSubscription;
|
||||
StreamSubscription<BluetoothConnectionState>? _deviceConnectionSubscription;
|
||||
|
||||
/// 防止异步竞态:stopScan() 后 in-flight 的 startScan() 不应生效
|
||||
int _scanGen = 0;
|
||||
|
||||
/// 协商后的 MTU 值,用于分片写入
|
||||
int _negotiatedMtu = 23;
|
||||
|
||||
/// 已收到的数据包存储(跨页面持久化)
|
||||
final List<BlePacket> receivedPacketStore = [];
|
||||
static const int _maxStoredPackets = 100;
|
||||
|
||||
void clearReceivedPackets() {
|
||||
receivedPacketStore.clear();
|
||||
}
|
||||
|
||||
bool get isConnected => _connectedDevice != null;
|
||||
BluetoothDevice? get connectedDevice => _connectedDevice;
|
||||
BluetoothDevice? get connectingDevice => _connectingDevice;
|
||||
Stream<List<ScanResult>> get scanResults => _scanController.stream;
|
||||
Stream<BlePacket> get packetStream => _packetController.stream;
|
||||
Stream<BluetoothAdapterState> get adapterState =>
|
||||
FlutterBluePlus.adapterState;
|
||||
|
||||
/// 连接状态变化流:连接成功时发出 device,断开时发出 null
|
||||
Stream<BluetoothDevice?> get connectionStream => _connectionController.stream;
|
||||
|
||||
/// 连接中状态流:开始连接时发出 device,连接完成/失败时发出 null
|
||||
Stream<BluetoothDevice?> get connectingStream => _connectingController.stream;
|
||||
|
||||
Future<bool> checkBluetooth() async {
|
||||
final state = await FlutterBluePlus.adapterState.first;
|
||||
return state == BluetoothAdapterState.on;
|
||||
}
|
||||
|
||||
Future<bool> requestPermissions() async {
|
||||
final connStatus = await Permission.bluetoothConnect.status;
|
||||
developer.log(
|
||||
'[BLE] bluetoothConnect status: $connStatus',
|
||||
name: 'BleManager',
|
||||
);
|
||||
if (connStatus.isDenied) {
|
||||
developer.log('[BLE] requesting bluetoothConnect...', name: 'BleManager');
|
||||
await Permission.bluetoothConnect.request();
|
||||
}
|
||||
|
||||
final scanStatus = await Permission.bluetoothScan.status;
|
||||
developer.log(
|
||||
'[BLE] bluetoothScan status: $scanStatus',
|
||||
name: 'BleManager',
|
||||
);
|
||||
if (scanStatus.isDenied) {
|
||||
developer.log('[BLE] requesting bluetoothScan...', name: 'BleManager');
|
||||
await Permission.bluetoothScan.request();
|
||||
}
|
||||
|
||||
final locStatus = await Permission.locationWhenInUse.status;
|
||||
developer.log(
|
||||
'[BLE] locationWhenInUse status: $locStatus',
|
||||
name: 'BleManager',
|
||||
);
|
||||
if (locStatus.isDenied) {
|
||||
developer.log(
|
||||
'[BLE] requesting locationWhenInUse...',
|
||||
name: 'BleManager',
|
||||
);
|
||||
await Permission.locationWhenInUse.request();
|
||||
}
|
||||
|
||||
final allGranted =
|
||||
await Permission.bluetoothConnect.isGranted &&
|
||||
await Permission.bluetoothScan.isGranted;
|
||||
developer.log(
|
||||
'[BLE] all permissions granted: $allGranted',
|
||||
name: 'BleManager',
|
||||
);
|
||||
return allGranted;
|
||||
}
|
||||
|
||||
Future<void> openBluetooth() async {
|
||||
try {
|
||||
await FlutterBluePlus.turnOn();
|
||||
} catch (e) {
|
||||
await _goToSystemSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _goToSystemSettings() async {
|
||||
await openAppSettings();
|
||||
}
|
||||
|
||||
Future<void> _openLocationSettings() async {
|
||||
await openAppSettings();
|
||||
}
|
||||
|
||||
Future<bool> _checkLocationService() async {
|
||||
final locStatus = await Permission.location.serviceStatus;
|
||||
developer.log(
|
||||
'[BLE] location service status: $locStatus',
|
||||
name: 'BleManager',
|
||||
);
|
||||
return locStatus == ServiceStatus.enabled;
|
||||
}
|
||||
|
||||
Future<void> startScan({bool continuous = true}) async {
|
||||
final int myGen = ++_scanGen;
|
||||
developer.log(
|
||||
'[BLE] startScan called, gen=$myGen, continuous=$continuous',
|
||||
name: 'BleManager',
|
||||
);
|
||||
// 先停止任何残留扫描,确保干净启动
|
||||
try { await FlutterBluePlus.stopScan(); } catch (_) {}
|
||||
_scanResults.clear();
|
||||
_scanController.add([]);
|
||||
|
||||
final isOn = await checkBluetooth();
|
||||
if (myGen != _scanGen) return;
|
||||
developer.log('[BLE] Bluetooth adapter on: $isOn', name: 'BleManager');
|
||||
if (!isOn) {
|
||||
developer.log(
|
||||
'[BLE] Bluetooth is OFF, aborting scan',
|
||||
name: 'BleManager',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final granted = await requestPermissions();
|
||||
if (myGen != _scanGen) return;
|
||||
developer.log('[BLE] Permissions granted: $granted', name: 'BleManager');
|
||||
if (!granted) {
|
||||
developer.log(
|
||||
'[BLE] Permissions not granted, aborting scan',
|
||||
name: 'BleManager',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final locEnabled = await _checkLocationService();
|
||||
if (myGen != _scanGen) return;
|
||||
if (!locEnabled) {
|
||||
developer.log(
|
||||
'[BLE] Location service OFF, directing to settings',
|
||||
name: 'BleManager',
|
||||
);
|
||||
await _openLocationSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
_scanSubscription?.cancel();
|
||||
_scanSubscription = FlutterBluePlus.scanResults.listen(
|
||||
(results) {
|
||||
for (final result in results) {
|
||||
_scanResults[result.device.remoteId] = result;
|
||||
}
|
||||
developer.log(
|
||||
'[BLE] 📡 扫描到 ${results.length} 个设备: ${results.map((r) => '${r.device.platformName.isNotEmpty ? r.device.platformName : r.device.advName.isNotEmpty ? r.device.advName : r.device.remoteId} (${r.rssi}dBm)').join(', ')}',
|
||||
name: 'BleManager',
|
||||
);
|
||||
_scanController.add(_scanResults.values.toList());
|
||||
},
|
||||
onError: (e) {
|
||||
developer.log('[BLE] scanResults error: $e', name: 'BleManager');
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
if (continuous) {
|
||||
developer.log('[BLE] Starting continuous scan...', name: 'BleManager');
|
||||
await FlutterBluePlus.startScan(
|
||||
timeout: const Duration(days: 1),
|
||||
androidUsesFineLocation: true,
|
||||
androidScanMode: AndroidScanMode.lowLatency,
|
||||
androidLegacy: true,
|
||||
);
|
||||
} else {
|
||||
developer.log('[BLE] Starting 10s scan...', name: 'BleManager');
|
||||
await FlutterBluePlus.startScan(
|
||||
timeout: const Duration(seconds: 10),
|
||||
androidUsesFineLocation: true,
|
||||
androidScanMode: AndroidScanMode.lowLatency,
|
||||
androidLegacy: true,
|
||||
);
|
||||
}
|
||||
developer.log('[BLE] startScan completed', name: 'BleManager');
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'[BLE] startScan error: $e\n$stackTrace',
|
||||
name: 'BleManager',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshScan() async {
|
||||
await stopScan();
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
await startScan(continuous: true);
|
||||
}
|
||||
|
||||
Future<void> stopScan() async {
|
||||
_scanGen++;
|
||||
developer.log('[BLE] stopScan, gen=$_scanGen', name: 'BleManager');
|
||||
_scanSubscription?.cancel();
|
||||
await FlutterBluePlus.stopScan();
|
||||
}
|
||||
|
||||
/// 连接设备,返回 null 表示成功,返回错误信息字符串表示失败
|
||||
Future<String?> connect(BluetoothDevice device) async {
|
||||
// 标记正在连接,通知所有监听者
|
||||
_connectingDevice = device;
|
||||
_connectingController.add(device);
|
||||
try {
|
||||
await device
|
||||
.connect(license: License.nonprofit, mtu: 512)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
_connectedDevice = device;
|
||||
// 连接成功,立即停止扫描(避免扫描射频干扰导致连接断开)
|
||||
await stopScan();
|
||||
// 清除连接中状态
|
||||
_connectingDevice = null;
|
||||
_connectingController.add(null);
|
||||
// 监听设备连接状态,任何一方断开都能感知
|
||||
_deviceConnectionSubscription?.cancel();
|
||||
_deviceConnectionSubscription = device.connectionState.listen((state) {
|
||||
developer.log(
|
||||
'[BLE] device connectionState: $state',
|
||||
name: 'BleManager',
|
||||
);
|
||||
if (state == BluetoothConnectionState.disconnected) {
|
||||
_onDeviceDisconnected();
|
||||
}
|
||||
});
|
||||
_connectionController.add(device);
|
||||
await _discoverServices(device);
|
||||
// 确认协商后的 MTU
|
||||
try {
|
||||
final mtu = await device.requestMtu(512);
|
||||
_negotiatedMtu = mtu;
|
||||
developer.log('[BLE] MTU: $mtu', name: 'BleManager');
|
||||
} catch (e) {
|
||||
developer.log('[BLE] requestMtu failed: $e', name: 'BleManager');
|
||||
}
|
||||
return null; // 成功
|
||||
} catch (e) {
|
||||
// 连接失败,清除连接中状态
|
||||
_connectingDevice = null;
|
||||
_connectingController.add(null);
|
||||
// 返回具体的错误原因
|
||||
if (e is TimeoutException) {
|
||||
return '连接超时(15秒),请确认设备在附近且已开启';
|
||||
}
|
||||
return '连接失败: $e';
|
||||
}
|
||||
}
|
||||
|
||||
void _onDeviceDisconnected() {
|
||||
_connectedDevice = null;
|
||||
_connectingDevice = null;
|
||||
_writeCharacteristic = null;
|
||||
_readCharacteristic = null;
|
||||
_negotiatedMtu = 23;
|
||||
_readSubscription?.cancel();
|
||||
_deviceConnectionSubscription?.cancel();
|
||||
_connectionController.add(null);
|
||||
_connectingController.add(null);
|
||||
// 清空协议解析器缓冲区
|
||||
_parser.clear();
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
final device = _connectedDevice;
|
||||
if (device == null) return;
|
||||
developer.log('[BLE] 发起断开连接...', name: 'BleManager');
|
||||
await device.disconnect();
|
||||
// 等待连接状态流确认已断开,设置超时防止无限等待
|
||||
try {
|
||||
await device.connectionState
|
||||
.firstWhere((s) => s == BluetoothConnectionState.disconnected)
|
||||
.timeout(const Duration(seconds: 3));
|
||||
developer.log('[BLE] 连接已确认断开', name: 'BleManager');
|
||||
} catch (_) {
|
||||
developer.log('[BLE] 等待断开超时,强制清理', name: 'BleManager');
|
||||
}
|
||||
_onDeviceDisconnected();
|
||||
}
|
||||
|
||||
Future<void> _discoverServices(BluetoothDevice device) async {
|
||||
final services = await device.discoverServices();
|
||||
for (final service in services) {
|
||||
for (final characteristic in service.characteristics) {
|
||||
if (characteristic.properties.write) {
|
||||
_writeCharacteristic = characteristic;
|
||||
}
|
||||
if (characteristic.properties.notify) {
|
||||
_readCharacteristic = characteristic;
|
||||
await characteristic.setNotifyValue(true);
|
||||
_readSubscription?.cancel();
|
||||
_readSubscription = characteristic.lastValueStream.listen((value) {
|
||||
_onDataReceived(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onDataReceived(List<int> data) {
|
||||
developer.log(
|
||||
'[BLE] 📩 收: ${data.length}B ${_bytesToHex(data)}',
|
||||
name: 'BleManager',
|
||||
);
|
||||
_parser.append(data);
|
||||
final packets = _parser.parse();
|
||||
developer.log(
|
||||
'[BLE] 📦 解析 ${packets.length}包 (buf ${_parser.bufferLength}B)',
|
||||
name: 'BleManager',
|
||||
);
|
||||
if (packets.isEmpty && data.isNotEmpty) {
|
||||
developer.log(
|
||||
'[BLE] ⚠️ 非标准帧 raw(${data.length}B)',
|
||||
name: 'BleManager',
|
||||
);
|
||||
final packet = BlePacket(
|
||||
command: 0x00,
|
||||
payload: Uint8List.fromList(data),
|
||||
);
|
||||
_storePacket(packet);
|
||||
_packetController.add(packet);
|
||||
return;
|
||||
}
|
||||
for (final packet in packets) {
|
||||
developer.log(
|
||||
'[BLE] cmd=0x${packet.command.toRadixString(16).toUpperCase().padLeft(2, '0')} '
|
||||
'payload(${packet.payload.length}B): ${_bytesToHex(packet.payload)}',
|
||||
name: 'BleManager',
|
||||
);
|
||||
_storePacket(packet);
|
||||
_packetController.add(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void _storePacket(BlePacket packet) {
|
||||
receivedPacketStore.insert(0, packet);
|
||||
if (receivedPacketStore.length > _maxStoredPackets) {
|
||||
receivedPacketStore.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
String _bytesToHex(List<int> bytes) {
|
||||
if (bytes.isEmpty) return '';
|
||||
return bytes
|
||||
.map((b) => b.toRadixString(16).toUpperCase().padLeft(2, '0'))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Future<void> sendCommand(int command, List<int> payload) async {
|
||||
final char = _writeCharacteristic;
|
||||
if (char == null) {
|
||||
developer.log('[BLE] ❌ 写特征为空,无法发送', name: 'BleManager');
|
||||
return;
|
||||
}
|
||||
final frame = ProtocolParser.pack(command, payload);
|
||||
final crc = frame.length >= 5
|
||||
? 'CRC16=0x${frame[frame.length - 4].toRadixString(16).padLeft(2, '0')}${frame[frame.length - 3].toRadixString(16).padLeft(2, '0')}'
|
||||
: '';
|
||||
developer.log(
|
||||
'[BLE] 📤 发送: 0x${command.toRadixString(16).toUpperCase().padLeft(2, '0')}, '
|
||||
'帧(${frame.length}B) $crc\n'
|
||||
' ${_bytesToHex(frame)}',
|
||||
name: 'BleManager',
|
||||
);
|
||||
// 分片写入:每次最多写 (MTU - 3) 字节,全部走 withResponse 确保可靠性
|
||||
final maxWriteLen = _negotiatedMtu - 3;
|
||||
final totalChunks = (frame.length + maxWriteLen - 1) ~/ maxWriteLen;
|
||||
if (totalChunks > 1) {
|
||||
developer.log(
|
||||
'[BLE] 🔀 分${totalChunks}片写入 (MTU=$_negotiatedMtu, 每片≤${maxWriteLen}B)',
|
||||
name: 'BleManager',
|
||||
);
|
||||
}
|
||||
for (int offset = 0; offset < frame.length; offset += maxWriteLen) {
|
||||
final end = (offset + maxWriteLen <= frame.length)
|
||||
? offset + maxWriteLen
|
||||
: frame.length;
|
||||
final chunk = frame.sublist(offset, end);
|
||||
if (totalChunks > 1) {
|
||||
final chunkIdx = offset ~/ maxWriteLen + 1;
|
||||
developer.log(
|
||||
'[BLE] 片$chunkIdx/$totalChunks: offset=$offset len=${chunk.length}B ${_bytesToHex(chunk)}',
|
||||
name: 'BleManager',
|
||||
);
|
||||
}
|
||||
await char.write(chunk, withoutResponse: false);
|
||||
}
|
||||
developer.log('[BLE] ✅ 写入完成', name: 'BleManager');
|
||||
}
|
||||
|
||||
Future<void> sendRawBytes(List<int> bytes) async {
|
||||
if (_writeCharacteristic == null) return;
|
||||
await _writeCharacteristic!.write(bytes, withoutResponse: false);
|
||||
}
|
||||
|
||||
Future<void> sendHeartbeat() async {
|
||||
await sendCommand(0xFF, []);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_scanSubscription?.cancel();
|
||||
_readSubscription?.cancel();
|
||||
_adapterStateSubscription?.cancel();
|
||||
_deviceConnectionSubscription?.cancel();
|
||||
_scanController.close();
|
||||
_packetController.close();
|
||||
_connectionController.close();
|
||||
_connectingController.close();
|
||||
}
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:typed_data';
|
||||
import '../protocol/machine_protocol_constants.dart';
|
||||
import 'mc700_device_config.dart';
|
||||
|
||||
/// BLE 协议解析出的单个字段
|
||||
class BleField {
|
||||
final String label;
|
||||
final String value;
|
||||
const BleField({required this.label, required this.value});
|
||||
}
|
||||
|
||||
/// BLE 协议解析结果
|
||||
class BleDecodedResult {
|
||||
final List<BleField> fields;
|
||||
final String rawHex;
|
||||
|
||||
/// 0x05 读配置时携带的配置实体(可修改后回写)
|
||||
final Mc700DeviceConfig? configEntity;
|
||||
const BleDecodedResult({
|
||||
required this.fields,
|
||||
required this.rawHex,
|
||||
this.configEntity,
|
||||
});
|
||||
bool get isEmpty => fields.isEmpty;
|
||||
}
|
||||
|
||||
/// BLE 协议解析器
|
||||
/// 与 TCP net_message_dispatcher 使用相同的协议格式:AB AA cmd [payload] ... AA AB
|
||||
/// payload 通常为 UTF-8 逗号分隔文本,部分指令为二进制
|
||||
class BleProtocolDecoder {
|
||||
/// 入口:按命令类型分发解析
|
||||
static BleDecodedResult decode(int command, Uint8List payload) {
|
||||
final hex = _bytesToHex(payload);
|
||||
try {
|
||||
switch (command) {
|
||||
case MachineProtocolConstants.cmdStatusInfo:
|
||||
return _decodeStatusInfo(payload, hex);
|
||||
case MachineProtocolConstants.cmdRemoteControl:
|
||||
return _decodeRemoteControl(payload, hex);
|
||||
case MachineProtocolConstants.cmdGetId:
|
||||
return _decodeGetId(payload, hex);
|
||||
case MachineProtocolConstants.cmdGetAuth:
|
||||
return _decodeGetAuth(payload, hex);
|
||||
case MachineProtocolConstants.cmdReadConfig:
|
||||
return _decodeReadConfig(payload, hex);
|
||||
case MachineProtocolConstants.cmdWriteConfig:
|
||||
return _decodeWriteConfig(payload, hex);
|
||||
case MachineProtocolConstants.cmdHeartbeat:
|
||||
return _decodeHeartbeat(payload, hex);
|
||||
case MachineProtocolConstants.cmdPathPlanning:
|
||||
return _decodePathPlanning(payload, hex);
|
||||
case MachineProtocolConstants.cmdObstacleAvoid:
|
||||
return _decodeObstacleAvoid(payload, hex);
|
||||
default:
|
||||
return _decodeGeneric(command, payload, hex);
|
||||
}
|
||||
} catch (e) {
|
||||
return BleDecodedResult(
|
||||
fields: [const BleField(label: '解析错误', value: '异常')],
|
||||
rawHex: hex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 0x02 状态信息(与 TCP RunningStatusEntity.fromFields 一致,24字段逗号分隔) ───
|
||||
static BleDecodedResult _decodeStatusInfo(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
|
||||
try {
|
||||
final text = utf8.decode(data);
|
||||
if (text.contains(',')) {
|
||||
final parts = text.split(',');
|
||||
return BleDecodedResult(fields: _parseStatusFields(parts), rawHex: hex);
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
final text = latin1.decode(data);
|
||||
if (text.contains(',')) {
|
||||
final parts = text.split(',');
|
||||
return BleDecodedResult(
|
||||
fields: _parseStatusFields(parts),
|
||||
rawHex: hex,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (data.isNotEmpty) {
|
||||
final status = data[0];
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '状态码',
|
||||
value: '0x${status.toRadixString(16).toUpperCase().padLeft(2, '0')}',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.length >= 2) {
|
||||
final mode = data[1];
|
||||
const modes = {0x00: '待机', 0x01: '遥控', 0x02: '自动', 0x03: '急停'};
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '控制模式',
|
||||
value: modes[mode] ?? '未知(0x${mode.toRadixString(16)})',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.length >= 3)
|
||||
fields.add(BleField(label: '电量', value: '${data[2]}%'));
|
||||
if (data.length >= 4) {
|
||||
final fault = data[3];
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '故障状态',
|
||||
value: fault == 0
|
||||
? '无故障'
|
||||
: '故障码: 0x${fault.toRadixString(16).toUpperCase().padLeft(2, '0')}',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.length >= 6) {
|
||||
final speed = (data[5] << 8) | data[4];
|
||||
fields.add(BleField(label: '速度', value: '$speed'));
|
||||
}
|
||||
if (data.length > 8) {
|
||||
try {
|
||||
final text = String.fromCharCodes(data.sublist(8));
|
||||
if (text.isNotEmpty && !text.contains('\x00')) {
|
||||
fields.add(BleField(label: '附加', value: text));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
/// 解析逗号分隔的 24 字段状态数据(与 RunningStatusEntity.fromFields 完全一致)
|
||||
static List<BleField> _parseStatusFields(List<String> parts) {
|
||||
while (parts.length < 24) {
|
||||
parts.add('');
|
||||
}
|
||||
|
||||
String controlModeText(String v) {
|
||||
return switch (v) {
|
||||
'0' => '待机',
|
||||
'1' => '遥控',
|
||||
'2' => '自动',
|
||||
'3' => '急停',
|
||||
_ => v.isNotEmpty ? v : '--',
|
||||
};
|
||||
}
|
||||
|
||||
String qualText(String v) {
|
||||
final q = int.tryParse(v) ?? -1;
|
||||
return switch (q) {
|
||||
0 => '无效',
|
||||
1 => '单点定位',
|
||||
2 => '差分定位',
|
||||
4 => '固定解',
|
||||
5 => '浮点解',
|
||||
_ => v.isNotEmpty ? v : '--',
|
||||
};
|
||||
}
|
||||
|
||||
String headingText(String v) {
|
||||
final h = int.tryParse(v) ?? -1;
|
||||
return switch (h) {
|
||||
0 => '未初始化',
|
||||
1 => '已初始化',
|
||||
_ => v.isNotEmpty ? v : '--',
|
||||
};
|
||||
}
|
||||
|
||||
String obstacleText(String v) {
|
||||
final o = int.tryParse(v) ?? -1;
|
||||
return switch (o) {
|
||||
0 => '无障碍',
|
||||
1 => '有障碍',
|
||||
_ => v.isNotEmpty ? v : '--',
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
BleField(label: '电压', value: '${_tryParseDouble(parts[0])} V'),
|
||||
BleField(label: '左目标速度', value: _tryParseDouble(parts[1])),
|
||||
BleField(label: '右目标速度', value: _tryParseDouble(parts[2])),
|
||||
BleField(label: '左实测速度', value: _tryParseDouble(parts[3])),
|
||||
BleField(label: '右实测速度', value: _tryParseDouble(parts[4])),
|
||||
BleField(label: '左电流', value: '${_tryParseDouble(parts[5])} A'),
|
||||
BleField(label: '右电流', value: '${_tryParseDouble(parts[6])} A'),
|
||||
BleField(label: '左电机温度', value: '${_tryParseDouble(parts[7])} ℃'),
|
||||
BleField(label: '右电机温度', value: '${_tryParseDouble(parts[8])} ℃'),
|
||||
BleField(label: '芯片温度', value: '${_tryParseDouble(parts[9])} ℃'),
|
||||
BleField(label: '偏航角', value: '${_tryParseDouble(parts[10])} °'),
|
||||
BleField(label: '俯仰角', value: '${_tryParseDouble(parts[11])} °'),
|
||||
BleField(label: '翻滚角', value: '${_tryParseDouble(parts[12])} °'),
|
||||
BleField(label: '卫星数量', value: parts[13].isNotEmpty ? parts[13] : '--'),
|
||||
BleField(label: '定位质量', value: qualText(parts[14])),
|
||||
BleField(label: '航向状态', value: headingText(parts[15])),
|
||||
BleField(label: '纬度', value: parts[16].isNotEmpty ? parts[16] : '--'),
|
||||
BleField(label: '经度', value: parts[17].isNotEmpty ? parts[17] : '--'),
|
||||
BleField(label: '时间戳', value: parts[18].isNotEmpty ? parts[18] : '--'),
|
||||
BleField(
|
||||
label: '割刀速度',
|
||||
value: parts[19].isNotEmpty ? '${parts[19]} rpm' : '--',
|
||||
),
|
||||
BleField(label: '控制模式', value: controlModeText(parts[20])),
|
||||
BleField(
|
||||
label: '电量',
|
||||
value: parts[21].isNotEmpty ? '${parts[21]}%' : '--',
|
||||
),
|
||||
BleField(label: '工作面积', value: parts[22].isNotEmpty ? parts[22] : '--'),
|
||||
BleField(label: '障碍物', value: obstacleText(parts[23])),
|
||||
];
|
||||
}
|
||||
|
||||
static String _tryParseDouble(String s) {
|
||||
if (s.isEmpty) return '--';
|
||||
final v = double.tryParse(s);
|
||||
if (v == null) return s;
|
||||
if (v == v.roundToDouble() && v.abs() < 10000) return v.toInt().toString();
|
||||
return v.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// ─── 0x00 远程遥控 ───
|
||||
static BleDecodedResult _decodeRemoteControl(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
try {
|
||||
final text = utf8.decode(data, allowMalformed: true);
|
||||
if (text.contains(',')) {
|
||||
final parts = text.split(',');
|
||||
while (parts.length < 5) parts.add('');
|
||||
fields.add(
|
||||
BleField(label: '左右速度', value: parts[0].isNotEmpty ? parts[0] : '--'),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '前后速度', value: parts[1].isNotEmpty ? parts[1] : '--'),
|
||||
);
|
||||
final modeStr = switch (parts[2]) {
|
||||
'0' => '停止',
|
||||
'1' => '遥控',
|
||||
'2' => '自动',
|
||||
_ => parts[2].isNotEmpty ? parts[2] : '--',
|
||||
};
|
||||
fields.add(BleField(label: '控制模式', value: modeStr));
|
||||
if (parts.length > 3 && parts[3].isNotEmpty)
|
||||
fields.add(BleField(label: '参数4', value: parts[3]));
|
||||
if (parts.length > 4 && parts[4].isNotEmpty)
|
||||
fields.add(BleField(label: '参数5', value: parts[4]));
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (data.length >= 5) {
|
||||
fields.add(BleField(label: '左右速度', value: '${(data[1] << 8) | data[0]}'));
|
||||
fields.add(BleField(label: '前后速度', value: '${(data[3] << 8) | data[2]}'));
|
||||
const modes = {0: '停止', 1: '遥控', 2: '自动'};
|
||||
fields.add(
|
||||
BleField(label: '控制模式', value: modes[data[4]] ?? '未知(${data[4]})'),
|
||||
);
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 0x03 查询ID ───
|
||||
static BleDecodedResult _decodeGetId(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
try {
|
||||
final text = utf8.decode(data, allowMalformed: true);
|
||||
if (text.replaceAll('\x00', '').trim().isNotEmpty) {
|
||||
fields.add(
|
||||
BleField(label: '设备ID', value: text.replaceAll('\x00', '').trim()),
|
||||
);
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
final ascii = String.fromCharCodes(data);
|
||||
if (RegExp(r'^[\x20-\x7E]+$').hasMatch(ascii)) {
|
||||
fields.add(BleField(label: '设备ID', value: ascii));
|
||||
} else {
|
||||
fields.add(BleField(label: '设备ID(Hex)', value: hex));
|
||||
}
|
||||
} catch (_) {
|
||||
fields.add(BleField(label: '设备ID(Hex)', value: hex));
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 0x04 获取授权 ───
|
||||
static BleDecodedResult _decodeGetAuth(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
try {
|
||||
final text = utf8.decode(data, allowMalformed: true);
|
||||
final trimmed = text.replaceAll('\x00', '').trim();
|
||||
if (trimmed == '1' || trimmed.toLowerCase() == 'true') {
|
||||
fields.add(const BleField(label: '授权状态', value: '已授权'));
|
||||
} else if (trimmed == '0' || trimmed.toLowerCase() == 'false') {
|
||||
fields.add(const BleField(label: '授权状态', value: '未授权'));
|
||||
} else if (trimmed.isNotEmpty) {
|
||||
fields.add(BleField(label: '授权状态', value: trimmed));
|
||||
}
|
||||
if (fields.isNotEmpty)
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
} catch (_) {}
|
||||
|
||||
if (data.isNotEmpty) {
|
||||
if (data[0] == 0x01) {
|
||||
fields.add(const BleField(label: '授权状态', value: '已授权'));
|
||||
} else if (data[0] == 0x00) {
|
||||
fields.add(const BleField(label: '授权状态', value: '未授权'));
|
||||
} else {
|
||||
fields.add(
|
||||
BleField(label: '授权状态(Hex)', value: '0x${data[0].toRadixString(16)}'),
|
||||
);
|
||||
}
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 0x05 读配置(按嵌入式协议规范精确解析,171字节) ───
|
||||
static BleDecodedResult _decodeReadConfig(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
if (data.isEmpty) {
|
||||
fields.add(const BleField(label: '状态', value: '空数据'));
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
final bd = ByteData.sublistView(data);
|
||||
final len = data.length;
|
||||
|
||||
// 1. UID固化标志 @payload[0] (帧字节3)
|
||||
if (len >= 1) {
|
||||
fields.add(
|
||||
BleField(
|
||||
label: 'UID固化标志',
|
||||
value: data[0] == 1 ? '已固化' : '未固化(${data[0]})',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 芯片UID @payload[1..45] (帧字节4-48, 45字节)
|
||||
if (len >= 46) {
|
||||
fields.add(
|
||||
BleField(label: '芯片UID', value: _readNullTermString(data, 1, 45)),
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 遥控器通道配置 @payload[46..64] (帧字节49-67, 19字节)
|
||||
if (len >= 65) {
|
||||
fields.add(
|
||||
BleField(label: '遥控器通道配置', value: _bytesToHex(data.sublist(46, 65))),
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 字节68位域 @payload[65] (帧字节68)
|
||||
if (len >= 66) {
|
||||
final b = data[65];
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '割刀电机模式',
|
||||
value: (b & 0x03) == 0 ? '纯电' : '油电(${b & 0x03})',
|
||||
),
|
||||
);
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '行走电机模式',
|
||||
value: ((b >> 2) & 0x03) == 0 ? '轮式' : '履带(${(b >> 2) & 0x03})',
|
||||
),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '左轮电机极性', value: (b & 0x10) == 0 ? '高极性' : '低极性(反取)'),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '右轮电机极性', value: (b & 0x20) == 0 ? '高极性' : '低极性(反取)'),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '左右轮通道交换', value: (b & 0x40) == 0 ? '不交换' : '通道互换'),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '联网目标', value: (b & 0x80) == 0 ? 'WiFi' : '4G'),
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 前进速度限制 @payload[66..67] (帧字节69-70, uint16 LE)
|
||||
if (len >= 68) {
|
||||
fields.add(
|
||||
BleField(label: '前进速度限制', value: '${bd.getUint16(66, Endian.little)}'),
|
||||
);
|
||||
}
|
||||
|
||||
// 6. 转向速度限制 @payload[68..69] (帧字节71-72, uint16 LE)
|
||||
if (len >= 70) {
|
||||
fields.add(
|
||||
BleField(label: '转向速度限制', value: '${bd.getUint16(68, Endian.little)}'),
|
||||
);
|
||||
}
|
||||
|
||||
// 7. 字节73位域 @payload[70] (帧字节73)
|
||||
if (len >= 71) {
|
||||
final b = data[70];
|
||||
fields.add(
|
||||
BleField(label: '割刀通道极性', value: (b & 0x01) == 0 ? '高极性' : '低极性(反取)'),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '风门通道极性', value: (b & 0x02) == 0 ? '高极性' : '低极性(反取)'),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '油门通道极性', value: (b & 0x04) == 0 ? '高极性' : '低极性(反取)'),
|
||||
);
|
||||
fields.add(BleField(label: '底盘升降保护时间', value: '${(b >> 3) & 0x1F} 秒'));
|
||||
fields.add(
|
||||
BleField(label: 'RTK配置', value: (b & 0x80) == 0 ? '单天线' : '双天线'),
|
||||
);
|
||||
}
|
||||
|
||||
// 8. 字节74位域 @payload[71] (帧字节74)
|
||||
if (len >= 72) {
|
||||
final b = data[71];
|
||||
fields.add(BleField(label: '割刀通道配置', value: '${b & 0x0F}'));
|
||||
fields.add(BleField(label: '风门通道配置', value: '${(b >> 4) & 0x0F}'));
|
||||
}
|
||||
|
||||
// 9. 字节75位域 @payload[72] (帧字节75)
|
||||
if (len >= 73) {
|
||||
final b = data[72];
|
||||
fields.add(BleField(label: '油门通道配置', value: '${b & 0x0F}'));
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '遥控器类型',
|
||||
value: (b >> 4) & 0x07 == 0 ? '飞控' : '自定义遥控器(${(b >> 4) & 0x07})',
|
||||
),
|
||||
);
|
||||
fields.add(
|
||||
BleField(label: '是否搭载继电器板', value: (b & 0x80) == 0 ? '未搭载' : '搭载'),
|
||||
);
|
||||
}
|
||||
|
||||
// 10. 字节76位域 @payload[73] (帧字节76)
|
||||
if (len >= 74) {
|
||||
final b = data[73];
|
||||
fields.add(BleField(label: '底盘升降通道配置', value: '${b & 0x0F}'));
|
||||
fields.add(BleField(label: '底盘通道配置', value: '${(b >> 4) & 0x0F}'));
|
||||
}
|
||||
|
||||
// 11. 字节77位域 @payload[74] (帧字节77)
|
||||
if (len >= 75) {
|
||||
final b = data[74];
|
||||
fields.add(BleField(label: '机械臂通道配置', value: '${b & 0x0F}'));
|
||||
fields.add(BleField(label: '燃油泵通道配置', value: '${(b >> 4) & 0x0F}'));
|
||||
}
|
||||
|
||||
// 12. WiFi名称 @payload[75..94] (帧字节78-97, 20字节)
|
||||
if (len >= 95) {
|
||||
fields.add(
|
||||
BleField(label: 'WiFi名称', value: _readNullTermString(data, 75, 20)),
|
||||
);
|
||||
}
|
||||
|
||||
// 13. WiFi密码 @payload[95..114] (帧字节98-117, 20字节)
|
||||
if (len >= 115) {
|
||||
fields.add(
|
||||
BleField(label: 'WiFi密码', value: _readNullTermString(data, 95, 20)),
|
||||
);
|
||||
}
|
||||
|
||||
// 14. 字节118位域 @payload[115] (帧字节118)
|
||||
if (len >= 116) {
|
||||
final b = data[115];
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '电池类型',
|
||||
value: (b & 0x03) == 0 ? '铅酸' : '锂电(${b & 0x03})',
|
||||
),
|
||||
);
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '行走驱动配置',
|
||||
value: switch ((b >> 2) & 0x03) {
|
||||
0 => '风德控',
|
||||
1 => '山东神澜BLDC',
|
||||
2 => '山东神澜FOC',
|
||||
_ => '未知(${(b >> 2) & 0x03})',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 15. 转速比 @payload[116..119] (帧字节119-122, float LE)
|
||||
if (len >= 120) {
|
||||
final v = bd.getFloat32(116, Endian.little);
|
||||
fields.add(BleField(label: '转速比', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 16. 机器人长度 @payload[120..123] (帧字节123-126, float LE)
|
||||
if (len >= 124) {
|
||||
final v = bd.getFloat32(120, Endian.little);
|
||||
fields.add(BleField(label: '机器人长度', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 17. 机器人宽度 @payload[124..127] (帧字节127-130, float LE)
|
||||
if (len >= 128) {
|
||||
final v = bd.getFloat32(124, Endian.little);
|
||||
fields.add(BleField(label: '机器人宽度', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 18. 机器人高度 @payload[128..131] (帧字节131-134, float LE)
|
||||
if (len >= 132) {
|
||||
final v = bd.getFloat32(128, Endian.little);
|
||||
fields.add(BleField(label: '机器人高度', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 19. 割刀宽度 @payload[132..135] (帧字节135-138, float LE)
|
||||
if (len >= 136) {
|
||||
final v = bd.getFloat32(132, Endian.little);
|
||||
fields.add(BleField(label: '割刀宽度', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 20. 轮胎尺寸 @payload[136..139] (帧字节139-142, float LE)
|
||||
if (len >= 140) {
|
||||
final v = bd.getFloat32(136, Endian.little);
|
||||
fields.add(BleField(label: '轮胎尺寸', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 21. 左轮前进增益 @payload[140..143] (帧字节143-146, float LE)
|
||||
if (len >= 144) {
|
||||
final v = bd.getFloat32(140, Endian.little);
|
||||
fields.add(BleField(label: '左轮前进增益', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 22. 左轮后退增益 @payload[144..147] (帧字节147-150, float LE)
|
||||
if (len >= 148) {
|
||||
final v = bd.getFloat32(144, Endian.little);
|
||||
fields.add(BleField(label: '左轮后退增益', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 23. 右轮前进增益 @payload[148..151] (帧字节151-154, float LE)
|
||||
if (len >= 152) {
|
||||
final v = bd.getFloat32(148, Endian.little);
|
||||
fields.add(BleField(label: '右轮前进增益', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 24. 右轮后退增益 @payload[152..155] (帧字节155-158, float LE)
|
||||
if (len >= 156) {
|
||||
final v = bd.getFloat32(152, Endian.little);
|
||||
fields.add(BleField(label: '右轮后退增益', value: v.toStringAsFixed(2)));
|
||||
}
|
||||
|
||||
// 25. 固件版本 @payload[156..170] (帧字节159-173, 15字节)
|
||||
if (len >= 171) {
|
||||
fields.add(
|
||||
BleField(label: '固件版本', value: _readNullTermString(data, 156, 15)),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建配置实体(用于后续编辑和回写)
|
||||
Mc700DeviceConfig? configEntity;
|
||||
if (data.length >= 171) {
|
||||
try {
|
||||
configEntity = Mc700DeviceConfig.fromBytes(data);
|
||||
} catch (e) {
|
||||
developer.log('[BLE] 配置实体解析失败: $e', name: 'BleProtocolDecoder');
|
||||
}
|
||||
}
|
||||
|
||||
return BleDecodedResult(
|
||||
fields: fields,
|
||||
rawHex: hex,
|
||||
configEntity: configEntity,
|
||||
);
|
||||
}
|
||||
|
||||
/// 读取 null 终止字符串(最多读取 [maxLen] 字节)
|
||||
static String _readNullTermString(Uint8List data, int offset, int maxLen) {
|
||||
final buffer = StringBuffer();
|
||||
for (int i = 0; i < maxLen && offset + i < data.length; i++) {
|
||||
final b = data[offset + i];
|
||||
if (b == 0x00) break;
|
||||
if (b >= 0x20 && b <= 0x7E) {
|
||||
buffer.writeCharCode(b);
|
||||
} else {
|
||||
buffer.writeCharCode(0x2E);
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// ─── 0x06 写配置 ───
|
||||
static BleDecodedResult _decodeWriteConfig(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
if (data.isNotEmpty) {
|
||||
final ok = data[0];
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '写入结果',
|
||||
value: ok == 0x01
|
||||
? '成功'
|
||||
: ok == 0x00
|
||||
? '失败'
|
||||
: '0x${ok.toRadixString(16)}',
|
||||
),
|
||||
);
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 0xFF 心跳包 ───
|
||||
static BleDecodedResult _decodeHeartbeat(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
if (data.isEmpty) {
|
||||
fields.add(const BleField(label: '心跳', value: '正常'));
|
||||
} else if (data.length >= 2) {
|
||||
final interval = (data[1] << 8) | data[0];
|
||||
fields.add(BleField(label: '心跳间隔', value: '${interval}ms'));
|
||||
} else {
|
||||
fields.add(BleField(label: '心跳数据', value: hex));
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 0x01 路径规划 ───
|
||||
static BleDecodedResult _decodePathPlanning(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
try {
|
||||
final text = utf8.decode(data, allowMalformed: true);
|
||||
if (text.contains(',')) {
|
||||
final parts = text.split(',');
|
||||
if (parts.length >= 2) {
|
||||
fields.add(BleField(label: '点编号', value: parts[0]));
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '状态',
|
||||
value: switch (parts[1]) {
|
||||
'1' => '已到达',
|
||||
'2' => '收到指令',
|
||||
_ => parts[1],
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (parts.length >= 4) {
|
||||
fields.add(BleField(label: '纬度', value: parts[2]));
|
||||
fields.add(BleField(label: '经度', value: parts[3]));
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (data.length >= 3) {
|
||||
fields.add(BleField(label: '点编号', value: '${data[0]}'));
|
||||
final status = data.length > 1 ? data[1] : -1;
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '状态',
|
||||
value: status == 0x01
|
||||
? '已到达'
|
||||
: status == 0x02
|
||||
? '收到指令'
|
||||
: '0x${status.toRadixString(16)}',
|
||||
),
|
||||
);
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 0x07 避障 ───
|
||||
static BleDecodedResult _decodeObstacleAvoid(Uint8List data, String hex) {
|
||||
final fields = <BleField>[];
|
||||
if (data.isNotEmpty) {
|
||||
final flag = data[0];
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '避障状态',
|
||||
value: flag == 0x01
|
||||
? '触发'
|
||||
: flag == 0x00
|
||||
? '正常'
|
||||
: '0x${flag.toRadixString(16)}',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.length >= 3) {
|
||||
fields.add(BleField(label: '距离', value: '${(data[2] << 8) | data[1]}mm'));
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 通用/未知指令 ───
|
||||
static BleDecodedResult _decodeGeneric(
|
||||
int command,
|
||||
Uint8List data,
|
||||
String hex,
|
||||
) {
|
||||
final fields = <BleField>[];
|
||||
try {
|
||||
final text = utf8.decode(data, allowMalformed: true);
|
||||
final trimmed = text.replaceAll('\x00', '').trim();
|
||||
if (trimmed.isNotEmpty) {
|
||||
fields.add(
|
||||
BleField(
|
||||
label: '文本内容',
|
||||
value: trimmed.length > 50
|
||||
? '${trimmed.substring(0, 50)}...'
|
||||
: trimmed,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (fields.isEmpty) {
|
||||
fields.add(BleField(label: '数据长度', value: '${data.length}B'));
|
||||
}
|
||||
return BleDecodedResult(fields: fields, rawHex: hex);
|
||||
}
|
||||
|
||||
// ─── 工具方法 ───
|
||||
static String _bytesToHex(List<int> bytes) {
|
||||
if (bytes.isEmpty) return '';
|
||||
return bytes
|
||||
.map((b) => b.toRadixString(16).toUpperCase().padLeft(2, '0'))
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class BytesUtil {
|
||||
static String bytesToHex(List<int> bytes) {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join();
|
||||
}
|
||||
|
||||
static Uint8List hexToBytes(String hex) {
|
||||
hex = hex.replaceAll(' ', '').replaceAll('-', '');
|
||||
if (hex.length % 2 != 0) hex = '0' + hex;
|
||||
final bytes = Uint8List(hex.length ~/ 2);
|
||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||
bytes[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static String bytesToString(List<int> bytes) {
|
||||
try {
|
||||
return utf8.decode(bytes);
|
||||
} catch (_) {
|
||||
return String.fromCharCodes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
static Uint8List stringToBytes(String str) {
|
||||
return Uint8List.fromList(utf8.encode(str));
|
||||
}
|
||||
|
||||
static List<int> toByteList(Uint8List data) {
|
||||
return List<int>.from(data);
|
||||
}
|
||||
|
||||
static String bytesToHexSpaced(List<int> bytes) {
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(' ');
|
||||
}
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// MC700设备配置实体类(与嵌入式协议规范一一对应)
|
||||
/// 所有偏移均为 payload 内偏移(ProtocolParser 已剥离帧头/命令/校验/帧尾)
|
||||
/// payload 共 171 字节,索引 0~170
|
||||
class Mc700DeviceConfig {
|
||||
// ====== 字段定义(按嵌入式协议规范) ======
|
||||
|
||||
// --- 基础标识 ---
|
||||
late int uidSolidifiedFlag; // payload[0] UID固化标志 (0=未固化, 1=已固化)
|
||||
late String chipUid; // payload[1..45] 芯片UID (45字节)
|
||||
late List<int> remoteChannelConfig; // payload[46..64] 遥控器通道配置 (19字节)
|
||||
|
||||
// --- 字节68位域 (payload[65]) ---
|
||||
late int knifeMotorMode; // bit0-1 割刀电机模式 (0=纯电, 1=油电)
|
||||
late int walkMotorMode; // bit2-3 行走电机模式 (0=轮式, 1=履带)
|
||||
late bool leftWheelPolarity; // bit4 左轮极性 (false=高, true=低)
|
||||
late bool rightWheelPolarity; // bit5 右轮极性 (false=高, true=低)
|
||||
late bool channelSwap; // bit6 左右轮通道交换
|
||||
late bool use4g; // bit7 联网目标 (false=WiFi, true=4G)
|
||||
|
||||
// --- 速度限制 ---
|
||||
late int forwardSpeedLimit; // payload[66..67] 前进速度限制 (uint16 LE)
|
||||
late int turnSpeedLimit; // payload[68..69] 转向速度限制 (uint16 LE)
|
||||
|
||||
// --- 字节73位域 (payload[70]) ---
|
||||
late bool knifeChannelPolarity; // bit0 割刀通道极性
|
||||
late bool fanChannelPolarity; // bit1 风门通道极性
|
||||
late bool throttleChannelPolarity; // bit2 油门通道极性
|
||||
late int liftProtectTime; // bit3-6 底盘升降保护时间 (秒)
|
||||
late bool dualRtk; // bit7 RTK配置 (false=单天线, true=双天线)
|
||||
|
||||
// --- 字节74位域 (payload[71]) ---
|
||||
late int knifeChannelConfig; // bit0-3 割刀通道配置
|
||||
late int fanChannelConfig; // bit4-7 风门通道配置
|
||||
|
||||
// --- 字节75位域 (payload[72]) ---
|
||||
late int throttleChannelConfig; // bit0-3 油门通道配置
|
||||
late int remoteType; // bit4-6 遥控器类型 (0=飞控, 1=自定义1)
|
||||
late bool relayBoard; // bit7 是否搭载继电器板
|
||||
|
||||
// --- 字节76位域 (payload[73]) ---
|
||||
late int chassisLiftChannel; // bit0-3 底盘升降通道配置
|
||||
late int chassisChannel; // bit4-7 底盘通道配置
|
||||
|
||||
// --- 字节77位域 (payload[74]) ---
|
||||
late int armChannel; // bit0-3 机械臂通道配置
|
||||
late int fuelPumpChannel; // bit4-7 燃油泵通道配置
|
||||
|
||||
// --- WiFi ---
|
||||
late String wifiName; // payload[75..94] WiFi名称 (20字节)
|
||||
late String wifiPassword; // payload[95..114] WiFi密码 (20字节)
|
||||
|
||||
// --- 字节118位域 (payload[115]) ---
|
||||
late int batteryType; // bit0-1 电池类型 (0=铅酸, 1=锂电)
|
||||
late int walkDriveConfig; // bit2-3 行走驱动配置
|
||||
|
||||
// --- 尺寸参数 (float LE) ---
|
||||
late double gearRatio; // payload[116..119] 转速比
|
||||
late double robotLength; // payload[120..123] 机器人长度
|
||||
late double robotWidth; // payload[124..127] 机器人宽度
|
||||
late double robotHeight; // payload[128..131] 机器人高度
|
||||
late double knifeWidth; // payload[132..135] 割刀宽度
|
||||
late double tireSize; // payload[136..139] 轮胎尺寸
|
||||
|
||||
// --- 增益 (float LE) ---
|
||||
late double leftForwardGain; // payload[140..143] 左轮前进增益
|
||||
late double leftBackwardGain; // payload[144..147] 左轮后退增益
|
||||
late double rightForwardGain; // payload[148..151] 右轮前进增益
|
||||
late double rightBackwardGain; // payload[152..155] 右轮后退增益
|
||||
|
||||
// --- 固件版本 ---
|
||||
late String firmwareVersion; // payload[156..170] 固件版本 (15字节)
|
||||
|
||||
Mc700DeviceConfig();
|
||||
|
||||
// ====== 从二进制数据解析(171字节 payload) ======
|
||||
factory Mc700DeviceConfig.fromBytes(Uint8List data) {
|
||||
if (data.length < 171) {
|
||||
throw Exception('数据长度不足171字节,实际${data.length}字节');
|
||||
}
|
||||
final bd = ByteData.sublistView(data);
|
||||
final obj = Mc700DeviceConfig();
|
||||
|
||||
// 1. UID固化标志 @0
|
||||
obj.uidSolidifiedFlag = bd.getUint8(0);
|
||||
// 2. 芯片UID @1..45 (45字节)
|
||||
obj.chipUid = _readNullTerminatedString(data, 1, 45);
|
||||
// 3. 遥控器通道配置 @46..64 (19字节)
|
||||
obj.remoteChannelConfig = List.unmodifiable(data.sublist(46, 65));
|
||||
|
||||
// 4. 字节68位域 @65
|
||||
final b68 = bd.getUint8(65);
|
||||
obj.knifeMotorMode = b68 & 0x03;
|
||||
obj.walkMotorMode = (b68 >> 2) & 0x03;
|
||||
obj.leftWheelPolarity = (b68 & 0x10) != 0;
|
||||
obj.rightWheelPolarity = (b68 & 0x20) != 0;
|
||||
obj.channelSwap = (b68 & 0x40) != 0;
|
||||
obj.use4g = (b68 & 0x80) != 0;
|
||||
|
||||
// 5. 前进速度限制 @66..67
|
||||
obj.forwardSpeedLimit = bd.getUint16(66, Endian.little);
|
||||
// 6. 转向速度限制 @68..69
|
||||
obj.turnSpeedLimit = bd.getUint16(68, Endian.little);
|
||||
|
||||
// 7. 字节73位域 @70
|
||||
final b73 = bd.getUint8(70);
|
||||
obj.knifeChannelPolarity = (b73 & 0x01) != 0;
|
||||
obj.fanChannelPolarity = (b73 & 0x02) != 0;
|
||||
obj.throttleChannelPolarity = (b73 & 0x04) != 0;
|
||||
obj.liftProtectTime = (b73 >> 3) & 0x1F;
|
||||
obj.dualRtk = (b73 & 0x80) != 0;
|
||||
|
||||
// 8. 字节74位域 @71
|
||||
final b74 = bd.getUint8(71);
|
||||
obj.knifeChannelConfig = b74 & 0x0F;
|
||||
obj.fanChannelConfig = (b74 >> 4) & 0x0F;
|
||||
|
||||
// 9. 字节75位域 @72
|
||||
final b75 = bd.getUint8(72);
|
||||
obj.throttleChannelConfig = b75 & 0x0F;
|
||||
obj.remoteType = (b75 >> 4) & 0x07;
|
||||
obj.relayBoard = (b75 & 0x80) != 0;
|
||||
|
||||
// 10. 字节76位域 @73
|
||||
final b76 = bd.getUint8(73);
|
||||
obj.chassisLiftChannel = b76 & 0x0F;
|
||||
obj.chassisChannel = (b76 >> 4) & 0x0F;
|
||||
|
||||
// 11. 字节77位域 @74
|
||||
final b77 = bd.getUint8(74);
|
||||
obj.armChannel = b77 & 0x0F;
|
||||
obj.fuelPumpChannel = (b77 >> 4) & 0x0F;
|
||||
|
||||
// 12. WiFi名称 @75..94 (20字节)
|
||||
obj.wifiName = _readNullTerminatedString(data, 75, 20);
|
||||
// 13. WiFi密码 @95..114 (20字节)
|
||||
obj.wifiPassword = _readNullTerminatedString(data, 95, 20);
|
||||
|
||||
// 14. 字节118位域 @115
|
||||
final b118 = bd.getUint8(115);
|
||||
obj.batteryType = b118 & 0x03;
|
||||
obj.walkDriveConfig = (b118 >> 2) & 0x03;
|
||||
|
||||
// 15. 转速比 @116..119
|
||||
obj.gearRatio = bd.getFloat32(116, Endian.little);
|
||||
// 16. 机器人长度 @120..123
|
||||
obj.robotLength = bd.getFloat32(120, Endian.little);
|
||||
// 17. 机器人宽度 @124..127
|
||||
obj.robotWidth = bd.getFloat32(124, Endian.little);
|
||||
// 18. 机器人高度 @128..131
|
||||
obj.robotHeight = bd.getFloat32(128, Endian.little);
|
||||
// 19. 割刀宽度 @132..135
|
||||
obj.knifeWidth = bd.getFloat32(132, Endian.little);
|
||||
// 20. 轮胎尺寸 @136..139
|
||||
obj.tireSize = bd.getFloat32(136, Endian.little);
|
||||
|
||||
// 21. 左轮前进增益 @140..143
|
||||
obj.leftForwardGain = bd.getFloat32(140, Endian.little);
|
||||
// 22. 左轮后退增益 @144..147
|
||||
obj.leftBackwardGain = bd.getFloat32(144, Endian.little);
|
||||
// 23. 右轮前进增益 @148..151
|
||||
obj.rightForwardGain = bd.getFloat32(148, Endian.little);
|
||||
// 24. 右轮后退增益 @152..155
|
||||
obj.rightBackwardGain = bd.getFloat32(152, Endian.little);
|
||||
|
||||
// 25. 固件版本 @156..170 (15字节)
|
||||
obj.firmwareVersion = _readNullTerminatedString(data, 156, 15);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// ====== 转换回二进制字节(用于写配置下发) ======
|
||||
/// [originalPayload] 是上次读取的 171 字节 payload,用于保留未修改字段
|
||||
Uint8List toBytes(Uint8List originalPayload) {
|
||||
final config = Uint8List.fromList(originalPayload);
|
||||
final bd = ByteData.sublistView(config);
|
||||
|
||||
// UID固化标志 @0(独立字段,不属于字节68)
|
||||
bd.setUint8(0, uidSolidifiedFlag & 0x01);
|
||||
|
||||
// 字节68位域 @65
|
||||
int b68 = 0;
|
||||
b68 |= (knifeMotorMode & 0x03);
|
||||
b68 |= (walkMotorMode & 0x03) << 2;
|
||||
if (leftWheelPolarity) b68 |= 0x10;
|
||||
if (rightWheelPolarity) b68 |= 0x20;
|
||||
if (channelSwap) b68 |= 0x40;
|
||||
if (use4g) b68 |= 0x80;
|
||||
bd.setUint8(65, b68);
|
||||
|
||||
// 速度限制
|
||||
bd.setUint16(66, forwardSpeedLimit, Endian.little);
|
||||
bd.setUint16(68, turnSpeedLimit, Endian.little);
|
||||
|
||||
// 字节73位域 @70
|
||||
int b73 = (liftProtectTime & 0x1F) << 3;
|
||||
if (knifeChannelPolarity) b73 |= 0x01;
|
||||
if (fanChannelPolarity) b73 |= 0x02;
|
||||
if (throttleChannelPolarity) b73 |= 0x04;
|
||||
if (dualRtk) b73 |= 0x80;
|
||||
bd.setUint8(70, b73);
|
||||
|
||||
// 字节74位域 @71
|
||||
int b74 = knifeChannelConfig & 0x0F;
|
||||
b74 |= (fanChannelConfig & 0x0F) << 4;
|
||||
bd.setUint8(71, b74);
|
||||
|
||||
// 字节75位域 @72
|
||||
int b75 = throttleChannelConfig & 0x0F;
|
||||
b75 |= (remoteType & 0x07) << 4;
|
||||
if (relayBoard) b75 |= 0x80;
|
||||
bd.setUint8(72, b75);
|
||||
|
||||
// 字节76位域 @73
|
||||
int b76 = chassisLiftChannel & 0x0F;
|
||||
b76 |= (chassisChannel & 0x0F) << 4;
|
||||
bd.setUint8(73, b76);
|
||||
|
||||
// 字节77位域 @74
|
||||
int b77 = armChannel & 0x0F;
|
||||
b77 |= (fuelPumpChannel & 0x0F) << 4;
|
||||
bd.setUint8(74, b77);
|
||||
|
||||
// WiFi
|
||||
_writeNullTerminatedString(config, 75, wifiName, 20);
|
||||
_writeNullTerminatedString(config, 95, wifiPassword, 20);
|
||||
|
||||
// 字节118位域 @115
|
||||
int b118 = batteryType & 0x03;
|
||||
b118 |= (walkDriveConfig & 0x03) << 2;
|
||||
bd.setUint8(115, b118);
|
||||
|
||||
// 尺寸参数
|
||||
bd.setFloat32(116, gearRatio, Endian.little);
|
||||
bd.setFloat32(120, robotLength, Endian.little);
|
||||
bd.setFloat32(124, robotWidth, Endian.little);
|
||||
bd.setFloat32(128, robotHeight, Endian.little);
|
||||
bd.setFloat32(132, knifeWidth, Endian.little);
|
||||
bd.setFloat32(136, tireSize, Endian.little);
|
||||
|
||||
// 增益
|
||||
bd.setFloat32(140, leftForwardGain, Endian.little);
|
||||
bd.setFloat32(144, leftBackwardGain, Endian.little);
|
||||
bd.setFloat32(148, rightForwardGain, Endian.little);
|
||||
bd.setFloat32(152, rightBackwardGain, Endian.little);
|
||||
|
||||
// 固件版本
|
||||
_writeNullTerminatedString(config, 156, firmwareVersion, 15);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// 获取所有字段的 label→value 映射
|
||||
Map<String, String> toFieldMap() {
|
||||
return {
|
||||
'UID固化标志': '$uidSolidifiedFlag',
|
||||
'芯片UID': chipUid,
|
||||
'割刀电机模式': knifeMotorMode == 0 ? '纯电' : '油电($knifeMotorMode)',
|
||||
'行走电机模式': walkMotorMode == 0 ? '轮式' : '履带($walkMotorMode)',
|
||||
'左轮极性': leftWheelPolarity ? '低' : '高',
|
||||
'右轮极性': rightWheelPolarity ? '低' : '高',
|
||||
'通道交换': channelSwap ? '是' : '否',
|
||||
'联网目标': use4g ? '4G' : 'WiFi',
|
||||
'前进速度限制': '$forwardSpeedLimit',
|
||||
'转向速度限制': '$turnSpeedLimit',
|
||||
'割刀通道极性': knifeChannelPolarity ? '低' : '高',
|
||||
'风门通道极性': fanChannelPolarity ? '低' : '高',
|
||||
'油门通道极性': throttleChannelPolarity ? '低' : '高',
|
||||
'升降保护时间': '$liftProtectTime 秒',
|
||||
'RTK配置': dualRtk ? '双天线' : '单天线',
|
||||
'割刀通道配置': '$knifeChannelConfig',
|
||||
'风门通道配置': '$fanChannelConfig',
|
||||
'油门通道配置': '$throttleChannelConfig',
|
||||
'遥控器类型': '$remoteType',
|
||||
'搭载继电器板': relayBoard ? '是' : '否',
|
||||
'底盘升降通道': '$chassisLiftChannel',
|
||||
'底盘通道': '$chassisChannel',
|
||||
'机械臂通道': '$armChannel',
|
||||
'燃油泵通道': '$fuelPumpChannel',
|
||||
'WiFi名称': wifiName,
|
||||
'WiFi密码': wifiPassword,
|
||||
'电池类型': batteryType == 0 ? '铅酸' : '锂电',
|
||||
'行走驱动': walkDriveConfig.toString(),
|
||||
'转速比': gearRatio.toStringAsFixed(2),
|
||||
'机器人长度': robotLength.toStringAsFixed(2),
|
||||
'机器人宽度': robotWidth.toStringAsFixed(2),
|
||||
'机器人高度': robotHeight.toStringAsFixed(2),
|
||||
'割刀宽度': knifeWidth.toStringAsFixed(2),
|
||||
'轮胎尺寸': tireSize.toStringAsFixed(2),
|
||||
'左轮前进增益': leftForwardGain.toStringAsFixed(2),
|
||||
'左轮后退增益': leftBackwardGain.toStringAsFixed(2),
|
||||
'右轮前进增益': rightForwardGain.toStringAsFixed(2),
|
||||
'右轮后退增益': rightBackwardGain.toStringAsFixed(2),
|
||||
'固件版本': firmwareVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/// 根据字段标签设置新值(返回 null 表示成功,返回错误原因字符串)
|
||||
String? setField(String label, String value) {
|
||||
final intVal = int.tryParse(value);
|
||||
final doubleVal = double.tryParse(value);
|
||||
|
||||
switch (label) {
|
||||
case 'UID固化标志':
|
||||
if (intVal == null || intVal < 0 || intVal > 1) return '值范围: 0~1';
|
||||
uidSolidifiedFlag = intVal;
|
||||
return null;
|
||||
case '前进速度限制':
|
||||
if (intVal == null || intVal < 0 || intVal > 65535)
|
||||
return '值范围: 0~65535';
|
||||
forwardSpeedLimit = intVal;
|
||||
return null;
|
||||
case '转向速度限制':
|
||||
if (intVal == null || intVal < 0 || intVal > 65535)
|
||||
return '值范围: 0~65535';
|
||||
turnSpeedLimit = intVal;
|
||||
return null;
|
||||
case 'WiFi名称':
|
||||
if (value.length > 20) return '最多20字符';
|
||||
wifiName = value;
|
||||
return null;
|
||||
case 'WiFi密码':
|
||||
if (value.length > 20) return '最多20字符';
|
||||
wifiPassword = value;
|
||||
return null;
|
||||
case '转速比':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
gearRatio = doubleVal;
|
||||
return null;
|
||||
case '机器人长度':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
robotLength = doubleVal;
|
||||
return null;
|
||||
case '机器人宽度':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
robotWidth = doubleVal;
|
||||
return null;
|
||||
case '机器人高度':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
robotHeight = doubleVal;
|
||||
return null;
|
||||
case '割刀宽度':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
knifeWidth = doubleVal;
|
||||
return null;
|
||||
case '轮胎尺寸':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
tireSize = doubleVal;
|
||||
return null;
|
||||
case '左轮前进增益':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
leftForwardGain = doubleVal;
|
||||
return null;
|
||||
case '左轮后退增益':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
leftBackwardGain = doubleVal;
|
||||
return null;
|
||||
case '右轮前进增益':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
rightForwardGain = doubleVal;
|
||||
return null;
|
||||
case '右轮后退增益':
|
||||
if (doubleVal == null) return '请输入有效数字';
|
||||
rightBackwardGain = doubleVal;
|
||||
return null;
|
||||
case '固件版本':
|
||||
if (value.length > 15) return '最多15字符';
|
||||
firmwareVersion = value;
|
||||
return null;
|
||||
default:
|
||||
return '未知字段: $label';
|
||||
}
|
||||
}
|
||||
|
||||
// 工具:读取0结尾的ASCII字符串
|
||||
static String _readNullTerminatedString(
|
||||
Uint8List data,
|
||||
int offset,
|
||||
int maxLen,
|
||||
) {
|
||||
final end = data.indexOf(0, offset);
|
||||
final realEnd = (end == -1 || end > offset + maxLen)
|
||||
? offset + maxLen
|
||||
: end;
|
||||
return ascii.decode(data.sublist(offset, realEnd));
|
||||
}
|
||||
|
||||
// 工具:写入ASCII字符串,不足补0
|
||||
static void _writeNullTerminatedString(
|
||||
Uint8List data,
|
||||
int offset,
|
||||
String str,
|
||||
int maxLen,
|
||||
) {
|
||||
final bytes = ascii.encode(str);
|
||||
for (int i = 0; i < maxLen; i++) {
|
||||
data[offset + i] = i < bytes.length ? bytes[i] : 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
class BlePacket {
|
||||
final int command;
|
||||
final Uint8List payload;
|
||||
const BlePacket({required this.command, required this.payload});
|
||||
}
|
||||
|
||||
/// 有状态的 BLE 协议解析器
|
||||
/// 支持分片接收:多次调用 [append] 累积数据,[parse] 提取完整帧
|
||||
class ProtocolParser {
|
||||
static const int _header1 = 0xAB;
|
||||
static const int _header2 = 0xAA;
|
||||
static const int _tail1 = 0xAA;
|
||||
static const int _tail2 = 0xAB;
|
||||
|
||||
final List<int> _buffer = [];
|
||||
|
||||
/// 清空缓冲区
|
||||
void clear() {
|
||||
_buffer.clear();
|
||||
}
|
||||
|
||||
/// 获取缓冲区长度
|
||||
int get bufferLength => _buffer.length;
|
||||
|
||||
/// 打包命令为字节帧(用于发送)
|
||||
/// CRC16 覆盖 command + payload 确保整帧完整性
|
||||
static Uint8List pack(int command, List<int> payload) {
|
||||
final dataToCheck = [command, ...payload];
|
||||
final crc = _crc16(dataToCheck);
|
||||
final builder = BytesBuilder()
|
||||
..addByte(_header1)
|
||||
..addByte(_header2)
|
||||
..addByte(command)
|
||||
..add(payload)
|
||||
..addByte(crc & 0xFF)
|
||||
..addByte((crc >> 8) & 0xFF)
|
||||
..addByte(_tail1)
|
||||
..addByte(_tail2);
|
||||
return builder.takeBytes();
|
||||
}
|
||||
|
||||
/// 追加新接收的数据到缓冲区
|
||||
void append(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
}
|
||||
|
||||
/// 从缓冲区解析所有完整的数据包
|
||||
/// 未完成的帧保留在缓冲区等待后续数据
|
||||
List<BlePacket> parse() {
|
||||
final List<BlePacket> packets = [];
|
||||
|
||||
while (_buffer.length >= 6) {
|
||||
// 1. 查找帧头
|
||||
int headIdx = -1;
|
||||
for (int i = 0; i < _buffer.length - 1; i++) {
|
||||
if (_buffer[i] == _header1 && _buffer[i + 1] == _header2) {
|
||||
headIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headIdx == -1) {
|
||||
// 无有效帧头,清空缓冲区
|
||||
_buffer.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
// 移除帧头前的垃圾数据
|
||||
if (headIdx > 0) _buffer.removeRange(0, headIdx);
|
||||
|
||||
// 2. 查找帧尾
|
||||
int tailIdx = -1;
|
||||
for (int i = 2; i < _buffer.length - 1; i++) {
|
||||
if (_buffer[i] == _tail1 && _buffer[i + 1] == _tail2) {
|
||||
tailIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tailIdx == -1) {
|
||||
// 帧尾未找到,可能是分片数据,等待更多数据
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. 提取数据包
|
||||
if (_buffer.length >= 5) {
|
||||
final cmd = _buffer[2];
|
||||
final payload = Uint8List.fromList(_buffer.sublist(3, tailIdx));
|
||||
packets.add(BlePacket(command: cmd, payload: payload));
|
||||
}
|
||||
|
||||
// 4. 移除已处理的帧
|
||||
_buffer.removeRange(0, tailIdx + 2);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
|
||||
/// 便捷方法:追加数据并立即解析
|
||||
List<BlePacket> appendAndParse(List<int> data) {
|
||||
append(data);
|
||||
return parse();
|
||||
}
|
||||
|
||||
/// CRC16-Modbus: polynomial=0x8005, init=0xFFFF, refIn/refOut=true
|
||||
static int _crc16(List<int> data) {
|
||||
int crc = 0xFFFF;
|
||||
for (final b in data) {
|
||||
crc ^= b;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if ((crc & 0x0001) != 0) {
|
||||
crc = (crc >> 1) ^ 0xA001;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
class HttpApiConsts {
|
||||
// static const String baseUrl = "http://8.159.134.0:8012"; // 旧地址
|
||||
// static const String baseUrl = "http://1.95.137.212:59015"; // 测试地址
|
||||
static const String baseUrl = "http://1.95.137.212:8081"; // 生产地址
|
||||
static const String baseUrl = "http://1.95.137.212:8081";
|
||||
|
||||
/// 账号相关
|
||||
// 登录
|
||||
@@ -11,108 +9,16 @@ class HttpApiConsts {
|
||||
// 获取设备列表
|
||||
static const String getUserDevicesList = "$baseUrl/iot/device/list";
|
||||
// 绑定设备
|
||||
static const String bindDevice = "$baseUrl/iot/device/bind";
|
||||
static const String bindDevice = "$baseUrl/forward/device/bind";
|
||||
// 解绑设备
|
||||
static const String unbindDevice = "$baseUrl/forward/device/unbind";
|
||||
// 切换设备
|
||||
static const String switchDevice = "$baseUrl/forward/device/switchDevice";
|
||||
|
||||
// 获取光伏电站列表
|
||||
static const String getSiteList = "$baseUrl/system/site/selectByUserId";
|
||||
|
||||
// 获取场站下的设备列表
|
||||
static const String getSiteDeviceList = "$baseUrl/iot/device/getSiteList";
|
||||
|
||||
// 获取场站下的无人机机场列表
|
||||
static const String getSiteUAVList = "$baseUrl/iot/UAV/getSiteUAVList";
|
||||
|
||||
// 获取UAV状态详情
|
||||
static const String getUAVState = "$baseUrl/iot/UAV/getUAVState";
|
||||
|
||||
// 切换摄像头获取视频流
|
||||
static const String changeCamera = "$baseUrl/iot/UAV/changeCamera";
|
||||
|
||||
// 获取设备位置
|
||||
static const String getDeviceLocation = "$baseUrl/iot/device/userDevice";
|
||||
|
||||
// 获取机器人列表
|
||||
static const String getRobotList = "$baseUrl/iot/device/getSiteList";
|
||||
|
||||
// 获取飞行任务列表
|
||||
static const String getFlightTask = "$baseUrl/iot/UAV/getFlightTask";
|
||||
|
||||
// 获取飞行任务详情
|
||||
static const String getFlightTaskDetail =
|
||||
"$baseUrl/iot/UAV/getFlightTaskDetail";
|
||||
|
||||
// 获取航线列表
|
||||
static const String getWayline = "$baseUrl/iot/UAV/getWayline";
|
||||
|
||||
// 创建飞行任务
|
||||
static const String createFlightTask = "$baseUrl/iot/UAV/createFlightTask";
|
||||
|
||||
// 更新飞行任务状态
|
||||
static const String updateFlightTaskStatus =
|
||||
"$baseUrl/iot/UAV/updateFlightTaskStatus";
|
||||
|
||||
// 切换无人机镜头获取视频流
|
||||
static const String changeUAVLens = "$baseUrl/iot/UAV/changeLens";
|
||||
|
||||
// 获取无人机详情
|
||||
static const String getUAVDetail = "$baseUrl/iot/UAV/getUAVDetail";
|
||||
|
||||
// 飞行任务命令控制(暂停、返航等)
|
||||
static const String flightTaskCommand = "$baseUrl/iot/UAV/flightTaskCommand";
|
||||
|
||||
/// 告警相关
|
||||
static const String alarmBaseUrl = "http://1.95.137.212:8081";
|
||||
// 获取告警工单配置列表
|
||||
static const String alarmOrderConfigList =
|
||||
"$alarmBaseUrl/iot/alarmOrderConfig/list";
|
||||
// 获取告警列表
|
||||
static const String alarmList = "$baseUrl/iot/alarm/list";
|
||||
// 获取告警详情
|
||||
static const String alarmDetail = "$baseUrl/iot/alarm";
|
||||
// 处理告警(确认/关闭等)
|
||||
static const String alarmHandle = "$baseUrl/iot/alarm/handle";
|
||||
|
||||
/// 工单相关
|
||||
// 获取工单模型配置列表
|
||||
static const String orderModelList = "$baseUrl/iot/orderModel/list";
|
||||
// 添加工单(上报)
|
||||
static const String workOrderAdd = "$baseUrl/iot/ioTworkOrder/add";
|
||||
// 获取工单列表
|
||||
static const String workOrderList = "$baseUrl/iot/ioTworkOrder/list";
|
||||
// 获取工单详情
|
||||
static const String workOrderDetail = "$baseUrl/iot/ioTworkOrder";
|
||||
// 派发工单
|
||||
static const String workOrderDispat = "$baseUrl/iot/ioTworkOrder/dispatch";
|
||||
// 挂起工单
|
||||
static const String workOrderSuspend = "$baseUrl/iot/ioTworkOrder/suspend";
|
||||
// 完成工单
|
||||
static const String workOrderComplete = "$baseUrl/iot/ioTworkOrder/complete";
|
||||
// 开始执行工单
|
||||
static const String workOrderStart = "$baseUrl/iot/ioTworkOrder/start";
|
||||
// 获取工单统计
|
||||
static const String workOrderCount = "$baseUrl/iot/ioTworkOrder/count";
|
||||
// 设备运行参数查询
|
||||
static const String deviceRunParamSelect =
|
||||
"$baseUrl/iot/deviceRunParam/selectByDeviceId";
|
||||
// 设备运行参数保存
|
||||
static const String deviceRunParamSave = "$baseUrl/iot/deviceRunParam/save";
|
||||
|
||||
// 设备操作权限校验
|
||||
static const String hasPermission = "$baseUrl/iot/device/hasPermission";
|
||||
|
||||
/// 用户相关
|
||||
// 获取用户列表
|
||||
static const String systemUserList = "$baseUrl/system/user/list";
|
||||
|
||||
/// 组织与场站相关
|
||||
// 获取组织列表
|
||||
static const String orgList = "$baseUrl/system/org/list";
|
||||
// 根据组织ID获取场站列表
|
||||
static const String siteListByOrgId = "$baseUrl/system/site/selectByOrgId";
|
||||
// 根据场站ID获取用户列表
|
||||
static const String userListBySiteId = "$baseUrl/system/user/list";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import '../env/env_config.dart';
|
||||
|
||||
class TCPConsts {
|
||||
static String get TCP_IP => EnvConfig.tcpIp;
|
||||
static int get TCP_PORT => EnvConfig.tcpPort;
|
||||
static const String TCP_IP = "1.95.137.212";
|
||||
static const int TCP_PORT = 9001;
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/// 工单模块尺寸常量
|
||||
class WorkOrderDimensions {
|
||||
WorkOrderDimensions._();
|
||||
|
||||
/// 通用圆角
|
||||
static const double borderRadius = 16.0;
|
||||
|
||||
/// 水平边距
|
||||
static const double horizontalPadding = 16.0;
|
||||
|
||||
/// 模块间距
|
||||
static const double moduleSpacing = 20.0;
|
||||
|
||||
/// 导航栏高度
|
||||
static const double navigationBarHeight = 44.0;
|
||||
|
||||
/// 标签栏高度
|
||||
static const double tabBarHeight = 44.0;
|
||||
}
|
||||
|
||||
/// 全局应用常量 (供 UI 组件使用)
|
||||
class AppConstants {
|
||||
AppConstants._();
|
||||
|
||||
static const double borderRadius = WorkOrderDimensions.borderRadius;
|
||||
static const double horizontalPadding = WorkOrderDimensions.horizontalPadding;
|
||||
static const double moduleSpacing = WorkOrderDimensions.moduleSpacing;
|
||||
static const double navigationBarHeight = WorkOrderDimensions.navigationBarHeight;
|
||||
}
|
||||
|
||||
/// 工单状态枚举
|
||||
enum WorkOrderStatus {
|
||||
pending, // 待处理
|
||||
executing, // 执行中
|
||||
completed, // 已完成
|
||||
all, // 全部
|
||||
}
|
||||
|
||||
/// 工单优先级枚举
|
||||
enum WorkOrderPriority {
|
||||
high, // 高
|
||||
medium, // 中
|
||||
low, // 低
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/infrastructure/logging/sentry_logger_impl.dart';
|
||||
@@ -17,12 +16,9 @@ import 'package:maibu_satabot_v2/features/my/presentation/bloc/my_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/usecases/updatename_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/request_control_permission_usecase.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../features/auth/data/datasources/auth_http_datasource.dart';
|
||||
@@ -50,111 +46,21 @@ import '../../features/devices/domain/usecases/device_work_hostrirty_usecase.dar
|
||||
import '../../features/devices/domain/usecases/generate_path_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/get_device_location_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/get_work_record_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/get_work_records_by_site_id_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/create_device_task_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/route_planning_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/save_work_record_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/select_work_record_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/switch_device_usecase.dart';
|
||||
import '../../features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import '../../features/remote_control/data/datasources/remote_http_datasource.dart';
|
||||
import '../../features/remote_control/data/datasources/remote_tcp_datasource.dart';
|
||||
import '../../features/remote_control/domain/usecase/remote_control_usecase.dart';
|
||||
import '../../features/remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import '../../features/v2/home/data/datasources/home_remote_datasource.dart';
|
||||
import '../../features/v2/home/data/datasources/site_datasource.dart';
|
||||
import '../../features/v2/home/data/datasources/site_datasource_impl.dart';
|
||||
import '../../features/v2/home/data/repositories/home_repository_impl.dart';
|
||||
import '../../features/v2/home/data/repositories/site_repository_impl.dart';
|
||||
import '../../features/v2/home/domain/repositories/home_repository.dart';
|
||||
import '../../features/v2/home/domain/repositories/site_repository.dart';
|
||||
import '../../features/v2/home/domain/usecases/get_home_data_usecase.dart';
|
||||
import '../../features/v2/home/domain/usecases/get_site_list_usecase.dart';
|
||||
import '../../features/v2/home/presentation/bloc/home_v2_bloc.dart';
|
||||
import '../../features/v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../features/v2/device_list/data/datasources/device_remote_datasource.dart'
|
||||
as device_v2;
|
||||
import '../../features/v2/device_list/data/repositories/device_repository_impl.dart'
|
||||
as device_v2_repo;
|
||||
import '../../features/v2/device_list/domain/repositories/device_repository.dart'
|
||||
as device_v2_domain;
|
||||
import '../../features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart'
|
||||
as device_v2_usecase;
|
||||
import '../../features/v2/device_list/presentation/bloc/device_status_bloc.dart'
|
||||
as device_v2_bloc;
|
||||
import '../../features/v2/device_list/data/datasources/drone_station_datasource.dart';
|
||||
import '../../features/v2/device_list/data/datasources/drone_station_datasource_impl.dart';
|
||||
import '../../features/v2/device_list/data/repositories/drone_station_repository_impl.dart';
|
||||
import '../../features/v2/device_list/domain/repositories/drone_station_repository.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/get_drone_station_list_usecase.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/get_video_stream_usecase.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/get_uav_video_stream_usecase.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/update_flight_task_status_usecase.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/pause_flight_task_usecase.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/return_home_usecase.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/drone_station_bloc.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/robot_list_bloc.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/device_realtime_bloc.dart';
|
||||
import '../../features/v2/device_list/data/datasources/bind_device_datasource.dart';
|
||||
import '../../features/v2/device_list/data/datasources/impl/bind_device_datasource_impl.dart';
|
||||
import '../../features/v2/device_list/data/repositories/bind_device_repository_impl.dart';
|
||||
import '../../features/v2/device_list/domain/repositories/bind_device_repository.dart';
|
||||
import '../../features/v2/device_list/domain/usecases/bind_device_usecases.dart';
|
||||
import '../../features/v2/device_list/presentation/cubit/bind_device_cubit.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart';
|
||||
import '../../features/v2/waring_center/data/repositories/alarm_repository_impl.dart';
|
||||
import '../../features/v2/waring_center/domain/repositories/alarm_repository.dart';
|
||||
import '../../features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart';
|
||||
import '../../features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart';
|
||||
import '../../features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/alarm_detail_remote_datasource.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/impl/alarm_detail_remote_datasource_impl.dart';
|
||||
import '../../features/v2/waring_center/data/repositories/alarm_detail_repository_impl.dart';
|
||||
import '../../features/v2/waring_center/domain/repositories/alarm_detail_repository.dart';
|
||||
import '../../features/v2/waring_center/domain/usecases/get_alarm_detail_usecase.dart';
|
||||
import '../../features/v2/waring_center/domain/usecases/alarm_actions_usecase.dart';
|
||||
import '../../features/v2/waring_center/presentation/bloc/alarm_detail_cubit.dart';
|
||||
import '../../features/v2/message_center/data/datasources/message_center_remote_datasource.dart';
|
||||
import '../../features/v2/message_center/data/datasources/impl/message_center_remote_datasource_impl.dart';
|
||||
import '../../features/v2/message_center/data/repositories/message_center_repository_impl.dart';
|
||||
import '../../features/v2/message_center/domain/repositories/message_center_repository.dart';
|
||||
import '../../features/v2/message_center/domain/usecases/message_center_usecase.dart';
|
||||
import '../../features/v2/message_center/presentation/bloc/message_center_cubit.dart';
|
||||
import '../../features/v2/work_order/data/datasources/work_order_remote_datasource.dart';
|
||||
import '../../features/v2/work_order/data/datasources/work_order_remote_datasource_impl.dart';
|
||||
import '../../features/v2/work_order/data/repositories/work_order_repository_impl.dart';
|
||||
import '../../features/v2/work_order/domain/repositories/work_order_repository.dart';
|
||||
import '../../features/v2/work_order/domain/usecases/work_order_usecases.dart';
|
||||
import '../../features/v2/work_order/presentation/cubit/work_order_cubit.dart';
|
||||
import '../../features/v2/device_run_param/data/datasources/device_run_param_remote_datasource.dart';
|
||||
import '../../features/v2/device_run_param/data/datasources/device_run_param_remote_datasource_impl.dart';
|
||||
import '../../features/v2/device_run_param/data/repositories/device_run_param_repository_impl.dart';
|
||||
import '../../features/v2/device_run_param/domain/repositories/device_run_param_repository.dart';
|
||||
import '../../features/v2/device_run_param/domain/usecases/device_run_param_usecases.dart';
|
||||
import '../services/device_permission_service.dart';
|
||||
import '../../features/devices/data/datasources/device_task_datasource.dart';
|
||||
import '../../features/devices/data/repositories/device_task_repository_impl.dart';
|
||||
import '../../features/devices/domain/repositories/device_task_repository.dart';
|
||||
import '../../features/devices/domain/usecases/get_device_task_pool_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/cancel_task_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/pause_task_usecase.dart';
|
||||
import '../../features/devices/domain/usecases/recovery_task_usecase.dart';
|
||||
import '../../features/devices/presentation/bloc/device_task_cubit.dart';
|
||||
import '../app/app_user_cubit.dart';
|
||||
import '../localization/locale_cubit.dart';
|
||||
import '../theme/theme_cubit.dart';
|
||||
import '../../features/home/presentation/bloc/permission_request_bloc.dart';
|
||||
import '../network/dio_client.dart';
|
||||
import '../network/net_message_dispatcher.dart';
|
||||
import '../network/tcp/tcp_client.dart';
|
||||
import '../network/tcp/tcp_status_cubit.dart';
|
||||
import '../network/mqtt/domain/interfaces/mqtt_client.dart';
|
||||
import '../network/mqtt/data/infrastructure/mqtt_client_impl.dart';
|
||||
import '../network/mqtt/data/datasources/drone_osd_datasource.dart';
|
||||
import '../network/mqtt/data/datasources/task_message_datasource.dart';
|
||||
import '../network/mqtt/domain/repositories/drone_osd_repository.dart';
|
||||
import '../network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import '../network/mqtt/data/repositories/drone_osd_repository_impl.dart';
|
||||
import '../network/mqtt/data/repositories/task_message_repository_impl.dart';
|
||||
import '../router/app_router.dart';
|
||||
import '../storage/impl/user_storage_impl.dart';
|
||||
import '../storage/user_storage.dart';
|
||||
@@ -169,33 +75,17 @@ Future<void> init() async {
|
||||
|
||||
/// 1.1.2 TcpClient:TCP客户端
|
||||
sl.registerLazySingleton(
|
||||
() => TcpClient(
|
||||
sl<UserStorage>(),
|
||||
getUserDeviceUseCase: sl<GetUserDeviceUseCase>(),
|
||||
switchDeviceUseCase: sl<SwitchDeviceUseCase>(),
|
||||
),
|
||||
() => TcpClient(sl<UserStorage>(), getUserDeviceUseCase: sl<GetUserDeviceUseCase>(), switchDeviceUseCase: sl<SwitchDeviceUseCase>()),
|
||||
);
|
||||
sl.registerLazySingleton(() => PathPlanningService());
|
||||
|
||||
/// 1.1.3 MQTT Clients - 两个独立的 MQTT 客户端实例
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(),
|
||||
instanceName: 'droneOsdClient',
|
||||
);
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(),
|
||||
instanceName: 'taskMessageClient',
|
||||
);
|
||||
|
||||
/// 1.1.4 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||
sl.registerLazySingleton(
|
||||
() => NetMessageDispatcher(
|
||||
sl<TcpClient>(),
|
||||
sl<RoutePlanningRepository>(),
|
||||
sl<PathPlanningService>(),
|
||||
getAppState: () => sl<DevicesCubit>().state.appState,
|
||||
),
|
||||
);
|
||||
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||
sl.registerLazySingleton(() => NetMessageDispatcher(
|
||||
sl<TcpClient>(),
|
||||
sl<RoutePlanningRepository>(),
|
||||
sl<PathPlanningService>(),
|
||||
getAppState: () => sl<DevicesCubit>().state.appState,
|
||||
));
|
||||
|
||||
/// 1.2 --- 本地存储 (LocalStorage) ---
|
||||
/// 1.2.1 SharedPreferences:本地存储库
|
||||
@@ -211,44 +101,17 @@ Future<void> init() async {
|
||||
/// 1.3 Log工具Sentry
|
||||
sl.registerLazySingleton<ILoggerService>(() => SentryLoggerImpl());
|
||||
|
||||
/// 1.4 --- Route Observer (路由监听器) ---
|
||||
sl.registerLazySingleton<RouteObserver>(
|
||||
() => RouteObserver<ModalRoute<void>>(),
|
||||
);
|
||||
|
||||
/// 1.5 --- MQTT Data Sources ---
|
||||
/// 改为 LazySingleton:列表页预启动订阅和详情页共享同一实例,stream 互通
|
||||
sl.registerLazySingleton<DroneOsdDataSource>(
|
||||
() =>
|
||||
DroneOsdDataSourceImpl(sl<MqttClient>(instanceName: 'droneOsdClient')),
|
||||
);
|
||||
sl.registerFactory<TaskMessageDataSource>(
|
||||
() => TaskMessageDataSourceImpl(
|
||||
sl<MqttClient>(instanceName: 'taskMessageClient'),
|
||||
),
|
||||
);
|
||||
|
||||
/// 1.5 --- MQTT Repositories ---
|
||||
sl.registerLazySingleton<DroneOsdRepository>(
|
||||
() => DroneOsdRepositoryImpl(sl<DroneOsdDataSource>()),
|
||||
);
|
||||
sl.registerLazySingleton<TaskMessageRepository>(
|
||||
() => TaskMessageRepositoryImpl(sl<TaskMessageDataSource>()),
|
||||
);
|
||||
|
||||
/// 2. 数据源 (DataSource)
|
||||
sl.registerLazySingleton<AuthHttpDataSource>(
|
||||
() => AuthHttpDataSourceImpl(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<AuthHttpDataSource>(() => AuthHttpDataSourceImpl(sl()));
|
||||
sl.registerLazySingleton<DeviceHttpDatasource>(
|
||||
() => DeviceHttpDatasourceImpl(sl(), sl<UserStorage>()), //
|
||||
);
|
||||
sl.registerLazySingleton<RemoteHttpDatasource>(
|
||||
() => RemoteHttpDatasource(sl(), sl<UserStorage>()),
|
||||
() => RemoteHttpDatasource(sl(), sl<UserStorage>()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<RemoteTcpDatasource>(
|
||||
() => RemoteTcpDatasource(sl<TcpClient>(), sl<UserStorage>()),
|
||||
() => RemoteTcpDatasource(sl<TcpClient>(), sl<UserStorage>()),
|
||||
);
|
||||
|
||||
/// 3. 仓库 (Repository)
|
||||
@@ -294,27 +157,19 @@ Future<void> init() async {
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<DeviceRepository>(() => DeviceRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<RemoteControlRepository>(
|
||||
() => RemoteControlRepositoryImpl(sl(), sl(), sl(), sl()),
|
||||
);
|
||||
sl.registerLazySingleton<RemoteControlRepository>(() => RemoteControlRepositoryImpl(sl(), sl() ,sl(),sl()));
|
||||
|
||||
/// 4. 用例 (UseCase)
|
||||
sl.registerLazySingleton(() => LoginUseCase(sl()));
|
||||
sl.registerLazySingleton(() => GetUserDeviceUseCase(sl()));
|
||||
sl.registerLazySingleton(() => DiffSteerUseCase());
|
||||
sl.registerLazySingleton(() => DeviceWorkHostrirty(sl()));
|
||||
sl.registerLazySingleton<SelectWorkRecordUseCase>(
|
||||
() => SelectWorkRecordUseCase(sl<PathRepository>()),
|
||||
);
|
||||
sl.registerLazySingleton<SelectWorkRecordUseCase>(() => SelectWorkRecordUseCase(sl<PathRepository>()));
|
||||
sl.registerLazySingleton(() => UnbindDeviceUseCase(sl()));
|
||||
sl.registerLazySingleton(() => BindDeviceUseCase(sl()));
|
||||
sl.registerLazySingleton(() => UpdateDevicenameUsecase(sl()));
|
||||
sl.registerLazySingleton<SaveWorkRecordUseCase>(
|
||||
() => SaveWorkRecordUseCase(sl<PathRepository>()),
|
||||
);
|
||||
sl.registerLazySingleton<SwitchDeviceUseCase>(
|
||||
() => SwitchDeviceUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<SaveWorkRecordUseCase>(() => SaveWorkRecordUseCase(sl<PathRepository>()));
|
||||
sl.registerLazySingleton<SwitchDeviceUseCase>(() => SwitchDeviceUseCase(sl()));
|
||||
|
||||
sl.registerLazySingleton<AuthTcpDatasource>(
|
||||
() => AuthTcpDatasourceImpl(
|
||||
@@ -325,189 +180,32 @@ Future<void> init() async {
|
||||
),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton<RequestControlPermissionUseCase>(
|
||||
() => RequestControlPermissionUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<RequestControlPermissionUseCase>(() => RequestControlPermissionUseCase(sl()));
|
||||
|
||||
/// Home V2
|
||||
sl.registerLazySingleton<HomeRemoteDataSource>(
|
||||
() => HomeRemoteDataSourceImpl(),
|
||||
);
|
||||
sl.registerLazySingleton<HomeRepository>(() => HomeRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetHomeDataUseCase>(() => GetHomeDataUseCase(sl()));
|
||||
|
||||
// Site (场站)
|
||||
sl.registerLazySingleton<SiteDataSource>(
|
||||
() => SiteDataSourceImpl(sl<Dio>(), sl<UserStorage>(), sl<AppUserCubit>()),
|
||||
);
|
||||
sl.registerLazySingleton<SiteRepository>(() => SiteRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetSiteListUseCase>(() => GetSiteListUseCase(sl()));
|
||||
|
||||
sl.registerFactory<HomeV2Bloc>(
|
||||
() => HomeV2Bloc(sl(), sl(), sl<AppUserCubit>(), sl<SiteCubit>()),
|
||||
);
|
||||
|
||||
/// Device Status V2
|
||||
sl.registerLazySingleton<device_v2.DeviceRemoteDataSource>(
|
||||
() => device_v2.DeviceRemoteDataSourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<device_v2_domain.DeviceRepository>(
|
||||
() => device_v2_repo.DeviceRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
sl.registerLazySingleton<device_v2_usecase.GetDeviceStatusDataUseCase>(
|
||||
() => device_v2_usecase.GetDeviceStatusDataUseCase(repository: sl()),
|
||||
);
|
||||
sl.registerFactory<device_v2_bloc.DeviceStatusBloc>(
|
||||
() => device_v2_bloc.DeviceStatusBloc(sl()),
|
||||
);
|
||||
|
||||
/// Drone Station V2
|
||||
sl.registerLazySingleton<DroneStationDataSource>(
|
||||
() => DroneStationDataSourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<DroneStationRepository>(
|
||||
() => DroneStationRepositoryImpl(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetDroneStationListUseCase>(
|
||||
() => GetDroneStationListUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetUAVDetailUseCase>(
|
||||
() => GetUAVDetailUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetVideoStreamUseCase>(
|
||||
() => GetVideoStreamUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetUavVideoStreamUseCase>(
|
||||
() => GetUavVideoStreamUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<UpdateFlightTaskStatusUseCase>(
|
||||
() => UpdateFlightTaskStatusUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<PauseFlightTaskUseCase>(
|
||||
() => PauseFlightTaskUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<ReturnHomeUseCase>(() => ReturnHomeUseCase(sl()));
|
||||
sl.registerFactory<DroneStationBloc>(
|
||||
() => DroneStationBloc(sl(), sl(), sl(), sl()),
|
||||
);
|
||||
|
||||
/// Robot List V2
|
||||
sl.registerFactory<RobotListBloc>(() => RobotListBloc(sl<Dio>()));
|
||||
|
||||
/// Device Realtime V2 (MQTT)
|
||||
sl.registerFactory<DeviceRealtimeBloc>(
|
||||
() => DeviceRealtimeBloc(sl<TaskMessageRepository>()),
|
||||
);
|
||||
|
||||
/// Alarm Center V2
|
||||
sl.registerLazySingleton<AlarmRemoteDataSource>(
|
||||
() => AlarmRemoteDataSourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<AlarmRepository>(() => AlarmRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetAlarmListUseCase>(
|
||||
() => GetAlarmListUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetAlarmCountUseCase>(
|
||||
() => GetAlarmCountUseCase(sl()),
|
||||
);
|
||||
sl.registerFactory<AlarmCubit>(() => AlarmCubit(getAlarmListUseCase: sl()));
|
||||
|
||||
/// Alarm Detail V2
|
||||
sl.registerLazySingleton<AlarmDetailRemoteDataSource>(
|
||||
() => AlarmDetailRemoteDataSourceImpl(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<AlarmDetailRepository>(
|
||||
() => AlarmDetailRepositoryImpl(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetAlarmDetailUseCase>(
|
||||
() => GetAlarmDetailUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<ConfirmAlarmUseCase>(
|
||||
() => ConfirmAlarmUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<HandleAlarmUseCase>(() => HandleAlarmUseCase(sl()));
|
||||
sl.registerLazySingleton<AIDiagnosisUseCase>(() => AIDiagnosisUseCase(sl()));
|
||||
sl.registerFactory<AlarmDetailCubit>(
|
||||
() => AlarmDetailCubit(
|
||||
getAlarmDetailUseCase: sl(),
|
||||
confirmAlarmUseCase: sl(),
|
||||
handleAlarmUseCase: sl(),
|
||||
aiDiagnosisUseCase: sl(),
|
||||
),
|
||||
);
|
||||
|
||||
/// 5. 状态管理 (Cubit/Bloc)
|
||||
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
|
||||
|
||||
// TCP状态管理 Cubit
|
||||
sl.registerLazySingleton(() => TcpStatusCubit());
|
||||
|
||||
// 🔥 SiteCubit (全局共享,持久化选中场站)
|
||||
sl.registerLazySingleton(() => SiteCubit(sl<SharedPreferences>()));
|
||||
|
||||
// 🔥 FloatBarSettingService (全局共享,控制悬浮条显示)
|
||||
sl.registerLazySingleton(
|
||||
() => FloatBarSettingService(sl<SharedPreferences>()),
|
||||
);
|
||||
|
||||
|
||||
// 🔥 语言管理 Cubit (单例)
|
||||
sl.registerLazySingleton(() => LocaleCubit());
|
||||
|
||||
// 🔥 主题管理 Cubit (单例,管理亮/暗/跟随系统)
|
||||
sl.registerLazySingleton(() => ThemeCubit());
|
||||
|
||||
// Tab 配置 Cubit (单例)
|
||||
sl.registerLazySingleton(() => TabConfigCubit(sl<SharedPreferences>()));
|
||||
|
||||
|
||||
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
|
||||
sl.registerLazySingleton(
|
||||
() => DevicesCubit(
|
||||
sl(), // repository
|
||||
sl(), // GetUserDeviceUseCase
|
||||
sl(), // GetDeviceLocationUseCase
|
||||
sl(), // GetWorkRecordUseCase
|
||||
sl(), // GetWorkRecordsBySiteIdUseCase (NEW)
|
||||
sl(), // DeleteWorkRecordUseCase
|
||||
sl(), // UnbindDeviceUseCase
|
||||
sl(), // UpdateDevicenameUsecase
|
||||
sl(), // SelectWorkRecordUseCase
|
||||
sl(), // SaveWorkRecordUseCase
|
||||
sl(), // GeneratePathUseCase
|
||||
sl(), // RoutePlanningUseCase
|
||||
sl(), // BindDeviceUseCase
|
||||
sl(), // DeviceStatusBloc
|
||||
sl(), // TcpClient
|
||||
sl(), // PathPlanningService
|
||||
),
|
||||
);
|
||||
|
||||
// DeviceStatusBloc 必须在 RemoteControlCubit 之前注册(单例)
|
||||
sl.registerLazySingleton(
|
||||
() => DeviceStatusBloc(
|
||||
sl<NetMessageDispatcher>(),
|
||||
client: sl<TcpClient>(), // 🔥 传入 TcpClient
|
||||
),
|
||||
);
|
||||
|
||||
// 🔥 RemoteControlCubit 注入 DeviceStatusBloc(单例模式,全局共享)
|
||||
sl.registerLazySingleton(
|
||||
() => RemoteControlCubit(
|
||||
sl(), // RemoteControlRepository
|
||||
sl(), // RequestControlPermissionUseCase
|
||||
sl(), // DeviceRepository
|
||||
sl<TcpClient>(), // TcpClient
|
||||
sl(), // NetMessageDispatcher
|
||||
sl(), // DeviceStatusBloc
|
||||
),
|
||||
);
|
||||
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl(),sl()));
|
||||
|
||||
// 🔥 DeviceStatusBloc 必须在 RemoteControlCubit 之前注册(单例)
|
||||
sl.registerLazySingleton(() => DeviceStatusBloc(
|
||||
sl<NetMessageDispatcher>(),
|
||||
client: sl<TcpClient>(), // 🔥 传入 TcpClient
|
||||
));
|
||||
|
||||
// 🔥 RemoteControlCubit 注入 DeviceStatusBloc(工厂模式,每次新建)
|
||||
sl.registerFactory(() => RemoteControlCubit(sl(), sl(), sl(), sl()));
|
||||
|
||||
// 🔥 PermissionRequestBloc 用于首页权限弹窗(单例,通过 NetMessageDispatcher 监听)
|
||||
sl.registerLazySingleton(
|
||||
() => PermissionRequestBloc(
|
||||
sl<NetMessageDispatcher>(),
|
||||
sl<RemoteControlRepository>(),
|
||||
),
|
||||
);
|
||||
sl.registerLazySingleton(() => PermissionRequestBloc(
|
||||
sl<NetMessageDispatcher>(),
|
||||
sl<RemoteControlRepository>(),
|
||||
));
|
||||
|
||||
/// . 路径生成
|
||||
sl.registerLazySingleton<PathHttpDatasource>(
|
||||
@@ -516,36 +214,16 @@ Future<void> init() async {
|
||||
sl<UserStorage>(),
|
||||
),
|
||||
);
|
||||
sl.registerLazySingleton<PathRepository>(
|
||||
() => PathRepositoryImpl(datasource: sl<PathHttpDatasource>()),
|
||||
);
|
||||
sl.registerLazySingleton<GeneratePathUseCase>(
|
||||
() => GeneratePathUseCaseImpl(sl<PathRepository>()),
|
||||
);
|
||||
sl.registerLazySingleton<PathRepository>(() => PathRepositoryImpl(datasource: sl<PathHttpDatasource>()));
|
||||
sl.registerLazySingleton<GeneratePathUseCase>(() => GeneratePathUseCaseImpl(sl<PathRepository>()));
|
||||
|
||||
///.路径获取和删除
|
||||
sl.registerLazySingleton(() => GetWorkRecordUseCase(sl<PathRepository>()));
|
||||
sl.registerLazySingleton(() => DeleteWorkRecordUseCase(sl<PathRepository>()));
|
||||
|
||||
/// 根据场站ID获取工作记录(XML格式)
|
||||
sl.registerLazySingleton(
|
||||
() => GetWorkRecordsBySiteIdUseCase(sl<PathRepository>()),
|
||||
);
|
||||
|
||||
/// 创建设备任务(通过接口执行作业)
|
||||
sl.registerLazySingleton(() => CreateDeviceTaskUseCase(sl<PathRepository>()));
|
||||
|
||||
/// 6. 认证 (Auth)
|
||||
// --- 关键修改点 1: AuthCubit 必须在 GoRouter 之前注册,并传入参数 ---
|
||||
sl.registerLazySingleton(
|
||||
() => AuthCubit(
|
||||
sl<UserStorage>(),
|
||||
sl<TcpClient>(),
|
||||
sl<AppUserCubit>(),
|
||||
sl<NetMessageDispatcher>(),
|
||||
sl<AuthTcpDatasource>(),
|
||||
),
|
||||
);
|
||||
sl.registerLazySingleton(() => AuthCubit(sl<UserStorage>(), sl<TcpClient>(), sl<AppUserCubit>(), sl<NetMessageDispatcher>(), sl<AuthTcpDatasource>()));
|
||||
|
||||
/// 7. 路由 (Router)
|
||||
sl.registerSingleton<GoRouter>(createRouter(sl<AuthCubit>()));
|
||||
@@ -553,116 +231,4 @@ Future<void> init() async {
|
||||
/// 8. 页面级 Bloc/Cubit (Factory)
|
||||
sl.registerFactory(() => LoginBloc(sl()));
|
||||
sl.registerFactory(() => LoginCubit(sl(), sl()));
|
||||
|
||||
/// 9. 消息中心 (Message Center)
|
||||
sl.registerLazySingleton<MessageCenterRemoteDataSource>(
|
||||
() => MessageCenterRemoteDataSourceImpl(),
|
||||
);
|
||||
sl.registerLazySingleton<MessageCenterRepository>(
|
||||
() => MessageCenterRepositoryImpl(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetMessageListUseCase>(
|
||||
() => GetMessageListUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<MarkAllAsReadUseCase>(
|
||||
() => MarkAllAsReadUseCase(sl()),
|
||||
);
|
||||
sl.registerFactory<MessageCenterCubit>(
|
||||
() => MessageCenterCubit(
|
||||
getMessageListUseCase: sl(),
|
||||
markAllAsReadUseCase: sl(),
|
||||
),
|
||||
);
|
||||
|
||||
/// 10. 设备任务管理 (Device Task)
|
||||
sl.registerLazySingleton<DeviceTaskDatasource>(
|
||||
() => DeviceTaskDatasourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<DeviceTaskRepository>(
|
||||
() => DeviceTaskRepositoryImpl(datasource: sl<DeviceTaskDatasource>()),
|
||||
);
|
||||
sl.registerLazySingleton(() => GetDeviceTaskPoolUseCase(sl()));
|
||||
sl.registerLazySingleton(() => CancelTaskUseCase(sl()));
|
||||
sl.registerLazySingleton(() => PauseTaskUseCase(sl()));
|
||||
sl.registerLazySingleton(() => RecoveryTaskUseCase(sl()));
|
||||
sl.registerLazySingleton(() => DeviceTaskCubit(sl(), sl(), sl(), sl()));
|
||||
|
||||
/// 11. 工单管理 (Work Order)
|
||||
sl.registerLazySingleton<WorkOrderRemoteDataSource>(
|
||||
() => WorkOrderRemoteDataSourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<WorkOrderRepository>(
|
||||
() => WorkOrderRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetWorkOrderListUseCase>(
|
||||
() => GetWorkOrderListUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetWorkOrderDetailUseCase>(
|
||||
() => GetWorkOrderDetailUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<DispatchWorkOrderUseCase>(
|
||||
() => DispatchWorkOrderUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<SuspendWorkOrderUseCase>(
|
||||
() => SuspendWorkOrderUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<CompleteWorkOrderUseCase>(
|
||||
() => CompleteWorkOrderUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<StartWorkOrderUseCase>(
|
||||
() => StartWorkOrderUseCase(sl()),
|
||||
);
|
||||
sl.registerFactory<WorkOrderCubit>(
|
||||
() => WorkOrderCubit(
|
||||
getWorkOrderListUseCase: sl(),
|
||||
getWorkOrderDetailUseCase: sl(),
|
||||
dispatchWorkOrderUseCase: sl(),
|
||||
suspendWorkOrderUseCase: sl(),
|
||||
completeWorkOrderUseCase: sl(),
|
||||
startWorkOrderUseCase: sl(),
|
||||
),
|
||||
);
|
||||
|
||||
/// 12. 设备运行参数管理 (Device Run Param)
|
||||
sl.registerLazySingleton<DeviceRunParamRemoteDataSource>(
|
||||
() => DeviceRunParamRemoteDataSourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<DeviceRunParamRepository>(
|
||||
() => DeviceRunParamRepositoryImpl(remoteDataSource: sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetDeviceRunParamUseCase>(
|
||||
() => GetDeviceRunParamUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<SaveDeviceRunParamUseCase>(
|
||||
() => SaveDeviceRunParamUseCase(sl()),
|
||||
);
|
||||
|
||||
/// 13. 设备操作权限校验服务 (Device Permission Service)
|
||||
sl.registerLazySingleton<DevicePermissionService>(
|
||||
() => DevicePermissionService(sl<Dio>()),
|
||||
);
|
||||
|
||||
/// 14. 绑定智能装备 (Bind Device)
|
||||
sl.registerLazySingleton<BindDeviceDatasource>(
|
||||
() => BindDeviceDatasourceImpl(sl<Dio>()),
|
||||
);
|
||||
sl.registerLazySingleton<BindDeviceRepository>(
|
||||
() => BindDeviceRepositoryImpl(sl<BindDeviceDatasource>()),
|
||||
);
|
||||
sl.registerLazySingleton<GetOrgListUseCase>(() => GetOrgListUseCase(sl()));
|
||||
sl.registerLazySingleton<GetSitesByOrgUseCase>(
|
||||
() => GetSitesByOrgUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetUsersBySiteUseCase>(
|
||||
() => GetUsersBySiteUseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<BindDeviceV2UseCase>(
|
||||
() => BindDeviceV2UseCase(sl()),
|
||||
);
|
||||
sl.registerLazySingleton<IsDeviceAtSiteUseCase>(
|
||||
() => IsDeviceAtSiteUseCase(sl()),
|
||||
);
|
||||
sl.registerFactory<BindDeviceCubit>(
|
||||
() => BindDeviceCubit(sl(), sl(), sl(), sl(), sl(), sl<AppUserCubit>()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,24 +5,18 @@ class UserEntity extends Equatable {
|
||||
final String username;
|
||||
final String nickname;
|
||||
final String token;
|
||||
final int orgId;
|
||||
final String? avatar;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String? roleKey;
|
||||
final int? siteId;
|
||||
|
||||
const UserEntity({
|
||||
UserEntity({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.nickname,
|
||||
required this.token,
|
||||
required this.orgId,
|
||||
this.avatar,
|
||||
this.email,
|
||||
this.phone,
|
||||
this.roleKey,
|
||||
this.siteId,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -32,36 +26,7 @@ class UserEntity extends Equatable {
|
||||
nickname,
|
||||
avatar,
|
||||
token,
|
||||
orgId,
|
||||
email,
|
||||
phone,
|
||||
roleKey,
|
||||
siteId,
|
||||
];
|
||||
|
||||
UserEntity copyWith({
|
||||
String? userId,
|
||||
String? username,
|
||||
String? nickname,
|
||||
String? token,
|
||||
int? orgId,
|
||||
String? avatar,
|
||||
String? email,
|
||||
String? phone,
|
||||
String? roleKey,
|
||||
int? siteId,
|
||||
}) {
|
||||
return UserEntity(
|
||||
userId: userId ?? this.userId,
|
||||
username: username ?? this.username,
|
||||
nickname: nickname ?? this.nickname,
|
||||
token: token ?? this.token,
|
||||
orgId: orgId ?? this.orgId,
|
||||
avatar: avatar ?? this.avatar,
|
||||
email: email ?? this.email,
|
||||
phone: phone ?? this.phone,
|
||||
roleKey: roleKey ?? this.roleKey,
|
||||
siteId: siteId ?? this.siteId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
lib/core/env/env_config.dart
vendored
13
lib/core/env/env_config.dart
vendored
@@ -1,7 +1,7 @@
|
||||
class EnvConfig {
|
||||
static const String environment = String.fromEnvironment(
|
||||
'ENV',
|
||||
defaultValue: 'prod',
|
||||
defaultValue: 'dev',
|
||||
);
|
||||
|
||||
static String get sentryDsn {
|
||||
@@ -13,15 +13,4 @@ class EnvConfig {
|
||||
}
|
||||
|
||||
static bool get isProduction => environment == 'prod';
|
||||
|
||||
/// TCP 服务器配置
|
||||
static String get tcpIp => '1.95.137.212';
|
||||
|
||||
static int get tcpPort {
|
||||
// 测试服: 59016, 生产服: 9001
|
||||
if (environment == 'prod') {
|
||||
return 9001;
|
||||
}
|
||||
return 9001; // TCP 端口
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,3 @@ class Failure {
|
||||
class NetworkFailure extends Failure {
|
||||
NetworkFailure(super.message);
|
||||
}
|
||||
|
||||
class ServerFailure extends Failure {
|
||||
ServerFailure(super.message);
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
|
||||
/// 工单模块特定的失败类型
|
||||
class WorkOrderFailure extends Failure {
|
||||
WorkOrderFailure(super.message);
|
||||
}
|
||||
|
||||
class UnknownFailure extends WorkOrderFailure {
|
||||
UnknownFailure({required String message}) : super(message);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../features/v2/device_list/domain/entities/drone_station_entity.dart';
|
||||
|
||||
/// 无人机轨迹点(轻量级,避免在核心层引入 latlong2 依赖)
|
||||
class DroneTrajectoryPoint {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double? heading;
|
||||
|
||||
const DroneTrajectoryPoint({
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.heading,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'DroneTrajectoryPoint(lat=$latitude, lng=$longitude, heading=$heading)';
|
||||
}
|
||||
|
||||
/// 无人机任务信息
|
||||
class DroneTaskInfo {
|
||||
final String droneSn;
|
||||
final String gatewaySn;
|
||||
final List<CameraInfo>? cameraList;
|
||||
final bool isOnline; // 无人机是否在线
|
||||
final String stationName; // 机场名称/呼号
|
||||
final String droneName; // 无人机名称/呼号
|
||||
|
||||
DroneTaskInfo({
|
||||
required this.droneSn,
|
||||
required this.gatewaySn,
|
||||
this.cameraList,
|
||||
this.isOnline = false,
|
||||
this.stationName = '',
|
||||
this.droneName = '',
|
||||
});
|
||||
}
|
||||
|
||||
/// 无人机任务状态管理器
|
||||
/// 使用 ValueNotifier 实现响应式状态管理
|
||||
/// 支持在机场页面点击无人机卡片时实时同步到 FloatBarWidget
|
||||
class DroneTaskStateManager {
|
||||
static final DroneTaskStateManager _instance =
|
||||
DroneTaskStateManager._internal();
|
||||
factory DroneTaskStateManager() => _instance;
|
||||
DroneTaskStateManager._internal();
|
||||
|
||||
/// 当前无人机任务信息(响应式状态)
|
||||
final ValueNotifier<DroneTaskInfo?> _currentTaskInfo =
|
||||
ValueNotifier<DroneTaskInfo?>(null);
|
||||
|
||||
/// 任务下发成功后,等待无人机 OSD 推送数据(实时信息)
|
||||
final ValueNotifier<bool> _isWaitingForOsdPush = ValueNotifier<bool>(false);
|
||||
|
||||
/// 任务下发成功后,等待无人机视频流
|
||||
final ValueNotifier<bool> _isWaitingForVideo = ValueNotifier<bool>(false);
|
||||
|
||||
/// 无人机飞行轨迹点(跨页面持久化,退出视频页后不丢失)
|
||||
final ValueNotifier<List<DroneTrajectoryPoint>> _trajectoryPoints =
|
||||
ValueNotifier<List<DroneTrajectoryPoint>>([]);
|
||||
|
||||
/// 超时定时器,避免一直转圈(推送/视频迟迟不到)
|
||||
Timer? _osdWaitTimeoutTimer;
|
||||
Timer? _videoWaitTimeoutTimer;
|
||||
|
||||
/// 监听无人机任务信息变化
|
||||
ValueListenable<DroneTaskInfo?> get currentTaskInfo => _currentTaskInfo;
|
||||
|
||||
/// 监听“等待 OSD 推送”状态
|
||||
ValueListenable<bool> get isWaitingForOsdPush => _isWaitingForOsdPush;
|
||||
|
||||
/// 监听“等待视频流”状态
|
||||
ValueListenable<bool> get isWaitingForVideo => _isWaitingForVideo;
|
||||
|
||||
/// 监听无人机轨迹点
|
||||
ValueListenable<List<DroneTrajectoryPoint>> get trajectoryPoints =>
|
||||
_trajectoryPoints;
|
||||
|
||||
/// 获取当前无人机任务信息
|
||||
DroneTaskInfo? get currentInfo => _currentTaskInfo.value;
|
||||
|
||||
/// 设置当前无人机任务信息
|
||||
/// 支持两种触发方式:
|
||||
/// 1. 任务下发时自动触发
|
||||
/// 2. 点击机场页面无人机卡片时触发
|
||||
void setDroneTaskInfo(DroneTaskInfo info) {
|
||||
debugPrint('🛸 [DroneTaskStateManager] 设置无人机任务信息');
|
||||
debugPrint(' droneSn: ${info.droneSn}');
|
||||
debugPrint(' gatewaySn: ${info.gatewaySn}');
|
||||
_currentTaskInfo.value = info;
|
||||
}
|
||||
|
||||
/// 任务下发成功后调用:开始监测无人机实时推送与视频流
|
||||
/// 在接口返回成功之后调用
|
||||
void markTaskIssued() {
|
||||
debugPrint('🚀 [DroneTaskStateManager] 任务已下发,开始监测推送数据与视频流');
|
||||
_isWaitingForOsdPush.value = true;
|
||||
_isWaitingForVideo.value = true;
|
||||
|
||||
_osdWaitTimeoutTimer?.cancel();
|
||||
_videoWaitTimeoutTimer?.cancel();
|
||||
|
||||
// 超时兜底:90 秒后仍未收到 OSD 推送,自动停止转圈
|
||||
_osdWaitTimeoutTimer = Timer(const Duration(seconds: 90), () {
|
||||
if (_isWaitingForOsdPush.value) {
|
||||
debugPrint('⚠️ [DroneTaskStateManager] 等待 OSD 推送超时,自动停止');
|
||||
_isWaitingForOsdPush.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 超时兜底:90 秒后仍未收到视频,自动停止 toast
|
||||
_videoWaitTimeoutTimer = Timer(const Duration(seconds: 90), () {
|
||||
if (_isWaitingForVideo.value) {
|
||||
debugPrint('⚠️ [DroneTaskStateManager] 等待视频流超时,自动停止');
|
||||
_isWaitingForVideo.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 收到无人机 OSD 推送数据后调用:停止转圈,恢复绿点
|
||||
void markOsdPushReceived() {
|
||||
if (_isWaitingForOsdPush.value) {
|
||||
debugPrint('✅ [DroneTaskStateManager] 收到 OSD 推送数据,停止监测');
|
||||
_isWaitingForOsdPush.value = false;
|
||||
_osdWaitTimeoutTimer?.cancel();
|
||||
_osdWaitTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 收到无人机视频流后调用:隐藏"视频获取中" toast
|
||||
void markVideoReceived() {
|
||||
if (_isWaitingForVideo.value) {
|
||||
debugPrint('✅ [DroneTaskStateManager] 收到视频流,停止监测');
|
||||
_isWaitingForVideo.value = false;
|
||||
_videoWaitTimeoutTimer?.cancel();
|
||||
_videoWaitTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加无人机轨迹点
|
||||
void addTrajectoryPoint(DroneTrajectoryPoint point) {
|
||||
final list = List<DroneTrajectoryPoint>.from(_trajectoryPoints.value);
|
||||
list.add(point);
|
||||
// 性能优化:只保留最近 1000 个点
|
||||
if (list.length > 1000) {
|
||||
list.removeAt(0);
|
||||
}
|
||||
_trajectoryPoints.value = list;
|
||||
}
|
||||
|
||||
/// 批量设置轨迹点(恢复历史轨迹时使用)
|
||||
void setTrajectoryPoints(List<DroneTrajectoryPoint> points) {
|
||||
_trajectoryPoints.value = List<DroneTrajectoryPoint>.from(points);
|
||||
}
|
||||
|
||||
/// 清空轨迹
|
||||
void clearTrajectory() {
|
||||
_trajectoryPoints.value = [];
|
||||
}
|
||||
|
||||
/// 清除无人机任务信息
|
||||
void clearDroneTaskInfo() {
|
||||
debugPrint('🛸 [DroneTaskStateManager] 清除无人机任务信息');
|
||||
_currentTaskInfo.value = null;
|
||||
_osdWaitTimeoutTimer?.cancel();
|
||||
_videoWaitTimeoutTimer?.cancel();
|
||||
_osdWaitTimeoutTimer = null;
|
||||
_videoWaitTimeoutTimer = null;
|
||||
_isWaitingForOsdPush.value = false;
|
||||
_isWaitingForVideo.value = false;
|
||||
}
|
||||
|
||||
/// 检查是否有当前任务
|
||||
bool get hasTask => _currentTaskInfo.value != null;
|
||||
}
|
||||
|
||||
/// 全局管理器实例
|
||||
final droneTaskStateManager = DroneTaskStateManager();
|
||||
@@ -15,13 +15,7 @@ class DioClient {
|
||||
),
|
||||
);
|
||||
|
||||
dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
requestHeader: true,
|
||||
),
|
||||
);
|
||||
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
|
||||
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
@@ -36,29 +30,22 @@ class DioClient {
|
||||
return handler.next(options);
|
||||
},
|
||||
|
||||
// ======================
|
||||
// ✅ 关键:401 自动拦截
|
||||
// ======================
|
||||
onResponse: (response, handler) {
|
||||
if (response.data is Map<String, dynamic>) {
|
||||
final code = response.data['code'];
|
||||
if (code == 401 || code == 403) {
|
||||
print(
|
||||
'>>> [DIO] 🚨🚨🚨 收到业务错误码 $code,触发 Token 过期处理!URL: ${response.requestOptions.uri},时间: ${DateTime.now()}',
|
||||
);
|
||||
try {
|
||||
sl<AuthCubit>().tokenExpired();
|
||||
} catch (ex) {}
|
||||
}
|
||||
}
|
||||
return handler.next(response);
|
||||
},
|
||||
|
||||
onError: (DioException e, handler) async {
|
||||
if (e.response?.statusCode == 401 || e.response?.statusCode == 403) {
|
||||
print(
|
||||
'>>> [DIO] 🚨🚨🚨 收到 HTTP ${e.response?.statusCode},触发 Token 过期处理!URL: ${e.requestOptions.uri},时间: ${DateTime.now()}',
|
||||
);
|
||||
// 401 = token 过期 / 未授权
|
||||
if (e.response?.statusCode == 401) {
|
||||
try {
|
||||
sl<AuthCubit>().tokenExpired();
|
||||
} catch (ex) {}
|
||||
// 调用 logout 清除本地缓存 + 跳登录
|
||||
await sl<AuthCubit>().logout();
|
||||
} catch (ex) {
|
||||
// 防止报错
|
||||
}
|
||||
}
|
||||
|
||||
return handler.next(e);
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 统一错误处理器
|
||||
/// 所有接口错误都应该通过这个类来处理,避免显示原始错误信息
|
||||
class ErrorHandler {
|
||||
/// 处理错误并显示友好提示
|
||||
static void handleError(BuildContext context, Object error) {
|
||||
String message = _getFriendlyErrorMessage(error);
|
||||
|
||||
// 显示友好提示(自动消失)
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取友好的错误消息
|
||||
static String _getFriendlyErrorMessage(Object error) {
|
||||
if (error is DioException) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return '网络连接超时,请检查网络设置';
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return '网络连接失败,请检查网络';
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode;
|
||||
if (statusCode == 401) {
|
||||
return '登录已过期,请重新登录';
|
||||
} else if (statusCode == 403) {
|
||||
return '没有权限执行此操作';
|
||||
} else if (statusCode == 404) {
|
||||
return '请求的资源不存在';
|
||||
} else if (statusCode == 500) {
|
||||
return '服务器异常,请稍后重试';
|
||||
} else {
|
||||
return '服务器响应异常';
|
||||
}
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return '请求已取消';
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return '证书验证失败';
|
||||
|
||||
default:
|
||||
return '请求失败,请稍后重试';
|
||||
}
|
||||
} else if (error is Exception) {
|
||||
// 业务逻辑异常
|
||||
return '操作失败,请稍后重试';
|
||||
} else {
|
||||
// 未知错误
|
||||
return '未知错误,请稍后重试';
|
||||
}
|
||||
}
|
||||
|
||||
/// 在 Cubit/Bloc 中使用的静态方法(不需要 context)
|
||||
static String getErrorMessage(Object error) {
|
||||
return _getFriendlyErrorMessage(error);
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
import '../../domain/entities/drone_osd_entity.dart';
|
||||
|
||||
abstract class DroneOsdDataSource {
|
||||
Stream<DroneOsdEntity> get droneOsdStream;
|
||||
Stream<DroneOsdEntity> get stationOsdStream;
|
||||
|
||||
Future<void> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
});
|
||||
|
||||
Future<void> stopListening();
|
||||
|
||||
void dispose();
|
||||
}
|
||||
|
||||
class DroneOsdDataSourceImpl implements DroneOsdDataSource {
|
||||
final MqttClient mqttClient;
|
||||
final _droneOsdController = StreamController<DroneOsdEntity>.broadcast();
|
||||
final _stationOsdController = StreamController<DroneOsdEntity>.broadcast();
|
||||
|
||||
StreamSubscription<MqttMessage>? _subscription;
|
||||
String? _deviceSn;
|
||||
String? _gatewaySn;
|
||||
int _listenerCount = 0;
|
||||
bool _isDisposed = false;
|
||||
|
||||
DroneOsdDataSourceImpl(this.mqttClient);
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get droneOsdStream => _droneOsdController.stream;
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get stationOsdStream => _stationOsdController.stream;
|
||||
|
||||
@override
|
||||
Future<void> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
}) async {
|
||||
if (_isDisposed) return;
|
||||
|
||||
// 同一设备(SN 相同):只增加引用计数,共享订阅
|
||||
if (_listenerCount > 0 &&
|
||||
_deviceSn == deviceSn &&
|
||||
_gatewaySn == gatewaySn &&
|
||||
_subscription != null) {
|
||||
_listenerCount++;
|
||||
debugPrint('[DroneOsdDataSource] 引用计数+1: $_listenerCount');
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到不同设备:强制清理旧订阅
|
||||
// 不管引用计数多少,SN 变了就必须先取消旧订阅再订阅新的
|
||||
if (_deviceSn != null || _gatewaySn != null) {
|
||||
// 重置引用计数为 1,让 stopListening 能真正取消 MQTT 订阅
|
||||
_listenerCount = 1;
|
||||
await stopListening();
|
||||
}
|
||||
|
||||
_deviceSn = deviceSn;
|
||||
_gatewaySn = gatewaySn;
|
||||
|
||||
final droneTopic = 'thing/product/$deviceSn/osd';
|
||||
final stationTopic = 'thing/product/$gatewaySn/osd';
|
||||
|
||||
debugPrint('🛸 [DroneOsdDataSource] 开始监听:');
|
||||
debugPrint(' 无人机: $droneTopic');
|
||||
debugPrint(' 机场: $stationTopic');
|
||||
|
||||
if (deviceSn.isNotEmpty) {
|
||||
await mqttClient.subscribe(droneTopic);
|
||||
}
|
||||
if (gatewaySn.isNotEmpty) {
|
||||
await mqttClient.subscribe(stationTopic);
|
||||
}
|
||||
|
||||
_subscription = mqttClient.messageStream?.listen((message) {
|
||||
_handleMessage(message);
|
||||
});
|
||||
|
||||
_listenerCount = 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
_listenerCount--;
|
||||
if (_listenerCount > 0) {
|
||||
debugPrint('[DroneOsdDataSource] 引用计数-1: $_listenerCount (保留订阅)');
|
||||
return;
|
||||
}
|
||||
|
||||
_listenerCount = 0;
|
||||
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
if (_deviceSn != null && _deviceSn!.isNotEmpty) {
|
||||
final deviceSn = _deviceSn!;
|
||||
await mqttClient.unsubscribe('thing/product/$deviceSn/osd');
|
||||
}
|
||||
if (_gatewaySn != null && _gatewaySn!.isNotEmpty) {
|
||||
final gatewaySn = _gatewaySn!;
|
||||
await mqttClient.unsubscribe('thing/product/$gatewaySn/osd');
|
||||
}
|
||||
|
||||
_deviceSn = null;
|
||||
_gatewaySn = null;
|
||||
}
|
||||
|
||||
void _handleMessage(MqttMessage message) {
|
||||
if (_isDisposed) return;
|
||||
try {
|
||||
final jsonData = jsonDecode(message.payload) as Map<String, dynamic>;
|
||||
|
||||
if (_gatewaySn != null &&
|
||||
_gatewaySn!.isNotEmpty &&
|
||||
message.topic.contains(_gatewaySn!)) {
|
||||
// 注入来源 SN,供下游解析层验证消息归属
|
||||
jsonData['_sourceSn'] = _gatewaySn;
|
||||
final osdData = DroneOsdEntity.fromJson(jsonData);
|
||||
_stationOsdController.add(osdData);
|
||||
} else if (_deviceSn != null &&
|
||||
_deviceSn!.isNotEmpty &&
|
||||
message.topic.contains(_deviceSn!)) {
|
||||
jsonData['_sourceSn'] = _deviceSn;
|
||||
final osdData = DroneOsdEntity.fromJson(jsonData);
|
||||
_droneOsdController.add(osdData);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [DroneOsdDataSource] 解析 OSD 数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (_isDisposed) return;
|
||||
_isDisposed = true;
|
||||
|
||||
// 强制清理:不受引用计数限制,确保 MQTT 订阅被真正取消
|
||||
// 避免切换机场时旧订阅残留导致数据串台
|
||||
_listenerCount = 0;
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
if (_deviceSn != null && _deviceSn!.isNotEmpty) {
|
||||
final deviceSn = _deviceSn!;
|
||||
mqttClient.unsubscribe('thing/product/$deviceSn/osd');
|
||||
}
|
||||
if (_gatewaySn != null && _gatewaySn!.isNotEmpty) {
|
||||
final gatewaySn = _gatewaySn!;
|
||||
mqttClient.unsubscribe('thing/product/$gatewaySn/osd');
|
||||
}
|
||||
|
||||
_deviceSn = null;
|
||||
_gatewaySn = null;
|
||||
|
||||
_droneOsdController.close();
|
||||
_stationOsdController.close();
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
import '../../domain/entities/task_arrive_entity.dart';
|
||||
import '../../domain/entities/task_status_entity.dart';
|
||||
import '../../domain/entities/real_time_message_entity.dart';
|
||||
|
||||
abstract class TaskMessageDataSource {
|
||||
Stream<TaskStatusEntity> get taskStatusStream;
|
||||
Stream<TaskArriveEntity> get taskArriveStream;
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream;
|
||||
|
||||
Future<void> startListening({required String deviceId, int? taskId});
|
||||
Future<void> stopListening();
|
||||
}
|
||||
|
||||
class TaskMessageDataSourceImpl implements TaskMessageDataSource {
|
||||
final MqttClient mqttClient;
|
||||
final _taskStatusController = StreamController<TaskStatusEntity>.broadcast();
|
||||
final _taskArriveController = StreamController<TaskArriveEntity>.broadcast();
|
||||
final _realTimeMessageController =
|
||||
StreamController<RealTimeMessageEntity>.broadcast();
|
||||
|
||||
StreamSubscription<MqttMessage>? _subscription;
|
||||
String? _deviceId;
|
||||
int? _taskId;
|
||||
|
||||
TaskMessageDataSourceImpl(this.mqttClient);
|
||||
|
||||
@override
|
||||
Stream<TaskStatusEntity> get taskStatusStream => _taskStatusController.stream;
|
||||
|
||||
@override
|
||||
Stream<TaskArriveEntity> get taskArriveStream => _taskArriveController.stream;
|
||||
|
||||
@override
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream =>
|
||||
_realTimeMessageController.stream;
|
||||
|
||||
@override
|
||||
Future<void> startListening({required String deviceId, int? taskId}) async {
|
||||
if (_deviceId == deviceId && _taskId == taskId && _subscription != null) {
|
||||
debugPrint('[TaskMessageDataSource] already listening: deviceId=$deviceId, taskId=$taskId');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deviceId != null && _deviceId != deviceId) {
|
||||
await stopListening();
|
||||
} else {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
|
||||
_deviceId = deviceId;
|
||||
_taskId = taskId;
|
||||
|
||||
// 🔥 status/arrive 话题使用 taskId,realTimeMessage 使用 deviceId
|
||||
final taskStatusTopic = taskId != null ? 'task/$taskId/status' : null;
|
||||
final taskArriveTopic = taskId != null ? 'task/$taskId/arrive' : null;
|
||||
final realTimeTopic = 'device/$deviceId/realTimeMessage';
|
||||
|
||||
debugPrint('📋 [TaskMessageDataSource] 开始监听:');
|
||||
if (taskStatusTopic != null) debugPrint(' 任务状态: $taskStatusTopic');
|
||||
if (taskArriveTopic != null) debugPrint(' 到达通知: $taskArriveTopic');
|
||||
debugPrint(' 实时消息: $realTimeTopic');
|
||||
|
||||
if (taskStatusTopic != null) await mqttClient.subscribe(taskStatusTopic);
|
||||
if (taskArriveTopic != null) await mqttClient.subscribe(taskArriveTopic);
|
||||
await mqttClient.subscribe(realTimeTopic);
|
||||
|
||||
_subscription = mqttClient.messageStream?.listen((message) {
|
||||
_handleMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
if (_deviceId != null) {
|
||||
final deviceId = _deviceId!;
|
||||
// 🔥 status/arrive 用 taskId 取消订阅
|
||||
if (_taskId != null) {
|
||||
await mqttClient.unsubscribe('task/$_taskId/status');
|
||||
await mqttClient.unsubscribe('task/$_taskId/arrive');
|
||||
}
|
||||
await mqttClient.unsubscribe('device/$deviceId/realTimeMessage');
|
||||
}
|
||||
|
||||
_deviceId = null;
|
||||
_taskId = null;
|
||||
}
|
||||
|
||||
void _handleMessage(MqttMessage message) {
|
||||
try {
|
||||
final jsonData = jsonDecode(message.payload) as Map<String, dynamic>;
|
||||
|
||||
if (message.topic.contains('/status')) {
|
||||
final taskStatus = TaskStatusEntity.fromJson(jsonData);
|
||||
debugPrint('📋 [TaskMessageDataSource] 任务状态更新: ${taskStatus.status}');
|
||||
_taskStatusController.add(taskStatus);
|
||||
} else if (message.topic.contains('/arrive')) {
|
||||
final arriveInfo = TaskArriveEntity.fromJson(jsonData);
|
||||
debugPrint('📍 [TaskMessageDataSource] 到达通知: 任务${arriveInfo.taskId}');
|
||||
_taskArriveController.add(arriveInfo);
|
||||
} else if (message.topic.contains('/realTimeMessage')) {
|
||||
final realTimeMsg = RealTimeMessageEntity.fromJson(jsonData);
|
||||
debugPrint(
|
||||
'💬 [TaskMessageDataSource] 实时消息 - 类型: ${realTimeMsg.type}, 数据点数: ${realTimeMsg.data.length}',
|
||||
);
|
||||
_realTimeMessageController.add(realTimeMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [TaskMessageDataSource] 解析任务消息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
stopListening();
|
||||
_taskStatusController.close();
|
||||
_taskArriveController.close();
|
||||
_realTimeMessageController.close();
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart' as mqtt;
|
||||
import 'package:mqtt_client/mqtt_server_client.dart' as mqtt_server;
|
||||
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_config.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
|
||||
class MqttClientImpl implements MqttClient {
|
||||
mqtt_server.MqttServerClient? _client;
|
||||
final _messageController = StreamController<MqttMessage>.broadcast();
|
||||
final Map<String, int> _subscriptionRefs = <String, int>{};
|
||||
|
||||
MqttConfig? _currentConfig;
|
||||
StreamSubscription? _updatesSubscription;
|
||||
Timer? _reconnectTimer;
|
||||
bool _isConnected = false;
|
||||
bool _isConnecting = false;
|
||||
bool _manualDisconnect = false;
|
||||
|
||||
@override
|
||||
Stream<MqttMessage>? get messageStream => _messageController.stream;
|
||||
|
||||
@override
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> connect(MqttConfig config) async {
|
||||
if (_isConnecting) {
|
||||
debugPrint('[MqttClient] connect ignored, already connecting');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isConnected) {
|
||||
debugPrint('[MqttClient] already connected, disconnect first');
|
||||
await disconnect();
|
||||
}
|
||||
|
||||
_manualDisconnect = false;
|
||||
_currentConfig = config;
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
_isConnecting = true;
|
||||
|
||||
try {
|
||||
await _updatesSubscription?.cancel();
|
||||
_updatesSubscription = null;
|
||||
_client = _createClient(config);
|
||||
_configureClient(_client!, config);
|
||||
|
||||
debugPrint('[MqttClient] connecting to ${config.connectionAddress}');
|
||||
await _client!.connect(config.username, config.password);
|
||||
|
||||
if (_client!.connectionStatus?.state ==
|
||||
mqtt.MqttConnectionState.connected) {
|
||||
_isConnected = true;
|
||||
debugPrint('[MqttClient] connected');
|
||||
_listenToMessages();
|
||||
_resubscribeAll();
|
||||
} else {
|
||||
throw Exception(
|
||||
'MQTT connect failed: ${_client!.connectionStatus?.returnCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MqttClient] connect error: $e');
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_scheduleReconnect();
|
||||
rethrow;
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
mqtt_server.MqttServerClient _createClient(MqttConfig config) {
|
||||
final clientId =
|
||||
'${config.clientId}_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
switch (config.protocol) {
|
||||
case MqttProtocol.websocket:
|
||||
case MqttProtocol.wss:
|
||||
final client = mqtt_server.MqttServerClient.withPort(
|
||||
config.connectionAddress,
|
||||
clientId,
|
||||
config.port,
|
||||
);
|
||||
client.useWebSocket = true;
|
||||
client.websocketProtocols = ['mqtt'];
|
||||
return client;
|
||||
case MqttProtocol.tcp:
|
||||
return mqtt_server.MqttServerClient.withPort(
|
||||
config.host,
|
||||
clientId,
|
||||
config.port,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _configureClient(
|
||||
mqtt_server.MqttServerClient client,
|
||||
MqttConfig config,
|
||||
) {
|
||||
client.logging(on: false);
|
||||
client.keepAlivePeriod = config.keepAlivePeriod;
|
||||
client.autoReconnect = false;
|
||||
client.resubscribeOnAutoReconnect = false;
|
||||
client.onDisconnected = _onDisconnected;
|
||||
client.onConnected = _onConnected;
|
||||
client.onSubscribed = _onSubscribed;
|
||||
|
||||
var connMessage = mqtt.MqttConnectMessage()
|
||||
.withClientIdentifier(client.clientIdentifier)
|
||||
.withWillQos(mqtt.MqttQos.atLeastOnce);
|
||||
|
||||
if (config.cleanSession) {
|
||||
connMessage = connMessage.startClean();
|
||||
}
|
||||
|
||||
client.connectionMessage = connMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
debugPrint('[MqttClient] disconnect');
|
||||
_manualDisconnect = true;
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
await _updatesSubscription?.cancel();
|
||||
_updatesSubscription = null;
|
||||
_client?.disconnect();
|
||||
_client = null;
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_currentConfig = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> subscribe(String topic) async {
|
||||
final previousRefs = _subscriptionRefs[topic] ?? 0;
|
||||
_subscriptionRefs[topic] = previousRefs + 1;
|
||||
|
||||
if (!_isConnected || _client == null) {
|
||||
debugPrint('[MqttClient] queued subscription while disconnected: $topic');
|
||||
_scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousRefs > 0) {
|
||||
debugPrint('[MqttClient] subscription already active: $topic');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[MqttClient] subscribe: $topic');
|
||||
_client!.subscribe(topic, mqtt.MqttQos.atLeastOnce);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unsubscribe(String topic) async {
|
||||
final currentRefs = _subscriptionRefs[topic] ?? 0;
|
||||
if (currentRefs <= 1) {
|
||||
_subscriptionRefs.remove(topic);
|
||||
} else {
|
||||
_subscriptionRefs[topic] = currentRefs - 1;
|
||||
debugPrint('[MqttClient] subscription still in use: $topic');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isConnected || _client == null) {
|
||||
debugPrint('[MqttClient] removed queued subscription: $topic');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[MqttClient] unsubscribe: $topic');
|
||||
_client!.unsubscribe(topic);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publish(String topic, String message) async {
|
||||
if (!_isConnected || _client == null) {
|
||||
throw Exception('MQTT is not connected');
|
||||
}
|
||||
|
||||
debugPrint('[MqttClient] publish: $topic');
|
||||
final builder = mqtt.MqttClientPayloadBuilder();
|
||||
builder.addString(message);
|
||||
_client!.publishMessage(topic, mqtt.MqttQos.atLeastOnce, builder.payload!);
|
||||
}
|
||||
|
||||
void _listenToMessages() {
|
||||
_updatesSubscription = _client!.updates!.listen(
|
||||
(List<mqtt.MqttReceivedMessage<mqtt.MqttMessage>> messages) {
|
||||
for (final msg in messages) {
|
||||
final topic = msg.topic;
|
||||
final payload = mqtt.MqttPublishPayload.bytesToStringAsString(
|
||||
(msg.payload as mqtt.MqttPublishMessage).payload.message,
|
||||
);
|
||||
|
||||
debugPrint('[MqttClient] received [$topic]: $payload');
|
||||
_messageController.add(MqttMessage(topic: topic, payload: payload));
|
||||
}
|
||||
},
|
||||
onError: (Object error) {
|
||||
debugPrint('[MqttClient] updates stream error: $error');
|
||||
_isConnected = false;
|
||||
_scheduleReconnect();
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _resubscribeAll() {
|
||||
if (_subscriptionRefs.isEmpty || _client == null) return;
|
||||
|
||||
for (final topic in _subscriptionRefs.keys) {
|
||||
debugPrint('[MqttClient] resubscribe: $topic');
|
||||
_client!.subscribe(topic, mqtt.MqttQos.atLeastOnce);
|
||||
}
|
||||
}
|
||||
|
||||
void _onConnected() {
|
||||
debugPrint('[MqttClient] connected callback');
|
||||
_isConnected = true;
|
||||
}
|
||||
|
||||
void _onDisconnected() {
|
||||
debugPrint('[MqttClient] disconnected callback');
|
||||
_isConnected = false;
|
||||
if (!_manualDisconnect) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _onSubscribed(String topic) {
|
||||
debugPrint('[MqttClient] subscribed: $topic');
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
final config = _currentConfig;
|
||||
if (_manualDisconnect || config == null || _isConnected || _isConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reconnectTimer?.isActive ?? false) return;
|
||||
|
||||
final delay = Duration(milliseconds: config.reconnectDelayMs);
|
||||
debugPrint('[MqttClient] reconnect scheduled in ${delay.inMilliseconds}ms');
|
||||
_reconnectTimer = Timer(delay, () async {
|
||||
if (_manualDisconnect || _isConnected || _isConnecting) return;
|
||||
|
||||
try {
|
||||
debugPrint('[MqttClient] reconnecting...');
|
||||
await connect(config);
|
||||
} catch (e) {
|
||||
debugPrint('[MqttClient] reconnect failed: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_messageController.close();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../datasources/drone_osd_datasource.dart';
|
||||
import '../../domain/repositories/drone_osd_repository.dart';
|
||||
import '../../domain/entities/drone_osd_entity.dart';
|
||||
|
||||
class DroneOsdRepositoryImpl implements DroneOsdRepository {
|
||||
final DroneOsdDataSource dataSource;
|
||||
|
||||
DroneOsdRepositoryImpl(this.dataSource);
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get droneOsdStream => dataSource.droneOsdStream;
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get stationOsdStream => dataSource.stationOsdStream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
}) async {
|
||||
try {
|
||||
await dataSource.startListening(
|
||||
deviceSn: deviceSn,
|
||||
gatewaySn: gatewaySn,
|
||||
);
|
||||
return right(null);
|
||||
} catch (e) {
|
||||
return left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await dataSource.stopListening();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../datasources/task_message_datasource.dart';
|
||||
import '../../domain/repositories/task_message_repository.dart';
|
||||
import '../../domain/entities/task_arrive_entity.dart';
|
||||
import '../../domain/entities/task_status_entity.dart';
|
||||
import '../../domain/entities/real_time_message_entity.dart';
|
||||
|
||||
class TaskMessageRepositoryImpl implements TaskMessageRepository {
|
||||
final TaskMessageDataSource dataSource;
|
||||
|
||||
TaskMessageRepositoryImpl(this.dataSource);
|
||||
|
||||
@override
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream =>
|
||||
dataSource.realTimeMessageStream;
|
||||
|
||||
@override
|
||||
Stream<TaskArriveEntity> get taskArriveStream => dataSource.taskArriveStream;
|
||||
|
||||
@override
|
||||
Stream<TaskStatusEntity> get taskStatusStream => dataSource.taskStatusStream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> startListening({required String deviceId, int? taskId}) async {
|
||||
try {
|
||||
await dataSource.startListening(deviceId: deviceId, taskId: taskId);
|
||||
return right(null);
|
||||
} catch (e) {
|
||||
return left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await dataSource.stopListening();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class DroneOsdEntity extends Equatable {
|
||||
final Map<String, dynamic> rawData;
|
||||
|
||||
const DroneOsdEntity({required this.rawData});
|
||||
|
||||
factory DroneOsdEntity.fromJson(Map<String, dynamic> json) {
|
||||
return DroneOsdEntity(rawData: json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [rawData];
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class RealTimeMessageEntity extends Equatable {
|
||||
final List<DeviceDataPoint> data;
|
||||
final String type;
|
||||
|
||||
const RealTimeMessageEntity({
|
||||
required this.data,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory RealTimeMessageEntity.fromJson(Map<String, dynamic> json) {
|
||||
final dataList = json['data'] as List<dynamic>? ?? [];
|
||||
return RealTimeMessageEntity(
|
||||
data: dataList.map((item) => DeviceDataPoint.fromJson(item)).toList(),
|
||||
type: json['type'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
double? getValueByName(String name) {
|
||||
final point = data.firstWhere(
|
||||
(p) => p.name == name,
|
||||
orElse: () => DeviceDataPoint(name: '', value: '', unit: ''),
|
||||
);
|
||||
if (point.value.isEmpty) return null;
|
||||
return double.tryParse(point.value);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'data': data.map((e) => e.toJson()).toList(),
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [data, type];
|
||||
}
|
||||
|
||||
class DeviceDataPoint extends Equatable {
|
||||
final String name;
|
||||
final String value;
|
||||
final String unit;
|
||||
|
||||
const DeviceDataPoint({
|
||||
required this.name,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
factory DeviceDataPoint.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceDataPoint(
|
||||
name: json['name'] as String? ?? '',
|
||||
value: json['value'] as String? ?? '',
|
||||
unit: json['unit'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'value': value,
|
||||
'unit': unit,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, value, unit];
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class TaskArriveEntity extends Equatable {
|
||||
final String type;
|
||||
final String deviceId;
|
||||
final int? taskId;
|
||||
final dynamic status;
|
||||
final ArriveLocation? entity;
|
||||
|
||||
const TaskArriveEntity({
|
||||
required this.type,
|
||||
required this.deviceId,
|
||||
this.taskId,
|
||||
this.status,
|
||||
this.entity,
|
||||
});
|
||||
|
||||
factory TaskArriveEntity.fromJson(Map<String, dynamic> json) {
|
||||
final entityJson = json['entity'] as Map<String, dynamic>?;
|
||||
return TaskArriveEntity(
|
||||
type: json['type'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskId: json['taskId'] as int?,
|
||||
status: json['status'],
|
||||
entity: entityJson != null ? ArriveLocation.fromJson(entityJson) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type,
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'status': status,
|
||||
'entity': entity?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, deviceId, taskId, status, entity];
|
||||
}
|
||||
|
||||
class ArriveLocation extends Equatable {
|
||||
final double lat;
|
||||
final double lng;
|
||||
|
||||
const ArriveLocation({required this.lat, required this.lng});
|
||||
|
||||
factory ArriveLocation.fromJson(Map<String, dynamic> json) {
|
||||
return ArriveLocation(
|
||||
lat: (json['lat'] as num?)?.toDouble() ?? 0.0,
|
||||
lng: (json['lng'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'lat': lat, 'lng': lng};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [lat, lng];
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class TaskStatusEntity extends Equatable {
|
||||
final String type;
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final String? status;
|
||||
final Map<String, dynamic>? extraData;
|
||||
|
||||
const TaskStatusEntity({
|
||||
required this.type,
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
this.status,
|
||||
this.extraData,
|
||||
});
|
||||
|
||||
factory TaskStatusEntity.fromJson(Map<String, dynamic> json) {
|
||||
return TaskStatusEntity(
|
||||
type: json['type'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskId: json['taskId'] as int? ?? 0,
|
||||
status: json['status'] as String?,
|
||||
extraData: json,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type,
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'status': status,
|
||||
if (extraData != null) ...extraData!,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, deviceId, taskId, status, extraData];
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'dart:async';
|
||||
import '../models/mqtt_config.dart';
|
||||
import '../models/mqtt_message.dart';
|
||||
|
||||
abstract class MqttClient {
|
||||
Stream<MqttMessage>? get messageStream;
|
||||
|
||||
Future<void> connect(MqttConfig config);
|
||||
Future<void> disconnect();
|
||||
Future<void> subscribe(String topic);
|
||||
Future<void> unsubscribe(String topic);
|
||||
Future<void> publish(String topic, String message);
|
||||
bool get isConnected;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// MQTT 传输协议类型
|
||||
enum MqttProtocol {
|
||||
/// 纯 TCP 连接(原生 MQTT)
|
||||
tcp,
|
||||
|
||||
/// WebSocket 连接(MQTT over WebSocket)
|
||||
websocket,
|
||||
|
||||
/// 加密的 WebSocket 连接(MQTT over WSS)
|
||||
wss,
|
||||
}
|
||||
|
||||
class MqttConfig extends Equatable {
|
||||
final String host;
|
||||
final int port;
|
||||
final String username;
|
||||
final String password;
|
||||
final MqttProtocol protocol;
|
||||
final String clientId;
|
||||
final bool cleanSession;
|
||||
final int keepAlivePeriod;
|
||||
final int reconnectDelayMs;
|
||||
|
||||
/// WebSocket 路径(仅 WebSocket 模式使用)
|
||||
final String? wsPath;
|
||||
|
||||
const MqttConfig({
|
||||
required this.host,
|
||||
required this.port,
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.protocol,
|
||||
required this.clientId,
|
||||
this.cleanSession = true,
|
||||
this.keepAlivePeriod = 60,
|
||||
this.reconnectDelayMs = 3000,
|
||||
this.wsPath,
|
||||
});
|
||||
|
||||
/// 获取完整的连接地址
|
||||
String get connectionAddress {
|
||||
switch (protocol) {
|
||||
case MqttProtocol.websocket:
|
||||
return 'ws://$host:$port${wsPath ?? '/mqtt'}';
|
||||
case MqttProtocol.wss:
|
||||
return 'wss://$host:$port${wsPath ?? '/mqtt'}';
|
||||
case MqttProtocol.tcp:
|
||||
default:
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
/// 无人机/机场 OSD 数据(WebSocket MQTT)
|
||||
factory MqttConfig.droneOsd() {
|
||||
return const MqttConfig(
|
||||
host: '1.95.137.212',
|
||||
port: 8083,
|
||||
username: 'maibu',
|
||||
password: 'jsmbzn520',
|
||||
protocol: MqttProtocol.websocket,
|
||||
clientId: 'drone_osd_client',
|
||||
wsPath: '/mqtt',
|
||||
cleanSession: true,
|
||||
reconnectDelayMs: 3000,
|
||||
);
|
||||
}
|
||||
|
||||
/// 任务状态消息(TCP MQTT)
|
||||
factory MqttConfig.taskMessage() {
|
||||
return const MqttConfig(
|
||||
host: '1.95.137.212',
|
||||
port: 1883,
|
||||
username: 'maibu',
|
||||
password: 'jsmbzn520',
|
||||
protocol: MqttProtocol.tcp,
|
||||
clientId: 'task_message_client',
|
||||
cleanSession: true,
|
||||
reconnectDelayMs: 3000,
|
||||
);
|
||||
}
|
||||
|
||||
MqttConfig copyWith({
|
||||
String? host,
|
||||
int? port,
|
||||
String? username,
|
||||
String? password,
|
||||
MqttProtocol? protocol,
|
||||
String? clientId,
|
||||
bool? cleanSession,
|
||||
int? keepAlivePeriod,
|
||||
int? reconnectDelayMs,
|
||||
String? wsPath,
|
||||
}) {
|
||||
return MqttConfig(
|
||||
host: host ?? this.host,
|
||||
port: port ?? this.port,
|
||||
username: username ?? this.username,
|
||||
password: password ?? this.password,
|
||||
protocol: protocol ?? this.protocol,
|
||||
clientId: clientId ?? this.clientId,
|
||||
cleanSession: cleanSession ?? this.cleanSession,
|
||||
keepAlivePeriod: keepAlivePeriod ?? this.keepAlivePeriod,
|
||||
reconnectDelayMs: reconnectDelayMs ?? this.reconnectDelayMs,
|
||||
wsPath: wsPath ?? this.wsPath,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
protocol,
|
||||
clientId,
|
||||
cleanSession,
|
||||
keepAlivePeriod,
|
||||
reconnectDelayMs,
|
||||
wsPath,
|
||||
];
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class MqttMessage extends Equatable {
|
||||
final String topic;
|
||||
final String payload;
|
||||
final DateTime timestamp;
|
||||
|
||||
MqttMessage({
|
||||
required this.topic,
|
||||
required this.payload,
|
||||
DateTime? timestamp,
|
||||
}) : timestamp = timestamp ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'topic': topic,
|
||||
'payload': payload,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [topic, payload, timestamp];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/drone_osd_entity.dart';
|
||||
|
||||
abstract class DroneOsdRepository {
|
||||
Stream<DroneOsdEntity> get droneOsdStream;
|
||||
Stream<DroneOsdEntity> get stationOsdStream;
|
||||
|
||||
Future<Either<Failure, void>> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
});
|
||||
|
||||
Future<void> stopListening();
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/task_arrive_entity.dart';
|
||||
import '../entities/task_status_entity.dart';
|
||||
import '../entities/real_time_message_entity.dart';
|
||||
|
||||
abstract class TaskMessageRepository {
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream;
|
||||
Stream<TaskArriveEntity> get taskArriveStream;
|
||||
Stream<TaskStatusEntity> get taskStatusStream;
|
||||
|
||||
Future<Either<Failure, void>> startListening({required String deviceId, int? taskId});
|
||||
Future<void> stopListening();
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../di/injection.dart';
|
||||
import 'data/datasources/drone_osd_datasource.dart';
|
||||
import 'data/datasources/task_message_datasource.dart';
|
||||
import 'domain/interfaces/mqtt_client.dart';
|
||||
import 'domain/models/mqtt_config.dart';
|
||||
|
||||
class MqttManager {
|
||||
static final MqttManager _instance = MqttManager._internal();
|
||||
factory MqttManager() => _instance;
|
||||
MqttManager._internal();
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) {
|
||||
debugPrint('⚠️ [MqttManager] 已初始化,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔧 [MqttManager] 开始初始化 MQTT 连接...');
|
||||
|
||||
final droneOsdClient = sl<MqttClient>(instanceName: 'droneOsdClient');
|
||||
final taskMessageClient = sl<MqttClient>(instanceName: 'taskMessageClient');
|
||||
|
||||
// 独立连接两个 MQTT 客户端,互不影响
|
||||
await _connectClient(droneOsdClient, MqttConfig.droneOsd(), 'droneOsdClient');
|
||||
await _connectClient(taskMessageClient, MqttConfig.taskMessage(), 'taskMessageClient');
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('✅ [MqttManager] MQTT 初始化完成');
|
||||
}
|
||||
|
||||
Future<void> _connectClient(
|
||||
MqttClient client,
|
||||
MqttConfig config,
|
||||
String clientName,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('🔌 [MqttManager] 连接 $clientName...');
|
||||
await client.connect(config);
|
||||
debugPrint('✅ [MqttManager] $clientName 连接成功');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MqttManager] $clientName 连接失败: $e');
|
||||
// 不抛出异常,允许其他客户端继续连接
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
debugPrint('🔌 [MqttManager] 断开所有 MQTT 连接...');
|
||||
|
||||
final droneOsdClient = sl<MqttClient>(instanceName: 'droneOsdClient');
|
||||
final taskMessageClient = sl<MqttClient>(instanceName: 'taskMessageClient');
|
||||
|
||||
await droneOsdClient.disconnect();
|
||||
await taskMessageClient.disconnect();
|
||||
|
||||
_isInitialized = false;
|
||||
debugPrint('✅ [MqttManager] 所有 MQTT 连接已断开');
|
||||
}
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class NetMessageDispatcher {
|
||||
//机器状态页面专用
|
||||
Stream<String> onStringMessageOfdeviceStatus() {
|
||||
//print("0x02--TCP拦截推送解析开始");
|
||||
_logger.logWithLevel('0x02--TCP拦截推送解析开始' ,shouldLog: true);
|
||||
_logger.log('0x02--TCP拦截推送解析开始');
|
||||
// 🔥 关键修复:使用asBroadcastStream()确保多个监听者都能收到数据
|
||||
return onCommand(0x02).map((p) {
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum TcpConnectionStatus {
|
||||
disconnected, // 未连接
|
||||
connecting, // 连接中
|
||||
connected, // 已连接
|
||||
error, // 连接错误
|
||||
}
|
||||
|
||||
class TcpStatusState {
|
||||
final TcpConnectionStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
const TcpStatusState({
|
||||
this.status = TcpConnectionStatus.disconnected,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
TcpStatusState copyWith({
|
||||
TcpConnectionStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return TcpStatusState(
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TcpStatusCubit extends Cubit<TcpStatusState> {
|
||||
TcpStatusCubit() : super(const TcpStatusState());
|
||||
|
||||
void setConnecting() {
|
||||
emit(const TcpStatusState(status: TcpConnectionStatus.connecting));
|
||||
}
|
||||
|
||||
void setConnected() {
|
||||
emit(const TcpStatusState(status: TcpConnectionStatus.connected));
|
||||
}
|
||||
|
||||
void setDisconnected() {
|
||||
emit(const TcpStatusState(status: TcpConnectionStatus.disconnected));
|
||||
}
|
||||
|
||||
void setError(String message) {
|
||||
emit(TcpStatusState(
|
||||
status: TcpConnectionStatus.error,
|
||||
errorMessage: message,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/router/route_paths.dart';
|
||||
import 'package:maibu_satabot_v2/features/ai/presentation/routes/ai_routes.dart';
|
||||
@@ -6,43 +5,26 @@ import 'package:maibu_satabot_v2/features/auth/presentation/pages/login_page.dar
|
||||
import 'package:maibu_satabot_v2/features/auth/presentation/routes/auth_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/routes/home_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/pages/tab_settings_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/routes/my_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/routes/alarm_center_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/routes/workorder_routes.dart';
|
||||
|
||||
import '../../features/auth/presentation/bloc/auth_cubit.dart';
|
||||
import '../../features/auth/presentation/bloc/auth_state.dart';
|
||||
import '../../features/machine_details/presentation/pages/machine_details_page.dart';
|
||||
import '../../features/main_container/presentation/main_wrapper.dart';
|
||||
import '../../main.dart'; // 🔥 导入 navigatorKey
|
||||
import 'go_router_refresh_stream.dart';
|
||||
|
||||
GoRouter createRouter(AuthCubit authCubit) {
|
||||
// 调试:打印所有路由
|
||||
debugPrint('🔍 [Router] 开始创建路由配置...');
|
||||
debugPrint(
|
||||
'🔍 [Router] AlarmCenterRoutes.routes 数量: ${AlarmCenterRoutes.routes.length}',
|
||||
);
|
||||
for (var route in AlarmCenterRoutes.routes) {
|
||||
debugPrint('🔍 [Router] 路由: ${route.toString()}');
|
||||
}
|
||||
|
||||
return GoRouter(
|
||||
navigatorKey: navigatorKey, // 🔥 绑定全局 navigatorKey,使异地登录弹窗能获取 context
|
||||
initialLocation: RoutePaths.login,
|
||||
refreshListenable: GoRouterRefreshStream(authCubit.stream),
|
||||
redirect: (context, state) {
|
||||
final loggedIn = authCubit.state is AuthAuthenticated;
|
||||
final loggingIn = state.matchedLocation == RoutePaths.login;
|
||||
final registering = state.matchedLocation == RoutePaths.register;
|
||||
|
||||
// 如果未登录,只允许访问 login 和 register 页面
|
||||
if (!loggedIn && !loggingIn && !registering) {
|
||||
if (!loggedIn && !loggingIn) {
|
||||
return RoutePaths.login;
|
||||
}
|
||||
// 如果已登录,尝试访问 login 或 register,则跳转到首页
|
||||
if (loggedIn && (loggingIn || registering)) {
|
||||
if (loggedIn && loggingIn) {
|
||||
return RoutePaths.home;
|
||||
}
|
||||
return null;
|
||||
@@ -61,23 +43,33 @@ GoRouter createRouter(AuthCubit authCubit) {
|
||||
},
|
||||
),
|
||||
|
||||
// 注册登录注册 路由
|
||||
GoRoute(
|
||||
path: RoutePaths.home,
|
||||
builder: (context, state) => const MainWrapper(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'settings',
|
||||
builder: (context, state) => const TabSettingsPage(),
|
||||
),
|
||||
],
|
||||
path: RoutePaths.login,
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
|
||||
// 1. 外部路由(无底部导航栏)
|
||||
...AuthRoutes.routes,
|
||||
...HomeRoutes.routes,
|
||||
...AiRoutes.routes,
|
||||
...MyRoutes.routes,
|
||||
...AlarmCenterRoutes.routes,
|
||||
...WorkOrderRoutes.routes,
|
||||
|
||||
// 2. 内部路由(带底部导航栏的容器)
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) {
|
||||
// 返回我们定义的 MainWrapper
|
||||
return MainWrapper(navigationShell: navigationShell);
|
||||
},
|
||||
branches: [
|
||||
// 拿一级页面
|
||||
HomeRoutes.branch,
|
||||
|
||||
AiRoutes.branch,
|
||||
|
||||
MyRoutes.branch,
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,6 @@ class RoutePaths {
|
||||
static const djMap = '/home/dj_map';
|
||||
static const productDesc = '/home/productDesc';
|
||||
static const usage = '/home/usage';
|
||||
static const warningCenter = '/home/warning_center';
|
||||
static const settings = '/my/settings';
|
||||
|
||||
static const machineDetails = '/machine_details/machine_details';
|
||||
|
||||
/// 工单相关页面
|
||||
static const workOrder = '/workorder';
|
||||
static const workOrderDetail = '/workorder/detail/:orderId';
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../consts/http_api_consts.dart';
|
||||
|
||||
/// 设备操作权限校验服务
|
||||
/// 接口: GET /iot/device/hasPermission?deviceId=xxx
|
||||
/// 通过条件: code==200 且 data==true,缺一不可
|
||||
/// 安全原则: 仅明确收到 code=200 && data=true 才放行,其余所有情况一律阻止
|
||||
class DevicePermissionService {
|
||||
final Dio _dio;
|
||||
|
||||
DevicePermissionService(this._dio);
|
||||
|
||||
/// 校验当前用户是否有权操作指定设备
|
||||
/// 返回 true 表示有权限,false 表示无权限或校验失败
|
||||
Future<bool> checkPermission(String deviceId) async {
|
||||
debugPrint('🔐 [权限校验] 开始校验 - deviceId: $deviceId');
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
HttpApiConsts.hasPermission,
|
||||
queryParameters: {'deviceId': deviceId},
|
||||
);
|
||||
|
||||
debugPrint('🔐 [权限校验] HTTP响应 - statusCode: ${response.statusCode}');
|
||||
|
||||
// HTTP 层面必须 200
|
||||
if (response.statusCode != 200) {
|
||||
debugPrint('🔐 [权限校验] ❌ HTTP状态码非200: ${response.statusCode}');
|
||||
return false;
|
||||
}
|
||||
|
||||
final body = response.data;
|
||||
debugPrint('🔐 [权限校验] 响应体: $body');
|
||||
|
||||
// body 必须是 Map
|
||||
if (body is! Map<String, dynamic>) {
|
||||
debugPrint('🔐 [权限校验] ❌ 响应体格式异常,非Map类型');
|
||||
return false;
|
||||
}
|
||||
|
||||
final code = body['code'];
|
||||
final data = body['data'];
|
||||
|
||||
debugPrint('🔐 [权限校验] code=$code (type: ${code.runtimeType}), data=$data (type: ${data.runtimeType})');
|
||||
|
||||
// code 必须是 200(兼容 int 和 String)
|
||||
final codeMatch = code == 200 || code.toString() == '200';
|
||||
// data 必须是 true
|
||||
final dataMatch = data == true || data.toString() == 'true';
|
||||
|
||||
if (codeMatch && dataMatch) {
|
||||
debugPrint('🔐 [权限校验] ✅ 校验通过 - code=200, data=true');
|
||||
return true;
|
||||
}
|
||||
|
||||
debugPrint('🔐 [权限校验] ❌ 校验未通过 - codeMatch=$codeMatch, dataMatch=$dataMatch');
|
||||
return false;
|
||||
} on DioException catch (e) {
|
||||
// 网络异常(断网、超时、服务器不可达等)
|
||||
debugPrint('🔐 [权限校验] ❌ DioException: ${e.type} - ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
// 任何未知异常
|
||||
debugPrint('🔐 [权限校验] ❌ 未知异常: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,159 +2,12 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 应用级语义色扩展(浅色/深色各一套,随主题自动切换)
|
||||
///
|
||||
/// 用法:`context.appColors.cardBackground`
|
||||
/// 原理:颜色挂载在 ThemeData.extensions 上,主题切换时组件树自动重建,
|
||||
/// 页面无需写 `isDark ? A : B` 判断
|
||||
@immutable
|
||||
class AppColors extends ThemeExtension<AppColors> {
|
||||
const AppColors({
|
||||
required this.pageBackground,
|
||||
required this.cardBackground,
|
||||
required this.fillBackground,
|
||||
required this.textPrimary,
|
||||
required this.textSecondary,
|
||||
required this.textTertiary,
|
||||
required this.divider,
|
||||
required this.cardShadow,
|
||||
required this.primary,
|
||||
required this.danger,
|
||||
required this.warning,
|
||||
required this.success,
|
||||
});
|
||||
|
||||
/// 页面背景色
|
||||
final Color pageBackground;
|
||||
|
||||
/// 卡片背景色
|
||||
final Color cardBackground;
|
||||
|
||||
/// 卡片内嵌块填充色(输入框、上传按钮等)
|
||||
final Color fillBackground;
|
||||
|
||||
/// 主要文字色
|
||||
final Color textPrimary;
|
||||
|
||||
/// 次要文字色
|
||||
final Color textSecondary;
|
||||
|
||||
/// 辅助文字色(占位、说明)
|
||||
final Color textTertiary;
|
||||
|
||||
/// 分割线/浅色边框
|
||||
final Color divider;
|
||||
|
||||
/// 卡片阴影色
|
||||
final Color cardShadow;
|
||||
|
||||
/// 品牌主色(两种模式下保持一致)
|
||||
final Color primary;
|
||||
|
||||
/// 危险/错误色(两种模式下保持一致)
|
||||
final Color danger;
|
||||
|
||||
/// 警告色(两种模式下保持一致)
|
||||
final Color warning;
|
||||
|
||||
/// 成功/完成色(两种模式下保持一致)
|
||||
final Color success;
|
||||
|
||||
/// 浅色语义色(取自设计稿)
|
||||
static const AppColors light = AppColors(
|
||||
pageBackground: Color(0xFFF5F7FA),
|
||||
cardBackground: Color(0xFFFFFFFF),
|
||||
fillBackground: Color(0xFFF8F9FA),
|
||||
textPrimary: Color(0xFF1D2129),
|
||||
textSecondary: Color(0xFF4E5969),
|
||||
textTertiary: Color(0xFF86909C),
|
||||
divider: Color(0xFFF0F0F0),
|
||||
cardShadow: Color(0x0D000000),
|
||||
primary: Color(0xFF165DFF),
|
||||
danger: Color(0xFFF53F3F),
|
||||
warning: Color(0xFFFF7D00),
|
||||
success: Color(0xFF00B42A),
|
||||
);
|
||||
|
||||
/// 深色语义色(与 AppTheme.darkTheme 的 surface 色保持一致)
|
||||
static const AppColors dark = AppColors(
|
||||
pageBackground: Color(0xFF1C1C1E),
|
||||
cardBackground: Color(0xFF2C2C2E),
|
||||
fillBackground: Color(0xFF3A3A3C),
|
||||
textPrimary: Color(0xFFF2F3F5),
|
||||
textSecondary: Color(0xFFC9CDD4),
|
||||
textTertiary: Color(0xFF86909C),
|
||||
divider: Color(0xFF3A3A3C),
|
||||
cardShadow: Color(0x00000000), // 深色下无阴影,靠色差分层级
|
||||
primary: Color(0xFF165DFF),
|
||||
danger: Color(0xFFF53F3F),
|
||||
warning: Color(0xFFFF7D00),
|
||||
success: Color(0xFF00B42A),
|
||||
);
|
||||
|
||||
@override
|
||||
AppColors copyWith({
|
||||
Color? pageBackground,
|
||||
Color? cardBackground,
|
||||
Color? fillBackground,
|
||||
Color? textPrimary,
|
||||
Color? textSecondary,
|
||||
Color? textTertiary,
|
||||
Color? divider,
|
||||
Color? cardShadow,
|
||||
Color? primary,
|
||||
Color? danger,
|
||||
Color? warning,
|
||||
Color? success,
|
||||
}) {
|
||||
return AppColors(
|
||||
pageBackground: pageBackground ?? this.pageBackground,
|
||||
cardBackground: cardBackground ?? this.cardBackground,
|
||||
fillBackground: fillBackground ?? this.fillBackground,
|
||||
textPrimary: textPrimary ?? this.textPrimary,
|
||||
textSecondary: textSecondary ?? this.textSecondary,
|
||||
textTertiary: textTertiary ?? this.textTertiary,
|
||||
divider: divider ?? this.divider,
|
||||
cardShadow: cardShadow ?? this.cardShadow,
|
||||
primary: primary ?? this.primary,
|
||||
danger: danger ?? this.danger,
|
||||
warning: warning ?? this.warning,
|
||||
success: success ?? this.success,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AppColors lerp(AppColors? other, double t) {
|
||||
if (other == null) return this;
|
||||
return AppColors(
|
||||
pageBackground: Color.lerp(pageBackground, other.pageBackground, t)!,
|
||||
cardBackground: Color.lerp(cardBackground, other.cardBackground, t)!,
|
||||
fillBackground: Color.lerp(fillBackground, other.fillBackground, t)!,
|
||||
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
|
||||
textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
|
||||
textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!,
|
||||
divider: Color.lerp(divider, other.divider, t)!,
|
||||
cardShadow: Color.lerp(cardShadow, other.cardShadow, t)!,
|
||||
primary: Color.lerp(primary, other.primary, t)!,
|
||||
danger: Color.lerp(danger, other.danger, t)!,
|
||||
warning: Color.lerp(warning, other.warning, t)!,
|
||||
success: Color.lerp(success, other.success, t)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 便捷访问扩展:`context.appColors`
|
||||
extension AppColorsX on BuildContext {
|
||||
AppColors get appColors => Theme.of(this).extension<AppColors>()!;
|
||||
}
|
||||
|
||||
class AppTheme {
|
||||
// 基础色值定义
|
||||
static const Color _pureWhite = Color(0xFFFFFFFF);
|
||||
static const Color _offWhite = Color(0xFFfafafc); // 稍微带点灰的白,用于背景增加层次感
|
||||
static const Color _pureBlack = Color(0xFF000000);
|
||||
static const Color _darkGrey = Color(0xFF1C1C1E); // iOS 风格的深灰黑色
|
||||
static const Color _surfaceDark = Color(0xFF2C2C2E); // 深色模式下卡片、弹窗表面色
|
||||
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
@@ -162,9 +15,6 @@ class AppTheme {
|
||||
brightness: Brightness.light,
|
||||
fontFamily: (Platform.isWindows) ? 'Microsoft YaHei' : null,
|
||||
|
||||
// 语义色扩展
|
||||
extensions: const [AppColors.light],
|
||||
|
||||
// 1. 整体背景色
|
||||
scaffoldBackgroundColor: _offWhite,
|
||||
|
||||
@@ -235,84 +85,4 @@ class AppTheme {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 深色主题 (与浅色主题对称的黑白简约风格)
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
fontFamily: (Platform.isWindows) ? 'Microsoft YaHei' : null,
|
||||
|
||||
// 语义色扩展
|
||||
extensions: const [AppColors.dark],
|
||||
|
||||
// 1. 整体背景色 (iOS 风格深灰黑)
|
||||
scaffoldBackgroundColor: _darkGrey,
|
||||
|
||||
// 2. 核心颜色方案 (与浅色主题反色:主色白色)
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: _pureWhite, // 主色设为白色
|
||||
onPrimary: _pureBlack, // 白色背景上的文字设为黑色
|
||||
surface: _surfaceDark, // 卡片、弹窗等表面颜色
|
||||
onSurface: _pureWhite, // 表面上的文字色
|
||||
),
|
||||
|
||||
// 3. 全局按钮主题 (白底黑字)
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _pureWhite,
|
||||
foregroundColor: _pureBlack,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||
),
|
||||
),
|
||||
|
||||
// 4. 填充按钮主题 (常用语 Material 3)
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _pureWhite,
|
||||
foregroundColor: _pureBlack,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 5. AppBar 主题 (深灰黑背景,白色文字)
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: _darkGrey,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: _pureWhite),
|
||||
titleTextStyle: TextStyle(
|
||||
color: _pureWhite,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
|
||||
// 6. 输入框主题 (深色填充,白色边框)
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: _surfaceDark,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.white12),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.white12),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _pureWhite, width: 1.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 全局主题管理 Cubit(仿 LocaleCubit 模式)
|
||||
/// 支持 light / dark / system 三态,持久化到 SharedPreferences
|
||||
class ThemeCubit extends Cubit<ThemeMode> {
|
||||
static const String _themeKey = 'app_theme_mode';
|
||||
|
||||
ThemeCubit() : super(ThemeMode.light) {
|
||||
_loadSavedThemeMode();
|
||||
}
|
||||
|
||||
/// 从本地存储恢复上次保存的主题模式
|
||||
Future<void> _loadSavedThemeMode() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final saved = prefs.getString(_themeKey);
|
||||
emit(_parseThemeMode(saved));
|
||||
} catch (e) {
|
||||
emit(ThemeMode.light);
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置主题模式并持久化
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_themeKey, mode.name);
|
||||
emit(mode);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ThemeCubit] 保存主题设置失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 夜间模式开关(Switch 二态切换,不涉及跟随系统)
|
||||
Future<void> toggleDarkMode(bool enabled) =>
|
||||
setThemeMode(enabled ? ThemeMode.dark : ThemeMode.light);
|
||||
|
||||
/// 当前是否为深色模式
|
||||
bool get isDarkMode => state == ThemeMode.dark;
|
||||
|
||||
/// 将持久化的字符串解析为 ThemeMode
|
||||
ThemeMode _parseThemeMode(String? saved) {
|
||||
switch (saved) {
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
case 'system':
|
||||
return ThemeMode.system;
|
||||
default:
|
||||
return ThemeMode.light;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'version_check_service.dart';
|
||||
import 'update_state.dart';
|
||||
|
||||
/// 更新 Cubit
|
||||
class UpdateCubit extends Cubit<UpdateState> {
|
||||
final VersionCheckService _versionService;
|
||||
final Logger _logger;
|
||||
|
||||
UpdateCubit(this._versionService)
|
||||
: _logger = Logger(),
|
||||
super(UpdateInitial());
|
||||
|
||||
/// 检查版本更新
|
||||
Future<void> checkUpdate() async {
|
||||
emit(UpdateChecking());
|
||||
|
||||
try {
|
||||
// 从 pubspec.yaml 读取版本信息
|
||||
final String versionString = await rootBundle.loadString('pubspec.yaml');
|
||||
final versionMatch = RegExp(r'version:\s*(\d+\.\d+\.\d+)\+(\d+)').firstMatch(versionString);
|
||||
|
||||
final currentVersion = versionMatch?.group(1) ?? '1.0.0';
|
||||
final currentVersionCode = int.tryParse(versionMatch?.group(2) ?? '1') ?? 1;
|
||||
|
||||
_logger.i('📱 当前应用版本: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 检查更新
|
||||
final versionInfo = await _versionService.checkUpdate(
|
||||
currentVersion: currentVersion,
|
||||
currentVersionCode: currentVersionCode,
|
||||
);
|
||||
|
||||
if (versionInfo == null) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
emit(UpdateUpToDate());
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.i('🔄 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('📦 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
|
||||
emit(UpdateAvailable(versionInfo));
|
||||
} catch (e) {
|
||||
_logger.e('❌ 检查更新失败: $e');
|
||||
emit(UpdateFailure('检查更新失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用差量补丁
|
||||
Future<void> applyPatch(String patchUrl, String version, int targetVersionCode, {String? md5}) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: true));
|
||||
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载并应用补丁');
|
||||
|
||||
final success = await _versionService.applyPatch(
|
||||
patchUrl: patchUrl,
|
||||
version: version,
|
||||
targetVersionCode: targetVersionCode,
|
||||
md5: md5,
|
||||
onProgress: (progress) {
|
||||
// 🔥 实时更新下载进度
|
||||
emit(UpdateDownloading(progress, isPatch: true));
|
||||
},
|
||||
);
|
||||
|
||||
if (success) {
|
||||
emit(const UpdateInstalling(isPatch: true));
|
||||
emit(UpdateSuccess());
|
||||
} else {
|
||||
emit(const UpdateFailure('补丁应用失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 应用补丁异常: $e');
|
||||
emit(UpdateFailure('应用补丁失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动下载并安装 APK(App 内下载)
|
||||
Future<void> downloadAndInstallApk(String apkUrl) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: false));
|
||||
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载完整 APK');
|
||||
|
||||
final apkPath = await _versionService.downloadApk(apkUrl, (progress) {
|
||||
emit(UpdateDownloading(progress, isPatch: false));
|
||||
});
|
||||
|
||||
if (apkPath != null) {
|
||||
_logger.i('✅ APK 下载完成: $apkPath');
|
||||
// 🔥 关键:先清理版本号记录,并等待用户看到提示
|
||||
emit(const UpdateClearingVersion());
|
||||
_logger.i('🗑️ 开始清除补丁版本记录');
|
||||
await _versionService.clearPatchVersionInfo();
|
||||
_logger.i('✅ 补丁版本记录已清除');
|
||||
|
||||
// 🔥 短暂延迟,让用户看到清理完成的提示
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
// 🔥 直接调起系统安装页面,不再等待用户点击
|
||||
_logger.i('📦 自动调起系统安装页面');
|
||||
await triggerInstall(apkPath);
|
||||
} else {
|
||||
emit(const UpdateFailure('APK 下载失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 下载安装 APK 异常: $e');
|
||||
emit(UpdateFailure('下载安装 APK 失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 触发系统安装页面
|
||||
Future<void> triggerInstall(String apkPath) async {
|
||||
emit(const UpdateInstalling(isPatch: false));
|
||||
|
||||
try {
|
||||
_logger.i('📦 开始调起系统安装页面');
|
||||
_logger.i('📁 APK 路径: $apkPath');
|
||||
|
||||
// 🔥 验证文件是否存在
|
||||
final file = File(apkPath);
|
||||
if (!await file.exists()) {
|
||||
_logger.e('❌ APK 文件不存在: $apkPath');
|
||||
emit(UpdateFailure('APK 文件不存在,请重新下载'));
|
||||
return;
|
||||
}
|
||||
|
||||
final fileSize = await file.length();
|
||||
_logger.i('📏 APK 文件大小: ${fileSize / 1024 / 1024} MB');
|
||||
|
||||
// 🔥 关键修复:将APK复制到外部存储,避免某些Android版本的安装限制
|
||||
final externalDir = await getExternalStorageDirectory();
|
||||
String finalApkPath = apkPath;
|
||||
|
||||
if (externalDir != null) {
|
||||
final publicApkPath = '${externalDir.path}/app-update-final.apk';
|
||||
_logger.i('📋 复制APK到外部存储: $publicApkPath');
|
||||
|
||||
try {
|
||||
await file.copy(publicApkPath);
|
||||
finalApkPath = publicApkPath;
|
||||
_logger.i('✅ APK复制成功');
|
||||
} catch (e) {
|
||||
_logger.w('⚠️ 复制APK失败,使用原路径: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开 APK 文件,触发系统安装界面
|
||||
_logger.i('🚀 调用 OpenFile.open...');
|
||||
_logger.i('🚀 最终APK路径: $finalApkPath');
|
||||
final result = await OpenFile.open(finalApkPath);
|
||||
_logger.i('📦 安装结果: ${result.message} (type: ${result.type})');
|
||||
|
||||
// 如果用户取消安装,显示提示信息并保留文件路径
|
||||
if (result.type != ResultType.done) {
|
||||
_logger.w('⚠️ 用户取消了安装或安装失败 (type: ${result.type})');
|
||||
|
||||
// 🔥 如果是权限问题,提供更友好的提示
|
||||
String errorMsg = '您取消了安装';
|
||||
if (result.type == ResultType.noAppToOpen) {
|
||||
errorMsg = '没有找到可以安装 APK 的应用,请检查系统设置';
|
||||
} else if (result.type == ResultType.permissionDenied) {
|
||||
errorMsg = '没有安装权限,请在系统设置中允许"安装未知应用"';
|
||||
} else if (result.type == ResultType.error) {
|
||||
errorMsg = '安装出错: ${result.message}';
|
||||
}
|
||||
|
||||
emit(UpdateFailure('$errorMsg,APK 文件位于: $finalApkPath'));
|
||||
} else {
|
||||
// 安装成功,关闭对话框
|
||||
emit(UpdateSuccess());
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 调起安装页面异常: $e');
|
||||
emit(UpdateFailure('调起安装页面失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 整包更新:浏览器下载 + 引导弹窗
|
||||
Future<void> fullApkUpdateWithBrowser(String apkUrl) async {
|
||||
try {
|
||||
_logger.i('🌐 整包更新:打开浏览器下载');
|
||||
|
||||
final uri = Uri.parse(apkUrl);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
_logger.i('✅ 已打开浏览器下载');
|
||||
|
||||
// 🔥 整包更新后,立即清除补丁版本记录(避免死循环)
|
||||
await _versionService.clearPatchVersionInfo();
|
||||
_logger.i('🗑️ 已清除补丁版本记录');
|
||||
|
||||
emit(UpdateSuccess());
|
||||
} else {
|
||||
_logger.e('❌ 无法打开浏览器');
|
||||
emit(const UpdateFailure('无法打开浏览器'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 打开浏览器异常: $e');
|
||||
emit(UpdateFailure('打开浏览器失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消更新
|
||||
void cancelUpdate() {
|
||||
_logger.i('❌ 用户取消更新');
|
||||
emit(UpdateInitial());
|
||||
}
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'update_cubit.dart';
|
||||
import 'update_state.dart';
|
||||
import 'version_check_service.dart';
|
||||
|
||||
/// 更新对话框(带完整流程提示)
|
||||
class UpdateDialog extends StatelessWidget {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const UpdateDialog({super.key, required this.versionInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<UpdateCubit, UpdateState>(
|
||||
listener: (context, state) {
|
||||
if (state is UpdateSuccess) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (versionInfo.updateType == 'patch') {
|
||||
_showRestartDialog(context);
|
||||
}
|
||||
} else if (state is UpdateFailure) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
String errorMessage = state.error;
|
||||
bool showApkPath = false;
|
||||
String? apkPath;
|
||||
|
||||
if (errorMessage.contains('APK 文件位于:')) {
|
||||
final parts = errorMessage.split('APK 文件位于:');
|
||||
if (parts.length > 1) {
|
||||
apkPath = parts[1].trim();
|
||||
showApkPath = true;
|
||||
errorMessage = '您取消了安装';
|
||||
}
|
||||
}
|
||||
|
||||
_showErrorDialog(context, errorMessage, showApkPath: showApkPath, apkPath: apkPath);
|
||||
}
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('发现新版本'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('版本号: ${versionInfo.version}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('更新类型: ${versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}'),
|
||||
const SizedBox(height: 8),
|
||||
if (versionInfo.forceUpdate)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'⚠️ 强制更新',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(versionInfo.updateDesc.isEmpty ? '优化用户体验,修复已知问题' : versionInfo.updateDesc),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (!versionInfo.forceUpdate)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<UpdateCubit>().cancelUpdate();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
BlocBuilder<UpdateCubit, UpdateState>(
|
||||
builder: (context, state) {
|
||||
if (state is UpdateDownloading) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
state.isPatch ? '⬇️ 正在下载补丁...' : '⬇️ 正在下载 APK...',
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: state.progress,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
Text('${(state.progress * 100).toInt()}%'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is UpdateClearingVersion) {
|
||||
return const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('✅ 版本号清理完成', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 4),
|
||||
Text('正在调起安装页面...', style: TextStyle(fontSize: 12)),
|
||||
SizedBox(height: 8),
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
);
|
||||
} else if (state is UpdateReadyToInstall) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.read<UpdateCubit>().triggerInstall(state.apkPath);
|
||||
},
|
||||
icon: const Icon(Icons.install_mobile),
|
||||
label: const Text('去安装'),
|
||||
);
|
||||
} else if (state is UpdateInstalling) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('📦 正在打开安装页面...', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('请在系统安装界面确认安装', style: TextStyle(fontSize: 12)),
|
||||
const SizedBox(height: 8),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
if (versionInfo.updateType == 'patch' && versionInfo.patchUrl != null) {
|
||||
context.read<UpdateCubit>().applyPatch(
|
||||
versionInfo.patchUrl!,
|
||||
versionInfo.version,
|
||||
versionInfo.versionCode,
|
||||
md5: versionInfo.patchMd5,
|
||||
);
|
||||
} else if (versionInfo.apkUrl != null) {
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(versionInfo.apkUrl!);
|
||||
}
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRestartDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('更新完成'),
|
||||
content: const Text('差量更新已应用,需要重启应用才能生效。\n是否立即重启?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
SystemNavigator.pop();
|
||||
},
|
||||
child: const Text('立即重启'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorDialog(BuildContext context, String message, {bool showApkPath = false, String? apkPath}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('更新提示'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(message),
|
||||
if (showApkPath && apkPath != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'📁 APK 文件位置',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
apkPath,
|
||||
style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'您可以到该目录手动点击 APK 文件进行安装',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示整包更新提示(在浏览器中下载)
|
||||
class FullApkUpdateDialog extends StatelessWidget {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const FullApkUpdateDialog({super.key, required this.versionInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('发现新版本'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('版本号: ${versionInfo.version}'),
|
||||
const SizedBox(height: 8),
|
||||
const Text('更新类型: 整包更新'),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade200),
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'📱 整包更新步骤',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'1️⃣ 点击"立即下载"打开浏览器\n'
|
||||
'2️⃣ 等待 APK 下载完成\n'
|
||||
'3️⃣ 手动安装新版本的 APK\n'
|
||||
'4️⃣ 安装完成后,卸载旧版本 APP\n'
|
||||
'5️⃣ 重新打开新版本 APP',
|
||||
style: TextStyle(fontSize: 13, height: 1.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (versionInfo.updateDesc.isNotEmpty) ...[
|
||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(versionInfo.updateDesc),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (!versionInfo.forceUpdate)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<UpdateCubit>().cancelUpdate();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (versionInfo.apkUrl != null) {
|
||||
// 🔥 调用新方法:浏览器下载 + 清除补丁记录
|
||||
context.read<UpdateCubit>().fullApkUpdateWithBrowser(versionInfo.apkUrl!);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// 显示后续引导弹窗
|
||||
_showManualInstallGuide(context);
|
||||
}
|
||||
},
|
||||
child: const Text('立即下载'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示手动安装引导弹窗
|
||||
void _showManualInstallGuide(BuildContext context) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('🔔 重要提示'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'请在浏览器中完成以下操作:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('✅ 等待 APK 下载完成'),
|
||||
const SizedBox(height: 8),
|
||||
const Text('✅ 点击 APK 文件进行安装'),
|
||||
const SizedBox(height: 8),
|
||||
const Text('✅ 安装完成后,卸载当前旧版本'),
|
||||
const SizedBox(height: 8),
|
||||
const Text('✅ 重新打开新版本 APP'),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: const Text(
|
||||
'⚠️ 注意:必须先卸载旧版本,否则无法正常使用新功能!',
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: const Text('我知道了'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'version_check_service.dart';
|
||||
|
||||
/// 更新状态基类
|
||||
abstract class UpdateState {
|
||||
const UpdateState();
|
||||
}
|
||||
|
||||
/// 初始状态
|
||||
class UpdateInitial extends UpdateState {
|
||||
const UpdateInitial();
|
||||
}
|
||||
|
||||
/// 检查中
|
||||
class UpdateChecking extends UpdateState {
|
||||
const UpdateChecking();
|
||||
}
|
||||
|
||||
/// 发现新版本
|
||||
class UpdateAvailable extends UpdateState {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const UpdateAvailable(this.versionInfo);
|
||||
}
|
||||
|
||||
/// 下载中
|
||||
class UpdateDownloading extends UpdateState {
|
||||
final double progress;
|
||||
final bool isPatch; // true=差量补丁, false=完整APK
|
||||
|
||||
const UpdateDownloading(this.progress, {required this.isPatch});
|
||||
}
|
||||
|
||||
/// 清理版本号中
|
||||
class UpdateClearingVersion extends UpdateState {
|
||||
const UpdateClearingVersion();
|
||||
}
|
||||
|
||||
/// 准备安装
|
||||
class UpdateReadyToInstall extends UpdateState {
|
||||
final String apkPath;
|
||||
|
||||
const UpdateReadyToInstall(this.apkPath);
|
||||
}
|
||||
|
||||
/// 安装中
|
||||
class UpdateInstalling extends UpdateState {
|
||||
final bool isPatch;
|
||||
|
||||
const UpdateInstalling({required this.isPatch});
|
||||
}
|
||||
|
||||
/// 更新成功
|
||||
class UpdateSuccess extends UpdateState {
|
||||
const UpdateSuccess();
|
||||
}
|
||||
|
||||
/// 已是最新版本
|
||||
class UpdateUpToDate extends UpdateState {
|
||||
const UpdateUpToDate();
|
||||
}
|
||||
|
||||
/// 更新失败
|
||||
class UpdateFailure extends UpdateState {
|
||||
final String error;
|
||||
|
||||
const UpdateFailure(this.error);
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_patcher/flutter_patcher.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
/// 应用版本信息模型
|
||||
class AppVersionInfo {
|
||||
final String version;
|
||||
final int versionCode;
|
||||
final String updateType;
|
||||
final bool forceUpdate;
|
||||
final String updateDesc;
|
||||
final String? patchUrl;
|
||||
final String? patchMd5;
|
||||
final String? apkUrl;
|
||||
final String? apkMd5;
|
||||
|
||||
AppVersionInfo({
|
||||
required this.version,
|
||||
required this.versionCode,
|
||||
required this.updateType,
|
||||
required this.forceUpdate,
|
||||
required this.updateDesc,
|
||||
this.patchUrl,
|
||||
this.patchMd5,
|
||||
this.apkUrl,
|
||||
this.apkMd5,
|
||||
});
|
||||
|
||||
factory AppVersionInfo.fromJson(Map<String, dynamic> json) {
|
||||
final patchData = json['patch'] as Map<String, dynamic>?;
|
||||
final fullApkData = json['fullApk'] as Map<String, dynamic>?;
|
||||
|
||||
return AppVersionInfo(
|
||||
version: json['version'] ?? '',
|
||||
versionCode: json['versionCode'] ?? 0,
|
||||
updateType: json['updateType'] ?? 'full',
|
||||
forceUpdate: json['forceUpdate'] ?? false,
|
||||
updateDesc: json['updateDesc'] ?? '',
|
||||
patchUrl: patchData?['patchUrl'] as String?,
|
||||
patchMd5: patchData?['md5'] as String?,
|
||||
apkUrl: fullApkData?['apkUrl'] as String?,
|
||||
apkMd5: fullApkData?['md5'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本检查服务
|
||||
class VersionCheckService {
|
||||
static final VersionCheckService _instance = VersionCheckService._internal();
|
||||
factory VersionCheckService() => _instance;
|
||||
VersionCheckService._internal();
|
||||
|
||||
final Logger _logger = Logger();
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
));
|
||||
|
||||
static const String apiUrl = 'http://8.159.134.0:8012/api/update/check';
|
||||
static const String _patchVersionFileName = '.patch_version';
|
||||
|
||||
/// 检查版本更新
|
||||
Future<AppVersionInfo?> checkUpdate({
|
||||
required String currentVersion,
|
||||
required int currentVersionCode,
|
||||
}) async {
|
||||
try {
|
||||
_logger.i('🔍 检查版本更新');
|
||||
_logger.i('📱 APK 自带版本: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 从文件读取已应用的补丁版本
|
||||
final patchInfo = await _loadPatchVersionInfo();
|
||||
final appliedPatchVersion = patchInfo['version'] as String?;
|
||||
final appliedPatchVersionCode = patchInfo['versionCode'] as int?;
|
||||
|
||||
_logger.i('📂 读取到的补丁版本: $appliedPatchVersion ($appliedPatchVersionCode)');
|
||||
|
||||
// 如果打过补丁,使用补丁的版本;否则使用 APK 自带版本
|
||||
final requestVersion = appliedPatchVersion ?? currentVersion;
|
||||
final requestVersionCode = appliedPatchVersionCode ?? currentVersionCode;
|
||||
|
||||
_logger.i('🚀 请求后端版本: $requestVersion ($requestVersionCode)');
|
||||
|
||||
// 🔥 打印完整的请求参数
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
_logger.i('📤 App 发送的请求参数:');
|
||||
_logger.i(' URL: $apiUrl');
|
||||
_logger.i(' version: $requestVersion');
|
||||
_logger.i(' versionCode: $requestVersionCode');
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
final response = await _dio.get(
|
||||
apiUrl,
|
||||
queryParameters: {
|
||||
'version': requestVersion,
|
||||
'versionCode': requestVersionCode,
|
||||
},
|
||||
);
|
||||
|
||||
// 🔥 打印完整的后端返回数据
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
_logger.i('📥 后端返回的完整数据:');
|
||||
_logger.i(' statusCode: ${response.statusCode}');
|
||||
_logger.i(' 原始响应: ${response.data}');
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data['data'];
|
||||
|
||||
if (data['hasUpdate'] == false) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
return null;
|
||||
}
|
||||
|
||||
final versionInfo = AppVersionInfo.fromJson(data);
|
||||
|
||||
// 🔥 关键修复:无论更新类型是什么,只要本地已应用此版本,就跳过
|
||||
if (appliedPatchVersion != null && appliedPatchVersion == versionInfo.version) {
|
||||
_logger.i('✅ 本地已应用版本 ${versionInfo.version},跳过更新(updateType: ${versionInfo.updateType})');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 🔥 额外检查:如果本地补丁版本code >= 服务端返回的versionCode,也跳过
|
||||
if (appliedPatchVersionCode != null && appliedPatchVersionCode >= versionInfo.versionCode) {
|
||||
_logger.i('✅ 本地补丁版本code ($appliedPatchVersionCode) >= 服务端versionCode (${versionInfo.versionCode}),跳过更新');
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.i('✅ 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('🔄 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e, stack) {
|
||||
_logger.e('❌ 版本检查失败: $e');
|
||||
_logger.e('❌ 堆栈信息: $stack');
|
||||
throw Exception('网络请求失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并应用差量补丁
|
||||
Future<bool> applyPatch({
|
||||
required String patchUrl,
|
||||
required String version,
|
||||
required int targetVersionCode,
|
||||
String? md5,
|
||||
Function(double)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载补丁: $patchUrl');
|
||||
_logger.i('🎯 目标版本: $version ($targetVersionCode)');
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final patchPath = '${tempDir.path}/update_patch.so';
|
||||
|
||||
await _dio.download(
|
||||
patchUrl,
|
||||
patchPath,
|
||||
onReceiveProgress: (received, total) {
|
||||
if (total != -1 && onProgress != null) {
|
||||
onProgress(received / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_logger.i('✅ 补丁下载完成,开始应用...');
|
||||
|
||||
final result = await FlutterPatcher.applyPatch(
|
||||
PatchInfo(
|
||||
version: version,
|
||||
patchUrl: 'file://$patchPath',
|
||||
targetVersionCode: targetVersionCode,
|
||||
md5: md5 ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
_logger.i('✅ 补丁应用成功');
|
||||
// 保存补丁版本号到文件
|
||||
await _savePatchVersionInfo(version, targetVersionCode);
|
||||
|
||||
// 验证保存
|
||||
final verifyInfo = await _loadPatchVersionInfo();
|
||||
_logger.i('💾 已保存并验证: ${verifyInfo['version']} (${verifyInfo['versionCode']})');
|
||||
} else {
|
||||
_logger.e('❌ 补丁应用失败: ${result.error}');
|
||||
}
|
||||
|
||||
return result.ok;
|
||||
} catch (e) {
|
||||
_logger.e('❌ 应用补丁异常: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载完整 APK
|
||||
Future<String?> downloadApk(String apkUrl, Function(double) onProgress) async {
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载 APK: $apkUrl');
|
||||
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
final apkPath = '${cacheDir.path}/app-update.apk';
|
||||
|
||||
_logger.i('📁 APK 下载路径: $apkPath');
|
||||
|
||||
await _dio.download(
|
||||
apkUrl,
|
||||
apkPath,
|
||||
onReceiveProgress: (received, total) {
|
||||
if (total != -1) {
|
||||
onProgress(received / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_logger.i('✅ APK 下载完成: $apkPath');
|
||||
return apkPath;
|
||||
} catch (e) {
|
||||
_logger.e('❌ 下载 APK 失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 回滚到内置版本
|
||||
Future<void> rollback() async {
|
||||
try {
|
||||
_logger.i('🔄 执行回滚操作');
|
||||
await FlutterPatcher.rollback();
|
||||
_logger.i('✅ 回滚成功');
|
||||
} catch (e) {
|
||||
_logger.e('❌ 回滚失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除补丁版本信息(整包更新后调用)
|
||||
Future<void> clearPatchVersionInfo() async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
_logger.i('🗑️ 已清除补丁版本记录');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 清除补丁版本信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存补丁版本信息到文件
|
||||
Future<void> _savePatchVersionInfo(String version, int versionCode) async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
final content = '$version|$versionCode';
|
||||
await file.writeAsString(content);
|
||||
|
||||
// 🔥 立即验证写入
|
||||
final verifyContent = await file.readAsString();
|
||||
_logger.i('💾 补丁版本已保存到: ${file.path}');
|
||||
_logger.i('💾 写入内容: $content');
|
||||
_logger.i('💾 验证读取: $verifyContent');
|
||||
_logger.i('💾 文件存在: ${await file.exists()}');
|
||||
} catch (e) {
|
||||
_logger.e('❌ 保存补丁版本信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从文件加载补丁版本信息
|
||||
Future<Map<String, dynamic>> _loadPatchVersionInfo() async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
|
||||
_logger.i('📂 尝试加载补丁版本文件: ${file.path}');
|
||||
_logger.i('📂 文件存在: ${await file.exists()}');
|
||||
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
_logger.i('📂 文件内容: $content');
|
||||
|
||||
final parts = content.split('|');
|
||||
if (parts.length == 2) {
|
||||
final version = parts[0];
|
||||
final versionCode = int.tryParse(parts[1]);
|
||||
if (versionCode != null) {
|
||||
_logger.i('📂 解析成功: version=$version, versionCode=$versionCode');
|
||||
return {'version': version, 'versionCode': versionCode};
|
||||
} else {
|
||||
_logger.e('📂 versionCode 解析失败: ${parts[1]}');
|
||||
}
|
||||
} else {
|
||||
_logger.e('📂 文件格式错误,parts.length=${parts.length}');
|
||||
}
|
||||
}
|
||||
|
||||
_logger.i('📂 未找到有效的补丁版本信息');
|
||||
return {'version': null, 'versionCode': null};
|
||||
} catch (e) {
|
||||
_logger.e('❌ 加载补丁版本信息失败: $e');
|
||||
return {'version': null, 'versionCode': null};
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/core/utils/json_safe.dart
Normal file
32
lib/core/utils/json_safe.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
class JsonSafe {
|
||||
static String asString(dynamic value, [String fallback = '']) {
|
||||
if (value is String) return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static String? asStringOrNull(dynamic value) {
|
||||
if (value is String) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
static int asInt(dynamic value, [int fallback = 0]) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static double asDouble(dynamic value, [double fallback = 0.0]) {
|
||||
if (value is double) return value;
|
||||
if (value is num) return value.toDouble();
|
||||
if (value is String) return double.tryParse(value) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static bool asBool(dynamic value, [bool fallback = false]) {
|
||||
if (value is bool) return value;
|
||||
if (value is int) return value != 0;
|
||||
if (value is String) return value.toLowerCase() == 'true';
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:maibu_satabot_v2/core/utils/json_safe.dart';
|
||||
|
||||
class Session {
|
||||
final String sessId;
|
||||
final String title;
|
||||
@@ -6,8 +8,8 @@ class Session {
|
||||
|
||||
factory Session.fromJson(Map<String, dynamic> json) {
|
||||
return Session(
|
||||
sessId: json['sessId'],
|
||||
title: json['title'],
|
||||
sessId: JsonSafe.asString(json['sessId']),
|
||||
title: JsonSafe.asString(json['title']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,48 +60,48 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
|
||||
debugPrint('✅ sendAuthPacket[AuthTcp] 认证包已发送');
|
||||
|
||||
// 获取用户设备列表
|
||||
// try {
|
||||
// // 1. 获取 Either 结果
|
||||
// final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
|
||||
//
|
||||
// // 2. 使用 fold 解包 Either
|
||||
// // left: 处理错误情况 (DeviceFailure)
|
||||
// // right: 处理成功情况 (List<DeviceEntity>)
|
||||
// await eitherResult.fold(
|
||||
// (failure) {
|
||||
// // 处理失败:打印日志或抛出异常
|
||||
// debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure');
|
||||
// throw Exception('获取设备列表失败:$failure');
|
||||
// },
|
||||
// (devices) async {
|
||||
// // 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
// if (devices.isEmpty) {
|
||||
// debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 取第一个设备
|
||||
// final DeviceEntity targetDevice = devices.first;
|
||||
// debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
//
|
||||
// // 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
|
||||
// final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
//
|
||||
// await switchResult.fold(
|
||||
// (failure) {
|
||||
// debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure');
|
||||
// throw Exception('切换设备失败:$failure');
|
||||
// },
|
||||
// (success) {
|
||||
// debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// } catch (e) {
|
||||
// debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e');
|
||||
// rethrow;
|
||||
// }
|
||||
try {
|
||||
// 1. 获取 Either 结果
|
||||
final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
|
||||
|
||||
// 2. 使用 fold 解包 Either
|
||||
// left: 处理错误情况 (DeviceFailure)
|
||||
// right: 处理成功情况 (List<DeviceEntity>)
|
||||
await eitherResult.fold(
|
||||
(failure) {
|
||||
// 处理失败:打印日志或抛出异常
|
||||
debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure');
|
||||
throw Exception('获取设备列表失败:$failure');
|
||||
},
|
||||
(devices) async {
|
||||
// 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
if (devices.isEmpty) {
|
||||
debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个设备
|
||||
final DeviceEntity targetDevice = devices.first;
|
||||
debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
|
||||
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
|
||||
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
|
||||
await switchResult.fold(
|
||||
(failure) {
|
||||
debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure');
|
||||
throw Exception('切换设备失败:$failure');
|
||||
},
|
||||
(success) {
|
||||
debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:maibu_satabot_v2/core/data/base_model.dart';
|
||||
import 'package:maibu_satabot_v2/core/utils/json_safe.dart';
|
||||
|
||||
import '../../../../core/domain/entities/user_entity.dart';
|
||||
|
||||
@@ -8,26 +9,20 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
required super.username,
|
||||
required super.nickname,
|
||||
required super.token,
|
||||
required super.orgId,
|
||||
super.avatar,
|
||||
super.email,
|
||||
super.phone,
|
||||
super.roleKey,
|
||||
super.siteId,
|
||||
});
|
||||
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
userId: json['userId']?.toString() ?? '',
|
||||
username: json['username'] ?? '',
|
||||
nickname: json['nickName'] ?? '',
|
||||
token: json['token'] ?? '',
|
||||
orgId: (json['orgId'] as num?)?.toInt() ?? 0,
|
||||
avatar: json['avatar'],
|
||||
email: json['email'],
|
||||
phone: json['phone'],
|
||||
roleKey: json['roleKey'],
|
||||
siteId: (json['siteId'] as num?)?.toInt(),
|
||||
userId: JsonSafe.asString(json['userId']),
|
||||
username: JsonSafe.asString(json['username']),
|
||||
nickname: JsonSafe.asString(json['nickName']),
|
||||
token: JsonSafe.asString(json['token']),
|
||||
avatar: JsonSafe.asStringOrNull(json['avatar']),
|
||||
email: JsonSafe.asStringOrNull(json['email']),
|
||||
phone: JsonSafe.asStringOrNull(json['phone']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,12 +32,9 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
'username': username,
|
||||
'nickName': nickname,
|
||||
'token': token,
|
||||
'orgId': orgId,
|
||||
'avatar': avatar,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'roleKey': roleKey,
|
||||
'siteId': siteId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,12 +44,9 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
username: username,
|
||||
nickname: nickname,
|
||||
token: token,
|
||||
orgId: orgId,
|
||||
avatar: avatar,
|
||||
email: email,
|
||||
phone: phone,
|
||||
roleKey: roleKey,
|
||||
siteId: siteId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,12 +56,9 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
username: entity.username,
|
||||
nickname: entity.nickname,
|
||||
token: entity.token,
|
||||
orgId: entity.orgId,
|
||||
avatar: entity.avatar,
|
||||
email: entity.email,
|
||||
phone: entity.phone,
|
||||
roleKey: entity.roleKey,
|
||||
siteId: entity.siteId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user