优化+摄像头+扫码绑定
This commit is contained in:
61
_fix_dup.py
Normal file
61
_fix_dup.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# 删除重复的 failMsg 声明和重复的检查逻辑 (行2402-2411,0索引为2401-2410)
|
||||||
|
# 但行号可能因之前的编辑有偏移,用内容查找
|
||||||
|
del_start = None
|
||||||
|
del_end = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'debugPrint' in line and '创建设备任务失败' in line and 'failure' in line:
|
||||||
|
# 检查下一行是否是重复的 final failMsg
|
||||||
|
if i+1 < len(lines) and 'final failMsg = failure.toString();' in lines[i+1]:
|
||||||
|
# 找到了重复的起点
|
||||||
|
del_start = i # debugPrint 行
|
||||||
|
# 找到这个重复块的结束:下一个 return; 之后的 },
|
||||||
|
for j in range(i+2, len(lines)):
|
||||||
|
if '// 其他失败' in lines[j] or (lines[j].strip() == 'return;' and 'taskCubit.clearCurrentTask();' in lines[j-1]):
|
||||||
|
# 更精确:找到 " return;" 且前一行是 taskCubit.clearCurrentTask
|
||||||
|
if lines[j].strip() == 'return;':
|
||||||
|
del_end = j + 1 # 包含 return; 行
|
||||||
|
break
|
||||||
|
if del_end is None:
|
||||||
|
# 备选:找到下一个 ' },' (fold 成功回调的开始)
|
||||||
|
for j in range(i+2, len(lines)):
|
||||||
|
if lines[j].strip().startswith('},') and 'taskId' in lines[j+1]:
|
||||||
|
del_end = j
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
if del_start is not None and del_end is not None:
|
||||||
|
print(f'Deleting lines {del_start+1} to {del_end}')
|
||||||
|
for k in range(del_start, del_end):
|
||||||
|
print(f' DEL: L{k+1}: {lines[k].rstrip()}')
|
||||||
|
del lines[del_start:del_end]
|
||||||
|
else:
|
||||||
|
print('Could not find duplicate block, checking for alternate pattern...')
|
||||||
|
# 备选:直接搜索两个 final failMsg 行
|
||||||
|
failmsg_lines = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'final failMsg = failure.toString();' in line:
|
||||||
|
failmsg_lines.append(i)
|
||||||
|
print(f'failMsg lines: {[l+1 for l in failmsg_lines]}')
|
||||||
|
if len(failmsg_lines) >= 2:
|
||||||
|
# 删除第二个 failMsg 及它所属的整个重复检查块
|
||||||
|
second = failmsg_lines[1]
|
||||||
|
# 从第二个 failMsg 前一行 (debugPrint) 开始删
|
||||||
|
start = second - 1
|
||||||
|
# 找到这个 block 的结束
|
||||||
|
end = second + 8 # 大约8行后是这个重复块的结束
|
||||||
|
print(f'Alt: deleting lines {start+1} to {end}')
|
||||||
|
for k in range(start, min(end, len(lines))):
|
||||||
|
print(f' DEL: L{k+1}: {lines[k].rstrip()}')
|
||||||
|
del lines[start:end]
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print('DONE')
|
||||||
92
_fix_flow.py
Normal file
92
_fix_flow.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Fix 1: Add var failed = false and if(failed) return, plus else branch for needRequery
|
||||||
|
old_block = """ var needRequery = false;
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
final failMsg = failure.toString();
|
||||||
|
if (failMsg.contains('存在任务') || failMsg.contains('already exists')) {
|
||||||
|
needRequery = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_showPageToast(message: '作业启动失败', type: ToastType.error);
|
||||||
|
taskCubit.clearCurrentTask();
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
(taskId) {
|
||||||
|
taskCubit.updateCurrentTaskId(taskId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (needRequery) {
|
||||||
|
await taskCubit.fetchAndFilterTask(deviceId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
final existingTasks = taskCubit.state.activeTasks;
|
||||||
|
if (existingTasks.isNotEmpty) {
|
||||||
|
taskCubit.selectTask(existingTasks.first);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
new_block = """ var needRequery = false;
|
||||||
|
var failed = false;
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
final failMsg = failure.toString();
|
||||||
|
if (failMsg.contains('存在任务') || failMsg.contains('already exists')) {
|
||||||
|
needRequery = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
failed = true;
|
||||||
|
debugPrint('[开始作业] 创建失败: $failMsg');
|
||||||
|
_showPageToast(message: '作业启动失败', type: ToastType.error);
|
||||||
|
taskCubit.clearCurrentTask();
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
(taskId) {
|
||||||
|
taskCubit.updateCurrentTaskId(taskId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failed) return;
|
||||||
|
if (needRequery) {
|
||||||
|
await taskCubit.fetchAndFilterTask(deviceId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
final existingTasks = taskCubit.state.activeTasks;
|
||||||
|
if (existingTasks.isNotEmpty) {
|
||||||
|
taskCubit.selectTask(existingTasks.first);
|
||||||
|
} else {
|
||||||
|
debugPrint('[开始作业] needRequery 后仍无活跃任务');
|
||||||
|
_showPageToast(message: '未找到活跃任务', type: ToastType.warn);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
if old_block in content:
|
||||||
|
content = content.replace(old_block, new_block)
|
||||||
|
print("Block replaced successfully")
|
||||||
|
else:
|
||||||
|
print("Block NOT FOUND in file!")
|
||||||
|
# Try tab indentation
|
||||||
|
old_block_tabs = old_block.replace(' ', '\t\t\t\t')
|
||||||
|
if old_block_tabs in content:
|
||||||
|
content = content.replace(old_block_tabs, new_block.replace(' ', '\t\t\t\t'))
|
||||||
|
print("Block replaced with TABS")
|
||||||
|
else:
|
||||||
|
# Find the unique line
|
||||||
|
for i, line in enumerate(content.split('\n')):
|
||||||
|
if 'var needRequery = false;' in line and 'result.fold(' in content.split('\n')[i+1]:
|
||||||
|
print(f"Found at line {i+1}: {repr(line)}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Could not find needRequery line")
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("Done")
|
||||||
51
_fix_impl.py
Normal file
51
_fix_impl.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import sys
|
||||||
|
filepath = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\devices\data\repositories\generate_path_repository_Impl.dart'
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
old = ''' if (response.statusCode != 200) {
|
||||||
|
throw Exception('创建设备任务失败: HTTP ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [创建设备任务] 错误: $e');
|
||||||
|
throw Exception('创建设备任务失败: $e');
|
||||||
|
}'''
|
||||||
|
|
||||||
|
new = ''' if (response.statusCode != 200) {
|
||||||
|
throw Exception('创建设备任务失败: HTTP ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应,提取 taskId
|
||||||
|
final data = jsonDecode(response.body) as Map<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)}')
|
||||||
43
_fix_json_data.py
Normal file
43
_fix_json_data.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import codecs
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
with codecs.open(path, 'r', 'utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
old = ''' return PlotData(
|
||||||
|
id:
|
||||||
|
record['id']?.toString() ??
|
||||||
|
DateTime.now().microsecondsSinceEpoch.toString(), // \u552f\u4e00ID
|
||||||
|
plotName: record['workName'] ?? '\u672a\u547d\u540d\u5730\u5757', // \u5730\u5757\u540d\u79f0\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff09
|
||||||
|
imageUrl: record['imgUrl'] ?? '', // \u56fe\u7247URL\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff0c\u65e0\u5219\u4e3a\u7a7a\uff09
|
||||||
|
jsonData: record['jsonData'], // \u539f\u59cb\u6570\u636e\u7684JSON\u5b57\u7b26\u4e32\uff08\u53ef\u9009\uff0c\u4fbf\u4e8e\u8c03\u8bd5\u6216\u540e\u7eed\u4f7f\u7528\uff09
|
||||||
|
);'''
|
||||||
|
|
||||||
|
new = ''' // \u517c\u5bb9 jsonData \u4e3a Map \u6216 String\uff1a\u7edf\u4e00\u8f6c\u4e3a JSON \u5b57\u7b26\u4e32
|
||||||
|
String? jsonDataStr;
|
||||||
|
final rawJsonData = record['jsonData'];
|
||||||
|
if (rawJsonData is String) {
|
||||||
|
jsonDataStr = rawJsonData;
|
||||||
|
} else if (rawJsonData is Map) {
|
||||||
|
jsonDataStr = jsonEncode(rawJsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlotData(
|
||||||
|
id:
|
||||||
|
record['id']?.toString() ??
|
||||||
|
DateTime.now().microsecondsSinceEpoch.toString(), // \u552f\u4e00ID
|
||||||
|
plotName: record['workName'] ?? '\u672a\u547d\u540d\u5730\u5757', // \u5730\u5757\u540d\u79f0\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff09
|
||||||
|
imageUrl: record['imgUrl'] ?? '', // \u56fe\u7247URL\uff08\u4ece\u63a5\u53e3\u5b57\u6bb5\u53d6\uff0c\u65e0\u5219\u4e3a\u7a7a\uff09
|
||||||
|
jsonData: jsonDataStr, // \u539f\u59cb\u6570\u636e\u7684JSON\u5b57\u7b26\u4e32\uff08\u53ef\u9009\uff0c\u4fbf\u4e8e\u8c03\u8bd5\u6216\u540e\u7eed\u4f7f\u7528\uff09
|
||||||
|
);'''
|
||||||
|
|
||||||
|
if old in content:
|
||||||
|
content = content.replace(old, new)
|
||||||
|
with codecs.open(path, 'w', 'utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print('SUCCESS: replaced')
|
||||||
|
else:
|
||||||
|
print('FAILED: old text not found')
|
||||||
|
lines = content.split('\n')
|
||||||
|
for i, line in enumerate(lines[3198:3208], start=3199):
|
||||||
|
print(f'{i}: {repr(line)}')
|
||||||
43
_fix_overflow.py
Normal file
43
_fix_overflow.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
filepath = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
old = ''' const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'设备: ${currentTask.deviceId}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF4E5969),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container('''
|
||||||
|
|
||||||
|
new = ''' const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'设备: ${currentTask.deviceId}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF4E5969),
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container('''
|
||||||
|
|
||||||
|
if old in content:
|
||||||
|
content = content.replace(old, new)
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
print('SUCCESS - replaced device ID text with wrapping support')
|
||||||
|
else:
|
||||||
|
print('NOT FOUND')
|
||||||
|
# Debug: find the line
|
||||||
|
for i, line in enumerate(content.split('\n'), 1):
|
||||||
|
if "设备: ${currentTask.deviceId}" in line:
|
||||||
|
print(f'Line {i}: {repr(line)}')
|
||||||
74
_fix_pause_stop.py
Normal file
74
_fix_pause_stop.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
print(f'Total lines: {len(lines)}')
|
||||||
|
|
||||||
|
# Find lines with "无可用任务"
|
||||||
|
target_lines = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if '无可用任务' in line:
|
||||||
|
target_lines.append(i)
|
||||||
|
# Print surrounding context
|
||||||
|
start = max(0, i-5)
|
||||||
|
end = min(len(lines), i+3)
|
||||||
|
for j in range(start, end):
|
||||||
|
print(f' L{j+1}: {lines[j].rstrip()}')
|
||||||
|
|
||||||
|
print(f'Found at lines: {[l+1 for l in target_lines]}')
|
||||||
|
|
||||||
|
# For each occurrence, replace the block:
|
||||||
|
# lines[i-3]: " final taskId = taskCubit.state.currentTaskId;"
|
||||||
|
# lines[i-2]: " if (taskId == null) {"
|
||||||
|
# lines[i-1]: " _showPageToast(message: "无可用任务", type: ToastType.warn);"
|
||||||
|
# lines[i]: " return;"
|
||||||
|
# lines[i+1]: " }"
|
||||||
|
|
||||||
|
for line_idx in target_lines:
|
||||||
|
# Find the start of the block
|
||||||
|
block_start = line_idx - 1
|
||||||
|
block_end = line_idx + 2
|
||||||
|
|
||||||
|
indent = ''
|
||||||
|
for ch in lines[block_start]:
|
||||||
|
if ch in (' ', '\t'):
|
||||||
|
indent += ch
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
new_lines = [
|
||||||
|
f'{indent}if (taskId == null) {{\n',
|
||||||
|
f'{indent} await taskCubit.fetchAndFilterTask(deviceId);\n',
|
||||||
|
f'{indent} final tasks = taskCubit.state.activeTasks;\n',
|
||||||
|
f'{indent} if (tasks.isNotEmpty) {{\n',
|
||||||
|
f'{indent} taskCubit.selectTask(tasks.first);\n',
|
||||||
|
f'{indent} taskId = tasks.first.id;\n',
|
||||||
|
f'{indent} }} else {{\n',
|
||||||
|
f'{indent} _showPageToast(message: "无可用任务", type: ToastType.warn);\n',
|
||||||
|
f'{indent} return;\n',
|
||||||
|
f'{indent} }}\n',
|
||||||
|
f'{indent}}}\n',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Also change "final taskId" to "var taskId"
|
||||||
|
var_line_idx = line_idx - 2
|
||||||
|
old_var_line = lines[var_line_idx]
|
||||||
|
lines[var_line_idx] = old_var_line.replace('final taskId', 'var taskId')
|
||||||
|
|
||||||
|
# Replace the block
|
||||||
|
old_block = lines[block_start:block_end+1]
|
||||||
|
print(f'Replacing lines {block_start+1}-{block_end+1}:')
|
||||||
|
for l in old_block:
|
||||||
|
print(f' OLD: {l.rstrip()}')
|
||||||
|
for l in new_lines:
|
||||||
|
print(f' NEW: {l.rstrip()}')
|
||||||
|
|
||||||
|
lines[block_start:block_end+1] = new_lines
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print('DONE')
|
||||||
77
_fix_startwork.py
Normal file
77
_fix_startwork.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find the block to replace
|
||||||
|
old_block_start = content.find('result.fold(')
|
||||||
|
if old_block_start < 0:
|
||||||
|
print('ERROR: result.fold( not found')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Find the end of the fold block: ');' after the fold call
|
||||||
|
# The fold ends with ');' on a line
|
||||||
|
old_block_end = content.find(' );', old_block_start)
|
||||||
|
if old_block_end < 0:
|
||||||
|
print('ERROR: fold end not found')
|
||||||
|
sys.exit(1)
|
||||||
|
old_block_end += len(' );')
|
||||||
|
|
||||||
|
old_block = content[old_block_start:old_block_end]
|
||||||
|
print('Old block found:')
|
||||||
|
print(repr(old_block[:100]))
|
||||||
|
print('...')
|
||||||
|
|
||||||
|
new_block = ''' final taskIdOrFlag = result.fold<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')
|
||||||
75
_fix_taskid.py
Normal file
75
_fix_taskid.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find the result.fold block
|
||||||
|
old_start = ' result.fold('
|
||||||
|
pos = content.find(old_start)
|
||||||
|
if pos < 0:
|
||||||
|
print('ERROR: result.fold not found')
|
||||||
|
sys.exit(1)
|
||||||
|
print(f'Found result.fold at char {pos}')
|
||||||
|
|
||||||
|
# Find the end of fold: " );" followed by " } catch"
|
||||||
|
# Search for the exact pattern
|
||||||
|
end_pattern = '\n );\n } catch'
|
||||||
|
end_pos = content.find(end_pattern, pos)
|
||||||
|
if end_pos < 0:
|
||||||
|
print(f'ERROR: fold end not found')
|
||||||
|
sys.exit(1)
|
||||||
|
end_pos += len('\n );')
|
||||||
|
print(f'Fold ends at char {end_pos}')
|
||||||
|
|
||||||
|
old_block = content[pos:end_pos]
|
||||||
|
print('Old block length:', len(old_block))
|
||||||
|
print('Old block preview:', repr(old_block[:80]))
|
||||||
|
|
||||||
|
new_block = ''' var needRequery = false;
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
final failMsg = failure.toString();
|
||||||
|
if (failMsg.contains('存在任务') || failMsg.contains('already exists')) {
|
||||||
|
needRequery = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_showPageToast(message: "作业启动失败", type: ToastType.error);
|
||||||
|
taskCubit.clearCurrentTask();
|
||||||
|
setState(() {
|
||||||
|
isStartWork = false;
|
||||||
|
_workStatus = WorkStatus.idle;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
(taskId) {
|
||||||
|
taskCubit.updateCurrentTaskId(taskId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (needRequery) {
|
||||||
|
debugPrint('⚠️ [开始作业] 设备已有任务,重新查询任务池...');
|
||||||
|
await taskCubit.fetchAndFilterTask(deviceId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
final existingTasks = taskCubit.state.activeTasks;
|
||||||
|
if (existingTasks.isNotEmpty) {
|
||||||
|
final existingTask = existingTasks.first;
|
||||||
|
taskCubit.selectTask(existingTask);
|
||||||
|
debugPrint('✅ [开始作业] 复用已有任务 #${existingTask.id},taskId已保存');
|
||||||
|
_showPageToast(message: "设备正在作业中", type: ToastType.success);
|
||||||
|
_saveDataToLocal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint('⚠️ [开始作业] 重新查询后仍未找到活跃任务');
|
||||||
|
_showPageToast(message: "设备正在作业中", type: ToastType.warn);
|
||||||
|
return;
|
||||||
|
}'''
|
||||||
|
|
||||||
|
new_content = content[:pos] + new_block + content[end_pos:]
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
print('SUCCESS - File updated')
|
||||||
99
_fix_taskid2.py
Normal file
99
_fix_taskid2.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find the "存在任务" branch that just returns without saving taskId
|
||||||
|
# We'll replace the "return;" in that branch with code that does async requery
|
||||||
|
# But fold callbacks can't be async...
|
||||||
|
|
||||||
|
# Instead, let's find the fold end and add async handler after it
|
||||||
|
# Find: "\n );\n } catch (e) {"
|
||||||
|
old_pat = '\n );\n } catch (e) {'
|
||||||
|
idx = content.find(old_pat)
|
||||||
|
if idx < 0:
|
||||||
|
print('Pattern not found, trying variants...')
|
||||||
|
# Try with different whitespace
|
||||||
|
old_pat = ' );\n } catch (e) {'
|
||||||
|
idx = content.find(old_pat)
|
||||||
|
if idx < 0:
|
||||||
|
print('Still not found')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f'Found at index {idx}')
|
||||||
|
|
||||||
|
# Insert code between the fold close and the catch
|
||||||
|
# We'll restructure to handle needRequery
|
||||||
|
insert_code = '''
|
||||||
|
if (needRequery) {
|
||||||
|
debugPrint('设备已有任务,重新查询任务池...');
|
||||||
|
await taskCubit.fetchAndFilterTask(deviceId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
final existingTasks = taskCubit.state.activeTasks;
|
||||||
|
if (existingTasks.isNotEmpty) {
|
||||||
|
final existingTask = existingTasks.first;
|
||||||
|
taskCubit.selectTask(existingTask);
|
||||||
|
debugPrint('复用已有任务 #${existingTask.id},taskId已保存');
|
||||||
|
_showPageToast(message: "设备正在作业中", type: ToastType.success);
|
||||||
|
_saveDataToLocal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_showPageToast(message: "设备正在作业中", type: ToastType.warn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {'''
|
||||||
|
|
||||||
|
# Now find and fix the fold callback to set needRequery flag
|
||||||
|
# Replace the "存在任务" branch
|
||||||
|
old_branch = '''if (failMsg.contains('存在任务') || failMsg.contains('already exists')) {
|
||||||
|
debugPrint('⚠️ [开始作业] 设备已有任务,保留作业状态');
|
||||||
|
_showPageToast(message: "设备正在作业中", type: ToastType.warn);
|
||||||
|
// 🔥 不重置 _workStatus,按钮保持可见
|
||||||
|
return;'''
|
||||||
|
|
||||||
|
new_branch = '''if (failMsg.contains('存在任务') || failMsg.contains('already exists')) {
|
||||||
|
needRequery = true;
|
||||||
|
return;'''
|
||||||
|
|
||||||
|
branch_idx = content.find(old_branch)
|
||||||
|
if branch_idx < 0:
|
||||||
|
# Try without the debug print variation
|
||||||
|
old_branch = '''if (failMsg.contains('存在任务') || failMsg.contains('already exists')) {
|
||||||
|
debugPrint'''
|
||||||
|
branch_idx = content.find(old_branch)
|
||||||
|
if branch_idx < 0:
|
||||||
|
print('Branch pattern not found')
|
||||||
|
else:
|
||||||
|
print(f'Branch found at {branch_idx} (partial match)')
|
||||||
|
else:
|
||||||
|
print(f'Branch found at {branch_idx}')
|
||||||
|
# Do the branch replacement
|
||||||
|
content = content[:branch_idx] + new_branch + content[branch_idx + len(old_branch):]
|
||||||
|
|
||||||
|
# Also add needRequery var declaration before fold
|
||||||
|
# Find " result.fold(" and add var before it
|
||||||
|
fold_marker = ' result.fold('
|
||||||
|
fold_idx = content.find(fold_marker)
|
||||||
|
if fold_idx >= 0:
|
||||||
|
# Check if needRequery is already declared
|
||||||
|
check = content.find('needRequery', fold_idx - 50, fold_idx)
|
||||||
|
if check < 0:
|
||||||
|
content = content[:fold_idx] + ' var needRequery = false;\n' + content[fold_idx:]
|
||||||
|
print('Added needRequery declaration')
|
||||||
|
|
||||||
|
# Now find and replace the fold end pattern again (after content modifications)
|
||||||
|
old_pat = '\n );\n } catch (e) {'
|
||||||
|
idx = content.find(old_pat)
|
||||||
|
if idx < 0:
|
||||||
|
print('Fold end pattern not found after modifications')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
content = content[:idx + len('\n );')] + insert_code + content[idx + len(old_pat):]
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print('DONE')
|
||||||
83
_fix_taskid3.py
Normal file
83
_fix_taskid3.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
start_marker = ' try {\n final result = await sl<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')
|
||||||
81
_patch_save.py
Normal file
81
_patch_save.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
path = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Target: lines 915-954 (0-indexed: 914-953) are duplicate response blocks
|
||||||
|
# We want to replace them with a single clean response block
|
||||||
|
# Lines before 915 (0-914) = request params + _showPageToast (already good)
|
||||||
|
# Line 915-954 = duplicates to remove
|
||||||
|
# Lines 955+ = list refresh, _clearLocalData, setState (keep)
|
||||||
|
|
||||||
|
# New response block (same indentation as original, 6 spaces)
|
||||||
|
clean_resp = [
|
||||||
|
' debugPrint(\'\U0001f4e5 [保存地块] 响应状态码: ${response.statusCode}\');\n',
|
||||||
|
' debugPrint(\'\U0001f4e5 [保存地块] 响应体(完整): $responseBody\');\n',
|
||||||
|
'\n',
|
||||||
|
' if (response.statusCode == 200) {\n',
|
||||||
|
' // \U0001f525 解析响应体确认后端是否真的成功\n',
|
||||||
|
' Map<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}")
|
||||||
61
fix_dup.py
Normal file
61
fix_dup.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
filepath = r'C:\Users\jsmbz\Documents\flutter-app-02\lib\features\home\presentation\widgets\map\testmap_pages.dart'
|
||||||
|
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Pattern: find the second duplicate method (with debugPrint in catch)
|
||||||
|
# It starts with "/// 从 state.pathData 实时解析" and ends with "}\n\n Widget _buildWorkPanel"
|
||||||
|
pattern1 = r'\n /// 从 state\.pathData 实时解析 startWorkList\n /// 解决 BlocBuilder 因 state\.pathData 变化触发重建时,onTap 的异步赋值还未完成的时序问题\n List<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
Normal file
107
flutter_01.log
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
106
flutter_02.log
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
107
flutter_03.log
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
106
flutter_04.log
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
106
flutter_05.log
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
106
flutter_06.log
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
106
flutter_07.log
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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
Normal file
106
flutter_08.log
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
Flutter crash report.
|
||||||
|
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||||
|
|
||||||
|
## command
|
||||||
|
|
||||||
|
flutter --no-color run --machine --track-widget-creation --device-id=4TRKWOEE4HR8AMEA --start-paused --dart-define=flutter.inspector.structuredErrors=true lib\main.dart
|
||||||
|
|
||||||
|
## exception
|
||||||
|
|
||||||
|
FormatException: FormatException: Unexpected character (at character 1)
|
||||||
|
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||||
|
^
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||||
|
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||||
|
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||||
|
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||||
|
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||||
|
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||||
|
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||||
|
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||||
|
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||||
|
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||||
|
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||||
|
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||||
|
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||||
|
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||||
|
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||||
|
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||||
|
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||||
|
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||||
|
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||||
|
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||||
|
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||||
|
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||||
|
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||||
|
#37 new _RawSocket.<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.
|
||||||
|
```
|
||||||
@@ -7,9 +7,9 @@ class AppUserCubit extends Cubit<AppUserState> {
|
|||||||
AppUserCubit() : super(const AppUserState());
|
AppUserCubit() : super(const AppUserState());
|
||||||
|
|
||||||
void setAuth(UserEntity user) {
|
void setAuth(UserEntity user) {
|
||||||
print('✅ [AppUserCubit] setAuth 被调用,用户: ${user.username}');
|
print('✅ [AppUserCubit] setAuth 被调用,用户: ${user.username}, roleKey=${user.roleKey}, orgId=${user.orgId}, siteId=${user.siteId}');
|
||||||
emit(state.copyWith(user));
|
emit(state.copyWith(user));
|
||||||
print('✅ [AppUserCubit] 状态已更新,当前用户: ${state.user?.username}');
|
print('✅ [AppUserCubit] 状态已更新,当前用户: ${state.user?.username}, roleKey=${state.user?.roleKey}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearAuth() {
|
void clearAuth() {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ class TcpClient {
|
|||||||
int _reconnectFailCount = 0; // 🔥 连续重连失败次数(含心跳超时),达到阈值后停止重连
|
int _reconnectFailCount = 0; // 🔥 连续重连失败次数(含心跳超时),达到阈值后停止重连
|
||||||
/// 🔥 重连耗尽回调:连续4次重连失败(含心跳超时)后触发,通知上层弹窗提示用户
|
/// 🔥 重连耗尽回调:连续4次重连失败(含心跳超时)后触发,通知上层弹窗提示用户
|
||||||
VoidCallback? onReconnectExhausted;
|
VoidCallback? onReconnectExhausted;
|
||||||
|
/// 🔥 重连开始回调:显示"TCP重连中"提示
|
||||||
|
VoidCallback? onReconnectStarted;
|
||||||
|
/// 🔥 重连成功回调:显示"连接成功"提示
|
||||||
|
VoidCallback? onReconnectSuccess;
|
||||||
static const Duration _heartbeatTimeout = Duration(seconds: 15); // 心跳超时时间
|
static const Duration _heartbeatTimeout = Duration(seconds: 15); // 心跳超时时间
|
||||||
|
|
||||||
String? _lastHost;
|
String? _lastHost;
|
||||||
@@ -418,11 +422,25 @@ class TcpClient {
|
|||||||
_logger.logWithLevel('✅ [TCP-重连定时器] 用户切换中,跳过', shouldLog: true);
|
_logger.logWithLevel('✅ [TCP-重连定时器] 用户切换中,跳过', shouldLog: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_logger.logWithLevel('⏰ 定时器触发,开始执行重连...', shouldLog: true);
|
// 🔥 显示"TCP重连中"提示
|
||||||
|
onReconnectStarted?.call();
|
||||||
|
final reconnectStart = DateTime.now();
|
||||||
|
final timeStr = '${reconnectStart.hour.toString().padLeft(2,'0')}:${reconnectStart.minute.toString().padLeft(2,'0')}:${reconnectStart.second.toString().padLeft(2,'0')}.${reconnectStart.millisecond.toString().padLeft(3,'0')}';
|
||||||
|
debugPrint('⏰ [TCP-重连] $timeStr 开始重连(第$_reconnectAttempt次,退避${delay}s)');
|
||||||
|
_logger.logWithLevel('⏰ [TCP-重连] $timeStr 开始重连(第$_reconnectAttempt次,退避${delay}s)', shouldLog: true);
|
||||||
try {
|
try {
|
||||||
await connect(host: _lastHost!, port: _lastPort!);
|
await connect(host: _lastHost!, port: _lastPort!);
|
||||||
|
final connectDone = DateTime.now();
|
||||||
|
final connectMs = connectDone.difference(reconnectStart).inMilliseconds;
|
||||||
|
debugPrint('✅ [TCP-重连] ${connectDone.hour.toString().padLeft(2,'0')}:${connectDone.minute.toString().padLeft(2,'0')}:${connectDone.second.toString().padLeft(2,'0')}.${connectDone.millisecond.toString().padLeft(3,'0')} connect() 完成,耗时 ${connectMs}ms');
|
||||||
_reconnectAttempt = 0; // 🔥 重连成功,重置退避计数
|
_reconnectAttempt = 0; // 🔥 重连成功,重置退避计数
|
||||||
// 🔥 重连成功后,补调 HTTP switchDevice 告知服务端推送目标设备
|
// 🔥 关键修复:等待5秒再调 HTTP switchDevice,给服务端时间完成相关操作
|
||||||
|
final delayStart = DateTime.now();
|
||||||
|
debugPrint('⏳ [TCP-重连] ${delayStart.hour.toString().padLeft(2,'0')}:${delayStart.minute.toString().padLeft(2,'0')}:${delayStart.second.toString().padLeft(2,'0')}.${delayStart.millisecond.toString().padLeft(3,'0')} 开始等待5秒...');
|
||||||
|
await Future.delayed(const Duration(milliseconds: 5000));
|
||||||
|
final afterDelay = DateTime.now();
|
||||||
|
final actualDelayMs = afterDelay.difference(delayStart).inMilliseconds;
|
||||||
|
debugPrint('⏳ [TCP-重连] ${afterDelay.hour.toString().padLeft(2,'0')}:${afterDelay.minute.toString().padLeft(2,'0')}:${afterDelay.second.toString().padLeft(2,'0')}.${afterDelay.millisecond.toString().padLeft(3,'0')} 等待结束,实际耗时 ${actualDelayMs}ms,开始调 HTTP switchDevice');
|
||||||
try {
|
try {
|
||||||
final remoteControlCubit = GetIt.I<RemoteControlCubit>();
|
final remoteControlCubit = GetIt.I<RemoteControlCubit>();
|
||||||
final targetDevice = remoteControlCubit.state.targetDevice;
|
final targetDevice = remoteControlCubit.state.targetDevice;
|
||||||
@@ -430,10 +448,14 @@ class TcpClient {
|
|||||||
debugPrint('🔄 [TCP-重连] 补调 HTTP switchDevice: ${targetDevice.deviceName}');
|
debugPrint('🔄 [TCP-重连] 补调 HTTP switchDevice: ${targetDevice.deviceName}');
|
||||||
_logger.logWithLevel('🔄 [TCP-重连] 补调 HTTP switchDevice: ${targetDevice.deviceName}', shouldLog: true);
|
_logger.logWithLevel('🔄 [TCP-重连] 补调 HTTP switchDevice: ${targetDevice.deviceName}', shouldLog: true);
|
||||||
await switchDeviceUseCase.deviceRepository.switchDevice('app', targetDevice.deviceName);
|
await switchDeviceUseCase.deviceRepository.switchDevice('app', targetDevice.deviceName);
|
||||||
debugPrint('✅ [TCP-重连] HTTP switchDevice 成功');
|
final switchDone = DateTime.now();
|
||||||
|
final totalMs = switchDone.difference(reconnectStart).inMilliseconds;
|
||||||
|
debugPrint('✅ [TCP-重连] ${switchDone.hour.toString().padLeft(2,'0')}:${switchDone.minute.toString().padLeft(2,'0')}:${switchDone.second.toString().padLeft(2,'0')}.${switchDone.millisecond.toString().padLeft(3,'0')} HTTP switchDevice 成功,重连总耗时 ${totalMs}ms');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⚠️ [TCP-重连] targetDevice 为 null,跳过 switchDevice');
|
debugPrint('⚠️ [TCP-重连] targetDevice 为 null,跳过 switchDevice');
|
||||||
}
|
}
|
||||||
|
// 🔥 显示"连接成功"提示
|
||||||
|
onReconnectSuccess?.call();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [TCP-重连] HTTP switchDevice 失败: $e');
|
debugPrint('❌ [TCP-重连] HTTP switchDevice 失败: $e');
|
||||||
_logger.logWithLevel('❌ [TCP-重连] HTTP switchDevice 失败: $e', shouldLog: true);
|
_logger.logWithLevel('❌ [TCP-重连] HTTP switchDevice 失败: $e', shouldLog: true);
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
_listenToAuthResponse();
|
_listenToAuthResponse();
|
||||||
// 🔥 设置重连耗尽回调:连续4次重连失败后弹窗提示用户退出登录
|
// 🔥 设置重连耗尽回调:连续4次重连失败后弹窗提示用户退出登录
|
||||||
tcp.onReconnectExhausted = _showReconnectFailedDialog;
|
tcp.onReconnectExhausted = _showReconnectFailedDialog;
|
||||||
|
// 🔥 设置重连提示回调:重连中/重连成功 SnackBar
|
||||||
|
tcp.onReconnectStarted = _showReconnectingToast;
|
||||||
|
tcp.onReconnectSuccess = _showReconnectSuccessToast;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// App 启动时检查本地缓存
|
/// App 启动时检查本地缓存
|
||||||
@@ -379,24 +382,42 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
SizedBox(
|
Row(
|
||||||
width: double.infinity,
|
children: [
|
||||||
child: ElevatedButton(
|
Expanded(
|
||||||
onPressed: () {
|
child: OutlinedButton(
|
||||||
Navigator.of(dialogContext).pop();
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
logout();
|
style: OutlinedButton.styleFrom(
|
||||||
},
|
foregroundColor: const Color(0xFF86909C),
|
||||||
style: ElevatedButton.styleFrom(
|
side: const BorderSide(color: Color(0xFFE5E6EB)),
|
||||||
backgroundColor: const Color(0xFF165DFF),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
foregroundColor: Colors.white,
|
shape: RoundedRectangleBorder(
|
||||||
elevation: 0,
|
borderRadius: BorderRadius.circular(8),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: const Text('取消', style: TextStyle(fontSize: 15)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('退出登录', style: TextStyle(fontSize: 15)),
|
const SizedBox(width: 12),
|
||||||
),
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
logout();
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF165DFF),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('退出登录', style: TextStyle(fontSize: 15)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -412,6 +433,39 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔥 重连中提示 SnackBar
|
||||||
|
void _showReconnectingToast() {
|
||||||
|
try {
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('TCP重连中...'),
|
||||||
|
backgroundColor: Color(0xFFFF7D00),
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 重连成功提示 SnackBar
|
||||||
|
void _showReconnectSuccessToast() {
|
||||||
|
try {
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context != null) {
|
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('连接成功'),
|
||||||
|
backgroundColor: Color(0xFF00B42A),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
/// 🔥 新增:清空所有业务状态,防止数据泄露到新账户
|
/// 🔥 新增:清空所有业务状态,防止数据泄露到新账户
|
||||||
void _clearAllBusinessState() {
|
void _clearAllBusinessState() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
/// 视频加载状态
|
||||||
|
enum WebRTCLoadState { loading, playing, error }
|
||||||
|
|
||||||
class WebRTCLocalPlayer extends StatefulWidget {
|
class WebRTCLocalPlayer extends StatefulWidget {
|
||||||
final String streamUrl;
|
final String streamUrl;
|
||||||
final bool showLeftPip;
|
final bool showLeftPip;
|
||||||
@@ -11,6 +15,16 @@ class WebRTCLocalPlayer extends StatefulWidget {
|
|||||||
final bool isFrontMain;
|
final bool isFrontMain;
|
||||||
final Alignment? mainViewAlignment; // 🔥 主视角对齐方式(指定后覆盖 isFrontMain)
|
final Alignment? mainViewAlignment; // 🔥 主视角对齐方式(指定后覆盖 isFrontMain)
|
||||||
final VoidCallback? onDoubleTap; // 🔥 添加双击回调
|
final VoidCallback? onDoubleTap; // 🔥 添加双击回调
|
||||||
|
final VoidCallback? onTap; // 🔥 单击回调(播放中点击停止拉流)
|
||||||
|
|
||||||
|
/// 🔥 视频加载状态回调(loading/playing/error)
|
||||||
|
/// - loading:正在建立连接
|
||||||
|
/// - playing:已收到视频流,开始播放
|
||||||
|
/// - error:连接失败或超时未收到画面
|
||||||
|
final ValueChanged<WebRTCLoadState>? onLoadingStateChanged;
|
||||||
|
|
||||||
|
/// 🔥 等待视频流的最大时长,超时判定为失败(默认 8 秒)
|
||||||
|
final Duration connectTimeout;
|
||||||
|
|
||||||
const WebRTCLocalPlayer({
|
const WebRTCLocalPlayer({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -20,6 +34,9 @@ class WebRTCLocalPlayer extends StatefulWidget {
|
|||||||
this.isFrontMain = true,
|
this.isFrontMain = true,
|
||||||
this.mainViewAlignment, // 🔥 可选,指定要显示的象限
|
this.mainViewAlignment, // 🔥 可选,指定要显示的象限
|
||||||
this.onDoubleTap, // 🔥 可选回调
|
this.onDoubleTap, // 🔥 可选回调
|
||||||
|
this.onTap, // 🔥 可选,播放中单击停止拉流
|
||||||
|
this.onLoadingStateChanged,
|
||||||
|
this.connectTimeout = const Duration(seconds: 8),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,18 +46,132 @@ class WebRTCLocalPlayer extends StatefulWidget {
|
|||||||
class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
||||||
// 🔥 显示器:只初始化一次,终生使用
|
// 🔥 显示器:只初始化一次,终生使用
|
||||||
final RTCVideoRenderer _renderer = RTCVideoRenderer();
|
final RTCVideoRenderer _renderer = RTCVideoRenderer();
|
||||||
|
|
||||||
// 🔥 信号线:可以更换,但必须先拔后插
|
// 🔥 信号线:可以更换,但必须先拔后插
|
||||||
RTCPeerConnection? _peerConnection;
|
RTCPeerConnection? _peerConnection;
|
||||||
|
|
||||||
// 🔥 URL 缓存:用于判断是否需要换线
|
// 🔥 URL 缓存:用于判断是否需要换线
|
||||||
String? _currentStreamUrl;
|
String? _currentStreamUrl;
|
||||||
|
|
||||||
// 画中画位置管理
|
// 画中画位置管理
|
||||||
final ValueNotifier<Offset> _leftPosNotifier = ValueNotifier(const Offset(20, 80));
|
final ValueNotifier<Offset> _leftPosNotifier = ValueNotifier(
|
||||||
final ValueNotifier<Offset> _rightPosNotifier = ValueNotifier(const Offset(200, 80));
|
const Offset(20, 80),
|
||||||
|
);
|
||||||
|
final ValueNotifier<Offset> _rightPosNotifier = ValueNotifier(
|
||||||
|
const Offset(200, 80),
|
||||||
|
);
|
||||||
bool _isPosInitialized = false;
|
bool _isPosInitialized = false;
|
||||||
|
|
||||||
|
// 🔥 加载状态相关
|
||||||
|
WebRTCLoadState _loadState = WebRTCLoadState.loading;
|
||||||
|
Timer? _connectTimer; // 超时判定计时器
|
||||||
|
Timer? _frameCheckTimer; // 🔥 真实视频帧检测计时器
|
||||||
|
|
||||||
|
/// 🔥 统一更新加载状态并通知外部
|
||||||
|
void _updateLoadState(WebRTCLoadState state) {
|
||||||
|
if (_loadState == state) return;
|
||||||
|
_loadState = state;
|
||||||
|
widget.onLoadingStateChanged?.call(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 启动超时计时器:超过 connectTimeout 仍未渲染出真实画面,判定为失败
|
||||||
|
void _startConnectTimer() {
|
||||||
|
_connectTimer?.cancel();
|
||||||
|
_connectTimer = Timer(widget.connectTimeout, () {
|
||||||
|
if (_loadState != WebRTCLoadState.playing) {
|
||||||
|
debugPrint('⏰ [WebRTC] 连接超时,未渲染出真实画面,判定为失败');
|
||||||
|
_updateLoadState(WebRTCLoadState.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 取消超时计时器
|
||||||
|
void _cancelConnectTimer() {
|
||||||
|
_connectTimer?.cancel();
|
||||||
|
_connectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 启动真实视频流检测:通过 WebRTC getStats 检查 inbound-rtp 的 bytesReceived 是否在增长
|
||||||
|
/// 只有真实有数据包流入,才算真的有视频(避免 track 建立了但帧是黑的被误判为有视频)
|
||||||
|
/// - videoWidth/videoHeight 在 SDP 协商成功后就 > 0,无法区分黑屏流,不可靠
|
||||||
|
/// - bytesReceived 持续增长 = 真实数据流;为 0 或停止增长 = 黑屏流
|
||||||
|
void _startFrameCheck() {
|
||||||
|
_frameCheckTimer?.cancel();
|
||||||
|
int lastBytesReceived = -1;
|
||||||
|
int stableCount = 0; // bytesReceived 未增长的连续次数
|
||||||
|
|
||||||
|
_frameCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (
|
||||||
|
_,
|
||||||
|
) async {
|
||||||
|
if (!mounted || _peerConnection == null) {
|
||||||
|
_frameCheckTimer?.cancel();
|
||||||
|
_frameCheckTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final stats = await _peerConnection!.getStats();
|
||||||
|
// 查找视频入站 RTP 统计
|
||||||
|
int currentBytes = 0;
|
||||||
|
for (final report in stats) {
|
||||||
|
if (report.type == 'inbound-rtp') {
|
||||||
|
final kind = report.values['kind'];
|
||||||
|
final bytes = report.values['bytesReceived'];
|
||||||
|
if (kind == 'video' && bytes is num) {
|
||||||
|
currentBytes = bytes.toInt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 还没收到任何字节,继续等
|
||||||
|
if (currentBytes == 0) {
|
||||||
|
debugPrint('⏳ [WebRTC] bytesReceived=0,等待数据流入...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字节数在增长 → 真实数据流 → 判定为 playing
|
||||||
|
if (currentBytes > lastBytesReceived && lastBytesReceived >= 0) {
|
||||||
|
debugPrint(
|
||||||
|
'✅ [WebRTC] 检测到真实视频流,bytesReceived: $currentBytes,判定为 playing',
|
||||||
|
);
|
||||||
|
_frameCheckTimer?.cancel();
|
||||||
|
_frameCheckTimer = null;
|
||||||
|
_cancelConnectTimer();
|
||||||
|
_updateLoadState(WebRTCLoadState.playing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字节数未增长
|
||||||
|
if (currentBytes == lastBytesReceived) {
|
||||||
|
stableCount++;
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [WebRTC] bytesReceived 未增长 ($currentBytes),stableCount=$stableCount',
|
||||||
|
);
|
||||||
|
// 连续 2 次未增长(约 1 秒)→ 判定为黑屏流
|
||||||
|
if (stableCount >= 2) {
|
||||||
|
debugPrint('❌ [WebRTC] 字节流停止增长,判定为黑屏流');
|
||||||
|
_frameCheckTimer?.cancel();
|
||||||
|
_frameCheckTimer = null;
|
||||||
|
_cancelConnectTimer();
|
||||||
|
_updateLoadState(WebRTCLoadState.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastBytesReceived = currentBytes;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('⚠️ [WebRTC] 获取 stats 失败: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 取消帧检测计时器
|
||||||
|
void _cancelFrameCheck() {
|
||||||
|
_frameCheckTimer?.cancel();
|
||||||
|
_frameCheckTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -62,9 +193,10 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(WebRTCLocalPlayer oldWidget) {
|
void didUpdateWidget(WebRTCLocalPlayer oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
// 🔥 只有 URL 真正变化时才换线
|
// 🔥 只有 URL 真正变化时才换线
|
||||||
if (oldWidget.streamUrl != widget.streamUrl && widget.streamUrl.isNotEmpty) {
|
if (oldWidget.streamUrl != widget.streamUrl &&
|
||||||
|
widget.streamUrl.isNotEmpty) {
|
||||||
debugPrint('🔄 [WebRTC] URL 变化,准备换线');
|
debugPrint('🔄 [WebRTC] URL 变化,准备换线');
|
||||||
_connectSignal();
|
_connectSignal();
|
||||||
}
|
}
|
||||||
@@ -75,22 +207,27 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
// 1️⃣ 如果 URL 为空,不插线
|
// 1️⃣ 如果 URL 为空,不插线
|
||||||
if (widget.streamUrl.isEmpty) {
|
if (widget.streamUrl.isEmpty) {
|
||||||
debugPrint('⚠️ [WebRTC] URL 为空,跳过连接');
|
debugPrint('⚠️ [WebRTC] URL 为空,跳过连接');
|
||||||
|
_updateLoadState(WebRTCLoadState.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ 如果 URL 没变,不换线(防止重复连接)
|
// 2️⃣ 如果 URL 没变,不换线(防止重复连接)
|
||||||
if (_currentStreamUrl == widget.streamUrl && _peerConnection != null) {
|
if (_currentStreamUrl == widget.streamUrl && _peerConnection != null) {
|
||||||
debugPrint('✅ [WebRTC] URL 未变化,保持当前连接');
|
debugPrint('✅ [WebRTC] URL 未变化,保持当前连接');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3️⃣ 拔掉旧线(彻底释放旧连接)
|
// 3️⃣ 拔掉旧线(彻底释放旧连接)
|
||||||
await _disconnectSignal();
|
await _disconnectSignal();
|
||||||
|
|
||||||
|
// 🔥 进入 loading 态,启动超时计时器
|
||||||
|
_updateLoadState(WebRTCLoadState.loading);
|
||||||
|
_startConnectTimer();
|
||||||
|
|
||||||
// 4️⃣ 插上新线(创建新连接)
|
// 4️⃣ 插上新线(创建新连接)
|
||||||
try {
|
try {
|
||||||
debugPrint('🔌 [WebRTC] 建立新连接...');
|
debugPrint('🔌 [WebRTC] 建立新连接...');
|
||||||
|
|
||||||
_peerConnection = await createPeerConnection({
|
_peerConnection = await createPeerConnection({
|
||||||
"sdpSemantics": "unified-plan",
|
"sdpSemantics": "unified-plan",
|
||||||
"iceServers": [
|
"iceServers": [
|
||||||
@@ -106,48 +243,70 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
// 🔥 收到视频流时,直接连接到显示器
|
// 🔥 收到视频流时,直接连接到显示器
|
||||||
_peerConnection!.onTrack = (RTCTrackEvent event) {
|
_peerConnection!.onTrack = (RTCTrackEvent event) {
|
||||||
if (event.track.kind == 'video' && event.streams.isNotEmpty) {
|
if (event.track.kind == 'video' && event.streams.isNotEmpty) {
|
||||||
debugPrint('✅ [WebRTC] 收到视频流,连接到显示器');
|
debugPrint('✅ [WebRTC] 收到视频 track,连接到显示器,等待真实帧渲染');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _renderer.srcObject = event.streams[0]);
|
setState(() => _renderer.srcObject = event.streams[0]);
|
||||||
|
// 🔥 关键:收到 track 不等于有画面,必须等真实帧渲染才判定 playing
|
||||||
|
_startFrameCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔥 SDP 协商
|
// 🔥 SDP 协商
|
||||||
RTCSessionDescription offer = await _peerConnection!.createOffer({'offerToReceiveVideo': 1});
|
RTCSessionDescription offer = await _peerConnection!.createOffer({
|
||||||
|
'offerToReceiveVideo': 1,
|
||||||
|
});
|
||||||
await _peerConnection!.setLocalDescription(offer);
|
await _peerConnection!.setLocalDescription(offer);
|
||||||
|
|
||||||
Uri uri = Uri.parse(widget.streamUrl.replaceFirst('webrtc://', 'http://'));
|
Uri uri = Uri.parse(
|
||||||
|
widget.streamUrl.replaceFirst('webrtc://', 'http://'),
|
||||||
|
);
|
||||||
String apiUrl = "http://${uri.host}:1985/rtc/v1/play/";
|
String apiUrl = "http://${uri.host}:1985/rtc/v1/play/";
|
||||||
|
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse(apiUrl),
|
Uri.parse(apiUrl),
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
"api": apiUrl,
|
"api": apiUrl,
|
||||||
"streamurl": widget.streamUrl,
|
"streamurl": widget.streamUrl,
|
||||||
"clientip": null,
|
"clientip": null,
|
||||||
"sdp": offer.sdp
|
"sdp": offer.sdp,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
final sdp = data['sdp'];
|
final sdp = data['sdp'];
|
||||||
|
|
||||||
if (sdp != null && sdp is String && sdp.isNotEmpty) {
|
if (sdp != null && sdp is String && sdp.isNotEmpty) {
|
||||||
await _peerConnection!.setRemoteDescription(RTCSessionDescription(sdp, 'answer'));
|
await _peerConnection!.setRemoteDescription(
|
||||||
|
RTCSessionDescription(sdp, 'answer'),
|
||||||
|
);
|
||||||
_currentStreamUrl = widget.streamUrl; // 🔥 记录当前 URL
|
_currentStreamUrl = widget.streamUrl; // 🔥 记录当前 URL
|
||||||
debugPrint('✅ [WebRTC] 连接成功');
|
debugPrint('✅ [WebRTC] SDP 协商成功');
|
||||||
|
// 注意:此处仅代表协商成功,真正判定为 playing 须等 onTrack 收到画面
|
||||||
|
// 超时定时器仍在运行,未收到画面会自动判定为 error
|
||||||
|
} else {
|
||||||
|
debugPrint('❌ [WebRTC] 返回的 sdp 为空,判定为失败');
|
||||||
|
_cancelConnectTimer();
|
||||||
|
_updateLoadState(WebRTCLoadState.error);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('❌ [WebRTC] HTTP 状态码: ${response.statusCode},判定为失败');
|
||||||
|
_cancelConnectTimer();
|
||||||
|
_updateLoadState(WebRTCLoadState.error);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("❌ [WebRTC] 连接失败: $e");
|
debugPrint("❌ [WebRTC] 连接失败: $e");
|
||||||
|
_cancelConnectTimer();
|
||||||
|
_updateLoadState(WebRTCLoadState.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔥 拔线逻辑:彻底释放旧连接
|
/// 🔥 拔线逻辑:彻底释放旧连接
|
||||||
Future<void> _disconnectSignal() async {
|
Future<void> _disconnectSignal() async {
|
||||||
|
_cancelConnectTimer(); // 🔥 拔线时一并取消超时计时器
|
||||||
|
_cancelFrameCheck(); // 🔥 一并取消帧检测计时器
|
||||||
if (_peerConnection != null) {
|
if (_peerConnection != null) {
|
||||||
debugPrint('🗑️ [WebRTC] 释放旧连接...');
|
debugPrint('🗑️ [WebRTC] 释放旧连接...');
|
||||||
await _peerConnection?.close();
|
await _peerConnection?.close();
|
||||||
@@ -174,7 +333,11 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
// 🔥 俯视(center):显示完整视频帧,不做2倍裁剪
|
// 🔥 俯视(center):显示完整视频帧,不做2倍裁剪
|
||||||
if (alignment == Alignment.center) {
|
if (alignment == Alignment.center) {
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: RTCVideoView(_renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: false),
|
child: RTCVideoView(
|
||||||
|
_renderer,
|
||||||
|
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||||
|
mirror: false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +348,11 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
widthFactor: 2.0,
|
widthFactor: 2.0,
|
||||||
heightFactor: 2.0,
|
heightFactor: 2.0,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: RTCVideoView(_renderer, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, mirror: false),
|
child: RTCVideoView(
|
||||||
|
_renderer,
|
||||||
|
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||||
|
mirror: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -198,7 +365,12 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
return const LinearGradient(
|
return const LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent],
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.white,
|
||||||
|
Colors.white,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
stops: [0.0, 0.15, 0.85, 1.0],
|
stops: [0.0, 0.15, 0.85, 1.0],
|
||||||
).createShader(bounds);
|
).createShader(bounds);
|
||||||
},
|
},
|
||||||
@@ -208,20 +380,30 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
return const LinearGradient(
|
return const LinearGradient(
|
||||||
begin: Alignment.centerLeft,
|
begin: Alignment.centerLeft,
|
||||||
end: Alignment.centerRight,
|
end: Alignment.centerRight,
|
||||||
colors: [Colors.transparent, Colors.white, Colors.white, Colors.transparent],
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.white,
|
||||||
|
Colors.white,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
stops: [0.0, 0.05, 0.95, 1.0],
|
stops: [0.0, 0.05, 0.95, 1.0],
|
||||||
).createShader(bounds);
|
).createShader(bounds);
|
||||||
},
|
},
|
||||||
blendMode: BlendMode.dstIn,
|
blendMode: BlendMode.dstIn,
|
||||||
child: _buildQuadrantView(alignment: widget.mainViewAlignment ?? (widget.isFrontMain ? Alignment.topLeft : Alignment.topRight)),
|
child: _buildQuadrantView(
|
||||||
|
alignment:
|
||||||
|
widget.mainViewAlignment ??
|
||||||
|
(widget.isFrontMain ? Alignment.topLeft : Alignment.topRight),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 🔥 如果正在初始化,显示加载指示器
|
// 🔥 在真正渲染出视频帧(playing 态)之前,始终显示加载指示器
|
||||||
if (_peerConnection == null && _currentStreamUrl == null) {
|
// 避免出现"track 已收到但画面是黑的"黑屏情况
|
||||||
|
if (_loadState != WebRTCLoadState.playing) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -255,26 +437,47 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
|
|
||||||
if (!_isPosInitialized) {
|
if (!_isPosInitialized) {
|
||||||
_leftPosNotifier.value = const Offset(20, 80);
|
_leftPosNotifier.value = const Offset(20, 80);
|
||||||
_rightPosNotifier.value = Offset(constraints.maxWidth - pipW - 20, 80);
|
_rightPosNotifier.value = Offset(
|
||||||
|
constraints.maxWidth - pipW - 20,
|
||||||
|
80,
|
||||||
|
);
|
||||||
_isPosInitialized = true;
|
_isPosInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// ② 主视频层(🔥 恢复双击切换前后视角)
|
// ② 主视频层(🔥 双击切换前后视角 / 单击停止拉流)
|
||||||
Center(
|
Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
onTap: widget.onTap, // 🔥 播放中单击 → 停止拉流
|
||||||
onDoubleTap: () {
|
onDoubleTap: () {
|
||||||
debugPrint('👆 [WebRTC] 双击屏幕,切换前后视角');
|
debugPrint('👆 [WebRTC] 双击屏幕,切换前后视角');
|
||||||
widget.onDoubleTap?.call(); // 🔥 调用父组件回调
|
widget.onDoubleTap?.call(); // 🔥 调用父组件回调
|
||||||
},
|
},
|
||||||
child: AspectRatio(aspectRatio: 16 / 9, child: _buildMainViewWithFeathering()),
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: _buildMainViewWithFeathering(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ③ 悬浮小窗 - 解决阻尼感的 1:1 稳定拖拽
|
// ③ 悬浮小窗 - 解决阻尼感的 1:1 稳定拖拽
|
||||||
if (widget.showLeftPip) _buildFastPip(_leftPosNotifier, Alignment.bottomLeft, constraints, pipW, pipH),
|
if (widget.showLeftPip)
|
||||||
if (widget.showRightPip) _buildFastPip(_rightPosNotifier, Alignment.bottomRight, constraints, pipW, pipH),
|
_buildFastPip(
|
||||||
|
_leftPosNotifier,
|
||||||
|
Alignment.bottomLeft,
|
||||||
|
constraints,
|
||||||
|
pipW,
|
||||||
|
pipH,
|
||||||
|
),
|
||||||
|
if (widget.showRightPip)
|
||||||
|
_buildFastPip(
|
||||||
|
_rightPosNotifier,
|
||||||
|
Alignment.bottomRight,
|
||||||
|
constraints,
|
||||||
|
pipW,
|
||||||
|
pipH,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -282,7 +485,12 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _correctPosition(ValueNotifier<Offset> notifier, BoxConstraints constraints, double w, double h) {
|
void _correctPosition(
|
||||||
|
ValueNotifier<Offset> notifier,
|
||||||
|
BoxConstraints constraints,
|
||||||
|
double w,
|
||||||
|
double h,
|
||||||
|
) {
|
||||||
double maxX = (constraints.maxWidth - w).clamp(0.0, double.infinity);
|
double maxX = (constraints.maxWidth - w).clamp(0.0, double.infinity);
|
||||||
double maxY = (constraints.maxHeight - h).clamp(0.0, double.infinity);
|
double maxY = (constraints.maxHeight - h).clamp(0.0, double.infinity);
|
||||||
double newX = notifier.value.dx.clamp(0.0, maxX);
|
double newX = notifier.value.dx.clamp(0.0, maxX);
|
||||||
@@ -292,7 +500,13 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFastPip(ValueNotifier<Offset> notifier, Alignment align, BoxConstraints constraints, double w, double h) {
|
Widget _buildFastPip(
|
||||||
|
ValueNotifier<Offset> notifier,
|
||||||
|
Alignment align,
|
||||||
|
BoxConstraints constraints,
|
||||||
|
double w,
|
||||||
|
double h,
|
||||||
|
) {
|
||||||
return ValueListenableBuilder<Offset>(
|
return ValueListenableBuilder<Offset>(
|
||||||
valueListenable: notifier,
|
valueListenable: notifier,
|
||||||
builder: (context, pos, child) {
|
builder: (context, pos, child) {
|
||||||
@@ -302,8 +516,14 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
final double nextX = (notifier.value.dx + details.delta.dx).clamp(0.0, (constraints.maxWidth - w).clamp(0.0, double.infinity));
|
final double nextX = (notifier.value.dx + details.delta.dx).clamp(
|
||||||
final double nextY = (notifier.value.dy + details.delta.dy).clamp(0.0, (constraints.maxHeight - h).clamp(0.0, double.infinity));
|
0.0,
|
||||||
|
(constraints.maxWidth - w).clamp(0.0, double.infinity),
|
||||||
|
);
|
||||||
|
final double nextY = (notifier.value.dy + details.delta.dy).clamp(
|
||||||
|
0.0,
|
||||||
|
(constraints.maxHeight - h).clamp(0.0, double.infinity),
|
||||||
|
);
|
||||||
notifier.value = Offset(nextX, nextY);
|
notifier.value = Offset(nextX, nextY);
|
||||||
},
|
},
|
||||||
child: child!,
|
child: child!,
|
||||||
@@ -315,7 +535,13 @@ class _WebRTCLocalPlayerState extends State<WebRTCLocalPlayer> {
|
|||||||
height: h,
|
height: h,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.5), blurRadius: 20, offset: const Offset(0, 8))],
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: _buildQuadrantView(alignment: align),
|
child: _buildQuadrantView(alignment: align),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ abstract class BindDeviceDatasource {
|
|||||||
Future<void> bindDevice({
|
Future<void> bindDevice({
|
||||||
required List<String> deviceIds,
|
required List<String> deviceIds,
|
||||||
required int orgId,
|
required int orgId,
|
||||||
required int siteId,
|
int? siteId,
|
||||||
required int userId,
|
int? userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,11 +105,13 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DeviceDataModel _parseDeviceFromJson(Map<String, dynamic> json) {
|
DeviceDataModel _parseDeviceFromJson(Map<String, dynamic> json) {
|
||||||
|
// 在线状态:使用 onlineStatus 字段(1=在线, 0=离线)
|
||||||
|
final onlineStatus = json['onlineStatus'] ?? 0;
|
||||||
return DeviceDataModel(
|
return DeviceDataModel(
|
||||||
deviceId: json['deviceId']?.toString() ?? '',
|
deviceId: json['deviceId']?.toString() ?? '',
|
||||||
name: json['deviceName'] ?? json['name'] ?? '',
|
name: json['deviceName'] ?? json['name'] ?? '',
|
||||||
type: json['deviceTypeName'] ?? json['type'] ?? '未知设备',
|
type: json['deviceTypeName'] ?? json['type'] ?? '未知设备',
|
||||||
status: _getStatusText(json['status'] ?? 0),
|
status: onlineStatus == 1 ? '在线' : '离线',
|
||||||
power: (json['power'] as num?)?.toDouble() ?? 0,
|
power: (json['power'] as num?)?.toDouble() ?? 0,
|
||||||
todayEnergy: (json['todayEnergy'] as num?)?.toDouble() ?? 0,
|
todayEnergy: (json['todayEnergy'] as num?)?.toDouble() ?? 0,
|
||||||
temperature: (json['temperature'] as num?)?.toDouble(),
|
temperature: (json['temperature'] as num?)?.toDouble(),
|
||||||
|
|||||||
@@ -127,18 +127,17 @@ class BindDeviceDatasourceImpl implements BindDeviceDatasource {
|
|||||||
Future<void> bindDevice({
|
Future<void> bindDevice({
|
||||||
required List<String> deviceIds,
|
required List<String> deviceIds,
|
||||||
required int orgId,
|
required int orgId,
|
||||||
required int siteId,
|
int? siteId,
|
||||||
required int userId,
|
int? userId,
|
||||||
}) async {
|
}) async {
|
||||||
final token = await _getToken();
|
final token = await _getToken();
|
||||||
|
final Map<String, dynamic> body = {'deviceIds': deviceIds, 'orgId': orgId};
|
||||||
|
if (siteId != null) body['siteId'] = siteId;
|
||||||
|
if (userId != null) body['userId'] = userId;
|
||||||
|
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
HttpApiConsts.bindDevice,
|
HttpApiConsts.bindDevice,
|
||||||
data: {
|
data: body,
|
||||||
'deviceIds': deviceIds,
|
|
||||||
'orgId': orgId,
|
|
||||||
'siteId': siteId,
|
|
||||||
'userId': userId,
|
|
||||||
},
|
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': token != null ? 'Bearer $token' : '',
|
'Authorization': token != null ? 'Bearer $token' : '',
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ class RobotDataModel {
|
|||||||
final String id;
|
final String id;
|
||||||
final String? alias; // 设备别名
|
final String? alias; // 设备别名
|
||||||
final String type;
|
final String type;
|
||||||
final String status;
|
final String status; // '在线' / '离线',基于 onlineStatus 字段
|
||||||
|
final bool isOnline; // 在线状态,基于接口 onlineStatus 字段
|
||||||
|
final int statusCode; // 接口原始 status 状态码
|
||||||
final double battery;
|
final double battery;
|
||||||
final String task;
|
final String task;
|
||||||
|
|
||||||
@@ -14,6 +16,8 @@ class RobotDataModel {
|
|||||||
this.alias,
|
this.alias,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.status,
|
required this.status,
|
||||||
|
this.isOnline = false,
|
||||||
|
this.statusCode = 0,
|
||||||
required this.battery,
|
required this.battery,
|
||||||
required this.task,
|
required this.task,
|
||||||
});
|
});
|
||||||
@@ -25,12 +29,21 @@ class RobotDataModel {
|
|||||||
(json['capacity_percent'] as num?)?.toDouble() ??
|
(json['capacity_percent'] as num?)?.toDouble() ??
|
||||||
100.0;
|
100.0;
|
||||||
|
|
||||||
|
// 在线状态:优先使用 onlineStatus 字段(1=在线, 0=离线)
|
||||||
|
final onlineStatusValue = json['onlineStatus'] ?? 0;
|
||||||
|
final isOnline = onlineStatusValue == 1;
|
||||||
|
|
||||||
|
// 设备状态码(原始 status 字段)
|
||||||
|
final statusCode = json['status'] ?? 0;
|
||||||
|
|
||||||
return RobotDataModel(
|
return RobotDataModel(
|
||||||
name: json['deviceName'] ?? json['name'] ?? '',
|
name: json['deviceName'] ?? json['name'] ?? '',
|
||||||
id: json['deviceId']?.toString() ?? json['id']?.toString() ?? '',
|
id: json['deviceId']?.toString() ?? json['id']?.toString() ?? '',
|
||||||
alias: json['deviceAlias'] as String?, // 获取别名字段
|
alias: json['deviceAlias'] as String?, // 获取别名字段
|
||||||
type: _convertType(json['deviceTypeName'] ?? json['type'] ?? '未知类型'),
|
type: _convertType(json['deviceTypeName'] ?? json['type'] ?? '未知类型'),
|
||||||
status: _getStatusText(json['status'] ?? 0),
|
status: isOnline ? '在线' : '离线',
|
||||||
|
isOnline: isOnline,
|
||||||
|
statusCode: statusCode is int ? statusCode : 0,
|
||||||
battery: batteryValue,
|
battery: batteryValue,
|
||||||
task: json['task'] ?? json['currentTask'] ?? '待机中',
|
task: json['task'] ?? json['currentTask'] ?? '待机中',
|
||||||
);
|
);
|
||||||
@@ -44,25 +57,13 @@ class RobotDataModel {
|
|||||||
'alias': alias,
|
'alias': alias,
|
||||||
'type': type,
|
'type': type,
|
||||||
'status': status,
|
'status': status,
|
||||||
|
'isOnline': isOnline,
|
||||||
|
'statusCode': statusCode,
|
||||||
'battery': battery,
|
'battery': battery,
|
||||||
'task': task,
|
'task': task,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 状态码转换
|
|
||||||
static String _getStatusText(int status) {
|
|
||||||
switch (status) {
|
|
||||||
case 1:
|
|
||||||
return '在线';
|
|
||||||
case 2:
|
|
||||||
return '离线';
|
|
||||||
case 3:
|
|
||||||
return '异常';
|
|
||||||
default:
|
|
||||||
return '未知';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String _convertType(String type) {
|
static String _convertType(String type) {
|
||||||
if (type.contains('巡检')) {
|
if (type.contains('巡检')) {
|
||||||
return 'inspection';
|
return 'inspection';
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ class BindDeviceRepositoryImpl implements BindDeviceRepository {
|
|||||||
Future<Either<Failure, void>> bindDevice({
|
Future<Either<Failure, void>> bindDevice({
|
||||||
required List<String> deviceIds,
|
required List<String> deviceIds,
|
||||||
required int orgId,
|
required int orgId,
|
||||||
required int siteId,
|
int? siteId,
|
||||||
required int userId,
|
int? userId,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _datasource.bindDevice(
|
await _datasource.bindDevice(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ abstract class BindDeviceRepository {
|
|||||||
Future<Either<Failure, void>> bindDevice({
|
Future<Either<Failure, void>> bindDevice({
|
||||||
required List<String> deviceIds,
|
required List<String> deviceIds,
|
||||||
required int orgId,
|
required int orgId,
|
||||||
required int siteId,
|
int? siteId,
|
||||||
required int userId,
|
int? userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class BindDeviceV2UseCase {
|
|||||||
Future<Either<Failure, void>> call({
|
Future<Either<Failure, void>> call({
|
||||||
required List<String> deviceIds,
|
required List<String> deviceIds,
|
||||||
required int orgId,
|
required int orgId,
|
||||||
required int siteId,
|
int? siteId,
|
||||||
required int userId,
|
int? userId,
|
||||||
}) => _repository.bindDevice(
|
}) => _repository.bindDevice(
|
||||||
deviceIds: deviceIds,
|
deviceIds: deviceIds,
|
||||||
orgId: orgId,
|
orgId: orgId,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
||||||
@@ -24,15 +25,51 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
) : super(const BindDeviceState());
|
) : super(const BindDeviceState());
|
||||||
|
|
||||||
UserEntity? get _currentUser => _appUserCubit.state.user;
|
UserEntity? get _currentUser => _appUserCubit.state.user;
|
||||||
String get _roleKey => _currentUser?.roleKey ?? 'user';
|
String get roleKey => state.roleKey;
|
||||||
|
bool get isOrgEnabled => roleKey == 'admin';
|
||||||
|
bool get isSiteEnabled => roleKey == 'admin' || roleKey == 'manager';
|
||||||
|
bool get isUserEnabled =>
|
||||||
|
roleKey == 'admin' || roleKey == 'manager' || roleKey == 'siteManager';
|
||||||
|
|
||||||
|
// ── 去重辅助 ──
|
||||||
|
List<OrgEntity> _uniqueOrgs(List<OrgEntity> list) {
|
||||||
|
final seen = <int>{};
|
||||||
|
return list.where((e) => seen.add(e.id)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SiteEntity> _uniqueSites(List<SiteEntity> list) {
|
||||||
|
final seen = <int>{};
|
||||||
|
return list.where((e) => seen.add(e.id)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserSimpleEntity> _uniqueUsers(List<UserSimpleEntity> list) {
|
||||||
|
final seen = <int>{};
|
||||||
|
return list.where((e) => seen.add(e.id)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 确保 selectedId 存在于列表中,否则置 null
|
||||||
|
int? _validSelected(int? id, List<dynamic> list) {
|
||||||
|
if (id == null) return null;
|
||||||
|
final ids = list.map((e) => e.id as int).toSet();
|
||||||
|
return ids.contains(id) ? id : null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
final user = _currentUser;
|
final user = _currentUser;
|
||||||
if (user == null) return;
|
if (user == null) {
|
||||||
|
debugPrint(
|
||||||
|
'>>> [BindDevice] ERROR: _currentUser is null! AppUserCubit.state.user is null. Cannot proceed.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit(state.copyWith(isLoading: true));
|
debugPrint(
|
||||||
|
'>>> [BindDevice] roleKey=${user.roleKey}, orgId=${user.orgId}, siteId=${user.siteId}, userId=${user.userId}, username=${user.username}',
|
||||||
|
);
|
||||||
|
|
||||||
switch (_roleKey) {
|
emit(state.copyWith(isLoading: true, roleKey: user.roleKey ?? 'user'));
|
||||||
|
|
||||||
|
switch (roleKey) {
|
||||||
case 'admin':
|
case 'admin':
|
||||||
await _loadAllOrgs();
|
await _loadAllOrgs();
|
||||||
break;
|
break;
|
||||||
@@ -57,7 +94,15 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
result.fold(
|
result.fold(
|
||||||
(failure) =>
|
(failure) =>
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
(orgs) => emit(state.copyWith(orgList: orgs)),
|
(orgs) {
|
||||||
|
final unique = _uniqueOrgs(orgs);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
orgList: unique,
|
||||||
|
selectedOrgId: _validSelected(state.selectedOrgId, unique),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +112,9 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
(failure) =>
|
(failure) =>
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
(orgs) async {
|
(orgs) async {
|
||||||
emit(state.copyWith(orgList: orgs, selectedOrgId: user.orgId));
|
final unique = _ensureOrgInList(_uniqueOrgs(orgs), user.orgId);
|
||||||
await _loadSitesByOrg(user.orgId);
|
emit(state.copyWith(orgList: unique, selectedOrgId: user.orgId));
|
||||||
|
await _loadSitesByOrgForManager(user.orgId, user.siteId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,7 +125,8 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
(failure) =>
|
(failure) =>
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
(orgs) async {
|
(orgs) async {
|
||||||
emit(state.copyWith(orgList: orgs, selectedOrgId: user.orgId));
|
final unique = _ensureOrgInList(_uniqueOrgs(orgs), user.orgId);
|
||||||
|
emit(state.copyWith(orgList: unique, selectedOrgId: user.orgId));
|
||||||
await _loadSitesAndUsers(user);
|
await _loadSitesAndUsers(user);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -91,18 +138,65 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
(failure) =>
|
(failure) =>
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
(orgs) async {
|
(orgs) async {
|
||||||
emit(state.copyWith(orgList: orgs, selectedOrgId: user.orgId));
|
final unique = _ensureOrgInList(_uniqueOrgs(orgs), user.orgId);
|
||||||
|
emit(state.copyWith(orgList: unique, selectedOrgId: user.orgId));
|
||||||
await _loadSitesAndUsers(user);
|
await _loadSitesAndUsers(user);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<OrgEntity> _ensureOrgInList(List<OrgEntity> orgs, int orgId) {
|
||||||
|
if (orgs.any((e) => e.id == orgId)) return orgs;
|
||||||
|
return [...orgs, OrgEntity(id: orgId, name: '组织 #$orgId')];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SiteEntity> _ensureSiteInList(List<SiteEntity> sites, int? siteId) {
|
||||||
|
if (siteId == null) return sites;
|
||||||
|
if (sites.any((e) => e.id == siteId)) return sites;
|
||||||
|
return [...sites, SiteEntity(id: siteId, name: '场站 #$siteId')];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserSimpleEntity> _ensureUserInList(
|
||||||
|
List<UserSimpleEntity> users,
|
||||||
|
UserEntity user,
|
||||||
|
) {
|
||||||
|
final uid = int.tryParse(user.userId) ?? 0;
|
||||||
|
if (users.any((e) => e.id == uid)) return users;
|
||||||
|
return [...users, UserSimpleEntity(id: uid, name: user.nickname)];
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadSitesByOrg(int orgId) async {
|
Future<void> _loadSitesByOrg(int orgId) async {
|
||||||
final result = await _getSitesByOrgUseCase(orgId);
|
final result = await _getSitesByOrgUseCase(orgId);
|
||||||
result.fold(
|
result.fold(
|
||||||
(failure) =>
|
(failure) =>
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
(sites) => emit(state.copyWith(siteList: sites)),
|
(sites) {
|
||||||
|
final unique = _uniqueSites(sites);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
siteList: unique,
|
||||||
|
selectedSiteId: _validSelected(state.selectedSiteId, unique),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSitesByOrgForManager(int orgId, int? siteId) async {
|
||||||
|
final result = await _getSitesByOrgUseCase(orgId);
|
||||||
|
result.fold(
|
||||||
|
(failure) =>
|
||||||
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
|
(sites) {
|
||||||
|
final unique = _ensureSiteInList(_uniqueSites(sites), siteId);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
siteList: unique,
|
||||||
|
selectedSiteId:
|
||||||
|
siteId ?? _validSelected(state.selectedSiteId, unique),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +206,8 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
(failure) =>
|
(failure) =>
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
||||||
(sites) async {
|
(sites) async {
|
||||||
emit(state.copyWith(siteList: sites, selectedSiteId: user.siteId));
|
final unique = _ensureSiteInList(_uniqueSites(sites), user.siteId);
|
||||||
|
emit(state.copyWith(siteList: unique, selectedSiteId: user.siteId));
|
||||||
if (user.siteId != null) {
|
if (user.siteId != null) {
|
||||||
await _loadUsers(user);
|
await _loadUsers(user);
|
||||||
} else {
|
} else {
|
||||||
@@ -139,23 +234,21 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
name: user.nickname,
|
name: user.nickname,
|
||||||
);
|
);
|
||||||
result.fold(
|
result.fold(
|
||||||
(failure) =>
|
(failure) {
|
||||||
emit(state.copyWith(errorMessage: failure.message, isLoading: false)),
|
final list = _ensureUserInList([], user);
|
||||||
(users) {
|
|
||||||
final allUsers = <UserSimpleEntity>[];
|
|
||||||
final seenIds = <int>{};
|
|
||||||
for (final u in users) {
|
|
||||||
if (u.id != currentUser.id && !seenIds.contains(u.id)) {
|
|
||||||
allUsers.add(u);
|
|
||||||
seenIds.add(u.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!seenIds.contains(currentUser.id)) {
|
|
||||||
allUsers.add(currentUser);
|
|
||||||
}
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
userList: allUsers,
|
userList: list,
|
||||||
|
selectedUserId: currentUser.id,
|
||||||
|
isLoading: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(users) {
|
||||||
|
final unique = _ensureUserInList(_uniqueUsers(users), user);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
userList: unique,
|
||||||
selectedUserId: currentUser.id,
|
selectedUserId: currentUser.id,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
),
|
),
|
||||||
@@ -179,7 +272,10 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
final result = await _getSitesByOrgUseCase(orgId);
|
final result = await _getSitesByOrgUseCase(orgId);
|
||||||
result.fold(
|
result.fold(
|
||||||
(failure) => emit(state.copyWith(errorMessage: failure.message)),
|
(failure) => emit(state.copyWith(errorMessage: failure.message)),
|
||||||
(sites) => emit(state.copyWith(siteList: sites)),
|
(sites) {
|
||||||
|
final unique = _uniqueSites(sites);
|
||||||
|
emit(state.copyWith(siteList: unique));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +292,10 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
final result = await _getUsersBySiteUseCase(siteId);
|
final result = await _getUsersBySiteUseCase(siteId);
|
||||||
result.fold(
|
result.fold(
|
||||||
(failure) => emit(state.copyWith(errorMessage: failure.message)),
|
(failure) => emit(state.copyWith(errorMessage: failure.message)),
|
||||||
(users) => emit(state.copyWith(userList: users)),
|
(users) {
|
||||||
|
final unique = _uniqueUsers(users);
|
||||||
|
emit(state.copyWith(userList: unique));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,32 +304,39 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> submitBind(String deviceId) async {
|
Future<bool> submitBind(String deviceId) async {
|
||||||
if (state.selectedOrgId == null ||
|
if (state.selectedOrgId == null) {
|
||||||
state.selectedSiteId == null ||
|
emit(state.copyWith(errorMessage: '请选择所属组织'));
|
||||||
state.selectedUserId == null) {
|
return false;
|
||||||
emit(state.copyWith(errorMessage: '请选择完整的绑定信息'));
|
}
|
||||||
|
|
||||||
|
if (roleKey == 'user' && state.selectedUserId == null) {
|
||||||
|
emit(state.copyWith(errorMessage: '请选择负责人'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(isSubmitting: true, errorMessage: null));
|
emit(state.copyWith(isSubmitting: true, errorMessage: null));
|
||||||
|
|
||||||
final existResult = await _isDeviceAtSiteUseCase(
|
if (state.selectedSiteId != null) {
|
||||||
deviceId: deviceId,
|
final existResult = await _isDeviceAtSiteUseCase(
|
||||||
siteId: state.selectedSiteId!,
|
deviceId: deviceId,
|
||||||
);
|
siteId: state.selectedSiteId!,
|
||||||
|
);
|
||||||
|
|
||||||
final isAtSite = existResult.fold((failure) => false, (exists) => exists);
|
final isAtSite = existResult.fold((failure) => false, (exists) => exists);
|
||||||
|
|
||||||
if (isAtSite) {
|
if (isAtSite) {
|
||||||
emit(state.copyWith(isSubmitting: false, errorMessage: '该设备已在当前场站,无需绑定'));
|
emit(
|
||||||
return false;
|
state.copyWith(isSubmitting: false, errorMessage: '该设备已在当前场站,无需绑定'),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await _bindDeviceUseCase(
|
final result = await _bindDeviceUseCase(
|
||||||
deviceIds: [deviceId],
|
deviceIds: [deviceId],
|
||||||
orgId: state.selectedOrgId!,
|
orgId: state.selectedOrgId!,
|
||||||
siteId: state.selectedSiteId!,
|
siteId: state.selectedSiteId,
|
||||||
userId: state.selectedUserId!,
|
userId: state.selectedUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
@@ -246,11 +352,4 @@ class BindDeviceCubit extends Cubit<BindDeviceState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isOrgEnabled => _roleKey == 'admin';
|
|
||||||
bool get isSiteEnabled => _roleKey == 'admin' || _roleKey == 'manager';
|
|
||||||
bool get isUserEnabled =>
|
|
||||||
_roleKey == 'admin' || _roleKey == 'manager' || _roleKey == 'siteManager';
|
|
||||||
|
|
||||||
String get roleKey => _roleKey;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class BindDeviceState extends Equatable {
|
|||||||
final int? selectedOrgId;
|
final int? selectedOrgId;
|
||||||
final int? selectedSiteId;
|
final int? selectedSiteId;
|
||||||
final int? selectedUserId;
|
final int? selectedUserId;
|
||||||
|
final String roleKey;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isSubmitting;
|
final bool isSubmitting;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
@@ -20,6 +21,7 @@ class BindDeviceState extends Equatable {
|
|||||||
this.selectedOrgId,
|
this.selectedOrgId,
|
||||||
this.selectedSiteId,
|
this.selectedSiteId,
|
||||||
this.selectedUserId,
|
this.selectedUserId,
|
||||||
|
this.roleKey = 'user',
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
this.isSubmitting = false,
|
this.isSubmitting = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
@@ -33,6 +35,7 @@ class BindDeviceState extends Equatable {
|
|||||||
int? selectedOrgId,
|
int? selectedOrgId,
|
||||||
int? selectedSiteId,
|
int? selectedSiteId,
|
||||||
int? selectedUserId,
|
int? selectedUserId,
|
||||||
|
String? roleKey,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isSubmitting,
|
bool? isSubmitting,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
@@ -45,6 +48,7 @@ class BindDeviceState extends Equatable {
|
|||||||
selectedOrgId: selectedOrgId ?? this.selectedOrgId,
|
selectedOrgId: selectedOrgId ?? this.selectedOrgId,
|
||||||
selectedSiteId: selectedSiteId ?? this.selectedSiteId,
|
selectedSiteId: selectedSiteId ?? this.selectedSiteId,
|
||||||
selectedUserId: selectedUserId ?? this.selectedUserId,
|
selectedUserId: selectedUserId ?? this.selectedUserId,
|
||||||
|
roleKey: roleKey ?? this.roleKey,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
isSubmitting: isSubmitting ?? this.isSubmitting,
|
isSubmitting: isSubmitting ?? this.isSubmitting,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
@@ -60,9 +64,10 @@ class BindDeviceState extends Equatable {
|
|||||||
selectedOrgId,
|
selectedOrgId,
|
||||||
selectedSiteId,
|
selectedSiteId,
|
||||||
selectedUserId,
|
selectedUserId,
|
||||||
|
roleKey,
|
||||||
isLoading,
|
isLoading,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_buildRoleTag(state.roleKey),
|
||||||
|
const SizedBox(height: 16),
|
||||||
_buildRequiredField(
|
_buildRequiredField(
|
||||||
label: '已选装备 ID',
|
label: '已选装备 ID',
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -126,10 +128,15 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
child: _buildSiteDropdown(state),
|
child: _buildSiteDropdown(state),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildField(
|
_cubit.roleKey == 'user'
|
||||||
label: '负责人',
|
? _buildRequiredField(
|
||||||
child: _buildUserDropdown(state),
|
label: '负责人',
|
||||||
),
|
child: _buildUserDropdown(state),
|
||||||
|
)
|
||||||
|
: _buildField(
|
||||||
|
label: '负责人',
|
||||||
|
child: _buildUserDropdown(state),
|
||||||
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -206,6 +213,12 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? _validValue(List<dynamic> list, int? selectedId) {
|
||||||
|
if (selectedId == null) return null;
|
||||||
|
final count = list.where((e) => e.id == selectedId).length;
|
||||||
|
return count == 1 ? selectedId : null;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildOrgDropdown(BindDeviceState state) {
|
Widget _buildOrgDropdown(BindDeviceState state) {
|
||||||
final enabled = _cubit.isOrgEnabled;
|
final enabled = _cubit.isOrgEnabled;
|
||||||
final selectedName = state.selectedOrgId != null
|
final selectedName = state.selectedOrgId != null
|
||||||
@@ -214,23 +227,22 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
.fold<String?>(null, (_, org) => org.name)
|
.fold<String?>(null, (_, org) => org.name)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!enabled && selectedName != null) {
|
if (!enabled) {
|
||||||
return _buildReadonlyField(selectedName);
|
return _buildReadonlyField(selectedName ?? '未分配组织');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final validValue = _validValue(state.orgList, state.selectedOrgId);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: enabled ? Colors.white : const Color(0xFFF7F8FA),
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE5E6EB)),
|
border: Border.all(color: const Color(0xFFE5E6EB)),
|
||||||
),
|
),
|
||||||
child: DropdownButtonFormField<int>(
|
child: DropdownButtonFormField<int>(
|
||||||
value: state.selectedOrgId,
|
value: validValue,
|
||||||
decoration: _dropdownDecoration(enabled),
|
decoration: _dropdownDecoration(enabled, '请选择'),
|
||||||
icon: Icon(
|
icon: const Icon(Icons.expand_more, color: Color(0xFF86909C)),
|
||||||
Icons.expand_more,
|
|
||||||
color: enabled ? const Color(0xFF86909C) : const Color(0xFFC9CDD4),
|
|
||||||
),
|
|
||||||
items: state.orgList.map((org) {
|
items: state.orgList.map((org) {
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
value: org.id,
|
value: org.id,
|
||||||
@@ -241,11 +253,9 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: enabled
|
onChanged: (v) {
|
||||||
? (v) {
|
if (v != null) _cubit.onOrgChanged(v);
|
||||||
if (v != null) _cubit.onOrgChanged(v);
|
},
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,23 +268,23 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
.fold<String?>(null, (_, site) => site.name)
|
.fold<String?>(null, (_, site) => site.name)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!enabled && selectedName != null) {
|
if (!enabled) {
|
||||||
return _buildReadonlyField(selectedName);
|
return _buildReadonlyField(selectedName ?? '未分配场站');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hintText = state.selectedOrgId == null ? '请先选择组织' : '请选择';
|
||||||
|
final validValue = _validValue(state.siteList, state.selectedSiteId);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: enabled ? Colors.white : const Color(0xFFF7F8FA),
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE5E6EB)),
|
border: Border.all(color: const Color(0xFFE5E6EB)),
|
||||||
),
|
),
|
||||||
child: DropdownButtonFormField<int>(
|
child: DropdownButtonFormField<int>(
|
||||||
value: state.selectedSiteId,
|
value: validValue,
|
||||||
decoration: _dropdownDecoration(enabled),
|
decoration: _dropdownDecoration(enabled, hintText),
|
||||||
icon: Icon(
|
icon: const Icon(Icons.expand_more, color: Color(0xFF86909C)),
|
||||||
Icons.expand_more,
|
|
||||||
color: enabled ? const Color(0xFF86909C) : const Color(0xFFC9CDD4),
|
|
||||||
),
|
|
||||||
items: state.siteList.map((site) {
|
items: state.siteList.map((site) {
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
value: site.id,
|
value: site.id,
|
||||||
@@ -285,11 +295,9 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: enabled
|
onChanged: (v) {
|
||||||
? (v) {
|
if (v != null) _cubit.onSiteChanged(v);
|
||||||
if (v != null) _cubit.onSiteChanged(v);
|
},
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -302,23 +310,23 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
.fold<String?>(null, (_, user) => user.name)
|
.fold<String?>(null, (_, user) => user.name)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!enabled && selectedName != null) {
|
if (!enabled) {
|
||||||
return _buildReadonlyField(selectedName);
|
return _buildReadonlyField(selectedName ?? '未分配负责人');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hintText = state.selectedSiteId == null ? '请先选择场站' : '请选择';
|
||||||
|
final validValue = _validValue(state.userList, state.selectedUserId);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: enabled ? Colors.white : const Color(0xFFF7F8FA),
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFE5E6EB)),
|
border: Border.all(color: const Color(0xFFE5E6EB)),
|
||||||
),
|
),
|
||||||
child: DropdownButtonFormField<int>(
|
child: DropdownButtonFormField<int>(
|
||||||
value: state.selectedUserId,
|
value: validValue,
|
||||||
decoration: _dropdownDecoration(enabled),
|
decoration: _dropdownDecoration(enabled, hintText),
|
||||||
icon: Icon(
|
icon: const Icon(Icons.expand_more, color: Color(0xFF86909C)),
|
||||||
Icons.expand_more,
|
|
||||||
color: enabled ? const Color(0xFF86909C) : const Color(0xFFC9CDD4),
|
|
||||||
),
|
|
||||||
items: state.userList.map((user) {
|
items: state.userList.map((user) {
|
||||||
return DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
value: user.id,
|
value: user.id,
|
||||||
@@ -329,11 +337,37 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: enabled
|
onChanged: (v) {
|
||||||
? (v) {
|
if (v != null) _cubit.onUserChanged(v);
|
||||||
_cubit.onUserChanged(v);
|
},
|
||||||
}
|
),
|
||||||
: null,
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRoleTag(String roleKey) {
|
||||||
|
final roleLabel =
|
||||||
|
const {
|
||||||
|
'admin': '管理员',
|
||||||
|
'manager': '组织管理员',
|
||||||
|
'siteManager': '场站管理员',
|
||||||
|
'user': '普通用户',
|
||||||
|
}[roleKey] ??
|
||||||
|
'普通用户';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE8F3FF),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: const Color(0xFF165DFF), width: 0.5),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'当前角色:$roleLabel($roleKey)',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF165DFF),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -361,14 +395,14 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _dropdownDecoration(bool enabled) {
|
InputDecoration _dropdownDecoration(bool enabled, String hintText) {
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
enabledBorder: InputBorder.none,
|
enabledBorder: InputBorder.none,
|
||||||
focusedBorder: InputBorder.none,
|
focusedBorder: InputBorder.none,
|
||||||
hintText: enabled ? '请选择' : '请选择',
|
hintText: hintText,
|
||||||
hintStyle: const TextStyle(color: Color(0xFF86909C), fontSize: 14),
|
hintStyle: const TextStyle(color: Color(0xFF86909C), fontSize: 14),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -618,8 +618,8 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
productName: robot.type,
|
productName: robot.type,
|
||||||
tenantId: 0,
|
tenantId: 0,
|
||||||
tenantName: '',
|
tenantName: '',
|
||||||
status: robot.status == '在线' ? 1 : 0,
|
status: robot.isOnline ? 1 : 0,
|
||||||
onlineStatus: robot.status == '在线' ? 1 : 0,
|
onlineStatus: robot.isOnline ? 1 : 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
final remoteCubit = GetIt.I<RemoteControlCubit>();
|
final remoteCubit = GetIt.I<RemoteControlCubit>();
|
||||||
@@ -784,6 +784,13 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
DeviceListState.DeviceStatusLoaded state,
|
DeviceListState.DeviceStatusLoaded state,
|
||||||
) {
|
) {
|
||||||
|
// 从实际设备列表计算统计数据(不再使用硬编码的假数据)
|
||||||
|
final devices = state.devices;
|
||||||
|
final totalCount = devices.length;
|
||||||
|
final onlineCount = devices.where((d) => d.status == '在线').length;
|
||||||
|
final exceptionCount = devices.where((d) => d.status == '异常').length;
|
||||||
|
final offlineCount = totalCount - onlineCount - exceptionCount;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@@ -801,7 +808,7 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
value: state.deviceStatus.total.toString(),
|
value: totalCount.toString(),
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).translate('device_list_v2.total_devices'),
|
).translate('device_list_v2.total_devices'),
|
||||||
@@ -810,7 +817,7 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
),
|
),
|
||||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
value: state.deviceStatus.online.toString(),
|
value: onlineCount.toString(),
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).translate('device_list_v2.online'),
|
).translate('device_list_v2.online'),
|
||||||
@@ -819,7 +826,7 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
),
|
),
|
||||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
value: state.deviceStatus.exception.toString(),
|
value: exceptionCount.toString(),
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).translate('device_list_v2.alarm'),
|
).translate('device_list_v2.alarm'),
|
||||||
@@ -828,7 +835,7 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
),
|
),
|
||||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||||
_buildStatItem(
|
_buildStatItem(
|
||||||
value: state.deviceStatus.offline.toString(),
|
value: offlineCount.toString(),
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
).translate('device_list_v2.offline'),
|
).translate('device_list_v2.offline'),
|
||||||
|
|||||||
@@ -109,11 +109,11 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
final weedingRobots = allRobots.where((r) => r.type == 'weeding').toList();
|
final weedingRobots = allRobots.where((r) => r.type == 'weeding').toList();
|
||||||
|
|
||||||
final onlineInspection = inspectionRobots
|
final onlineInspection = inspectionRobots
|
||||||
.where((r) => r.status == '在线')
|
.where((r) => r.isOnline)
|
||||||
.length;
|
.length;
|
||||||
final onlineCleaning = cleaningRobots.where((r) => r.status == '在线').length;
|
final onlineCleaning = cleaningRobots.where((r) => r.isOnline).length;
|
||||||
// 暂且所有设备都视为除草机器人,在线数使用总数
|
// 暂且所有设备都视为除草机器人,在线数使用总数
|
||||||
final onlineWeeding = allRobots.where((r) => r.status == '在线').length;
|
final onlineWeeding = allRobots.where((r) => r.isOnline).length;
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
@@ -385,14 +385,12 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
|
|
||||||
Widget _buildQuickActions(RobotListLoaded state) {
|
Widget _buildQuickActions(RobotListLoaded state) {
|
||||||
// 计算统计数据
|
// 计算统计数据
|
||||||
final onlineCount = state.robots.where((r) => r.status == '在线').length;
|
final onlineCount = state.robots.where((r) => r.isOnline).length;
|
||||||
final workingCount = state.robots
|
final workingCount = state.robots
|
||||||
.where((r) => r.task != '待机中' && r.task != '充电中')
|
.where((r) => r.task != '待机中' && r.task != '充电中')
|
||||||
.length;
|
.length;
|
||||||
final standbyCount = state.robots.where((r) => r.task == '待机中').length;
|
final standbyCount = state.robots.where((r) => r.task == '待机中').length;
|
||||||
final faultCount = state.robots
|
final faultCount = state.robots.where((r) => !r.isOnline).length;
|
||||||
.where((r) => r.status == '异常' || r.status == '离线')
|
|
||||||
.length;
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
@@ -429,7 +427,7 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||||
_buildStatusItem(
|
_buildStatusItem(
|
||||||
value: '$faultCount',
|
value: '$faultCount',
|
||||||
label: '故障数',
|
label: '离线数',
|
||||||
valueColor: const Color(0xFFF53F3F),
|
valueColor: const Color(0xFFF53F3F),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -474,6 +472,8 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
id: '',
|
id: '',
|
||||||
type: '',
|
type: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
isOnline: false,
|
||||||
|
statusCode: 0,
|
||||||
battery: 0,
|
battery: 0,
|
||||||
task: '无任务',
|
task: '无任务',
|
||||||
),
|
),
|
||||||
@@ -641,8 +641,8 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
productName: robot.type,
|
productName: robot.type,
|
||||||
tenantId: 0,
|
tenantId: 0,
|
||||||
tenantName: '',
|
tenantName: '',
|
||||||
status: robot.status == '在线' ? 1 : 0,
|
status: robot.isOnline ? 1 : 0,
|
||||||
onlineStatus: robot.status == '在线' ? 1 : 0,
|
onlineStatus: robot.isOnline ? 1 : 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
|
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
|
||||||
|
|||||||
@@ -110,20 +110,20 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
|||||||
|
|
||||||
void _initFields() {
|
void _initFields() {
|
||||||
_osdFields.addAll([
|
_osdFields.addAll([
|
||||||
|
{
|
||||||
|
'icon': Icons.battery_full_rounded,
|
||||||
|
'label': '电量',
|
||||||
|
'key': 'battery',
|
||||||
|
'value': '未知',
|
||||||
|
'color': const Color(0xFF00B42A),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.height_rounded,
|
'icon': Icons.height_rounded,
|
||||||
'label': '飞行高度',
|
'label': '高度',
|
||||||
'key': 'height',
|
'key': 'height',
|
||||||
'value': '未知',
|
'value': '未知',
|
||||||
'color': const Color(0xFF165DFF),
|
'color': const Color(0xFF165DFF),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'icon': Icons.speed_rounded,
|
|
||||||
'label': '飞行速度',
|
|
||||||
'key': 'speed',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF722ED1),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'icon': Icons.trending_flat_rounded,
|
'icon': Icons.trending_flat_rounded,
|
||||||
'label': '水平速度',
|
'label': '水平速度',
|
||||||
@@ -138,34 +138,6 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
|||||||
'value': '未知',
|
'value': '未知',
|
||||||
'color': const Color(0xFF00B42A),
|
'color': const Color(0xFF00B42A),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'icon': Icons.route_rounded,
|
|
||||||
'label': '飞行距离',
|
|
||||||
'key': 'distance',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF00B42A),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.battery_full_rounded,
|
|
||||||
'label': '电量',
|
|
||||||
'key': 'battery',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF00B42A),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.bolt_rounded,
|
|
||||||
'label': '电池电压',
|
|
||||||
'key': 'voltage',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF00B42A),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.thermostat_rounded,
|
|
||||||
'label': '电池温度',
|
|
||||||
'key': 'batteryTemp',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF165DFF),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'icon': Icons.navigation_rounded,
|
'icon': Icons.navigation_rounded,
|
||||||
'label': '航向角',
|
'label': '航向角',
|
||||||
@@ -173,48 +145,20 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
|||||||
'value': '未知',
|
'value': '未知',
|
||||||
'color': const Color(0xFF165DFF),
|
'color': const Color(0xFF165DFF),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'icon': Icons.rotate_90_degrees_cw_rounded,
|
|
||||||
'label': '俯仰角',
|
|
||||||
'key': 'pitch',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF722ED1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.rotate_right_rounded,
|
|
||||||
'label': '横滚角',
|
|
||||||
'key': 'roll',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF722ED1),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'icon': Icons.satellite_rounded,
|
'icon': Icons.satellite_rounded,
|
||||||
'label': 'GPS卫星',
|
'label': 'GPS',
|
||||||
'key': 'gps',
|
'key': 'gps',
|
||||||
'value': '未知',
|
'value': '未知',
|
||||||
'color': const Color(0xFF00B42A),
|
'color': const Color(0xFF00B42A),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'icon': Icons.satellite_alt_rounded,
|
'icon': Icons.satellite_alt_rounded,
|
||||||
'label': 'RTK卫星',
|
'label': 'RTK',
|
||||||
'key': 'rtk',
|
'key': 'rtk',
|
||||||
'value': '未知',
|
'value': '未知',
|
||||||
'color': const Color(0xFF00B42A),
|
'color': const Color(0xFF00B42A),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'icon': Icons.home_rounded,
|
|
||||||
'label': '无人机状态',
|
|
||||||
'key': 'droneState',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF165DFF),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'icon': Icons.flight_rounded,
|
|
||||||
'label': '飞行模式',
|
|
||||||
'key': 'flightMode',
|
|
||||||
'value': '未知',
|
|
||||||
'color': const Color(0xFF165DFF),
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,78 +207,45 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
|||||||
(droneData['height'] as num?)?.toDouble() ??
|
(droneData['height'] as num?)?.toDouble() ??
|
||||||
(droneData['altitude'] as num?)?.toDouble();
|
(droneData['altitude'] as num?)?.toDouble();
|
||||||
|
|
||||||
double? groundSpeed = (droneData['ground_speed'] as num?)?.toDouble();
|
double? horizontalSpeed = (droneData['horizontal_speed'] as num?)
|
||||||
double? flightDistance = (droneData['flight_distance'] as num?)?.toDouble();
|
?.toDouble();
|
||||||
int? flightTime = droneData['flight_time'] as int?;
|
double? verticalSpeed = (droneData['vertical_speed'] as num?)?.toDouble();
|
||||||
|
|
||||||
final batteryMap = droneData['battery'] as Map?;
|
final batteryMap = droneData['battery'] as Map?;
|
||||||
int? batteryPercent =
|
int? batteryPercent =
|
||||||
droneData['battery_percent'] as int? ??
|
droneData['battery_percent'] as int? ??
|
||||||
batteryMap?['capacity_percent'] as int?;
|
batteryMap?['capacity_percent'] as int?;
|
||||||
|
|
||||||
double? batteryVoltage = (droneData['battery_voltage'] as num?)?.toDouble();
|
|
||||||
if (batteryVoltage == null && batteryMap != null) {
|
|
||||||
final batteries = batteryMap['batteries'] as List?;
|
|
||||||
if (batteries != null && batteries.isNotEmpty) {
|
|
||||||
final firstBattery = batteries[0] as Map?;
|
|
||||||
batteryVoltage = (firstBattery?['voltage'] as num?)?.toDouble();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (batteryVoltage != null && batteryVoltage > 1000) {
|
|
||||||
batteryVoltage = batteryVoltage / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
double? batteryTemp = (droneData['battery_temperature'] as num?)
|
|
||||||
?.toDouble();
|
|
||||||
if (batteryTemp == null && batteryMap != null) {
|
|
||||||
final batteries = batteryMap['batteries'] as List?;
|
|
||||||
if (batteries != null && batteries.isNotEmpty) {
|
|
||||||
final firstBattery = batteries[0] as Map?;
|
|
||||||
batteryTemp = (firstBattery?['temperature'] as num?)?.toDouble();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double? heading =
|
double? heading =
|
||||||
(droneData['heading'] as num?)?.toDouble() ??
|
(droneData['heading'] as num?)?.toDouble() ??
|
||||||
(droneData['attitude_head'] as num?)?.toDouble();
|
(droneData['attitude_head'] as num?)?.toDouble();
|
||||||
double? pitch =
|
|
||||||
(droneData['pitch'] as num?)?.toDouble() ??
|
|
||||||
(droneData['attitude_pitch'] as num?)?.toDouble();
|
|
||||||
double? roll =
|
|
||||||
(droneData['roll'] as num?)?.toDouble() ??
|
|
||||||
(droneData['attitude_roll'] as num?)?.toDouble();
|
|
||||||
|
|
||||||
int? gpsSatellites = droneData['gps_satellites'] as int?;
|
// GPS/RTK:DJI Cloud API 中 GPS 卫星数和 RTK 定位状态均位于 position_state 对象内
|
||||||
int? rtkSatellites = droneData['rtk_satellites'] as int?;
|
// - position_state.gps_number:GPS 卫星数
|
||||||
|
// - position_state.is_fixed:RTK 定位状态(0=未定位, 1=浮点解, 2=固定解)
|
||||||
double? horizontalSpeed = (droneData['horizontal_speed'] as num?)
|
final positionState = droneData['position_state'];
|
||||||
?.toDouble();
|
int? gpsSatellites;
|
||||||
double? verticalSpeed = (droneData['vertical_speed'] as num?)?.toDouble();
|
int? rtkFixed;
|
||||||
|
if (positionState is Map) {
|
||||||
String? droneState = droneData['drone_state'] as String?;
|
gpsSatellites = positionState['gps_number'] as int?;
|
||||||
if (droneState == null) {
|
rtkFixed = positionState['is_fixed'] as int?;
|
||||||
final inHangar = droneData['in_hangar'] as int?;
|
|
||||||
if (inHangar != null) {
|
|
||||||
droneState = inHangar == 1 ? '在库' : '外出';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (droneState == null) {
|
// 兼容直接放在顶层的旧字段名
|
||||||
final droneInDock = droneData['drone_in_dock'] as int?;
|
gpsSatellites = gpsSatellites ?? droneData['gps_satellites'] as int?;
|
||||||
if (droneInDock != null) {
|
final int? rtkSatellites = droneData['rtk_satellites'] as int?;
|
||||||
droneState = droneInDock == 1 ? '在库' : '外出';
|
|
||||||
}
|
if (batteryPercent != null) {
|
||||||
|
parsedValues['battery'] = '$batteryPercent%';
|
||||||
|
parsedColors['battery'] = batteryPercent > 50
|
||||||
|
? const Color(0xFF00B42A)
|
||||||
|
: batteryPercent > 20
|
||||||
|
? const Color(0xFFFF7D00)
|
||||||
|
: const Color(0xFFF53F3F);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? flightMode = droneData['flight_mode'] as String?;
|
|
||||||
|
|
||||||
if (height != null) {
|
if (height != null) {
|
||||||
parsedValues['height'] = '${height.toStringAsFixed(1)}m';
|
parsedValues['height'] = '${height.toStringAsFixed(1)}m';
|
||||||
parsedColors['height'] = const Color(0xFF165DFF);
|
parsedColors['height'] = const Color(0xFF165DFF);
|
||||||
}
|
}
|
||||||
if (groundSpeed != null) {
|
|
||||||
parsedValues['speed'] = '${groundSpeed.toStringAsFixed(1)}m/s';
|
|
||||||
parsedColors['speed'] = const Color(0xFF722ED1);
|
|
||||||
}
|
|
||||||
if (horizontalSpeed != null) {
|
if (horizontalSpeed != null) {
|
||||||
parsedValues['horizontalSpeed'] =
|
parsedValues['horizontalSpeed'] =
|
||||||
'${horizontalSpeed.toStringAsFixed(1)}m/s';
|
'${horizontalSpeed.toStringAsFixed(1)}m/s';
|
||||||
@@ -345,58 +256,40 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
|||||||
? const Color(0xFFF53F3F)
|
? const Color(0xFFF53F3F)
|
||||||
: const Color(0xFF00B42A);
|
: const Color(0xFF00B42A);
|
||||||
}
|
}
|
||||||
if (flightDistance != null) {
|
|
||||||
parsedValues['distance'] = '${flightDistance.toStringAsFixed(0)}m';
|
|
||||||
}
|
|
||||||
if (batteryPercent != null) {
|
|
||||||
parsedValues['battery'] = '$batteryPercent%';
|
|
||||||
parsedColors['battery'] = batteryPercent > 50
|
|
||||||
? const Color(0xFF00B42A)
|
|
||||||
: batteryPercent > 20
|
|
||||||
? const Color(0xFFFF7D00)
|
|
||||||
: const Color(0xFFF53F3F);
|
|
||||||
}
|
|
||||||
if (batteryVoltage != null) {
|
|
||||||
parsedValues['voltage'] = '${batteryVoltage.toStringAsFixed(1)}V';
|
|
||||||
}
|
|
||||||
if (batteryTemp != null) {
|
|
||||||
parsedValues['batteryTemp'] = '${batteryTemp.toStringAsFixed(0)}°C';
|
|
||||||
parsedColors['batteryTemp'] = batteryTemp > 50
|
|
||||||
? const Color(0xFFF53F3F)
|
|
||||||
: batteryTemp > 40
|
|
||||||
? const Color(0xFFFF7D00)
|
|
||||||
: const Color(0xFF165DFF);
|
|
||||||
}
|
|
||||||
if (heading != null) {
|
if (heading != null) {
|
||||||
parsedValues['heading'] = '${heading.toStringAsFixed(0)}°';
|
parsedValues['heading'] = '${heading.toStringAsFixed(0)}°';
|
||||||
}
|
}
|
||||||
if (pitch != null) {
|
|
||||||
parsedValues['pitch'] = '${pitch.toStringAsFixed(1)}°';
|
|
||||||
}
|
|
||||||
if (roll != null) {
|
|
||||||
parsedValues['roll'] = '${roll.toStringAsFixed(1)}°';
|
|
||||||
}
|
|
||||||
if (gpsSatellites != null) {
|
if (gpsSatellites != null) {
|
||||||
parsedValues['gps'] = '$gpsSatellites颗';
|
parsedValues['gps'] = '$gpsSatellites颗';
|
||||||
parsedColors['gps'] = gpsSatellites >= 6
|
parsedColors['gps'] = gpsSatellites >= 6
|
||||||
? const Color(0xFF00B42A)
|
? const Color(0xFF00B42A)
|
||||||
: const Color(0xFFFF7D00);
|
: const Color(0xFFFF7D00);
|
||||||
}
|
}
|
||||||
if (rtkSatellites != null) {
|
// RTK:优先展示定位状态(is_fixed),其次回退到卫星数
|
||||||
|
if (rtkFixed != null) {
|
||||||
|
String rtkStatus;
|
||||||
|
Color rtkColor;
|
||||||
|
switch (rtkFixed) {
|
||||||
|
case 2:
|
||||||
|
rtkStatus = '固定解';
|
||||||
|
rtkColor = const Color(0xFF00B42A);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
rtkStatus = '浮点解';
|
||||||
|
rtkColor = const Color(0xFFFF7D00);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
rtkStatus = '未定位';
|
||||||
|
rtkColor = const Color(0xFF86909C);
|
||||||
|
}
|
||||||
|
parsedValues['rtk'] = rtkStatus;
|
||||||
|
parsedColors['rtk'] = rtkColor;
|
||||||
|
} else if (rtkSatellites != null) {
|
||||||
parsedValues['rtk'] = '$rtkSatellites颗';
|
parsedValues['rtk'] = '$rtkSatellites颗';
|
||||||
parsedColors['rtk'] = rtkSatellites >= 4
|
parsedColors['rtk'] = rtkSatellites >= 4
|
||||||
? const Color(0xFF00B42A)
|
? const Color(0xFF00B42A)
|
||||||
: const Color(0xFFFF7D00);
|
: const Color(0xFFFF7D00);
|
||||||
}
|
}
|
||||||
if (droneState != null) {
|
|
||||||
parsedValues['droneState'] = droneState;
|
|
||||||
parsedColors['droneState'] = droneState == '在库'
|
|
||||||
? const Color(0xFF00B42A)
|
|
||||||
: const Color(0xFF165DFF);
|
|
||||||
}
|
|
||||||
if (flightMode != null) {
|
|
||||||
parsedValues['flightMode'] = flightMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import 'package:maibu_satabot_v2/core/app/app_user_state.dart'; // 🔥 导入 A
|
|||||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart';
|
import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/webrtc/webrtc_local_player.dart';
|
||||||
import '../../../device_run_param/presentation/pages/robot_param_settings_page.dart';
|
import '../../../device_run_param/presentation/pages/robot_param_settings_page.dart';
|
||||||
import '../../../device_run_param/presentation/pages/robot_param_settings_page.dart';
|
|
||||||
|
|
||||||
/// 机器人顶部信息卡片
|
/// 机器人顶部信息卡片
|
||||||
class RobotHeaderCard extends StatefulWidget {
|
class RobotHeaderCard extends StatefulWidget {
|
||||||
final Map<String, dynamic> robot;
|
final Map<String, dynamic> robot;
|
||||||
|
|
||||||
const RobotHeaderCard({super.key, required this.robot});
|
const RobotHeaderCard({super.key, required this.robot});
|
||||||
@@ -17,15 +16,31 @@ class RobotHeaderCard extends StatefulWidget {
|
|||||||
State<RobotHeaderCard> createState() => _RobotHeaderCardState();
|
State<RobotHeaderCard> createState() => _RobotHeaderCardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 视频展示状态
|
||||||
|
/// - idle:未播放,显示播放按钮
|
||||||
|
/// - loading:正在加载视频
|
||||||
|
/// - playing:视频播放中
|
||||||
|
/// - noStream:暂无视频(加载失败后也回到此态,并显示播放按钮供重试)
|
||||||
|
enum _VideoState { idle, loading, playing, noStream }
|
||||||
|
|
||||||
class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
||||||
int _currentViewIndex = 0; // 0=前, 1=后, 2=左, 3=右, 4=上
|
int _currentViewIndex = 0; // 0=前, 1=后, 2=左, 3=右, 4=上
|
||||||
|
_VideoState _videoState = _VideoState.idle;
|
||||||
|
|
||||||
// 视角配置
|
// 视角配置
|
||||||
final List<Map<String, dynamic>> _viewConfigs = [
|
final List<Map<String, dynamic>> _viewConfigs = [
|
||||||
{'name': '前视', 'alignment': Alignment.topLeft, 'icon': Icons.arrow_upward},
|
{'name': '前视', 'alignment': Alignment.topLeft, 'icon': Icons.arrow_upward},
|
||||||
{'name': '后视', 'alignment': Alignment.topRight, 'icon': Icons.arrow_downward},
|
{
|
||||||
|
'name': '后视',
|
||||||
|
'alignment': Alignment.topRight,
|
||||||
|
'icon': Icons.arrow_downward,
|
||||||
|
},
|
||||||
{'name': '左视', 'alignment': Alignment.bottomLeft, 'icon': Icons.arrow_back},
|
{'name': '左视', 'alignment': Alignment.bottomLeft, 'icon': Icons.arrow_back},
|
||||||
{'name': '右视', 'alignment': Alignment.bottomRight, 'icon': Icons.arrow_forward},
|
{
|
||||||
|
'name': '右视',
|
||||||
|
'alignment': Alignment.bottomRight,
|
||||||
|
'icon': Icons.arrow_forward,
|
||||||
|
},
|
||||||
{'name': '俯视', 'alignment': Alignment.center, 'icon': Icons.view_agenda},
|
{'name': '俯视', 'alignment': Alignment.center, 'icon': Icons.view_agenda},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -35,6 +50,42 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
|||||||
debugPrint('🎬 [RobotHeaderCard] 初始化 - robot: ${widget.robot}');
|
debugPrint('🎬 [RobotHeaderCard] 初始化 - robot: ${widget.robot}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔥 构造视频流 URL(仅在用户点击播放时使用)
|
||||||
|
String _buildStreamUrl(String? token) {
|
||||||
|
final deviceId = widget.robot['name'] as String?;
|
||||||
|
if (deviceId == null || deviceId.isEmpty || token == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=$token";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 点击播放按钮
|
||||||
|
void _onPlayTapped(String? token) {
|
||||||
|
final url = _buildStreamUrl(token);
|
||||||
|
if (url.isEmpty) {
|
||||||
|
// 无法构造 URL(设备 ID 或 token 缺失)→ 直接显示暂无视频
|
||||||
|
setState(() => _videoState = _VideoState.noStream);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _videoState = _VideoState.loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 接收 WebRTCLocalPlayer 的加载状态回调
|
||||||
|
void _onWebRTCStateChanged(WebRTCLoadState state) {
|
||||||
|
switch (state) {
|
||||||
|
case WebRTCLoadState.loading:
|
||||||
|
setState(() => _videoState = _VideoState.loading);
|
||||||
|
break;
|
||||||
|
case WebRTCLoadState.playing:
|
||||||
|
setState(() => _videoState = _VideoState.playing);
|
||||||
|
break;
|
||||||
|
case WebRTCLoadState.error:
|
||||||
|
// 打不开 → 显示暂无视频 → 视觉上仍展示播放按钮(可重试)
|
||||||
|
setState(() => _videoState = _VideoState.noStream);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -128,9 +179,8 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => RobotParamSettingsPage(
|
builder: (_) =>
|
||||||
robot: widget.robot,
|
RobotParamSettingsPage(robot: widget.robot),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -143,67 +193,53 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// 🔥 视频展示区域(替换原来的静态图片)
|
// 🔥 视频展示区域:默认显示播放按钮,点击后才加载视频
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black, // 🔥 强制整个视频区域为黑色背景
|
color: Colors.black, // 🔥 强制整个视频区域为黑色背景
|
||||||
child: BlocBuilder<AppUserCubit, AppUserState>(
|
child: BlocBuilder<AppUserCubit, AppUserState>(
|
||||||
builder: (context, userState) {
|
builder: (context, userState) {
|
||||||
final deviceId = widget.robot['name'] as String?; // 🔥 使用 name 字段(长序列号)
|
final token = userState.user?.token;
|
||||||
String videoStreamUrl = '';
|
|
||||||
|
// loading / playing 态:渲染 WebRTCLocalPlayer
|
||||||
debugPrint('🎬 [RobotHeaderCard] BlocBuilder 重建');
|
// - loading:WebRTCLocalPlayer 内部自带“视频加载中...”加载屏,并建立连接
|
||||||
debugPrint(' - deviceId (name): $deviceId');
|
// - playing:收到视频流后正常播放
|
||||||
debugPrint(' - user != null: ${userState.user != null}');
|
// 两种态都保持 WebRTCLocalPlayer 挂载,避免重建断流
|
||||||
debugPrint(' - token != null: ${userState.user?.token != null}');
|
if (_videoState == _VideoState.loading ||
|
||||||
debugPrint(' - TCP_IP: ${TCPConsts.TCP_IP}');
|
_videoState == _VideoState.playing) {
|
||||||
|
final url = _buildStreamUrl(token);
|
||||||
if (deviceId != null &&
|
return WebRTCLocalPlayer(
|
||||||
deviceId.isNotEmpty &&
|
key: ValueKey('play_$url'),
|
||||||
userState.user != null &&
|
streamUrl: url,
|
||||||
userState.user!.token != null) {
|
showLeftPip: false,
|
||||||
videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}";
|
showRightPip: false,
|
||||||
debugPrint('✅ [RobotHeaderCard] 视频URL构建成功: $videoStreamUrl');
|
mainViewAlignment:
|
||||||
} else {
|
_viewConfigs[_currentViewIndex]['alignment']
|
||||||
debugPrint('❌ [RobotHeaderCard] 视频URL构建失败');
|
as Alignment,
|
||||||
if (deviceId == null || deviceId.isEmpty) {
|
onLoadingStateChanged: _onWebRTCStateChanged,
|
||||||
debugPrint(' - 原因: deviceId 为空');
|
onTap: () {
|
||||||
}
|
// 🔥 播放中单击 → 停止拉流,回到播放按钮态
|
||||||
if (userState.user == null) {
|
// 移除 WebRTCLocalPlayer 后其 dispose 会自动断连
|
||||||
debugPrint(' - 原因: user 为 null');
|
setState(() => _videoState = _VideoState.idle);
|
||||||
}
|
},
|
||||||
if (userState.user?.token == null) {
|
);
|
||||||
debugPrint(' - 原因: token 为 null');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoStreamUrl.isNotEmpty
|
// idle / noStream 态:显示播放按钮
|
||||||
? WebRTCLocalPlayer(
|
// - idle:进入页面默认态
|
||||||
key: ValueKey(videoStreamUrl),
|
// - noStream:视频流打不开,显示“暂无视频”提示 + 播放按钮供重试
|
||||||
streamUrl: videoStreamUrl,
|
final bool showNoStreamHint =
|
||||||
showLeftPip: false,
|
_videoState == _VideoState.noStream;
|
||||||
showRightPip: false,
|
return _buildPlayOverlay(
|
||||||
mainViewAlignment: _viewConfigs[_currentViewIndex]['alignment'] as Alignment,
|
showNoStreamHint: showNoStreamHint,
|
||||||
)
|
onTap: () => _onPlayTapped(token),
|
||||||
: Center(
|
);
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.videocam_off, size: 48, color: Colors.white54),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'暂无视频',
|
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 🔥 视角切换按钮
|
// 🔥 视角切换按钮
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 12, 0, 0),
|
padding: const EdgeInsets.fromLTRB(0, 12, 0, 0),
|
||||||
@@ -213,7 +249,7 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
|||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final config = entry.value;
|
final config = entry.value;
|
||||||
final isSelected = _currentViewIndex == index;
|
final isSelected = _currentViewIndex == index;
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
@@ -222,13 +258,22 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_currentViewIndex = index;
|
_currentViewIndex = index;
|
||||||
});
|
});
|
||||||
debugPrint('🎬 [RobotHeaderCard] 切换视角: ${config['name']}');
|
debugPrint(
|
||||||
|
'🎬 [RobotHeaderCard] 切换视角: ${config['name']}',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: isSelected ? const Color(0xFF165DFF) : Colors.grey[200],
|
backgroundColor: isSelected
|
||||||
foregroundColor: isSelected ? Colors.white : Colors.grey[700],
|
? const Color(0xFF165DFF)
|
||||||
|
: Colors.grey[200],
|
||||||
|
foregroundColor: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: Colors.grey[700],
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
minimumSize: const Size(0, 36),
|
minimumSize: const Size(0, 36),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -257,6 +302,77 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔥 播放按钮遮罩层
|
||||||
|
/// - [showNoStreamHint]:true 时在按钮上方额外显示“暂无视频”提示(视频流打不开的回退态)
|
||||||
|
/// - [onTap]:点击播放按钮的回调
|
||||||
|
Widget _buildPlayOverlay({
|
||||||
|
required bool showNoStreamHint,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 暂无视频提示(仅在 noStream 态显示)
|
||||||
|
if (showNoStreamHint) ...[
|
||||||
|
const Icon(Icons.videocam_off, size: 36, color: Colors.white38),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'暂无视频',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
// 播放按钮(圆角矩形,与卡片矩形风格呼应)
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
color: const Color(0xFF165DFF).withOpacity(0.9),
|
||||||
|
boxShadow: [
|
||||||
|
// 主题色柔和光晕,与整体蓝色调呼应
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0xFF165DFF).withOpacity(0.35),
|
||||||
|
blurRadius: 16,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
// 细微黑色投影,与卡片阴影同色系更和谐
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.15),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.play_arrow_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// “播放”文字
|
||||||
|
const Text(
|
||||||
|
'播放',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -196,17 +196,9 @@ class RobotItemCard extends StatelessWidget {
|
|||||||
bgColor = const Color(0xFFF0F9FF);
|
bgColor = const Color(0xFFF0F9FF);
|
||||||
textColor = const Color(0xFF00B42A);
|
textColor = const Color(0xFF00B42A);
|
||||||
break;
|
break;
|
||||||
case '离线':
|
default:
|
||||||
bgColor = const Color(0xFFF5F0FF);
|
bgColor = const Color(0xFFF5F0FF);
|
||||||
textColor = const Color(0xFFF53F3F);
|
textColor = const Color(0xFFF53F3F);
|
||||||
break;
|
|
||||||
case '异常':
|
|
||||||
bgColor = const Color(0xFFFFF7E6);
|
|
||||||
textColor = const Color(0xFFFF7D00);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
bgColor = const Color(0xFFF5F5F5);
|
|
||||||
textColor = const Color(0xFF86909C);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||||
|
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
|
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart';
|
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart';
|
||||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
|
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
|
||||||
@@ -24,6 +28,10 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
|
|||||||
late AlarmCubit _cubit;
|
late AlarmCubit _cubit;
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
/// 监听场站切换
|
||||||
|
StreamSubscription<SiteState>? _siteSub;
|
||||||
|
int? _currentSiteId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -33,10 +41,22 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
|
|||||||
if (_cubit.state is AlarmInitial) {
|
if (_cubit.state is AlarmInitial) {
|
||||||
_cubit.loadAlarms();
|
_cubit.loadAlarms();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听场站切换,自动重新加载告警数据
|
||||||
|
_currentSiteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
|
||||||
|
_siteSub = GetIt.I<SiteCubit>().stream.listen((siteState) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final newSiteId = siteState.selectedSite?.id;
|
||||||
|
if (newSiteId != _currentSiteId) {
|
||||||
|
_currentSiteId = newSiteId;
|
||||||
|
_cubit.loadAlarms();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_siteSub?.cancel();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||||
import 'package:maibu_satabot_v2/core/network/dio_client.dart';
|
import 'package:maibu_satabot_v2/core/network/dio_client.dart';
|
||||||
|
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
|
||||||
import '../../../../../core/consts/workorder_consts.dart';
|
import '../../../../../core/consts/workorder_consts.dart';
|
||||||
import '../../../site/presentation/widgets/site_selector_widget.dart';
|
import '../../../site/presentation/widgets/site_selector_widget.dart';
|
||||||
import '../cubit/workorder_cubit.dart';
|
import '../cubit/workorder_cubit.dart';
|
||||||
@@ -26,6 +30,10 @@ class _WorkOrderPageState extends State<WorkOrderPage> {
|
|||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
late final WorkOrderCubit _cubit;
|
late final WorkOrderCubit _cubit;
|
||||||
|
|
||||||
|
/// 监听场站切换
|
||||||
|
StreamSubscription<SiteState>? _siteSub;
|
||||||
|
int? _currentSiteId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -41,10 +49,22 @@ class _WorkOrderPageState extends State<WorkOrderPage> {
|
|||||||
|
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
_cubit.loadInitialData();
|
_cubit.loadInitialData();
|
||||||
|
|
||||||
|
// 监听场站切换,自动重新加载工单数据
|
||||||
|
_currentSiteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
|
||||||
|
_siteSub = GetIt.I<SiteCubit>().stream.listen((siteState) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final newSiteId = siteState.selectedSite?.id;
|
||||||
|
if (newSiteId != _currentSiteId) {
|
||||||
|
_currentSiteId = newSiteId;
|
||||||
|
_cubit.loadInitialData();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_siteSub?.cancel();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_cubit.close();
|
_cubit.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user