1.优化补足了首页下拉刷新中对获取场站接口的加入,确保能够实时获取数据并渲染。

2.优化修改了tcp重连机制,具体操作:断开重连时候不再建立新链接 而是将旧链接重置,并重新发送数据。
3.大面积的给项目中的各个页面 手动接入了多语言的支持。同时也调整了相应带来的问题英文长度较大影响原有的样式的问题,已解决了对应的样式不和谐问题。保证了外观的统一性。
4.给项目中的主要操作加了几个核心的操作比如远程遥控和路径规划添加了双层的保护,设置里安全模式,即在此期间对服务器推送的重复登录操作后的自动退出登录做一个拦截 保证操作的安全性。
This commit is contained in:
2026-07-02 16:04:30 +08:00
parent d5a83c1221
commit 0db64a96e7
69 changed files with 5018 additions and 1532 deletions

View File

@@ -19,10 +19,11 @@
"password": "Password",
"login_button": "Login",
"logout": "Logout",
"confirm_logout": "Are you sure to logout?",
"username_hint": "Enter username",
"password_hint": "Enter password",
"login_success": "Login successful",
"login_failed": "Login failed"
"login_success": "Login Success",
"login_failed": "Login Failed"
},
"home": {
@@ -310,6 +311,185 @@
"refresh_failed": "Refresh failed"
},
"home_v2": {
"select_site": "Select Site",
"no_sites": "No available sites",
"site_code": "Code: %s",
"weather": "Cloudy 28°C",
"emergency_alarm": "Emergency Alarm",
"real_time_power": "Real-time Power",
"today_generation": "Today's Generation",
"equivalent_hours": "Equivalent Hours",
"alarm_count": "Alarm Count",
"pending_work_order": "Pending Orders",
"quick_entry": "Quick Entry",
"device_report": "Device Report",
"scan_qr": "Scan QR",
"work_order": "Work Order",
"alarm_center": "Alarm Center",
"power_plant_map": "Plant Map",
"device_list": "Device List",
"power_trend": "Power Trend",
"day": "Day",
"month": "Month",
"year": "Year",
"plant_overview": "Plant Overview",
"installed_capacity": "Installed Capacity",
"commission_date": "Commission Date",
"work_order_stats": "Work Order Stats",
"work_order_pending": "Pending",
"work_order_processing": "Processing",
"work_order_pending_acceptance": "Pending Acceptance",
"power_trend": "Power Trend (Today)",
"day": "Day",
"month": "Month"
},
"device_list_v2": {
"title": "Device Status",
"search_hint": "Search device name/ID",
"search": "Search",
"all": "All",
"robot": "Robot",
"drone_station": "Drone Station",
"inverter": "Inverter",
"combiner_box": "Combiner Box",
"module": "Module",
"monitor": "Monitor",
"total_devices": "All Devices",
"online": "Online",
"alarm": "Alarm",
"offline": "Offline",
"no_data": "No data",
"select_site_first": "Please select a site first",
"no_drone_stations": "No drone stations at this site",
"view_details_from_tab": "Please view details from the 'Drone Station' tab",
"device_type_not_supported": "Device type not supported: %s",
"battery_level": "Battery",
"current_task": "Current Task",
"task": "Task",
"power": "Power",
"today_energy": "Today Energy",
"current": "Current",
"temperature": "Temperature",
"radiation": "Radiation",
"data": "Data",
"inspection_robot": "Inspection Robot",
"cleaning_robot": "Cleaning Robot",
"weeding_robot": "Weeding Robot"
},
"alarm_center": {
"title": "Alarm Center",
"retry": "Retry",
"filter_title": "Filter",
"alarm_level": "Alarm Level",
"time_range": "Time Range",
"area": "Area",
"device": "Device",
"high_risk": "High Risk",
"medium_risk": "Medium Risk",
"low_risk": "Low Risk",
"hint": "Hint",
"today": "Today",
"this_week": "This Week",
"this_month": "This Month",
"all": "All",
"apply_filter": "Apply Filter",
"ai_diagnosis": "AI Diagnosis",
"view_details": "View Details",
"unprocessed": "Unprocessed",
"processing": "Processing",
"confirmed": "Confirmed",
"recovered": "Recovered",
"unconfirmed": "Unconfirmed",
"today_new": "Today's New"
},
"work_order_v2": {
"title": "Work Orders",
"retry": "Retry",
"add": "Add",
"pending": "Pending",
"executing": "Executing",
"completed": "Completed",
"all": "All",
"today_completed": "Today Completed",
"order_no": "Order No.",
"priority": "Priority",
"create_time": "Create Time",
"complete_time": "Complete Time",
"location": "Location",
"executor": "Executor",
"high": "High",
"medium": "Medium",
"low": "Low",
"progress": "Progress"
},
"my_v2": {
"title": "Profile",
"clear_cache": "Clear Cache",
"confirm_clear_cache": "Are you sure to clear all cache data?",
"cancel": "Cancel",
"confirm": "Confirm",
"night_mode": "Night Mode",
"system_settings": "System Settings",
"personal_info": "Personal Info",
"offline_cache": "Offline Cache",
"message": "My Messages",
"message_settings": "Message Settings",
"favorite": "My Favorites",
"work_order": "My Orders",
"help_feedback": "Help & Feedback",
"about_us": "About Us",
"suspend_bar": "Float Bar",
"suspend_bar_desc": "Show device status float bar at the bottom",
"tab_settings": "Tab Settings"
},
"report": {
"title": "Field Report",
"submit": "Submit",
"select_location": "Select Location",
"report_type": "Report Type",
"device_fault": "Device Fault",
"patrol_record": "Patrol Record",
"hidden_danger": "Hidden Danger",
"defect_report": "Defect Report",
"select_device": "Select Device",
"please_select_device": "Please select device",
"problem_description": "Problem Description",
"description_hint": "Please describe the problem in detail...",
"upload_media": "Upload Image/Video",
"take_photo": "Take Photo",
"record_video": "Record Video",
"problem_level": "Problem Level",
"normal": "Normal",
"important": "Important",
"urgent": "Urgent",
"cancel": "Cancel",
"select": "Select",
"submit_success": "Submitted",
"submit_success_message": "Your report has been submitted successfully",
"submit_failed": "Submission Failed",
"confirm": "OK",
"main_building_a": "Main Building A",
"main_building_b": "Main Building B",
"boiler_room": "Boiler Room",
"turbine_room": "Turbine Room",
"control_room": "Control Room",
"power_distribution_room": "Power Distribution Room",
"boiler_1": "Boiler #1",
"boiler_2": "Boiler #2",
"turbine_1": "Turbine #1",
"generator_1": "Generator #1",
"transformer_1": "Transformer #1",
"water_pump_1": "Water Pump #1",
"select_image": "Select Image",
"select_video": "Select Video"
},
"product": {
"intro": "Product Introduction",
"highlights": "Product Highlights",

View File

@@ -19,6 +19,7 @@
"password": "密码",
"login_button": "登录",
"logout": "退出登录",
"confirm_logout": "确定要退出登录吗?",
"username_hint": "请输入用户名",
"password_hint": "请输入密码",
"login_success": "登录成功",
@@ -323,6 +324,185 @@
"clear_cache_success": "已清除所有缓存,恢复初始状态",
"refresh_failed": "刷新失败"
},
"home_v2": {
"select_site": "选择电站",
"no_sites": "暂无可用电站",
"site_code": "编号: %s",
"weather": "多云 28°C",
"emergency_alarm": "紧急告警",
"real_time_power": "实时发电功率",
"today_generation": "今日发电量",
"equivalent_hours": "等效小时数",
"alarm_count": "告警数",
"pending_work_order": "待办工单",
"quick_entry": "快捷入口",
"device_report": "设备上报",
"scan_qr": "扫一扫",
"work_order": "工单任务",
"alarm_center": "告警中心",
"power_plant_map": "电厂地图",
"device_list": "设备列表",
"power_trend": "发电趋势",
"day": "日",
"month": "月",
"year": "年",
"plant_overview": "电站概览",
"installed_capacity": "装机容量",
"commission_date": "投运日期",
"work_order_stats": "工单统计",
"work_order_pending": "待处理",
"work_order_processing": "处理中",
"work_order_pending_acceptance": "待验收",
"power_trend": "发电趋势(今日)",
"day": "日",
"month": "月"
},
"device_list_v2": {
"title": "设备状态",
"search_hint": "搜索设备名称/编号",
"search": "搜索",
"all": "全部",
"robot": "机器人",
"drone_station": "无人机机场",
"inverter": "逆变器",
"combiner_box": "汇流箱",
"module": "组件",
"monitor": "监控",
"total_devices": "全部设备",
"online": "在线",
"alarm": "告警",
"offline": "离线",
"no_data": "暂无数据",
"select_site_first": "请先选择场站",
"no_drone_stations": "该场站暂无无人机机场",
"view_details_from_tab": "请从\"无人机机场\"标签页查看详情",
"device_type_not_supported": "该设备类型暂不支持查看详情: %s",
"battery_level": "电量",
"current_task": "当前任务",
"task": "任务",
"power": "功率",
"today_energy": "今日发电",
"current": "电流",
"temperature": "温度",
"radiation": "辐照度",
"data": "数据",
"inspection_robot": "巡检机器人",
"cleaning_robot": "清洗机器人",
"weeding_robot": "除草机器人"
},
"alarm_center": {
"title": "告警中心",
"retry": "重试",
"filter_title": "筛选条件",
"alarm_level": "告警等级",
"time_range": "时间范围",
"area": "区域",
"device": "设备",
"high_risk": "高危",
"medium_risk": "中危",
"low_risk": "低危",
"hint": "提示",
"today": "今天",
"this_week": "本周",
"this_month": "本月",
"all": "全部",
"apply_filter": "应用筛选",
"ai_diagnosis": "AI 诊断建议",
"view_details": "查看详情",
"unprocessed": "未处理",
"processing": "处理中",
"confirmed": "已确认",
"recovered": "已恢复",
"unconfirmed": "未确认",
"today_new": "今日新增"
},
"work_order_v2": {
"title": "工单任务",
"retry": "重试",
"add": "添加",
"pending": "待处理",
"executing": "执行中",
"completed": "已完成",
"all": "全部",
"today_completed": "今日完成",
"order_no": "工单号",
"priority": "优先级",
"create_time": "创建时间",
"complete_time": "完成时间",
"location": "地点",
"executor": "执行人",
"high": "高",
"medium": "中",
"low": "低",
"progress": "进度"
},
"my_v2": {
"title": "我的",
"clear_cache": "清除缓存",
"confirm_clear_cache": "确定要清除所有缓存数据吗?",
"cancel": "取消",
"confirm": "确定",
"night_mode": "夜间模式",
"system_settings": "系统设置",
"personal_info": "个人信息",
"offline_cache": "离线缓存",
"message": "我的消息",
"message_settings": "消息设置",
"favorite": "我的收藏",
"work_order": "我的工单",
"help_feedback": "帮助与反馈",
"about_us": "关于我们",
"suspend_bar": "悬浮条",
"suspend_bar_desc": "在页面底部显示设备状态悬浮条",
"tab_settings": "Tab 设置"
},
"report": {
"title": "现场上报",
"submit": "提交",
"select_location": "选择位置",
"report_type": "上报类型",
"device_fault": "设备故障",
"patrol_record": "巡检记录",
"hidden_danger": "隐患上报",
"defect_report": "缺陷上报",
"select_device": "选择设备",
"please_select_device": "请选择设备",
"problem_description": "问题描述",
"description_hint": "请详细描述问题现象...",
"upload_media": "上传图片/视频",
"take_photo": "拍照",
"record_video": "录像",
"problem_level": "问题等级",
"normal": "一般",
"important": "重要",
"urgent": "紧急",
"cancel": "取消",
"select": "选择",
"submit_success": "提交成功",
"submit_success_message": "您的上报已成功提交",
"submit_failed": "提交失败",
"confirm": "确定",
"main_building_a": "主厂房A区",
"main_building_b": "主厂房B区",
"boiler_room": "锅炉房",
"turbine_room": "汽机房",
"control_room": "控制室",
"power_distribution_room": "配电室",
"boiler_1": "锅炉1号",
"boiler_2": "锅炉2号",
"turbine_1": "汽轮机1号",
"generator_1": "发电机1号",
"transformer_1": "变压器1号",
"water_pump_1": "水泵1号",
"select_image": "选择图片",
"select_video": "选择视频"
},
"product": {
"intro": "产品介绍",
"highlights": "产品亮点",

View File

@@ -1,6 +1,7 @@
class HttpApiConsts {
// static const String baseUrl = "http://8.159.134.0:8012"; // 旧地址
static const String baseUrl = "http://1.95.137.212:59015";
// static const String baseUrl = "http://1.95.137.212:59015"; // 测试地址
static const String baseUrl = "http://1.95.137.212:8081"; // 生产地址
/// 账号相关
// 登录
@@ -54,4 +55,7 @@ class HttpApiConsts {
// 切换无人机镜头获取视频流
static const String changeUAVLens = "$baseUrl/iot/UAV/changeLens";
// 获取无人机详情
static const String getUAVDetail = "$baseUrl/iot/UAV/getUAVDetail";
}

View File

@@ -17,6 +17,7 @@ import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart';
import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart';
import 'package:maibu_satabot_v2/features/my/usecases/updatename_usecase.dart';
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart';
import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart';
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart';
@@ -415,6 +416,11 @@ Future<void> init() async {
// 🔥 SiteCubit (全局共享,持久化选中场站)
sl.registerLazySingleton(() => SiteCubit(sl<SharedPreferences>()));
// 🔥 FloatBarSettingService (全局共享,控制悬浮条显示)
sl.registerLazySingleton(
() => FloatBarSettingService(sl<SharedPreferences>()),
);
// 🔥 语言管理 Cubit (单例)
sl.registerLazySingleton(() => LocaleCubit());

View File

@@ -0,0 +1,64 @@
import 'package:flutter/foundation.dart';
import '../../features/v2/device_list/domain/entities/drone_station_entity.dart';
/// 无人机任务信息
class DroneTaskInfo {
final String droneSn;
final String gatewaySn;
final List<CameraInfo>? cameraList;
final bool isOnline; // 无人机是否在线
final String stationName; // 机场名称/呼号
final String droneName; // 无人机名称/呼号
DroneTaskInfo({
required this.droneSn,
required this.gatewaySn,
this.cameraList,
this.isOnline = false,
this.stationName = '',
this.droneName = '',
});
}
/// 无人机任务状态管理器
/// 使用 ValueNotifier 实现响应式状态管理
/// 支持在机场页面点击无人机卡片时实时同步到 FloatBarWidget
class DroneTaskStateManager {
static final DroneTaskStateManager _instance =
DroneTaskStateManager._internal();
factory DroneTaskStateManager() => _instance;
DroneTaskStateManager._internal();
/// 当前无人机任务信息(响应式状态)
final ValueNotifier<DroneTaskInfo?> _currentTaskInfo =
ValueNotifier<DroneTaskInfo?>(null);
/// 监听无人机任务信息变化
ValueListenable<DroneTaskInfo?> get currentTaskInfo => _currentTaskInfo;
/// 获取当前无人机任务信息
DroneTaskInfo? get currentInfo => _currentTaskInfo.value;
/// 设置当前无人机任务信息
/// 支持两种触发方式:
/// 1. 任务下发时自动触发
/// 2. 点击机场页面无人机卡片时触发
void setDroneTaskInfo(DroneTaskInfo info) {
debugPrint('🛸 [DroneTaskStateManager] 设置无人机任务信息');
debugPrint(' droneSn: ${info.droneSn}');
debugPrint(' gatewaySn: ${info.gatewaySn}');
_currentTaskInfo.value = info;
}
/// 清除无人机任务信息
void clearDroneTaskInfo() {
debugPrint('🛸 [DroneTaskStateManager] 清除无人机任务信息');
_currentTaskInfo.value = null;
}
/// 检查是否有当前任务
bool get hasTask => _currentTaskInfo.value != null;
}
/// 全局管理器实例
final droneTaskStateManager = DroneTaskStateManager();

View File

@@ -40,6 +40,7 @@ class DioClient {
onError: (DioException e, handler) async {
// 401 = token 过期 / 未授权
if (e.response?.statusCode == 401) {
print('>>> [DIO] 🚨🚨🚨 收到 401 响应,触发自动退出登录!URL: ${e.requestOptions.uri},时间: ${DateTime.now()}');
try {
// 调用 logout 清除本地缓存 + 跳登录
await sl<AuthCubit>().logout();

View File

@@ -16,6 +16,8 @@ abstract class DroneOsdDataSource {
});
Future<void> stopListening();
void dispose();
}
class DroneOsdDataSourceImpl implements DroneOsdDataSource {

View File

@@ -44,6 +44,52 @@ class TcpClient {
int? _lastPort;
bool isUserSwitch = false; // 用户是否切换设备
bool _isSwitching = false;
bool _connecting = false; // 🔥 是否正在建立连接(防止 connect() 和 connectBySwitch() 并发竞争)
Completer<void>? _connectionCompleter; // 🔥 用于通知连接就绪
// 🔥 关键标志:记录客户端最后一次发送 0x03 认证包的时间
// 用于区分"自身认证触发的 have_logged_in"和"真正的异地登录"
// 如果 have_logged_in 在发送 0x03 后短时间内到达,说明是服务端残留 session 导致的
DateTime? _lastAuthPacketSentAt;
/// 检查当前的 have_logged_in 是否是自身 0x03 认证触发的
/// 返回 true 表示应忽略(是服务端残留 session 导致的误报)
///
/// 🔥 恢复:用于登录阶段判断是否为服务端残留 session 导致的误报
/// 判断逻辑:
/// 1. 如果 TCP 已断开 (_socket == null) → 一定是真正的异地登录,返回 false
/// 2. 如果刚发送 0x03 后 2 秒内收到 → 认为是自身认证触发,返回 true
/// 3. 其他情况 → 认为是真正的异地登录,返回 false
bool isOwnAuthTriggeredKick() {
// 🔥 如果 TCP 已断开,说明服务端真的踢了,一定是异地登录
if (_socket == null) {
debugPrint('🔍 [TCP] TCP 已断开,判定为真正的异地登录');
return false;
}
final sentAt = _lastAuthPacketSentAt;
if (sentAt == null) {
debugPrint('🔍 [TCP] 无认证时间戳,判定为真正的异地登录');
return false;
}
final elapsed = DateTime.now().difference(sentAt);
// 🔥 缩短时间窗口到 2 秒,只过滤刚发送 0x03 时的误报
final isRecent = elapsed.inSeconds < 2;
if (isRecent) {
debugPrint('🔍 [TCP] 距离上次认证 ${elapsed.inMilliseconds}ms (<2s),判定为自身认证触发(服务端残留 session)');
} else {
debugPrint('🔍 [TCP] 距离上次认证 ${elapsed.inSeconds}s (>=2s),判定为真正的异地登录');
}
return isRecent;
}
/// 清除认证时间戳(在真正的异地登录时调用)
void clearAuthTimestamp() {
_lastAuthPacketSentAt = null;
}
final UserStorage _userStorage;
@@ -92,6 +138,9 @@ class TcpClient {
// 4. 重置标志
isUserSwitch = false;
_isSwitching = false;
_connecting = false;
_connectionCompleter = null;
_lastAuthPacketSentAt = null;
// 5. 销毁 Socket
if (_socket != null) {
@@ -112,7 +161,34 @@ class TcpClient {
// 通过参数配置,不硬编码
Future<void> connect({required String host, required int port}) async {
//debugPrint('🔌 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
// 🔥 关键修复:如果已连接,复用现有连接,不创建新 TCP 会话
// 避免重复发送 0x03 认证包导致服务端推送 0x12 have_logged_in
if (_socket != null) {
debugPrint('✅ [TCP-connect] 已连接,复用现有连接,不创建新TCP会话');
_logger.logWithLevel('✅ [TCP-connect] 已连接,复用现有连接', shouldLog: true);
_lastHost = host;
_lastPort = port;
return;
}
// 🔥 关键修复:如果用户正在切换设备(connectBySwitch),不干扰
if (isUserSwitch) {
debugPrint('✅ [TCP-connect] 用户正在切换设备,跳过 connect()');
_logger.logWithLevel('✅ [TCP-connect] 用户正在切换设备,跳过', shouldLog: true);
return;
}
// 🔥 关键修复:如果已有连接正在进行中,不创建新连接
if (_connecting) {
debugPrint('✅ [TCP-connect] 已有连接正在进行中,跳过');
_logger.logWithLevel('✅ [TCP-connect] 已有连接正在进行中,跳过', shouldLog: true);
return;
}
_connecting = true;
_connectionCompleter = Completer<void>();
debugPrint('🔌 [TCP-connect] 设置 _connecting=true');
_logger.logWithLevel('🔌 [TCP] 开始连接:$host:$port', shouldLog: true);
// 更新连接状态为连接中
@@ -130,6 +206,11 @@ class TcpClient {
port,
timeout: const Duration(seconds: 5),
);
_connecting = false; // 🔥 连接成功,重置标志
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.complete();
debugPrint('✅ [TCP-connect] 连接就绪,通知等待方');
}
// 🔥 关键修复:禁用Nagle算法,确保小包立即发送
_socket!.setOption(SocketOption.tcpNoDelay, true);
// debugPrint('✅ [TCP] 连接成功!'); // ✅ 必须看到这条
@@ -229,6 +310,10 @@ class TcpClient {
},
);
} catch (e) {
_connecting = false; // 🔥 连接失败,重置标志
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(e);
}
rethrow; // 向上抛出连接异常
}
}
@@ -258,11 +343,20 @@ class TcpClient {
_reconnectTimer = Timer(const Duration(seconds: 5), () {
_reconnectTimer = null;
//debugPrint('⏰ 定时器触发,开始执行重连...');
// 🔥 关键修复:定时器触发时检查是否已有连接,避免重复创建
if (_socket != null) {
debugPrint('✅ [TCP-重连定时器] 已有连接,跳过重连');
_logger.logWithLevel('✅ [TCP-重连定时器] 已有连接,跳过重连', shouldLog: true);
return;
}
if (isUserSwitch) {
debugPrint('✅ [TCP-重连定时器] 用户正在切换设备,跳过重连');
_logger.logWithLevel('✅ [TCP-重连定时器] 用户切换中,跳过', shouldLog: true);
return;
}
_logger.logWithLevel('⏰ 定时器触发,开始执行重连...', shouldLog: true);
connect(host: _lastHost!, port: _lastPort!);
});
await _sendAuthPacket();
}
// ✅ 新增:调度重连
@@ -276,7 +370,12 @@ class TcpClient {
_reconnectTimer = Timer(const Duration(seconds: 5), () async {
_reconnectTimer = null;
//debugPrint('⏰ 被动-定时器触发,开始执行重连...');
// 🔥 关键修复:定时器触发时检查是否已有连接,避免重复创建
if (_socket != null) {
debugPrint('✅ [TCP-被动重连定时器] 已有连接,跳过重连');
_logger.logWithLevel('✅ [TCP-被动重连定时器] 已有连接,跳过', shouldLog: true);
return;
}
_logger.logWithLevel('⏰ 被动-定时器触发,开始执行重连...', shouldLog: true);
// 🔥 修复:重连时使用 RemoteControlCubit.targetDevice,而不是记住的设备名
@@ -309,7 +408,6 @@ class TcpClient {
);
}
});
await _sendAuthPacketBySwitch(devname);
}
/// 发送数据
@@ -365,27 +463,35 @@ class TcpClient {
/// 🔥 封装完整的TCP初始化方法:连接 + 认证 + 心跳
/// 在登录成功后调用此方法即可建立并维护TCP连接
/// 🔥 关键:设置 _connecting 标志,防止 connectBySwitch() 并发创建连接
Future<void> initializeTcp({required String host, required int port}) async {
// 🔥 关键:如果已有连接,复用
if (_socket != null) {
_logger.logWithLevel('✅ [TCP] 已连接,复用现有连接', shouldLog: true);
return;
}
// 🔥 关键:如果正在连接中,跳过
if (_connecting) {
_logger.logWithLevel('✅ [TCP] 正在连接中,跳过', shouldLog: true);
return;
}
_connecting = true;
_connectionCompleter = Completer<void>();
try {
// 1. 如果已经连接,先断开
if (_socket != null) {
_logger.logWithLevel('🔄 [TCP] 已有连接,先断开重新连接...', shouldLog: true);
disconnect();
await Future.delayed(const Duration(milliseconds: 500));
}
// 2. 更新状态为连接中
// 更新状态为连接中
try {
GetIt.I<TcpStatusCubit>().setConnecting();
} catch (e) {
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
}
// 3. 执行连接
// 执行连接
_logger.logWithLevel('🔌 [TCP] 开始初始化连接:$host:$port', shouldLog: true);
_lastHost = host;
_lastPort = port;
_socket = await Socket.connect(
host,
port,
@@ -393,27 +499,36 @@ class TcpClient {
);
_socket!.setOption(SocketOption.tcpNoDelay, true);
_logger.logWithLevel('✅ [TCP] Socket 连接成功', shouldLog: true);
// 4. 更新状态为已连接
// 更新状态为已连接
try {
GetIt.I<TcpStatusCubit>().setConnected();
} catch (e) {
_logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false);
}
// 5. 发送认证包
// 发送认证包
await _sendAuthPacket();
_logger.logWithLevel('✅ [TCP] 认证包已发送', shouldLog: true);
// 6. 启动心跳
// 启动心跳
startHeartbeat();
_logger.logWithLevel('✅ [TCP] 心跳已启动', shouldLog: true);
// 7. 设置数据监听(复用原有的监听逻辑)
// 设置数据监听(复用现有的监听逻辑)
_setupDataListener();
_connecting = false;
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.complete();
}
_logger.logWithLevel('🎉 [TCP] TCP 初始化完成!', shouldLog: true);
} catch (e) {
_connecting = false;
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(e);
}
_logger.logWithLevel('❌ [TCP] 初始化失败:$e', shouldLog: true);
try {
GetIt.I<TcpStatusCubit>().setError(e.toString());
@@ -512,6 +627,8 @@ class TcpClient {
// 4. 重置标志
isUserSwitch = false;
_isSwitching = false;
_connecting = false;
_connectionCompleter = null;
// 5. 销毁 Socket
if (_socket != null) {
@@ -591,8 +708,14 @@ class TcpClient {
username = user.username;
token = user.token;
final authString = '$username:app:$token';
//debugPrint('🔑 [TCP] 认证字符串:$authString');
_logger.log('🔑 [TCP] 认证字符串:$authString');
// 🔥 关键日志:打印认证字符串,方便核对 Token
debugPrint('🔑 [TCP] 正在发送认证包 (0x03)');
debugPrint(' - Username: $username');
debugPrint(' - Token: ${token?.substring(0, token!.length > 10 ? 10 : token!.length)}...'); // 只打印前10位保护隐私
debugPrint(' - Full Auth String: $authString');
_logger.logWithLevel('🔑 [TCP] 认证字符串: $authString', shouldLog: true);
final authBytes = utf8.encode(authString);
// 构造包结构:Head(2) + Cmd(1) + Payload(N) + CRC(2) + Foot(2)
@@ -611,6 +734,7 @@ class TcpClient {
builder.addByte(0xAA);
builder.addByte(0xAB);
_lastAuthPacketSentAt = DateTime.now(); // 🔥 记录发送 0x03 的时间
_socket!.add(builder.takeBytes());
//debugPrint('🔑 [TCP] 已发送认证包 (0x03): $authString');
_logger.logWithLevel('🔑 [TCP] 已发送认证包 (0x03): $authString');
@@ -686,22 +810,50 @@ class TcpClient {
debugPrint('🔌 [TCP-connectBySwitch] ========== 开始连接 ==========');
debugPrint('🔌 [TCP-connectBySwitch] Host: $host, Port: $port, Device: $deviceName');
// 🔥 关键:取消之前的重连定时器,防止冲突
// 🔥 关键:取消之前的重连定时器,防止连接冲突
_reconnectTimer?.cancel();
_reconnectTimer = null;
debugPrint('✅ [TCP-connectBySwitch] 已取消旧的重连定时器');
// 🔥 关键修复:如果已有连接正在进行中,等待连接就绪后只调 HTTP switchDevice
if (_connecting) {
debugPrint('✅ [TCP-connectBySwitch] 检测到 connect() 正在进行中,等待连接就绪...');
_logger.logWithLevel('✅ [TCP-connectBySwitch] 连接进行中,等待后只做HTTP切换', shouldLog: true);
try {
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
await _connectionCompleter!.future.timeout(const Duration(seconds: 10));
}
// 连接就绪,只做 HTTP switchDevice,不创建新 TCP 连接
await switchDeviceUseCase.deviceRepository.switchDevice("app", deviceName);
debugPrint('✅ [TCP-connectBySwitch] 等待连接就绪后HTTP切换成功: $deviceName');
} catch (e) {
debugPrint('❌ [TCP-connectBySwitch] 等待连接或HTTP切换失败: $e');
_logger.logWithLevel('❌ [TCP-connectBySwitch] 等待连接或HTTP切换失败: $e', shouldLog: true);
}
return;
}
isUserSwitch = true;
_isSwitching = true; // 🔥 关键修复:防止旧 socket 的 onDone/onError 触发重连
_connecting = true; // 🔥 防止 connect() 并发创建新连接
debugPrint('🔌 [TCP-connectBySwitch] 设置 _connecting=true');
// 🔥 关键修复:如果 TCP 已连接,先断开旧连接再创建新连接
// 原因:旧连接可能是 _handleResume() 创建的“空连接”(没有关联设备),
// 服务端残留的 session 与当前连接不匹配,导致推送 have_logged_in
if (_socket != null) {
debugPrint('🔌 [TCP-connectBySwitch] 检测到已有连接,先断开再创建新连接(确保服务端 session 干净)');
stopHeartbeat();
_socket!.destroy();
_socket = null;
_isSwitching = false; // 防止 onDone 触发重连
}
// debugPrint('🔌被动 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
_logger.logWithLevel('🔌被动 [TCP] 开始连接:$host:$port');
_lastHost = host;
_lastPort = port;
if (_socket != null) {
_socket!.destroy();
_socket = null;
}
try {
_socket = await Socket.connect(
host,
@@ -770,30 +922,38 @@ class TcpClient {
}
},
onDone: () {
// TODO: 断线重连
// debugPrint('onDone❌ 被动[TCP] 连接已断开!');
_logger.logWithLevel('onDone❌ 被动[TCP] 连接已断开!');
debugPrint('onDone [TCP] 被动连接已断开!_isSwitching=$_isSwitching');
if (!_isSwitching) {
_handleDisconnectBySwitch(deviceName);
} else {
//debugPrint('🚫 [TCP] 检测到切换过程中的断开,忽略重连调度,防止死循环');
_logger.logWithLevel('🚫 [TCP] 检测到切换过程中的断开,忽略重连调度,防止死循环');
// 可选:如果是因为切换导致的断开,这里不需要做任何事,因为主流程已经在运行了
// 但为了安全,可以重置一下标志位,防止后续逻辑误判
debugPrint('🚫 [TCP] 切换过程中的断开,忽略重连调度');
_isSwitching = false;
}
},
onError: (e) {
// debugPrint('❌ 被动[TCP] 发生错误:$e');
_logger.logWithLevel('❌ 被动[TCP] 发生错误:$e');
_handleDisconnectBySwitch(deviceName); // 统一走重连逻辑,保护 Controller 不被关闭
if (!_isSwitching) {
_handleDisconnectBySwitch(deviceName);
} else {
debugPrint('🚫 [TCP] 切换过程中的错误,忽略重连调度');
_isSwitching = false;
}
},
);
// 开始认证tcp
await _sendAuthPacketBySwitch(deviceName);
debugPrint('✅ [TCP-connectBySwitch] 认证和HTTP切换全部完成');
} catch (e) {
_connecting = false; // 🔥 连接失败,重置标志
rethrow; // 向上抛出连接异常
}
// 🔥 关键修复:只有在所有操作成功后才重置 _connecting
_connecting = false;
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.complete();
debugPrint('✅ [TCP-connectBySwitch] 连接就绪,通知等待方');
}
}
Future<void> _sendAuthPacketBySwitch(String deviceName) async {
@@ -833,6 +993,7 @@ class TcpClient {
builder.addByte(0xAA);
builder.addByte(0xAB);
_lastAuthPacketSentAt = DateTime.now(); // 🔥 记录发送 0x03 的时间
_socket!.add(builder.takeBytes());
//debugPrint('🔑 被动[TCP] 已发送认证包 (0x03): $authString');
_logger.logWithLevel('🔑 被动[TCP] 已发送认证包 (0x03): $authString');

View File

@@ -14,6 +14,7 @@ import '../../features/auth/presentation/bloc/auth_cubit.dart';
import '../../features/auth/presentation/bloc/auth_state.dart';
import '../../features/machine_details/presentation/pages/machine_details_page.dart';
import '../../features/main_container/presentation/main_wrapper.dart';
import '../../main.dart'; // 🔥 导入 navigatorKey
import 'go_router_refresh_stream.dart';
GoRouter createRouter(AuthCubit authCubit) {
@@ -25,6 +26,7 @@ GoRouter createRouter(AuthCubit authCubit) {
}
return GoRouter(
navigatorKey: navigatorKey, // 🔥 绑定全局 navigatorKey,使异地登录弹窗能获取 context
initialLocation: RoutePaths.login,
refreshListenable: GoRouterRefreshStream(authCubit.stream),
redirect: (context, state) {

View File

@@ -39,6 +39,13 @@ class AuthCubit extends Cubit<AuthState> {
final ILoggerService _logger = GetIt.I<ILoggerService>();
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
// 🔥 登录验证 Completer:用于等待登录阶段的 have_logged_in 推送
Completer<bool>? _loginVerificationCompleter;
// 🔥 安全模式标志位:保护关键操作阶段不被异地登录踢下线
bool _isSafeMode = false;
Timer? _safeModeExitTimer;
AuthCubit(
this.storage,
@@ -63,17 +70,14 @@ class AuthCubit extends Cubit<AuthState> {
);
if (user != null) {
// 🔥 冷启动时不再自动连接TCP,改为选择设备时再连接
// await tcp.initializeTcp(
// host: TCPConsts.TCP_IP,
// port: TCPConsts.TCP_PORT,
// );
// 🔥 冷启动时不创建TCP连接,改为用户选择设备时再连接
// 避免发送 0x03 触发服务端残留 session 的 have_logged_in
// 2. 同步全局 App 状态
appCubit.setAuth(user);
// 3. 进入已登录状态
emit(AuthAuthenticated(user));
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态(TCP将在选择设备时连接)', level: 'INFO');
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态', level: 'INFO');
} else {
logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO');
emit(AuthUnauthenticated());
@@ -88,8 +92,44 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> loginSuccess(UserEntity user) async {
await storage.saveUser(user);
// 🔥 登录后不再立即连接TCP,改为选择设备时再连接
// await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
// 🔥 登录后立即创建TCP连接并验证
try {
debugPrint('>>> [AUTH] 登录成功,开始建立TCP连接...');
_logger.logWithLevel('>>> [AUTH] 登录成功,开始建立TCP连接...', shouldLog: true);
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
tcp.startHeartbeat(interval: const Duration(seconds: 4));
// 🔥 关键:等待 2.5 秒,看是否收到 have_logged_in
bool isKicked = await _waitForLoginVerification();
if (isKicked) {
// 🔥 不能进入 APP,必须清除所有登录信息
debugPrint('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息...');
_logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息', shouldLog: true);
// 1. 删除已保存的用户信息
await storage.deleteUser();
debugPrint('✅ [AUTH] 已删除用户信息');
// 2. 断开 TCP 连接
tcp.forceDisconnect();
debugPrint('✅ [AUTH] 已断开 TCP 连接');
// 3. 显示 Toast
_showLoginFailedToast("账号已在其他设备登录");
debugPrint('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页');
_logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页', shouldLog: true);
return; // 停留在登录页
}
debugPrint('✅ [AUTH] TCP连接成功,验证通过');
_logger.logWithLevel('✅ [AUTH] TCP连接成功,验证通过', shouldLog: true);
} catch (e) {
debugPrint('❌ [AUTH] TCP连接失败:$e');
_logger.logWithLevel('❌ [AUTH] TCP连接失败:$e', level: 'ERROR');
// TCP 连接失败不影响登录流程,继续执行
}
appCubit.setAuth(user);
emit(AuthAuthenticated(user));
@@ -97,7 +137,8 @@ class AuthCubit extends Cubit<AuthState> {
/// 退出登录 (主动或被动)
Future<void> logout() async {
//print("退出登录");
print('>>> [AUTH] 🚨🚨🚨 logout() 被调用!时间: ${DateTime.now()}');
print('>>> [AUTH] 调用栈:\n${StackTrace.current}');
// 🔥 关键修复:在断开TCP前,先清空所有业务状态,避免重连触发旧数据
_clearAllBusinessState();
@@ -108,6 +149,25 @@ class AuthCubit extends Cubit<AuthState> {
emit(AuthUnauthenticated());
}
/// 🔥 新增:显示登录失败 Toast(黑色背景,和登录错误提示一致)
void _showLoginFailedToast(String message) {
try {
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.black,
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
debugPrint('>>> [AUTH] ❌ 显示Toast失败:$e');
}
}
/// 🔥 新增:显示异地登录提示弹窗
void _showKickOutDialog() {
try {
@@ -177,6 +237,21 @@ class AuthCubit extends Cubit<AuthState> {
}
}
/// 🔥 等待登录验证结果(2.5 秒内看是否收到 have_logged_in)
Future<bool> _waitForLoginVerification() async {
_loginVerificationCompleter = Completer<bool>();
// 等待 2.5 秒
await Future.delayed(const Duration(milliseconds: 2500));
// 如果 completer 还没完成,说明没收到 have_logged_in,返回 false(可以进入)
if (!_loginVerificationCompleter!.isCompleted) {
_loginVerificationCompleter!.complete(false);
}
return _loginVerificationCompleter!.future;
}
/// TCP 指令监听
void _listenToAuthResponse() {
debugPrint('>>> [AUTH] _listenToAuthResponse() 被调用,开始监听 0x12 指令');
@@ -214,13 +289,36 @@ class AuthCubit extends Cubit<AuthState> {
);
if (respond == 'have_logged_in') {
debugPrint('>>> [AUTH] ⚠️ 检测到异地登录 (respond=have_logged_in),开始退出...');
// 🔥 登录阶段策略:HTTP 已成功,TCP 认证阶段的推送视为服务端状态同步,直接放行
if (_loginVerificationCompleter != null && !_loginVerificationCompleter!.isCompleted) {
debugPrint('>>> [AUTH] 🛡️ 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入 APP');
_logger.logWithLevel(
'🛡️ [AUTH] 登录阶段收到 have_logged_in,视为服务端状态同步,允许进入',
shouldLog: true,
);
_loginVerificationCompleter!.complete(false); // 标记为可以进入
return;
}
// 🔥 已登录阶段策略:检查是否处于安全模式
if (_isSafeMode) {
print('>>> [AUTH] 🛡️ 安全模式下拦截 have_logged_in 推送,防止控制中断');
_logger.logWithLevel(
'🛡️ [AUTH] 安全模式下拦截异地登录推送',
level: 'WARN',
);
return; // 拦截退出逻辑
}
print('>>> [AUTH] ⚠️ 已登录状态下收到 TCP 0x12 指令 respond=have_logged_in');
debugPrint('>>> [AUTH] ℹ️ 暂时忽略该推送,观察是否影响业务操作...');
_logger.logWithLevel(
'⚠️ [AUTH] 检测到异地登录 (respond=have_logged_in),开始退出...',
level: 'INFO',
'⚠️ [AUTH] 已登录状态收到 have_logged_in,暂不处理,观察业务影响',
level: 'WARN',
);
// 🔥 修复:先显示友好提示,再执行退出
_showKickOutDialog();
// 如果你希望依然保持严格的安全策略,可以取消下面注释恢复退出逻辑:
// _showKickOutDialog();
} else {
debugPrint('>>> [AUTH] ℹ️ 收到 0x12 消息,respond="$respond",不处理');
}
@@ -231,9 +329,92 @@ class AuthCubit extends Cubit<AuthState> {
_logger.logWithLevel('[AUTH] 监听 TCP 0x12 指令...', level: 'INFO');
}
/// 🔥 安全模式管理:进入关键页面时调用
void enterSafeMode() {
_isSafeMode = true;
_safeModeExitTimer?.cancel(); // 取消之前的退出倒计时
debugPrint('>>> [AUTH] 🛡️ 已进入安全模式,异地登录推送将被拦截');
_logger.logWithLevel('>>> [AUTH] 🛡️ 已进入安全模式', shouldLog: true);
_showSafeModeEnteredToast(); // 🔥 显示进入提示
}
/// 🔥 安全模式管理:离开关键页面时调用
void exitSafeMode() {
// 启动 90 秒倒计时
_safeModeExitTimer?.cancel();
_safeModeExitTimer = Timer(const Duration(seconds: 90), () {
if (_isSafeMode) {
_isSafeMode = false;
debugPrint('>>> [AUTH] ⏰ 90秒无操作,已自动关闭安全模式');
_logger.logWithLevel('>>> [AUTH] ⏰ 90秒无操作,已自动关闭安全模式', shouldLog: true);
_showSafeModeClosedToast();
}
});
debugPrint('>>> [AUTH] 🕒 已离开安全页面,90秒后若无操作将关闭安全模式');
_showSafeModeCountdownToast();
}
/// 🔥 显示“已进入安全模式”提示
void _showSafeModeEnteredToast() {
try {
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('已进入安全模式,防止异地登录干扰'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
debugPrint('>>> [AUTH] ❌ 显示安全模式提示失败:$e');
}
}
/// 🔥 显示“90秒后关闭”提示
void _showSafeModeCountdownToast() {
try {
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('90秒后无操作将关闭安全模式'),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
debugPrint('>>> [AUTH] ❌ 显示倒计时提示失败:$e');
}
}
/// 🔥 显示“安全模式已关闭”提示
void _showSafeModeClosedToast() {
try {
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('安全模式已关闭,恢复异地登录检测'),
backgroundColor: Colors.grey,
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
debugPrint('>>> [AUTH] ❌ 显示关闭提示失败:$e');
}
}
@override
Future<void> close() {
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
_safeModeExitTimer?.cancel(); // 清理倒计时
return super.close();
}

View File

@@ -17,7 +17,6 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
final UserStorage _userStorage; // 🔥 新增字段
final ILoggerService _logger = GetIt.I<ILoggerService>();
// 🔥 修改构造函数,注入 UserStorage
DeviceHttpDatasourceImpl(this.dio, this._userStorage);
@@ -32,22 +31,41 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
Future<int> bindDevice(String deviceId, String deviceAlias) async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
logger.logWithLevel('用户点击bindDevice方法开始触发', level: 'INFO');
logger.logWithLevel('用户点击bindDevice方法API 请求开始', level: 'INFO', data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'});
var response = await dio.post(HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias});
logger.logWithLevel(
'用户点击bindDevice方法API 请求开始',
level: 'INFO',
data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'},
);
var response = await dio.post(
HttpApiConsts.bindDevice,
data: {'deviceId': deviceId, 'deviceAlias': deviceAlias},
);
logger.logWithLevel(
'请求详细参数',
level: 'DEBUG',
data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice', 'deviceId': deviceId, 'deviceAlias': deviceAlias},
data: {
'url': 'https://serviceri.satabot.com/iot/device/bindDevice',
'deviceId': deviceId,
'deviceAlias': deviceAlias,
},
);
if (response.statusCode != 200) {
logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data});
logger.logWithLevel(
'API 响应失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data},
);
throw Exception('网络请求失败:${response.statusCode}');
}
final responseData = response.data;
if (responseData['code'] != 200 || responseData['data'] != true) {
logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data});
logger.logWithLevel(
'API 响应失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data},
);
throw Exception(responseData['msg'] ?? '业务异常');
}
@@ -70,6 +88,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
),
);
if (response.statusCode != 200) {
print('>>> [getUserDevices] 🚨🚨🚨 状态码异常 ${response.statusCode},触发自动退出登录!时间: ${DateTime.now()}');
await sl<AuthCubit>().logout();
throw Exception('网络请求失败:${response.statusCode}');
}
@@ -77,6 +96,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
final responseData = response.data;
if (responseData['code'] != 200) {
print('>>> [getUserDevices] 🚨🚨🚨 业务码异常 code=${responseData['code']},触发自动退出登录!时间: ${DateTime.now()}');
await sl<AuthCubit>().logout();
throw Exception(responseData['msg'] ?? '业务异常');
}
@@ -96,10 +116,14 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
Future<int> switchDevice(String platform, String deviceId) async {
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
final token = await _getToken();
// print('用户点击switchDevice方法开始触发: $token');
// print('用户点击switchDevice方法开始触发: $token');
_logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG');
logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG');
logger.logWithLevel('用户点击switchDevice方法API 请求开始', level: 'INFO', data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'});
// logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG');
logger.logWithLevel(
'用户点击switchDevice方法API 请求开始',
level: 'INFO',
data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'},
);
var response = await dio.post(
HttpApiConsts.switchDevice,
data: {'platform': platform, 'deviceId': deviceId},
@@ -113,17 +137,33 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
logger.logWithLevel(
'请求详细参数',
level: 'DEBUG',
data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice', 'platform': platform, 'deviceId': deviceId},
data: {
'url': 'https://serviceri.satabot.com/iot/device/switchDevice',
'platform': platform,
'deviceId': deviceId,
},
);
if (response.statusCode != 200) {
logger.logWithLevel('API 请求失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data});
logger.logWithLevel(
'API 请求失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data},
);
throw Exception('网络请求失败:${response.statusCode}');
}
final responseData = response.data;
logger.logWithLevel('API 响应成功', level: 'INFO', data: {'statusCode': response.statusCode, 'data': response.data});
logger.logWithLevel(
'API 响应成功',
level: 'INFO',
data: {'statusCode': response.statusCode, 'data': response.data},
);
if (responseData['code'] != 200) {
logger.logWithLevel('API 响应失败', level: 'ERROR', data: {'statusCode': response.statusCode, 'data': response.data});
logger.logWithLevel(
'API 响应失败',
level: 'ERROR',
data: {'statusCode': response.statusCode, 'data': response.data},
);
throw Exception(responseData['msg'] ?? '业务异常');
}
return 1;
@@ -131,7 +171,10 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<int> unbindDevice(String deviceId, String deviceName) async {
final response = await dio.post('/forward/device/unbind', data: {"deviceId": deviceId, "deviceName": deviceName});
final response = await dio.post(
'/forward/device/unbind',
data: {"deviceId": deviceId, "deviceName": deviceName},
);
final data = response.data;
@@ -147,7 +190,10 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
}
Future<int> updateDeviceName(String deviceId, String deviceName) async {
final response = await dio.post('/forward/device/updateDeviceAlias', data: {"deviceId": deviceId, "deviceAlias": deviceName});
final response = await dio.post(
'/forward/device/updateDeviceAlias',
data: {"deviceId": deviceId, "deviceAlias": deviceName},
);
final data = response.data;
@@ -169,7 +215,14 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
try {
// 发起 GET 请求
logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO');
logger.logWithLevel('请求详细参数', level: 'DEBUG', data: {'url': 'https://serviceri.satabot.com/iot/device/userDevice', 'tenantName': deviceame});
logger.logWithLevel(
'请求详细参数',
level: 'DEBUG',
data: {
'url': 'https://serviceri.satabot.com/iot/device/userDevice',
'tenantName': deviceame,
},
);
final response = await dio.get(
'https://serviceri.satabot.com/iot/device/userDevice',
queryParameters: {'tenantName': deviceame},
@@ -184,12 +237,27 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
// 检查响应状态码
if (response.statusCode == 200) {
final data = response.data;
logger.logWithLevel('API 响应接收', level: 'DEBUG', data: {'statusCode': response.statusCode, 'data': response.data});
logger.logWithLevel(
'API 响应接收',
level: 'DEBUG',
data: {'statusCode': response.statusCode, 'data': response.data},
);
if (data['code'] == 200) {
//return data['data']; // 返回设备数据
final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化
logger.logWithLevel('成功获取设备位置', level: 'INFO', data: {'lat': locationData['latitude'], 'lng': locationData['longitude']});
return DeviceLocationEntity(deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude']);
logger.logWithLevel(
'成功获取设备位置',
level: 'INFO',
data: {
'lat': locationData['latitude'],
'lng': locationData['longitude'],
},
);
return DeviceLocationEntity(
deviceName: locationData['deviceName'],
latitude: locationData['latitude'],
longitude: locationData['longitude'],
);
} else {
logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR');
throw Exception(data['msg'] ?? '业务异常');

View File

@@ -281,35 +281,14 @@ class DevicesCubit extends Cubit<DevicesState> {
},
// 成功处理:改为 async 函数块
(r) async {
// 3. 最后更新 UI 状态
try {
// 🔥 关键修复 1:先强制断开旧连接!
// 这一步会销毁旧 Socket,清除旧 Listener,防止旧数据继续推送
if (_tcpClient.isConnected) {
//debugPrint('🛑 检测到已连接,先断开旧 TCP 连接...');
_logger.logWithLevel('🔥 检测到已连接,先断开旧 TCP 连接...');
_tcpClient.disconnects(forSwitch: true);
// 稍微等待一下,确保底层 Socket 资源释放 (可选,但推荐)
// await Future.delayed(const Duration(milliseconds: 100));
}
// 🔥 关键修复:不再断开 TCP 并重新连接
// TCP 已在登录时建立,设备切换只用 HTTP
// 避免创建新 TCP 连接发送 0x03 触发服务端推送 have_logged_in
// 重置设备状态 Bloc,清除旧设备图表数据
_deviceStatusBloc.add(DeviceStatusReset());
// 🔥 关键修复 2:发起新连接
// connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备
// debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}');
_logger.logWithLevel('🔌 启动重新新连接 TCP,将自动订阅新设备:${device.deviceName}');
await _tcpClient.connectBySwitch(
host: TCPConsts.TCP_IP,
port: TCPConsts.TCP_PORT,
deviceName: device.deviceName,
);
// 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据
_deviceStatusBloc.add(DeviceStatusReset());
debugPrint('✅ 新设备切换流程完成');
} catch (e) {
debugPrint('⚠️ 设备切换成功,但 TCP 重连或状态重置失败:$e');
// 即使 TCP 失败,也更新 UI 选中状态,让用户知道切换了,只是没数据
}
debugPrint('✅ 新设备切换完成(HTTP-only,无新TCP连接): ${device.deviceName}');
emit(state.copyWith(isLoading: false, selectedDevice: device));
},

View File

@@ -1,14 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../auth/presentation/bloc/auth_cubit.dart'; // 🔥 导入 AuthCubit
import '../../../remote_control/presentation/bloc/remote_control_cubit.dart';
import '../widgets/map/test_amap_page.dart';
import '../widgets/map/testmap_pages.dart';
class RoutePlanPage extends StatelessWidget {
class RoutePlanPage extends StatefulWidget {
const RoutePlanPage({super.key});
@override
State<RoutePlanPage> createState() => _RoutePlanPageState();
}
class _RoutePlanPageState extends State<RoutePlanPage> {
@override
void initState() {
super.initState();
// 🔥 进入安全模式:防止异地登录推送导致路径规划中断
GetIt.I<AuthCubit>().enterSafeMode();
}
@override
void dispose() {
// 🔥 离开安全模式:启动 90 秒倒计时
GetIt.I<AuthCubit>().exitSafeMode();
super.dispose();
}
@override
Widget build(BuildContext context) {
try {

View File

@@ -619,7 +619,7 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
return AlertDialog(
title: Text(AppLocalizations.of(dialogContext).translate('machine_details.unbind_confirm_title')),
content: Text(AppLocalizations.of(dialogContext).translate('machine_details.unbind_confirm_content').replaceAll('%s', _deviceName)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
actions: [
TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(AppLocalizations.of(dialogContext).translate('common.cancel'))),
TextButton(
@@ -703,7 +703,7 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
);
}
print("调用updateDeviceName,deviceId: $_deviceId, newDeviceName: $newDeviceName");
//print("调用updateDeviceName,deviceId: $_deviceId, newDeviceName: $newDeviceName");
// 调用Cubit修改方法
await context.read<DevicesCubit>().updateDeviceName(_deviceId, newDeviceName);

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart';
import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart';
@@ -10,9 +11,12 @@ import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart' as my_v2;
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart'
as my_v2;
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart';
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
import 'package:maibu_satabot_v2/core/di/injection.dart';
class MainWrapper extends StatefulWidget {
@@ -31,6 +35,10 @@ class _MainWrapperState extends State<MainWrapper> {
@override
void initState() {
super.initState();
// 🔥 确保 FloatBarSettingService 已初始化
GetIt.I<FloatBarSettingService>();
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateCirclePosition();
});
@@ -38,7 +46,7 @@ class _MainWrapperState extends State<MainWrapper> {
void _updateCirclePosition() {
if (_tabKeys.isEmpty || _currentIndex >= _tabKeys.length) return;
final key = _tabKeys[_currentIndex];
final renderBox = key.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null) {
@@ -99,7 +107,7 @@ class _MainWrapperState extends State<MainWrapper> {
// 圆形应该在每个Tab项的中心
final tabCenterX = 12 + (currentIndex * itemWidth) + (itemWidth / 2);
final circularLeft = tabCenterX - 22; // 22 = 44/2,让圆形中心对齐Tab中心
// 边界限制
final maxLeft = screenWidth - 12 - 44;
return circularLeft.clamp(12.0, maxLeft);
@@ -109,7 +117,7 @@ class _MainWrapperState extends State<MainWrapper> {
switch (tab.id) {
case 'home_v2':
return BlocProvider(
create: (context) => sl<HomeV2Bloc>(),
create: (_) => sl<HomeV2Bloc>(),
child: const HomeV2Page(),
);
case 'home':
@@ -120,7 +128,7 @@ class _MainWrapperState extends State<MainWrapper> {
return const AiPage();
case 'warning':
return BlocProvider(
create: (context) => sl<AlarmCubit>(),
create: (_) => sl<AlarmCubit>(),
child: const AlarmCenterPage(),
);
case 'workorder':
@@ -141,18 +149,16 @@ class _MainWrapperState extends State<MainWrapper> {
return BlocBuilder<TabConfigCubit, TabConfigState>(
builder: (context, state) {
if (state is! TabConfigLoaded) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final config = state.config;
final enabledItems = config.enabledItems;
if (enabledItems.isEmpty) {
return const Scaffold(
body: Center(
child: Text('请至少启用一个 Tab'),
),
);
return const Scaffold(body: Center(child: Text('请至少启用一个 Tab')));
}
if (_currentIndex >= enabledItems.length) {
@@ -172,26 +178,43 @@ class _MainWrapperState extends State<MainWrapper> {
return Scaffold(
extendBody: false,
body: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
children: enabledItems.map((tab) => _buildCurrentPage(tab)).toList(),
body: Stack(
children: [
PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
children: enabledItems
.map((tab) => _buildCurrentPage(tab))
.toList(),
),
// 悬浮条 - 可拖动的悬浮按钮
ValueListenableBuilder<bool>(
valueListenable: FloatBarSettingService.settingNotifier,
builder: (context, isEnabled, child) {
return Visibility(
visible: isEnabled,
child: const FloatBarWidget(),
);
},
),
],
),
bottomNavigationBar: Container(
decoration: const BoxDecoration(
color: Colors.white,
),
decoration: const BoxDecoration(color: Colors.white),
child: SafeArea(
top: false,
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
padding: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: Colors.white,
@@ -217,26 +240,26 @@ class _MainWrapperState extends State<MainWrapper> {
curve: Curves.easeInOut,
left: _circleLeft! - 8, // 减去外层 margin + 向右偏移
top: 6,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFE5E6EB),
width: 1,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFE5E6EB),
width: 1,
),
),
),
),
),
// Tab 项
Row(
children: enabledItems.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final isSelected = _currentIndex == index;
return Expanded(
key: _tabKeys[index],
child: InkWell(
@@ -250,15 +273,21 @@ class _MainWrapperState extends State<MainWrapper> {
Icon(
_getIconData(item.icon),
size: 24,
color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C),
color: isSelected
? const Color(0xFF1D2129)
: const Color(0xFF86909C),
),
const SizedBox(height: 4),
Text(
item.name,
style: TextStyle(
fontSize: 11,
color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? const Color(0xFF1D2129)
: const Color(0xFF86909C),
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
),
),
],

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/components/tcp_status_indicator.dart';
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
@@ -11,6 +12,8 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_pag
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart';
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
import '../../../v2/waring_center/presentation/pages/alarm_center_page.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
@@ -25,7 +28,7 @@ class CustomMainContainer extends StatefulWidget {
class _CustomMainContainerState extends State<CustomMainContainer>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // 保持页面状态
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
@@ -33,6 +36,9 @@ class _CustomMainContainerState extends State<CustomMainContainer>
debugPrint('🔍 [CustomMainContainer] build 被调用');
// 🔥 确保 FloatBarSettingService 已初始化(懒加载单例第一次访问时创建)
GetIt.I<FloatBarSettingService>();
return BlocBuilder<TabConfigCubit, TabConfigState>(
builder: (context, state) {
if (state is! TabConfigLoaded) {
@@ -75,7 +81,7 @@ class _CustomMainContainerState extends State<CustomMainContainer>
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5), // 透明灰色背景
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Row(
@@ -92,6 +98,19 @@ class _CustomMainContainerState extends State<CustomMainContainer>
),
),
),
// 悬浮条 - 底部,位于导航栏上方
ValueListenableBuilder<bool>(
valueListenable: FloatBarSettingService.settingNotifier,
builder: (context, isEnabled, child) {
return Visibility(
visible: isEnabled,
child: const Align(
alignment: Alignment.bottomCenter,
child: FloatBarWidget(),
),
);
},
),
],
),
bottomNavigationBar: Column(

View File

@@ -22,14 +22,8 @@ class _CustomBottomNavBarState extends State<CustomBottomNavBar>
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
),
_animation = Tween<double>(begin: 0.0, end: 0.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
);
}
@@ -80,15 +74,16 @@ class _CustomBottomNavBarState extends State<CustomBottomNavBar>
final beginOffset = _previousIndex * tabWidth + tabWidth / 2;
final endOffset = selectedIndex * tabWidth + tabWidth / 2;
_animation = Tween<double>(
begin: beginOffset - circleDiameter / 2,
end: endOffset - circleDiameter / 2,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
),
);
_animation =
Tween<double>(
begin: beginOffset - circleDiameter / 2,
end: endOffset - circleDiameter / 2,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
),
);
_previousIndex = selectedIndex;
_animationController.forward(from: 0.0);
@@ -123,7 +118,8 @@ class _CustomBottomNavBarState extends State<CustomBottomNavBar>
icon: _getIconData(tab.icon),
label: tab.name,
isSelected: isSelected,
onTap: () => context.read<TabConfigCubit>().selectTab(index),
onTap: () =>
context.read<TabConfigCubit>().selectTab(index),
),
);
}),

View File

@@ -419,7 +419,17 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
}
// 情况 3: 异地登录通知 - {"request":"have_logged_in",...}
else if (requestType == 'have_logged_in') {
debugPrint('${_getTimePrefix()} ⚠️ [RemoteControl] 检测到异地登录');
// 🔥 关键修复:检查是否是自身 0x03 认证触发的
if (tcpClient.isOwnAuthTriggeredKick()) {
debugPrint('${_getTimePrefix()} 🛡️ [RemoteControl] 自身认证触发的 have_logged_in,忽略');
_logger.logWithLevel(
'${_getTimePrefix()} 🛡️ [RemoteControl] 自身认证触发的 have_logged_in,忽略',
shouldLog: true,
);
return;
}
debugPrint('${_getTimePrefix()} ⚠️ [RemoteControl] 检测到真正的异地登录');
_logger.logWithLevel(
'${_getTimePrefix()} ⚠️ [RemoteControl] 检测到异地登录,打开弹窗提示',
shouldLog: true,
@@ -949,41 +959,37 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
emit(state.copyWith(targetDevice: device));
debugPrint('✅ [RemoteControl] targetDevice状态已更新');
// 🔥 关键修改:选择设备时才连接TCP
if (!tcpClient.isConnected) {
debugPrint('🔌 [RemoteControl] TCP未连接,开始建立连接...');
// 🔥 关键修复:TCP 已在登录时建立,设备切换只用 HTTP
// 不再创建新 TCP 连接,避免重复发送 0x03 触发服务端推送 have_logged_in
if (tcpClient.isConnected) {
debugPrint('✅ [RemoteControl] TCP已连接,使用HTTP切换设备: ${device.deviceName}');
_deviceRepository.switchDevice("app", device.deviceName).then((result) {
result.fold(
(failure) {
debugPrint('❌ [RemoteControl] HTTP切换设备失败: ${failure.message}');
},
(success) {
debugPrint('✅ [RemoteControl] HTTP切换设备成功, code: $success');
},
);
});
} else {
// TCP未连接(登录时的初始化可能失败或还在进行中)
// 兜底:使用 connectBySwitch 建立连接
debugPrint('🔌 [RemoteControl] TCP未连接,兜底调用 connectBySwitch');
try {
debugPrint('🔌 [RemoteControl] 调用 connectBySwitch,目标: ${device.deviceName}');
await tcpClient.connectBySwitch(
host: TCPConsts.TCP_IP,
port: TCPConsts.TCP_PORT,
deviceName: device.deviceName,
);
debugPrint('✅ [RemoteControl] TCP连接成功!');
debugPrint('✅ [RemoteControl] 认证流程已完成(包含发送0x03认证包和设备订阅)');
debugPrint('✅ [RemoteControl] TCP兜底连接成功!');
} catch (e) {
debugPrint('❌ [RemoteControl] TCP连接失败: $e');
debugPrint('⚠️ [RemoteControl] 将继续执行,允许用户进入页面(但无实时数据)');
// 即使TCP连接失败,也继续设置设备,允许用户进入页面
debugPrint('❌ [RemoteControl] TCP兜底连接失败: $e');
}
} else {
debugPrint('✅ [RemoteControl] TCP已连接,跳过连接步骤');
debugPrint('🔄 [RemoteControl] 将直接切换设备订阅');
}
// 🔥 通知后端订阅该设备
debugPrint('📡 [RemoteControl] 开始HTTP切换设备订阅...');
_deviceRepository.switchDevice("app", device.deviceName).then((result) {
result.fold(
(failure) {
debugPrint('❌ [RemoteControl] HTTP切换设备失败: ${failure.message}');
},
(success) {
debugPrint('✅ [RemoteControl] HTTP切换设备成功, code: $success');
},
);
});
debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 ==========');
}

View File

@@ -3,12 +3,14 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../../core/router/route_paths.dart';
import '../../../auth/presentation/bloc/auth_cubit.dart'; // 🔥 导入 AuthCubit
import '../../../devices/domain/entities/device_entity.dart';
import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../bloc/remote_control_cubit.dart';
@@ -91,6 +93,9 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
@override
void initState() {
super.initState();
// 🔥 进入安全模式:防止异地登录推送导致控制中断
GetIt.I<AuthCubit>().enterSafeMode();
// 1. 锁定横屏
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
@@ -121,6 +126,9 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
@override
void dispose() {
// 🔥 离开安全模式:启动 90 秒倒计时
GetIt.I<AuthCubit>().exitSafeMode();
// 释放远程控制权限
_cubit.releasePermission("app");
//print("远程控制要推出啦");
@@ -566,4 +574,5 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
),
);
}
}

View File

@@ -54,7 +54,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
allDevices.addAll(result);
}
if (typeFilter != null && typeFilter != '全部') {
if (typeFilter != null && typeFilter != 'all') {
return allDevices.where((device) => device.type == typeFilter).toList();
}
@@ -124,7 +124,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
return DeviceDataModel(
deviceId: json['device_sn']?.toString() ?? '',
name: json['callsign'] ?? '无人机机场',
type: '无人机机场',
type: 'drone_station',
status: onlineStatus == 1 ? '在线' : '离线',
power: (json['capacity_percent'] as num?)?.toDouble() ?? 0,
todayEnergy: 0,
@@ -150,7 +150,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
const DeviceDataModel(
deviceId: '10001',
name: '逆变器 INV-001',
type: '逆变器',
type: 'inverter',
status: '在线',
power: 125.8,
todayEnergy: 752.6,
@@ -158,7 +158,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
const DeviceDataModel(
deviceId: '10002',
name: '逆变器 INV-002',
type: '逆变器',
type: 'inverter',
status: '在线',
power: 118.6,
todayEnergy: 698.2,
@@ -166,7 +166,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
const DeviceDataModel(
deviceId: '10003',
name: '汇流箱 SCB-001',
type: '汇流箱',
type: 'combiner_box',
status: '在线',
power: 0,
todayEnergy: 0,
@@ -176,7 +176,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
const DeviceDataModel(
deviceId: '10004',
name: '组件组串 STR-001',
type: '组件',
type: 'module',
status: '在线',
power: 85.3,
todayEnergy: 0,
@@ -185,7 +185,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
const DeviceDataModel(
deviceId: '10005',
name: '监控 MON-001',
type: '监控',
type: 'monitor',
status: '离线',
power: 0,
todayEnergy: 0,
@@ -193,14 +193,14 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
const DeviceDataModel(
deviceId: '10006',
name: '逆变器 INV-003',
type: '逆变器',
type: 'inverter',
status: '异常',
power: 45.2,
todayEnergy: 256.8,
),
];
if (typeFilter != null && typeFilter != '全部') {
if (typeFilter != null && typeFilter != 'all') {
return allDevices.where((device) => device.type == typeFilter).toList();
}

View File

@@ -20,14 +20,16 @@ class RobotDataModel {
/// 从 JSON 创建数据模型
factory RobotDataModel.fromJson(Map<String, dynamic> json) {
final batteryValue = (json['battery'] as num?)?.toDouble() ??
(json['capacity_percent'] as num?)?.toDouble() ?? 100.0;
final batteryValue =
(json['battery'] as num?)?.toDouble() ??
(json['capacity_percent'] as num?)?.toDouble() ??
100.0;
return RobotDataModel(
name: json['deviceName'] ?? json['name'] ?? '',
id: json['deviceId']?.toString() ?? json['id']?.toString() ?? '',
alias: json['deviceAlias'] as String?, // 获取别名字段
type: json['deviceTypeName'] ?? json['type'] ?? '未知类型',
type: _convertType(json['deviceTypeName'] ?? json['type'] ?? '未知类型'),
status: _getStatusText(json['status'] ?? 0),
battery: batteryValue,
task: json['task'] ?? json['currentTask'] ?? '待机中',
@@ -60,4 +62,15 @@ class RobotDataModel {
return '未知';
}
}
static String _convertType(String type) {
if (type.contains('巡检')) {
return 'inspection';
} else if (type.contains('清洗')) {
return 'cleaning';
} else if (type.contains('除草')) {
return 'weeding';
}
return type;
}
}

View File

@@ -49,7 +49,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
try {
final response = await getDeviceStatusDataUseCase.execute(
siteId: currentState.siteId,
typeFilter: currentState.selectedType == '全部'
typeFilter: currentState.selectedType == 'all'
? null
: currentState.selectedType,
);

View File

@@ -27,7 +27,7 @@ class DeviceStatusLoaded extends DeviceStatusState {
const DeviceStatusLoaded({
required this.deviceStatus,
required this.devices,
this.selectedType = '全部',
this.selectedType = 'all',
this.searchKeyword = '',
this.siteId,
});

View File

@@ -34,10 +34,12 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final result = await getDroneStationListUseCase(event.siteId);
result.fold(
(failure) => emit(DroneStationError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(failure) => emit(
DroneStationError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
),
),
(stations) => emit(DroneStationLoaded(stations)),
);
}
@@ -50,10 +52,12 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final result = await getDroneStationListUseCase(event.siteId);
result.fold(
(failure) => emit(DroneStationError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(failure) => emit(
DroneStationError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
),
),
(stations) => emit(DroneStationLoaded(stations)),
);
}
@@ -68,10 +72,12 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final result = await getUAVDetailUseCase(event.gatewaySn, event.deviceSn);
result.fold(
(failure) => emit(UAVDetailError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(failure) => emit(
UAVDetailError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
),
),
(detail) => emit(UAVDetailLoaded(detail)),
);
}
@@ -89,11 +95,14 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
);
result.fold(
(failure) => emit(VideoStreamError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
(failure) => emit(
VideoStreamError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
),
),
(videoStream) =>
emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
);
}
@@ -113,15 +122,19 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
);
result.fold(
(failure) => emit(UavVideoStreamError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(videoStream) => emit(UavVideoStreamLoaded(
videoStream: videoStream,
cameraIndex: event.cameraIndex,
lensType: event.lensType,
)),
(failure) => emit(
UavVideoStreamError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
),
),
(videoStream) => emit(
UavVideoStreamLoaded(
videoStream: videoStream,
cameraIndex: event.cameraIndex,
lensType: event.lensType,
),
),
);
}
}

View File

@@ -18,44 +18,30 @@ class FloatBarSettingService {
FloatBarSettingService(this._prefs) {
_instance = this;
// 从持久化读取初始状态
_isEnabled = _prefs.getBool(_key) ?? true;
_settingNotifier.value = _isEnabled;
print('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled');
debugPrint('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled');
}
/// 获取静态实例
static FloatBarSettingService? get instance => _instance;
/// 获取 ValueNotifier
static ValueNotifier<bool> get settingNotifier => _settingNotifier;
/// 获取当前是否启用(直接从静态变量读取)
static bool get isEnabled => _isEnabled;
/// 设置是否启用
static Future<void> setEnabled(bool enabled) async {
print('🔍 [FloatBarSettingService] setEnabled 被调用,新值: $enabled');
// 1. 更新静态变量
debugPrint('🔍 [FloatBarSettingService] setEnabled 被调用,新值: $enabled');
_isEnabled = enabled;
print('🔍 [FloatBarSettingService] 静态变量已更新: $_isEnabled');
// 2. 更新 ValueNotifier(通知所有监听者)
_settingNotifier.value = enabled;
print(
'🔍 [FloatBarSettingService] ValueNotifier 已更新: ${_settingNotifier.value}',
);
// 3. 持久化到 SharedPreferences
final instance = _instance;
if (instance != null) {
await instance._prefs.setBool(_key, enabled);
print('🔍 [FloatBarSettingService] 已保存到 SharedPreferences');
debugPrint('🔍 [FloatBarSettingService] 已保存到 SharedPreferences');
}
}
/// 切换开关
static Future<void> toggle() async {
await setEnabled(!_isEnabled);
}

View File

@@ -37,9 +37,7 @@ class FloatBarManager {
hide();
}
_overlayEntry = OverlayEntry(
builder: (context) => const FloatBarWidget(),
);
_overlayEntry = OverlayEntry(builder: (context) => const FloatBarWidget());
Overlay.of(_globalContext!)?.insert(_overlayEntry!);
}
@@ -66,4 +64,4 @@ class FloatBarManager {
_globalContext = null;
_isInitialized = false;
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
/// 清洗/除草机器人任务详情页
class CleaningWeedingRobotTaskPage extends StatelessWidget {
@@ -8,8 +9,10 @@ class CleaningWeedingRobotTaskPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isCleaning = robot['type'] == '清洗机器人';
final title = isCleaning ? '清洗机器人任务' : '除草机器人任务';
final isCleaning = robot['type'] == 'cleaning';
final title = isCleaning
? AppLocalizations.of(context).translate('device_list_v2.cleaning_robot') + ' ' + AppLocalizations.of(context).translate('device_list_v2.task')
: AppLocalizations.of(context).translate('device_list_v2.weeding_robot') + ' ' + AppLocalizations.of(context).translate('device_list_v2.task');
return Scaffold(
backgroundColor: const Color(0xFFF5F6F8),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/app/app_user_cubit.dart';
import '../../../../../components/tcp_status_indicator.dart';
@@ -47,6 +48,16 @@ class DeviceStatusPage extends StatelessWidget {
class DeviceStatusView extends StatelessWidget {
const DeviceStatusView({super.key});
/// 🔥 机器人设备名称前缀,用于从通用设备列表中过滤掉机器人
static const _robotPrefixes = [
'RCHETD-CN',
'MC700PLUS-CN',
'MC700-CN',
'MC700PRO-CN',
'RCETD-CN',
'MC700AIR-CN',
];
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
@@ -98,9 +109,9 @@ class DeviceStatusView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text(
'设备状态',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('device_list_v2.title'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -137,7 +148,9 @@ class DeviceStatusView extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(16.0, 8, 16.0, 8),
child: TextField(
decoration: InputDecoration(
hintText: '搜索设备名称/编号',
hintText: AppLocalizations.of(
context,
).translate('device_list_v2.search_hint'),
hintStyle: const TextStyle(color: Color(0xFF86909C), fontSize: 14),
prefixIcon: const Icon(
Icons.search,
@@ -148,9 +161,9 @@ class DeviceStatusView extends StatelessWidget {
onPressed: () {
// TODO: 触发搜索
},
child: const Text(
'搜索',
style: TextStyle(
child: Text(
AppLocalizations.of(context).translate('device_list_v2.search'),
style: const TextStyle(
color: Color(0xFF165DFF),
fontSize: 14,
fontWeight: FontWeight.w500,
@@ -188,14 +201,31 @@ class DeviceStatusView extends StatelessWidget {
}
Widget _buildTypeFilterBar(BuildContext context) {
final types = ['全部', '机器人', '无人机机场', '逆变器', '汇流箱', '组件', '监控'];
final typeCodes = [
'all',
'robot',
'drone_station',
'inverter',
'combiner_box',
'module',
'monitor',
];
final typeLabels = [
AppLocalizations.of(context).translate('device_list_v2.all'),
AppLocalizations.of(context).translate('device_list_v2.robot'),
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
AppLocalizations.of(context).translate('device_list_v2.inverter'),
AppLocalizations.of(context).translate('device_list_v2.combiner_box'),
AppLocalizations.of(context).translate('device_list_v2.module'),
AppLocalizations.of(context).translate('device_list_v2.monitor'),
];
return BlocBuilder<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
builder: (context, state) {
String selectedType = '全部';
String selectedType = 'all';
if (state is DeviceListState.DeviceStatusLoaded) {
selectedType = state.selectedType;
}
@@ -206,16 +236,17 @@ class DeviceStatusView extends StatelessWidget {
scrollDirection: Axis.horizontal,
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemCount: types.length,
itemCount: typeCodes.length,
separatorBuilder: (context, index) => const SizedBox(width: 24.0),
itemBuilder: (context, index) {
final type = types[index];
final isSelected = selectedType == type;
final typeCode = typeCodes[index];
final typeLabel = typeLabels[index];
final isSelected = selectedType == typeCode;
return GestureDetector(
onTap: () {
context.read<DeviceListBloc.DeviceStatusBloc>().add(
DeviceListEvent.DeviceStatusChangeType(type),
DeviceListEvent.DeviceStatusChangeType(typeCode),
);
},
child: Column(
@@ -223,7 +254,7 @@ class DeviceStatusView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
type,
typeLabel,
style: TextStyle(
fontSize: 15,
color: isSelected
@@ -259,19 +290,16 @@ class DeviceStatusView extends StatelessWidget {
BuildContext context,
DeviceListState.DeviceStatusState state,
) {
// 如果选择的是"机器人",显示机器人专属页面
if (state is DeviceListState.DeviceStatusLoaded &&
state.selectedType == '机器人') {
state.selectedType == 'robot') {
return const RobotListPage();
}
// 如果选择的是"无人机机场",显示机场列表
if (state is DeviceListState.DeviceStatusLoaded &&
state.selectedType == '无人机机场') {
state.selectedType == 'drone_station') {
return _buildDroneStationList(context);
}
// 其他类型显示原有设备列表
return _buildDeviceList(context, state);
}
@@ -293,12 +321,24 @@ class DeviceStatusView extends StatelessWidget {
}
if (state is DeviceListState.DeviceStatusLoaded) {
// 🔥 先根据类型过滤
List filteredByType = state.devices;
if (state.selectedType != '全部') {
if (state.selectedType != 'all') {
filteredByType = state.devices.where((device) {
return device.type == state.selectedType;
}).toList();
} else {
// 🔥 "全部"模式下,排除已由专用组件渲染的类型,避免重复展示
// drone_station 已通过 _buildDroneStationList 渲染
// 机器人(通过名称前缀识别)已通过 _buildEmbeddedRobotList 渲染
filteredByType = state.devices.where((device) {
if (device.type == 'drone_station') return false;
final isRobot = _robotPrefixes.any(
(prefix) =>
device.name.startsWith(prefix) ||
device.deviceId.startsWith(prefix),
);
return !isRobot;
}).toList();
}
// 🔥 再根据搜索关键词过滤
@@ -322,19 +362,32 @@ class DeviceStatusView extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.only(bottom: 16),
children: [
_buildStatusCard(state),
_buildStatusCard(context, state),
const SizedBox(height: 20.0),
// 🔥 机器人列表(直接复用 RobotListPage 的布局)
if (state.selectedType == '全部') _buildEmbeddedRobotList(context),
if (state.selectedType == 'all') _buildEmbeddedRobotList(context),
// 🔥 无人机机场列表(直接复用 _buildDroneStationList 的布局)
if (state.selectedType == '全部')
if (state.selectedType == 'all')
_buildDroneStationList(context, embedded: true),
// 🔥 普通设备列表
...filteredDevices
.map(
(device) => DeviceItemWidget(
device: device,
onTap: () => _navigateToDeviceDetail(context, device),
),
)
.toList(),
],
),
);
}
return const Center(child: Text('暂无数据'));
return Center(
child: Text(
AppLocalizations.of(context).translate('device_list_v2.no_data'),
),
);
}
/// 🔥 嵌入式机器人列表(直接复用 RobotListPage 的布局)
@@ -428,9 +481,11 @@ class DeviceStatusView extends StatelessWidget {
children: [
const Icon(Icons.location_off, size: 48, color: Color(0xFF86909C)),
const SizedBox(height: 16),
const Text(
'请先选择场站',
style: TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
Text(
AppLocalizations.of(
context,
).translate('device_list_v2.select_site_first'),
style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
),
],
),
@@ -471,10 +526,15 @@ class DeviceStatusView extends StatelessWidget {
final stations = state.stations;
if (stations.isEmpty) {
return const Center(
return Center(
child: Text(
'该场站暂无无人机机场',
style: TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
AppLocalizations.of(
context,
).translate('device_list_v2.no_drone_stations'),
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
),
),
);
}
@@ -512,13 +572,20 @@ class DeviceStatusView extends StatelessWidget {
);
}
return const Center(child: Text('暂无数据'));
return Center(
child: Text(
AppLocalizations.of(context).translate('device_list_v2.no_data'),
),
);
},
),
);
}
Widget _buildStatusCard(DeviceListState.DeviceStatusLoaded state) {
Widget _buildStatusCard(
BuildContext context,
DeviceListState.DeviceStatusLoaded state,
) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.all(16.0),
@@ -537,28 +604,36 @@ class DeviceStatusView extends StatelessWidget {
children: [
_buildStatItem(
value: state.deviceStatus.total.toString(),
label: '全部设备',
label: AppLocalizations.of(
context,
).translate('device_list_v2.total_devices'),
dotColor: const Color(0xFF86909C),
textColor: const Color(0xFF86909C), // 新增字体颜色
textColor: const Color(0xFF86909C),
),
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
_buildStatItem(
value: state.deviceStatus.online.toString(),
label: '在线',
label: AppLocalizations.of(
context,
).translate('device_list_v2.online'),
dotColor: const Color(0xFF00B42A),
textColor: const Color(0xFF00B42A),
),
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
_buildStatItem(
value: state.deviceStatus.exception.toString(),
label: '告警',
label: AppLocalizations.of(
context,
).translate('device_list_v2.alarm'),
dotColor: const Color(0xFFFF7D00),
textColor: const Color(0xFFFF7D00),
),
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
_buildStatItem(
value: state.deviceStatus.offline.toString(),
label: '离线',
label: AppLocalizations.of(
context,
).translate('device_list_v2.offline'),
dotColor: const Color(0xFFF53F3F),
textColor: const Color(0xFFF53F3F),
),
@@ -597,11 +672,16 @@ class DeviceStatusView extends StatelessWidget {
),
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: textColor ?? const Color(0xFF4E5969),
Flexible(
child: Text(
label,
style: TextStyle(
fontSize: 12,
color: textColor ?? const Color(0xFF4E5969),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
],
@@ -646,14 +726,7 @@ class DeviceStatusView extends StatelessWidget {
debugPrint('🔍 [DeviceStatusPage] 设备类型: $deviceType');
// 🔥 判断是否为机器人(根据设备类型前缀)
final robotPrefixes = [
'RCHETD-CN',
'MC700PLUS-CN',
'MC700-CN',
'MC700PRO-CN',
'RCETD-CN',
'MC700AIR-CN',
];
final robotPrefixes = _robotPrefixes;
bool isRobot = robotPrefixes.any((prefix) => deviceType.startsWith(prefix));
// 🔥 判断是否为无人机机场(根据设备类型包含关键字)

View File

@@ -4,9 +4,13 @@ import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import '../../../../../core/consts/http_api_consts.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/managers/drone_task_state_manager.dart';
import '../../domain/entities/flight_task_entity.dart';
import '../../domain/entities/flight_task_detail_entity.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/usecases/update_flight_task_status_usecase.dart';
import '../bloc/drone_station_bloc.dart';
import '../float_bar/view/float_bar_widget.dart';
/// 无人机任务与航线控制页面
class DroneMissionControlPage extends StatefulWidget {
@@ -120,11 +124,14 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
context,
).showSnackBar(SnackBar(content: Text(failure.message)));
},
(data) {
(data) async {
print('✅ [DroneMissionControl] 任务执行成功: $data');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务执行成功')));
// 🔥 获取无人机详情并设置给 FloatBarWidget
await _loadDroneDetailAndSetToFloatBar();
},
);
} catch (e) {
@@ -135,6 +142,65 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
}
}
/// 加载无人机详情并设置给 FloatBarWidget
Future<void> _loadDroneDetailAndSetToFloatBar() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
print('⚠️ [DroneMissionControl] 任务 SN 为空,无法获取无人机详情');
return;
}
try {
print('🔍 [DroneMissionControl] 开始获取无人机详情, sn: ${_detailTask!.sn}');
final response = await Dio().get(
HttpApiConsts.getUAVDetail,
queryParameters: {
'gatewaySn': _detailTask!.sn, // 机场 SN
'deviceSn': '', // 无人机 SN(可选)
},
);
print('📡 [DroneMissionControl] 响应状态码: ${response.statusCode}');
print('📦 [DroneMissionControl] 响应数据: ${response.data}');
if (response.statusCode == 200) {
final Map<String, dynamic> jsonData = (response.data is String)
? json.decode(response.data)
: Map<String, dynamic>.from(response.data);
if (jsonData['code'] == 0 || jsonData['code'] == 200) {
final detailData = jsonData['data'];
final Map<String, dynamic> detailMap = (detailData is Map)
? Map<String, dynamic>.from(detailData)
: <String, dynamic>{};
final droneDetail = UAVDetailEntity.fromJson(detailMap);
print('✅ [DroneMissionControl] 获取无人机详情成功');
print(' droneSn: ${droneDetail.deviceSn}');
print(' gatewaySn: ${droneDetail.gatewaySn}');
print(' cameraList: ${droneDetail.droneCameraList?.length ?? 0}');
// 创建无人机任务信息
final droneTaskInfo = DroneTaskInfo(
droneSn: droneDetail.deviceSn,
gatewaySn: droneDetail.gatewaySn,
cameraList: droneDetail.droneCameraList,
);
// 设置给 FloatBarWidget
droneTaskStateManager.setDroneTaskInfo(droneTaskInfo);
print('✅ [DroneMissionControl] 已设置无人机任务信息到 FloatBarWidget');
} else {
print('❌ [DroneMissionControl] 获取无人机详情失败: ${jsonData['message']}');
}
}
} catch (e) {
print('❌ [DroneMissionControl] 获取无人机详情异常: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/managers/drone_task_state_manager.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../bloc/drone_station_bloc.dart';
import '../bloc/drone_station_event.dart';
@@ -402,18 +403,36 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
}
Widget _buildDroneStatusCard(UAVDetailEntity detail) {
// 检查无人机是否在线
final bool isOnline = detail.droneOnlineStatus == 1;
return GestureDetector(
onTap: () {
// 扩展功能:无论在线还是离线,都同步到悬浮弹窗
debugPrint('👆 [DroneStationDetailPage] 点击无人机状态卡片,同步到悬浮弹窗');
debugPrint(' droneSn: ${detail.deviceSn}');
debugPrint(' gatewaySn: ${detail.gatewaySn}');
debugPrint(' 是否在线: ${isOnline ? '在线' : '离线'}');
final droneTaskInfo = DroneTaskInfo(
droneSn: detail.deviceSn,
gatewaySn: detail.gatewaySn,
cameraList: detail.droneCameraList,
isOnline: isOnline,
stationName: detail.callsign,
droneName: detail.droneCallsign,
);
droneTaskStateManager.setDroneTaskInfo(droneTaskInfo);
// 原有逻辑:跳转到无人机视频控制页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DroneVideoControlPage(
droneSn: detail.deviceSn,
gatewaySn: detail.gatewaySn,
// 使用无人机摄像头列表,如果为null则传空列表
cameraList: detail.droneCameraList ?? [],
// 传入网关摄像头列表作为备选视频源
gatewayCameraList: detail.gatewayCameraList ?? [],
cameraList: detail.droneCameraList,
gatewayCameraList: detail.gatewayCameraList,
),
),
);

View File

@@ -1,65 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/managers/drone_task_state_manager.dart';
import '../bloc/drone_station_bloc.dart';
import '../bloc/drone_station_event.dart';
import '../bloc/drone_station_state.dart';
import '../../domain/entities/drone_station_entity.dart';
import 'drone_video_control_page.dart';
/// 无人机机场与设备状态页面
class DroneStationStatusPage extends StatelessWidget {
const DroneStationStatusPage({super.key});
/// 支持从机场详情页进入,点击无人机卡片同步视频和轨迹到悬浮弹窗
class DroneStationStatusPage extends StatefulWidget {
final String? gatewaySn;
final String? deviceSn;
const DroneStationStatusPage({super.key, this.gatewaySn, this.deviceSn});
@override
State<DroneStationStatusPage> createState() => _DroneStationStatusPageState();
}
class _DroneStationStatusPageState extends State<DroneStationStatusPage> {
late final DroneStationBloc _bloc;
UAVDetailEntity? _uavDetail;
@override
void initState() {
super.initState();
_bloc = sl<DroneStationBloc>();
_loadUAVDetail();
}
@override
void dispose() {
_bloc.close();
super.dispose();
}
/// 加载无人机详情
void _loadUAVDetail() {
if (widget.gatewaySn != null && widget.gatewaySn!.isNotEmpty) {
_bloc.add(
UAVDetailLoad(
gatewaySn: widget.gatewaySn!,
deviceSn: widget.deviceSn ?? '',
),
);
}
}
/// 检查无人机是否在线
bool _isDroneOnline() {
return _uavDetail != null && _uavDetail!.droneOnlineStatus == 1;
}
/// 点击无人机卡片处理
/// 无论在线还是离线,都同步到悬浮弹窗,同时跳转到视频控制页面
void _onDroneCardTap() {
debugPrint('👆 [DroneStationStatusPage] 点击无人机卡片');
if (_uavDetail == null) {
debugPrint('⚠️ [DroneStationStatusPage] 无人机详情为空');
return;
}
// 扩展功能:无论在线还是离线,都同步到悬浮弹窗
debugPrint('🛸 [DroneStationStatusPage] 同步无人机信息到悬浮弹窗');
debugPrint(' droneSn: ${_uavDetail!.deviceSn}');
debugPrint(' gatewaySn: ${_uavDetail!.gatewaySn}');
debugPrint(' 是否在线: ${_isDroneOnline() ? '在线' : '离线'}');
final droneTaskInfo = DroneTaskInfo(
droneSn: _uavDetail!.deviceSn,
gatewaySn: _uavDetail!.gatewaySn,
cameraList: _uavDetail!.droneCameraList,
isOnline: _isDroneOnline(),
stationName: _uavDetail!.callsign,
droneName: _uavDetail!.droneCallsign,
);
debugPrint(' cameraList: ${droneTaskInfo.cameraList?.length ?? 0}');
droneTaskStateManager.setDroneTaskInfo(droneTaskInfo);
// 原有逻辑:跳转到无人机视频控制页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DroneVideoControlPage(
droneSn: _uavDetail!.deviceSn,
gatewaySn: _uavDetail!.gatewaySn,
cameraList: _uavDetail!.droneCameraList,
gatewayCameraList: _uavDetail!.gatewayCameraList,
),
),
);
}
/// 显示无人机不在线提示
void _showOfflineToast() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('无人机不在线,无法查看实时视频和轨迹'),
duration: Duration(seconds: 2),
backgroundColor: Color(0xFFFF7D00),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F6F8),
body: CustomScrollView(
slivers: [
// 顶部导航栏
SliverAppBar(
backgroundColor: Colors.white,
elevation: 0,
pinned: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'无人机机场与设备状态',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
return BlocProvider.value(
value: _bloc,
child: BlocListener<DroneStationBloc, DroneStationState>(
listener: (context, state) {
if (state is UAVDetailLoaded) {
setState(() {
_uavDetail = state.detail;
});
debugPrint('✅ [DroneStationStatusPage] 无人机详情加载成功');
debugPrint(
' droneOnlineStatus: ${state.detail.droneOnlineStatus}',
);
debugPrint(' deviceSn: ${state.detail.deviceSn}');
debugPrint(' gatewaySn: ${state.detail.gatewaySn}');
} else if (state is UAVDetailError) {
debugPrint(
'❌ [DroneStationStatusPage] 无人机详情加载失败: ${state.message}',
);
}
},
child: Scaffold(
backgroundColor: const Color(0xFFF5F6F8),
body: CustomScrollView(
slivers: [
// 顶部导航栏
SliverAppBar(
backgroundColor: Colors.white,
elevation: 0,
pinned: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'无人机机场与设备状态',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(
Icons.notifications_none,
color: Color(0xFF1D2129),
),
onPressed: () {},
),
],
),
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.notifications_none, color: Color(0xFF1D2129)),
onPressed: () {},
// 内容区域
SliverToBoxAdapter(
child: Column(
children: [
ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
children: [
// 机场状态卡片
_buildAirportStatusCard(),
const SizedBox(height: 12),
// 无人机状态卡片
_buildDroneStatusCard(),
const SizedBox(height: 12),
// 快捷操作
_buildQuickActions(),
const SizedBox(height: 20),
],
),
const SizedBox(height: 80), // 为底部 Tab 栏留出空间
],
),
),
],
),
// 内容区域
SliverToBoxAdapter(
child: Column(
children: [
ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
children: [
// 机场状态卡片
_buildAirportStatusCard(),
const SizedBox(height: 12),
// 无人机状态卡片
_buildDroneStatusCard(),
const SizedBox(height: 12),
// 快捷操作
_buildQuickActions(),
const SizedBox(height: 20),
],
),
const SizedBox(height: 80), // 为底部 Tab 栏留出空间
],
),
),
],
),
),
);
}
@@ -91,7 +219,11 @@ class DroneStationStatusPage extends StatelessWidget {
),
),
const SizedBox(height: 16),
_buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'),
_buildStatusRow(
'机场编号',
_uavDetail?.gatewaySn ?? 'AIRPORT-01',
status: '正常',
),
const SizedBox(height: 12),
_buildStatusRow('位置', '升压站旁 - 无人机机场'),
const SizedBox(height: 12),
@@ -108,87 +240,116 @@ class DroneStationStatusPage extends StatelessWidget {
}
/// 无人机状态卡片
/// 点击卡片时只有无人机在线才会同步到悬浮弹窗
Widget _buildDroneStatusCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'无人机状态',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF00B42A).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'在线',
final isOnline = _isDroneOnline();
return GestureDetector(
onTap: _onDroneCardTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
'无人机状态',
style: TextStyle(
fontSize: 12,
color: Color(0xFF00B42A),
fontWeight: FontWeight.w500,
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Image.asset(
'assets/images/xunjian.png',
width: 60,
height: 60,
fit: BoxFit.contain,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'无人机 - 01',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1D2129),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: isOnline
? const Color(0xFF00B42A).withOpacity(0.1)
: const Color(0xFFFF7D00).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
isOnline ? '在线' : '离线',
style: TextStyle(
fontSize: 12,
color: isOnline
? const Color(0xFF00B42A)
: const Color(0xFFFF7D00),
fontWeight: FontWeight.w500,
),
const SizedBox(height: 8),
_buildDroneBatteryRow(),
],
),
),
),
],
),
// const SizedBox(height: 12),
// _buildStatusRow('飞行模式', '待命'),
// const SizedBox(height: 12),
// _buildStatusRow('机身状态', '正常'),
// const SizedBox(height: 12),
// _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'),
],
],
),
const SizedBox(height: 16),
Row(
children: [
Image.asset(
'assets/images/xunjian.png',
width: 60,
height: 60,
fit: BoxFit.contain,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_uavDetail?.droneCallsign ?? '无人机 - 01',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 8),
_buildDroneBatteryRow(),
],
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.keyboard_arrow_right,
size: 16,
color: isOnline
? const Color(0xFF165DFF)
: const Color(0xFFC9CDD4),
),
const SizedBox(width: 4),
Text(
isOnline ? '点击查看实时视频和轨迹' : '无人机离线,无法查看视频和轨迹',
style: TextStyle(
fontSize: 12,
color: isOnline
? const Color(0xFF165DFF)
: const Color(0xFF86909C),
),
),
],
),
],
),
),
);
}
@@ -282,19 +443,12 @@ class DroneStationStatusPage extends StatelessWidget {
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF4E5969),
),
style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969)),
),
],
),
@@ -307,10 +461,7 @@ class DroneStationStatusPage extends StatelessWidget {
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
),
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
const Spacer(),
Text(
@@ -344,15 +495,16 @@ class DroneStationStatusPage extends StatelessWidget {
}
/// 带进度条的状态行
Widget _buildStatusRowWithProgress(String label, String value, String progress) {
Widget _buildStatusRowWithProgress(
String label,
String value,
String progress,
) {
return Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
),
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
const Spacer(),
Text(
@@ -373,11 +525,7 @@ class DroneStationStatusPage extends StatelessWidget {
),
),
const SizedBox(width: 4),
const Icon(
Icons.arrow_forward_ios,
size: 12,
color: Color(0xFF86909C),
),
const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFF86909C)),
],
);
}
@@ -388,17 +536,14 @@ class DroneStationStatusPage extends StatelessWidget {
children: [
const Text(
'气象条件',
style: TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
),
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
const Spacer(),
Row(
children: [
const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)),
const SizedBox(width: 4),
const Text(
children: const [
Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)),
SizedBox(width: 4),
Text(
'晴',
style: TextStyle(
fontSize: 13,
@@ -406,21 +551,15 @@ class DroneStationStatusPage extends StatelessWidget {
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
const Text(
SizedBox(width: 8),
Text(
'25°C',
style: TextStyle(
fontSize: 13,
color: Color(0xFF1D2129),
),
style: TextStyle(fontSize: 13, color: Color(0xFF1D2129)),
),
const SizedBox(width: 8),
const Text(
SizedBox(width: 8),
Text(
'东南风 2级',
style: TextStyle(
fontSize: 13,
color: Color(0xFF1D2129),
),
style: TextStyle(fontSize: 13, color: Color(0xFF1D2129)),
),
],
),
@@ -430,33 +569,38 @@ class DroneStationStatusPage extends StatelessWidget {
/// 无人机电量行
Widget _buildDroneBatteryRow() {
final capacity = _uavDetail?.liveCapacity ?? 78;
return Row(
children: [
const Text(
'电量',
style: TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
),
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
const SizedBox(width: 8),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: 0.78,
value: capacity / 100,
backgroundColor: const Color(0xFFF2F3F5),
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00B42A)),
valueColor: AlwaysStoppedAnimation<Color>(
capacity > 20
? const Color(0xFF00B42A)
: const Color(0xFFFF7D00),
),
minHeight: 6,
),
),
),
const SizedBox(width: 8),
const Text(
'78%',
Text(
'${capacity.toInt()}%',
style: TextStyle(
fontSize: 13,
color: Color(0xFF00B42A),
color: capacity > 20
? const Color(0xFF00B42A)
: const Color(0xFFFF7D00),
fontWeight: FontWeight.w600,
),
),

View File

@@ -158,8 +158,10 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
// 根据是否使用备选视频源决定使用哪个 SN
final effectiveSn = _useBackupSource ? widget.gatewaySn : widget.droneSn;
debugPrint('📤 请求视频流 - SN: $effectiveSn, 摄像头: ${_currentCamera!.cameraIndex}');
debugPrint(
'📤 请求视频流 - SN: $effectiveSn, 摄像头: ${_currentCamera!.cameraIndex}',
);
_bloc.add(
UavVideoStreamLoad(
@@ -190,20 +192,22 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
void _toggleBackupSource(bool useBackup) {
debugPrint('=== 🔄 切换视频源 ===');
debugPrint('切换到: ${useBackup ? "备选(网关)" : "主视频源(无人机)"}');
setState(() {
_useBackupSource = useBackup;
// 更新实际使用的摄像头列表
_effectiveCameraList = useBackup
_effectiveCameraList = useBackup
? (_backupCameraList ?? [])
: (widget.cameraList ?? []);
// 选择新列表的第一个摄像头
_currentCamera = _effectiveCameraList.isNotEmpty ? _effectiveCameraList.first : null;
_currentCamera = _effectiveCameraList.isNotEmpty
? _effectiveCameraList.first
: null;
});
debugPrint('新摄像头列表长度: ${_effectiveCameraList.length}');
debugPrint('选中的摄像头: ${_currentCamera?.cameraIndex}');
// 重新加载视频流
if (_currentCamera != null) {
_loadVideoStream(_currentLensType ?? UavLensType.wide);
@@ -372,7 +376,8 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
color: Color(0xFF1D2129),
),
),
if (_effectiveCameraList.isNotEmpty || _backupCameraList?.isNotEmpty == true)
if (_effectiveCameraList.isNotEmpty ||
_backupCameraList?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
@@ -391,9 +396,16 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.flight, size: 16, color: Color(0xFF165DFF)),
Icon(
Icons.flight,
size: 16,
color: Color(0xFF165DFF),
),
SizedBox(width: 4),
Text('无人机', style: TextStyle(fontSize: 12)),
Text(
'无人机',
style: TextStyle(fontSize: 12),
),
],
),
),
@@ -402,15 +414,23 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.home_work, size: 16, color: Color(0xFFFF7D00)),
Icon(
Icons.home_work,
size: 16,
color: Color(0xFFFF7D00),
),
SizedBox(width: 4),
Text('备选', style: TextStyle(fontSize: 12)),
Text(
'备选',
style: TextStyle(fontSize: 12),
),
],
),
),
],
onChanged: (bool? useBackup) {
if (useBackup != null && useBackup != _useBackupSource) {
if (useBackup != null &&
useBackup != _useBackupSource) {
_toggleBackupSource(useBackup);
}
},
@@ -440,9 +460,13 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_useBackupSource ? Icons.home_work : Icons.videocam,
_useBackupSource
? Icons.home_work
: Icons.videocam,
size: 16,
color: _useBackupSource ? const Color(0xFFFF7D00) : const Color(0xFF165DFF),
color: _useBackupSource
? const Color(0xFFFF7D00)
: const Color(0xFF165DFF),
),
const SizedBox(width: 4),
Text(

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/logging/i_logger_service.dart';
import '../../../../devices/presentation/bloc/devices_cubit.dart';
@@ -25,7 +26,8 @@ class RobotListPage extends StatelessWidget {
final siteId = selectedSite?.id;
return BlocProvider(
create: (_) => sl<RobotListBloc>()..add(RobotListLoadData(siteId: siteId)),
create: (_) =>
sl<RobotListBloc>()..add(RobotListLoadData(siteId: siteId)),
child: RobotListView(siteId: siteId),
);
}
@@ -63,9 +65,7 @@ class _RobotListViewState extends State<RobotListView> {
builder: (context, state) {
if (state is RobotListLoading) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xFF165DFF),
),
child: CircularProgressIndicator(color: Color(0xFF165DFF)),
);
}
@@ -99,13 +99,18 @@ class _RobotListViewState extends State<RobotListView> {
}
Widget _buildStatsCard(RobotListLoaded state) {
// 计算各类型机器人数量
final allRobots = state.robots;
final inspectionRobots = allRobots.where((r) => r.type.contains('巡检')).toList();
final cleaningRobots = allRobots.where((r) => r.type.contains('清洗')).toList();
final weedingRobots = allRobots.where((r) => r.type.contains('除草')).toList();
final onlineInspection = inspectionRobots.where((r) => r.status == '在线').length;
final inspectionRobots = allRobots
.where((r) => r.type == 'inspection')
.toList();
final cleaningRobots = allRobots
.where((r) => r.type == 'cleaning')
.toList();
final weedingRobots = allRobots.where((r) => r.type == 'weeding').toList();
final onlineInspection = inspectionRobots
.where((r) => r.status == '在线')
.length;
final onlineCleaning = cleaningRobots.where((r) => r.status == '在线').length;
final onlineWeeding = weedingRobots.where((r) => r.status == '在线').length;
return Container(
@@ -129,37 +134,43 @@ class _RobotListViewState extends State<RobotListView> {
onTap: () {
context.read<RobotListBloc>().add(
RobotListChangeType(
_selectedType == '巡检机器人' ? null : '巡检机器人',
_selectedType == 'inspection' ? null : 'inspection',
),
);
setState(() {
_selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人';
_selectedType = _selectedType == 'inspection'
? null
: 'inspection';
});
},
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
gradient: _selectedType == '巡检机器人'
gradient: _selectedType == 'inspection'
? const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFE8F4FF),
Color(0xFFFFFFFF),
],
colors: [Color(0xFFE8F4FF), Color(0xFFFFFFFF)],
)
: null,
color: _selectedType != '巡检机器人' ? Colors.transparent : null,
color: _selectedType != 'inspection'
? Colors.transparent
: null,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'巡检机器人',
AppLocalizations.of(
context,
).translate('device_list_v2.inspection_robot'),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _selectedType == '巡检机器人'
color: _selectedType == 'inspection'
? const Color(0xFF165DFF)
: const Color(0xFF1D2129),
),
@@ -169,21 +180,29 @@ class _RobotListViewState extends State<RobotListView> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/xunjian.png', width: 48, height: 48),
Image.asset(
'assets/images/xunjian.png',
width: 48,
height: 48,
),
const SizedBox(width: 8),
Text(
'${inspectionRobots.length}',
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF)),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF165DFF),
),
),
],
),
const SizedBox(height: 8),
Text(
'在线$onlineInspection台',
'${AppLocalizations.of(context).translate('device_list_v2.online')}$onlineInspection台',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: _selectedType == '巡检机器人'
color: _selectedType == 'inspection'
? const Color(0xFF165DFF)
: const Color(0xFF00B42A),
),
@@ -198,37 +217,43 @@ class _RobotListViewState extends State<RobotListView> {
onTap: () {
context.read<RobotListBloc>().add(
RobotListChangeType(
_selectedType == '清洗机器人' ? null : '清洗机器人',
_selectedType == 'cleaning' ? null : 'cleaning',
),
);
setState(() {
_selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人';
_selectedType = _selectedType == 'cleaning'
? null
: 'cleaning';
});
},
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
gradient: _selectedType == '清洗机器人'
gradient: _selectedType == 'cleaning'
? const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFE8F4FF),
Color(0xFFFFFFFF),
],
colors: [Color(0xFFE8F4FF), Color(0xFFFFFFFF)],
)
: null,
color: _selectedType != '清洗机器人' ? Colors.transparent : null,
color: _selectedType != 'cleaning'
? Colors.transparent
: null,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'清洗机器人',
AppLocalizations.of(
context,
).translate('device_list_v2.cleaning_robot'),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _selectedType == '清洗机器人'
color: _selectedType == 'cleaning'
? const Color(0xFF165DFF)
: const Color(0xFF1D2129),
),
@@ -238,21 +263,29 @@ class _RobotListViewState extends State<RobotListView> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/qinxi.png', width: 48, height: 48),
Image.asset(
'assets/images/qinxi.png',
width: 48,
height: 48,
),
const SizedBox(width: 8),
Text(
'${cleaningRobots.length}',
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF)),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF165DFF),
),
),
],
),
const SizedBox(height: 8),
Text(
'在线$onlineCleaning台',
'${AppLocalizations.of(context).translate('device_list_v2.online')}$onlineCleaning台',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: _selectedType == '清洗机器人'
color: _selectedType == 'cleaning'
? const Color(0xFF165DFF)
: const Color(0xFF00B42A),
),
@@ -267,37 +300,41 @@ class _RobotListViewState extends State<RobotListView> {
onTap: () {
context.read<RobotListBloc>().add(
RobotListChangeType(
_selectedType == '除草机器人' ? null : '除草机器人',
_selectedType == 'weeding' ? null : 'weeding',
),
);
setState(() {
_selectedType = _selectedType == '除草机器人' ? null : '除草机器人';
_selectedType = _selectedType == 'weeding' ? null : 'weeding';
});
},
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
gradient: _selectedType == '除草机器人'
gradient: _selectedType == 'weeding'
? const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFE8F4FF),
Color(0xFFFFFFFF),
],
colors: [Color(0xFFE8F4FF), Color(0xFFFFFFFF)],
)
: null,
color: _selectedType != '除草机器人' ? Colors.transparent : null,
color: _selectedType != 'weeding'
? Colors.transparent
: null,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'除草机器人',
AppLocalizations.of(
context,
).translate('device_list_v2.weeding_robot'),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: _selectedType == '除草机器人'
color: _selectedType == 'weeding'
? const Color(0xFF165DFF)
: const Color(0xFF1D2129),
),
@@ -307,14 +344,18 @@ class _RobotListViewState extends State<RobotListView> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/chucao.png', width: 48, height: 48),
Image.asset(
'assets/images/chucao.png',
width: 48,
height: 48,
),
const SizedBox(width: 8),
Text(
'${weedingRobots.length}',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: _selectedType == '除草机器人'
color: _selectedType == 'weeding'
? const Color(0xFF165DFF)
: const Color(0xFF00B42A),
),
@@ -323,11 +364,11 @@ class _RobotListViewState extends State<RobotListView> {
),
const SizedBox(height: 8),
Text(
'在线$onlineWeeding台',
'${AppLocalizations.of(context).translate('device_list_v2.online')}$onlineWeeding台',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: _selectedType == '除草机器人'
color: _selectedType == 'weeding'
? const Color(0xFF165DFF)
: const Color(0xFF00B42A),
),
@@ -344,38 +385,78 @@ class _RobotListViewState extends State<RobotListView> {
Widget _buildQuickActions(RobotListLoaded state) {
// 计算统计数据
final onlineCount = state.robots.where((r) => r.status == '在线').length;
final workingCount = state.robots.where((r) => r.task != '待机中' && r.task != '充电中').length;
final workingCount = state.robots
.where((r) => r.task != '待机中' && r.task != '充电中')
.length;
final standbyCount = state.robots.where((r) => r.task == '待机中').length;
final faultCount = state.robots.where((r) => r.status == '异常' || r.status == '离线').length;
final faultCount = state.robots
.where((r) => r.status == '异常' || r.status == '离线')
.length;
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Row(
children: [
_buildStatusItem(value: '$onlineCount', label: '在线设备', valueColor: const Color(0xFF165DFF)),
_buildStatusItem(
value: '$onlineCount',
label: '在线设备',
valueColor: const Color(0xFF165DFF),
),
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
_buildStatusItem(value: '$workingCount', label: '作业中', valueColor: const Color(0xFF00B42A)),
_buildStatusItem(
value: '$workingCount',
label: '作业中',
valueColor: const Color(0xFF00B42A),
),
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
_buildStatusItem(value: '$standbyCount', label: '待命', valueColor: const Color(0xFFFF7D00)),
_buildStatusItem(
value: '$standbyCount',
label: '待命',
valueColor: const Color(0xFFFF7D00),
),
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
_buildStatusItem(value: '$faultCount', label: '故障数', valueColor: const Color(0xFFF53F3F)),
_buildStatusItem(
value: '$faultCount',
label: '故障数',
valueColor: const Color(0xFFF53F3F),
),
],
),
);
}
Widget _buildStatusItem({required String value, required String label, required Color valueColor}) {
Widget _buildStatusItem({
required String value,
required String label,
required Color valueColor,
}) {
return Expanded(
child: Column(
children: [
Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: valueColor)),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: valueColor,
),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF4E5969))),
Text(
label,
style: const TextStyle(fontSize: 13, color: Color(0xFF4E5969)),
),
],
),
);
@@ -385,14 +466,16 @@ class _RobotListViewState extends State<RobotListView> {
// 获取第一个正在执行任务的机器人
final workingRobot = state.robots.firstWhere(
(r) => r.task != '待机中' && r.task != '充电中',
orElse: () => state.robots.isNotEmpty ? state.robots.first : RobotDataModel(
name: '',
id: '',
type: '',
status: '',
battery: 0,
task: '无任务',
),
orElse: () => state.robots.isNotEmpty
? state.robots.first
: RobotDataModel(
name: '',
id: '',
type: '',
status: '',
battery: 0,
task: '无任务',
),
);
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
@@ -400,18 +483,36 @@ class _RobotListViewState extends State<RobotListView> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('当前任务', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
const Text(
'当前任务',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 12),
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset('assets/images/xunjian.png', width: 80, height: 80, fit: BoxFit.cover),
child: Image.asset(
'assets/images/xunjian.png',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
@@ -420,16 +521,41 @@ class _RobotListViewState extends State<RobotListView> {
children: [
Row(
children: [
const Expanded(child: Text('巡检任务-逆变器区巡检', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1D2129)))),
const Expanded(
child: Text(
'巡检任务-逆变器区巡检',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1D2129),
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(color: const Color(0xFF165DFF).withOpacity(0.1), borderRadius: BorderRadius.circular(4)),
child: const Text('进行中', style: TextStyle(fontSize: 11, color: Color(0xFF165DFF), fontWeight: FontWeight.w500)),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFF165DFF).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'进行中',
style: TextStyle(
fontSize: 11,
color: Color(0xFF165DFF),
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 8),
_buildTaskInfo('机器人:', workingRobot.name.isNotEmpty ? workingRobot.name : '无'),
_buildTaskInfo(
'机器人:',
workingRobot.name.isNotEmpty ? workingRobot.name : '无',
),
_buildTaskInfo('区域:', '逆变器区A区'),
_buildTaskInfo('进度:', '62%'),
_buildTaskInfo('预计完成:', '12:30'),
@@ -449,8 +575,14 @@ class _RobotListViewState extends State<RobotListView> {
child: RichText(
text: TextSpan(
children: [
TextSpan(text: label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))),
TextSpan(text: value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129))),
TextSpan(
text: label,
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
),
TextSpan(
text: value,
style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129)),
),
],
),
),
@@ -461,64 +593,84 @@ class _RobotListViewState extends State<RobotListView> {
final allRobots = state.robots;
// 根据选中的类型过滤
final filteredRobots = state.selectedType == null
? allRobots
: allRobots.where((robot) => robot.type.contains(state.selectedType!)).toList();
final filteredRobots = state.selectedType == null
? allRobots
: allRobots
.where((robot) => robot.type.contains(state.selectedType!))
.toList();
if (filteredRobots.isEmpty) {
return [const Center(child: Padding(padding: EdgeInsets.all(32), child: Text('暂无机器人数据', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)))))];
return [
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text(
'暂无机器人数据',
style: TextStyle(fontSize: 14, color: Color(0xFF86909C)),
),
),
),
];
}
return filteredRobots.map((robot) => RobotItemCard(
name: robot.name,
id: robot.id,
alias: robot.alias, // 传递别名
type: robot.type,
status: robot.status,
battery: robot.battery,
task: robot.task,
onTap: () async {
// final logger = sl<ILoggerService>();
debugPrint('🔴🔴🔴 [选中机器人] ========== 点击事件触发 ==========');
debugPrint('🔴🔴🔴 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}');
debugPrint('🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}');
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
final device = DeviceEntity(
deviceName: robot.name, // ✅ 修正: 用 name 而不是 id
productId: -1,
productName: robot.type,
tenantId: 0,
tenantName: '',
status: robot.status == '在线' ? 1 : 0,
onlineStatus: robot.status == '在线' ? 1 : 0,
);
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
// 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例
final remoteCubit = GetIt.I<RemoteControlCubit>();
remoteCubit.setTargetDevice(device); // 🔥 TCP连接在后台异步执行
debugPrint('✅ [RobotListPage] setTargetDevice 已调用(TCP连接中)');
// 2. 跳转到机器人控制页面
final robotMap = {
'name': robot.name,
'id': robot.id,
'alias': robot.alias, // 传递别名
'type': robot.type,
'status': robot.status,
'battery': robot.battery,
'task': robot.task,
};
debugPrint('🚀 [RobotListPage] 准备跳转页面...');
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RobotControlPage(robot: robotMap),
return filteredRobots
.map(
(robot) => RobotItemCard(
name: robot.name,
id: robot.id,
alias: robot.alias, // 传递别名
type: robot.type,
status: robot.status,
battery: robot.battery,
task: robot.task,
onTap: () async {
// final logger = sl<ILoggerService>();
debugPrint('🔴🔴🔴 [选中机器人] ========== 点击事件触发 ==========');
debugPrint(
'🔴🔴🔴 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}',
);
debugPrint(
'🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}',
);
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
final device = DeviceEntity(
deviceName: robot.name, // ✅ 修正: 用 name 而不是 id
productId: -1,
productName: robot.type,
tenantId: 0,
tenantName: '',
status: robot.status == '在线' ? 1 : 0,
onlineStatus: robot.status == '在线' ? 1 : 0,
);
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
// 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例
final remoteCubit = GetIt.I<RemoteControlCubit>();
remoteCubit.setTargetDevice(device); // 🔥 TCP连接在后台异步执行
debugPrint('✅ [RobotListPage] setTargetDevice 已调用(TCP连接中)');
// 2. 跳转到机器人控制页面
final robotMap = {
'name': robot.name,
'id': robot.id,
'alias': robot.alias, // 传递别名
'type': robot.type,
'status': robot.status,
'battery': robot.battery,
'task': robot.task,
};
debugPrint('🚀 [RobotListPage] 准备跳转页面...');
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RobotControlPage(robot: robotMap),
),
);
},
),
);
},
)).toList();
)
.toList();
}
}

View File

@@ -17,10 +17,7 @@ class DeviceBottomNavBar extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: const Color(0xFFF2F3F5),
width: 0.5,
),
top: BorderSide(color: const Color(0xFFF2F3F5), width: 0.5),
),
),
child: SafeArea(
@@ -79,7 +76,9 @@ class DeviceBottomNavBar extends StatelessWidget {
children: [
Icon(
icon,
color: isSelected ? const Color(0xFF004CFD) : const Color(0xFF86909C),
color: isSelected
? const Color(0xFF004CFD)
: const Color(0xFF86909C),
size: 24,
),
const SizedBox(height: 2),
@@ -87,7 +86,9 @@ class DeviceBottomNavBar extends StatelessWidget {
label,
style: TextStyle(
fontSize: 11,
color: isSelected ? const Color(0xFF004FFF) : const Color(0xFF86909C),
color: isSelected
? const Color(0xFF004FFF)
: const Color(0xFF86909C),
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
),
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../domain/entities/device_entity.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
/// 设备列表项 Widget
class DeviceItemWidget extends StatelessWidget {
@@ -68,7 +69,7 @@ class DeviceItemWidget extends StatelessWidget {
),
const SizedBox(height: 12),
// 第二行:设备数据
_buildDeviceData(device),
_buildDeviceData(context, device),
],
),
),
@@ -112,11 +113,7 @@ class DeviceItemWidget extends StatelessWidget {
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 28,
),
child: Icon(icon, color: color, size: 28),
);
}
@@ -148,46 +145,71 @@ class DeviceItemWidget extends StatelessWidget {
color: bgColor,
borderRadius: BorderRadius.circular(12.0),
),
child: Text(
status,
style: TextStyle(
fontSize: 12,
color: textColor,
),
),
child: Text(status, style: TextStyle(fontSize: 12, color: textColor)),
);
}
Widget _buildDeviceData(DeviceEntity device) {
Widget _buildDeviceData(BuildContext context, DeviceEntity device) {
final dataItems = <Widget>[];
// 根据设备类型显示不同的数据
if (device.type == '逆变器') {
dataItems.add(_buildDataItem('${device.power}', 'kW | 功率'));
if (device.type == 'inverter') {
dataItems.add(
_buildDataItem(
'${device.power}',
'${AppLocalizations.of(context).translate('device_list_v2.inverter')} | ${AppLocalizations.of(context).translate('device_list_v2.power')}',
),
);
dataItems.add(const SizedBox(width: 16));
dataItems.add(_buildDataItem('${device.todayEnergy}', 'kWh | 今日发电'));
} else if (device.type == '汇流箱') {
dataItems.add(
_buildDataItem(
'${device.todayEnergy}',
'${AppLocalizations.of(context).translate('device_list_v2.today_energy')}',
),
);
} else if (device.type == 'combiner_box') {
if (device.current != null) {
dataItems.add(_buildDataItem('${device.current}', 'A | 电流'));
dataItems.add(
_buildDataItem(
'${device.current}',
'${AppLocalizations.of(context).translate('device_list_v2.current')}',
),
);
dataItems.add(const SizedBox(width: 16));
}
if (device.temperature != null) {
dataItems.add(_buildDataItem('${device.temperature}', '℃ | 温度'));
dataItems.add(
_buildDataItem(
'${device.temperature}',
'${AppLocalizations.of(context).translate('device_list_v2.temperature')}',
),
);
}
} else if (device.type == '组件') {
dataItems.add(_buildDataItem('${device.power}', 'kW | 功率'));
} else if (device.type == 'module') {
dataItems.add(
_buildDataItem(
'${device.power}',
'${AppLocalizations.of(context).translate('device_list_v2.power')}',
),
);
dataItems.add(const SizedBox(width: 16));
if (device.radiation != null) {
dataItems.add(_buildDataItem('${device.radiation}', 'W/m² | 辐照度'));
dataItems.add(
_buildDataItem(
'${device.radiation}',
'${AppLocalizations.of(context).translate('device_list_v2.radiation')}',
),
);
}
} else {
// 监控等其他类型
dataItems.add(_buildDataItem('--', '数据'));
dataItems.add(
_buildDataItem(
'--',
'${AppLocalizations.of(context).translate('device_list_v2.data')}',
),
);
}
return Row(
children: dataItems,
);
return Row(children: dataItems);
}
Widget _buildDataItem(String value, String unit) {
@@ -205,10 +227,7 @@ class DeviceItemWidget extends StatelessWidget {
const SizedBox(height: 4),
Text(
unit,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF4E5969),
),
style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969)),
),
],
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
/// 机器人卡片组件
class RobotItemCard extends StatelessWidget {
@@ -83,7 +84,9 @@ class RobotItemCard extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
alias != null && alias!.isNotEmpty ? '别名: $alias' : 'ID: $id',
alias != null && alias!.isNotEmpty
? '别名: $alias'
: 'ID: $id',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF86909C),
@@ -111,14 +114,14 @@ class RobotItemCard extends StatelessWidget {
battery > 50
? Icons.battery_full
: battery > 20
? Icons.battery_std
: Icons.battery_alert,
? Icons.battery_std
: Icons.battery_alert,
size: 16,
color: battery > 50
? const Color(0xFF00B42A)
: battery > 20
? const Color(0xFFFF7D00)
: const Color(0xFFF53F3F),
? const Color(0xFFFF7D00)
: const Color(0xFFF53F3F),
),
const SizedBox(width: 4),
Text(
@@ -129,15 +132,17 @@ class RobotItemCard extends StatelessWidget {
color: battery > 50
? const Color(0xFF00B42A)
: battery > 20
? const Color(0xFFFF7D00)
: const Color(0xFFF53F3F),
? const Color(0xFFFF7D00)
: const Color(0xFFF53F3F),
),
),
],
),
const SizedBox(height: 4),
const Text(
'电量',
Text(
AppLocalizations.of(
context,
).translate('device_list_v2.battery_level'),
style: TextStyle(
fontSize: 12,
color: Color(0xFF4E5969),
@@ -162,8 +167,10 @@ class RobotItemCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
const Text(
'当前任务',
Text(
AppLocalizations.of(
context,
).translate('device_list_v2.current_task'),
style: TextStyle(
fontSize: 12,
color: Color(0xFF4E5969),
@@ -208,23 +215,17 @@ class RobotItemCard extends StatelessWidget {
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: TextStyle(
fontSize: 12,
color: textColor,
),
),
child: Text(status, style: TextStyle(fontSize: 12, color: textColor)),
);
}
String _getRobotImage() {
switch (type) {
case '巡检机器人':
case 'inspection':
return 'assets/images/xunjian.png';
case '清洗机器人':
case 'cleaning':
return 'assets/images/qinxi.png';
case '除草机器人':
case 'weeding':
return 'assets/images/chucao.png';
default:
return 'assets/images/xunjian.png';

View File

@@ -64,6 +64,12 @@ class SiteDataSourceImpl implements SiteDataSource {
}
final List<dynamic> rows = responseData['rows'] ?? [];
print('📤 [SiteDataSource] 接口返回原始数据:');
for (var i = 0; i < rows.length && i < 3; i++) {
print(' 场站$i: ${rows[i]}');
}
return rows.map((item) => SiteEntity.fromJson(item)).toList();
}
}

View File

@@ -24,7 +24,7 @@ class SiteEntity extends Equatable {
factory SiteEntity.fromJson(Map<String, dynamic> json) {
return SiteEntity(
id: json['id'] as int,
siteName: json['siteName'] as String? ?? '',
siteName: json['name'] as String? ?? json['siteName'] as String? ?? '',
siteCode: json['siteCode'] as String?,
orgId: json['orgId'] as int? ?? 0,
longitude: (json['longitude'] as num?)?.toDouble(),

View File

@@ -91,7 +91,18 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
if (state is HomeV2Loaded) {
final currentState = state as HomeV2Loaded;
final user = appUserCubit.state.user;
if (user == null) {
emit(const HomeV2Error(
message: '用户未登录',
shouldShowError: true,
));
return;
}
// 并行刷新首页数据和电站列表
final homeResult = await getHomeDataUseCase(const NoParams());
final siteResult = await getSiteListUseCase(user.orgId);
homeResult.fold(
(failure) => emit(HomeV2Error(
@@ -99,11 +110,38 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(homeData) {
List<SiteEntity> sites = currentState.sites;
SiteEntity? selectedSite = currentState.selectedSite;
siteResult.fold(
(failure) {
print('刷新场站列表失败: ${failure.message}');
},
(siteList) {
sites = siteList;
// 保持当前选中的场站
final savedSelectedSite = siteCubit.state.selectedSite;
if (savedSelectedSite != null && siteList.isNotEmpty) {
selectedSite = siteList.firstWhere(
(site) => site.id == savedSelectedSite.id,
orElse: () => siteList.first,
);
} else if (siteList.isNotEmpty) {
selectedSite = siteList.first;
}
// 更新全局 SiteCubit 的选中状态
if (selectedSite != null) {
siteCubit.selectSite(selectedSite!);
}
},
);
emit(HomeV2Loaded(
homeData: homeData,
trendType: currentState.trendType,
sites: currentState.sites,
selectedSite: currentState.selectedSite,
sites: sites,
selectedSite: selectedSite,
));
},
);

View File

@@ -25,7 +25,7 @@ class HomeV2Loaded extends HomeV2State {
const HomeV2Loaded({
required this.homeData,
this.trendType = 'kW',
this.trendType = 'day',
this.sites = const [],
this.selectedSite,
});

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/di/injection.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart';
@@ -53,10 +54,23 @@ class _HomeV2PageState extends State<HomeV2Page> {
}
},
builder: (context, state) {
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
// 🔥 错误状态显示重试按钮
if (state is HomeV2Error) {
// 返回空容器保持页面
return const SizedBox.shrink();
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.orange),
const SizedBox(height: 12),
Text(state.message, style: const TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _bloc.add(const HomeV2LoadData()),
child: const Text('重试'),
),
],
),
);
}
if (state is HomeV2Loaded) {
@@ -92,7 +106,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
sl<SiteCubit>().state;
return Text(
siteState.selectedSite?.siteName ??
'选择电站',
AppLocalizations.of(
context,
).translate(
'home_v2.select_site',
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
@@ -149,9 +167,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
color: Color(0xFF165DFF),
),
const SizedBox(width: 6),
const Text(
'多云 28°C',
style: TextStyle(
Text(
AppLocalizations.of(
context,
).translate('home_v2.weather'),
style: const TextStyle(
fontSize: 13,
color: Color(0xFF4E5969),
),
@@ -180,10 +200,12 @@ class _HomeV2PageState extends State<HomeV2Page> {
color: Color(0xFFF53F3F),
),
const SizedBox(width: 6),
const Expanded(
Expanded(
child: Text(
'紧急告警',
style: TextStyle(
AppLocalizations.of(
context,
).translate('home_v2.emergency_alarm'),
style: const TextStyle(
fontSize: 13,
color: Color(0xFFF53F3F),
),
@@ -229,7 +251,8 @@ class _HomeV2PageState extends State<HomeV2Page> {
);
}
return const SizedBox();
// 🔥 Initial/Loading 状态显示加载指示器
return const Center(child: CircularProgressIndicator(color: Color(0xFF165DFF)));
},
),
);
@@ -256,9 +279,13 @@ class _HomeV2PageState extends State<HomeV2Page> {
final sites = currentState.sites;
if (sites.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('暂无可用电站')));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).translate('home_v2.no_sites'),
),
),
);
return;
}
@@ -282,9 +309,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'选择电站',
style: TextStyle(
Text(
AppLocalizations.of(
context,
).translate('home_v2.select_site'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -303,7 +332,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
title: Text(site.siteName),
subtitle:
site.siteCode != null && site.siteCode!.isNotEmpty
? Text('编号: ${site.siteCode}')
? Text(
AppLocalizations.of(context)
.translate('home_v2.site_code')
.replaceAll('%s', site.siteCode!),
)
: null,
trailing: isSelected
? const Icon(
@@ -347,11 +380,9 @@ class _HomeV2PageState extends State<HomeV2Page> {
builder: (BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<DeviceStatusBloc>()),
BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
),
BlocProvider.value(
value: GetIt.I<RemoteControlCubit>(), // 🔥 注入 RemoteControlCubit
value: GetIt.I<RemoteControlCubit>(), // 🔥 注入 RemoteControlCubit
),
],
child: const DeviceStatusModal(),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart';
class PlantOverviewCard extends StatelessWidget {
@@ -11,9 +12,9 @@ class PlantOverviewCard extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'电站概况',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('home_v2.plant_overview'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -42,10 +43,7 @@ class PlantOverviewCard extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.6),
],
colors: [Colors.transparent, Colors.black.withOpacity(0.6)],
),
),
child: Padding(
@@ -57,47 +55,66 @@ class PlantOverviewCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'装机容量',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
Flexible(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(
context,
).translate('home_v2.installed_capacity'),
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 4),
Text(
'${overview.installedCapacity} MWp',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
const SizedBox(height: 4),
Text(
'${overview.installedCapacity} MWp',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'投运日期',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
const SizedBox(width: 16),
Flexible(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
AppLocalizations.of(
context,
).translate('home_v2.commission_date'),
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 4),
Text(
overview.commissionDate,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
const SizedBox(height: 4),
Text(
overview.commissionDate,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
),
],
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart';
class PowerCard extends StatelessWidget {
@@ -40,12 +41,11 @@ class PowerCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'实时发电功率',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
Text(
AppLocalizations.of(
context,
).translate('home_v2.real_time_power'),
style: const TextStyle(color: Colors.white, fontSize: 14),
),
const SizedBox(height: 4),
Row(
@@ -68,10 +68,7 @@ class PowerCard extends StatelessWidget {
padding: EdgeInsets.only(bottom: 4),
child: Text(
'kW',
style: TextStyle(
color: Colors.white,
fontSize: 14,
),
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
],
@@ -86,7 +83,11 @@ class PowerCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.more_horiz, size: 20, color: Colors.white),
icon: const Icon(
Icons.more_horiz,
size: 20,
color: Colors.white,
),
onPressed: () => print('点击更多'),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart';
class PowerTrendChart extends StatelessWidget {
@@ -22,9 +23,9 @@ class PowerTrendChart extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'发电趋势(今日)',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('home_v2.power_trend'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -38,8 +39,14 @@ class PowerTrendChart extends StatelessWidget {
),
child: Row(
children: [
_buildTypeButton('kW'),
_buildTypeButton('周'),
_buildTypeButton(
'day',
AppLocalizations.of(context).translate('home_v2.day'),
),
_buildTypeButton(
'month',
AppLocalizations.of(context).translate('home_v2.month'),
),
],
),
),
@@ -88,7 +95,11 @@ class PowerTrendChart extends StatelessWidget {
interval: 6,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index == 0 || index == 6 || index == 12 || index == 18 || index == 23) {
if (index == 0 ||
index == 6 ||
index == 12 ||
index == 18 ||
index == 23) {
String time;
if (index == 23) {
time = '23:00';
@@ -174,7 +185,7 @@ class PowerTrendChart extends StatelessWidget {
);
}
Widget _buildTypeButton(String type) {
Widget _buildTypeButton(String type, String label) {
final isSelected = trendType == type;
return GestureDetector(
onTap: () => onToggleType(type),
@@ -187,7 +198,7 @@ class PowerTrendChart extends StatelessWidget {
),
alignment: Alignment.center,
child: Text(
type,
label,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF86909C),
fontSize: 14,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
class QuickEntryCard extends StatelessWidget {
const QuickEntryCard({super.key});
@@ -11,9 +12,9 @@ class QuickEntryCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'快捷入口',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('home_v2.quick_entry'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -36,32 +37,44 @@ class QuickEntryCard extends StatelessWidget {
children: [
_buildQuickEntryCard(
icon: Icons.upload_file,
label: '设备上报',
label: AppLocalizations.of(
context,
).translate('home_v2.device_report'),
),
const SizedBox(width: 8),
_buildQuickEntryCard(
icon: Icons.qr_code_scanner,
label: '扫一扫',
label: AppLocalizations.of(
context,
).translate('home_v2.scan_qr'),
),
const SizedBox(width: 8),
_buildQuickEntryCard(
icon: Icons.assignment,
label: '工单任务',
label: AppLocalizations.of(
context,
).translate('home_v2.work_order'),
),
const SizedBox(width: 8),
_buildQuickEntryCard(
icon: Icons.notifications_active,
label: '告警中心',
label: AppLocalizations.of(
context,
).translate('home_v2.alarm_center'),
),
const SizedBox(width: 8),
_buildQuickEntryCard(
icon: Icons.map,
label: '电厂地图',
label: AppLocalizations.of(
context,
).translate('home_v2.power_plant_map'),
),
const SizedBox(width: 8),
_buildQuickEntryCard(
icon: Icons.devices,
label: '设备列表',
label: AppLocalizations.of(
context,
).translate('home_v2.device_list'),
),
const SizedBox(width: 8),
],
@@ -71,10 +84,7 @@ class QuickEntryCard extends StatelessWidget {
);
}
Widget _buildQuickEntryCard({
required IconData icon,
required String label,
}) {
Widget _buildQuickEntryCard({required IconData icon, required String label}) {
return InkWell(
onTap: () => print('点击:$label'),
borderRadius: BorderRadius.circular(12),
@@ -97,11 +107,7 @@ class QuickEntryCard extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: const Color(0xFF165DFF),
size: 26,
),
Icon(icon, color: const Color(0xFF165DFF), size: 26),
const SizedBox(height: 6),
Text(
label,

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart';
class StatsGrid extends StatelessWidget {
@@ -18,21 +19,25 @@ class StatsGrid extends StatelessWidget {
padding: EdgeInsets.zero,
children: [
_buildStatCard(
title: '今日发电量',
title: AppLocalizations.of(
context,
).translate('home_v2.today_generation'),
value: _formatNumber(data.todayGeneration),
unit: 'kWh',
valueColor: const Color(0xFF00B42A),
showArrow: false,
),
_buildStatCard(
title: '等效小时数',
title: AppLocalizations.of(
context,
).translate('home_v2.equivalent_hours'),
value: data.equivalentHours.toStringAsFixed(1),
unit: 'h',
valueColor: const Color(0xFF165DFF),
showArrow: false,
),
_buildAlarmCard(data.alarmCount),
_buildWorkOrderCard(data.pendingWorkOrders),
_buildAlarmCard(context, data.alarmCount),
_buildWorkOrderCard(context, data.pendingWorkOrders),
],
);
}
@@ -81,10 +86,7 @@ class StatsGrid extends StatelessWidget {
else
Text(
title,
style: const TextStyle(
color: Color(0xFF4E5969),
fontSize: 14,
),
style: const TextStyle(color: Color(0xFF4E5969), fontSize: 14),
),
const Spacer(),
Row(
@@ -101,10 +103,7 @@ class StatsGrid extends StatelessWidget {
const SizedBox(width: 4),
Text(
unit,
style: const TextStyle(
color: Color(0xFF4E5969),
fontSize: 14,
),
style: const TextStyle(color: Color(0xFF4E5969), fontSize: 14),
),
],
),
@@ -113,7 +112,7 @@ class StatsGrid extends StatelessWidget {
);
}
Widget _buildAlarmCard(int count) {
Widget _buildAlarmCard(BuildContext context, int count) {
return Container(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
decoration: BoxDecoration(
@@ -134,12 +133,14 @@ class StatsGrid extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'告警数',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('home_v2.alarm_count'),
style: const TextStyle(
color: Color(0xFFF53F3F),
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
@@ -171,7 +172,7 @@ class StatsGrid extends StatelessWidget {
);
}
Widget _buildWorkOrderCard(int count) {
Widget _buildWorkOrderCard(BuildContext context, int count) {
return Container(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
decoration: BoxDecoration(
@@ -192,12 +193,16 @@ class StatsGrid extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'待办工单',
style: TextStyle(
Text(
AppLocalizations.of(
context,
).translate('home_v2.pending_work_order'),
style: const TextStyle(
color: Color(0xFF4E5969),
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/home/domain/entities/home_entity.dart';
class WorkOrderCard extends StatelessWidget {
@@ -14,9 +15,11 @@ class WorkOrderCard extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'待办工单',
style: TextStyle(
Text(
AppLocalizations.of(
context,
).translate('home_v2.pending_work_order'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -34,7 +37,9 @@ class WorkOrderCard extends StatelessWidget {
children: [
Expanded(
child: _buildWorkOrderCard(
title: '待处理',
title: AppLocalizations.of(
context,
).translate('home_v2.work_order_pending'),
count: stats.pending,
color: const Color(0xFFF53F3F),
bgColor: Colors.white,
@@ -43,7 +48,9 @@ class WorkOrderCard extends StatelessWidget {
const SizedBox(width: 8),
Expanded(
child: _buildWorkOrderCard(
title: '处理中',
title: AppLocalizations.of(
context,
).translate('home_v2.work_order_processing'),
count: stats.processing,
color: const Color(0xFFFF7D00),
bgColor: const Color(0xFFFFF7E6),
@@ -52,7 +59,9 @@ class WorkOrderCard extends StatelessWidget {
const SizedBox(width: 8),
Expanded(
child: _buildWorkOrderCard(
title: '待验收',
title: AppLocalizations.of(
context,
).translate('home_v2.work_order_pending_acceptance'),
count: stats.pendingAcceptance,
color: const Color(0xFF1D2129),
bgColor: Colors.white,
@@ -70,36 +79,31 @@ class WorkOrderCard extends StatelessWidget {
required Color color,
required Color bgColor,
}) {
return InkWell(
onTap: () {
// TODO: 点击跳转逻辑
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: color,
fontSize: 13,
),
style: TextStyle(color: color, fontSize: 13),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
@@ -112,13 +116,13 @@ class WorkOrderCard extends StatelessWidget {
),
],
),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: color.withOpacity(0.5),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: color.withOpacity(0.5),
),
],
),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/services.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/cubit/my_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/states/my_state.dart';
import 'package:maibu_satabot_v2/features/v2/my/di/my_di.dart';
@@ -41,9 +42,9 @@ class _MyPageContent extends StatelessWidget {
body: BlocConsumer<MyCubit, MyState>(
listener: (context, state) {
if (state is MyError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.message)));
}
},
builder: (context, state) {
@@ -57,11 +58,11 @@ class _MyPageContent extends StatelessWidget {
final cubit = context.read<MyCubit>();
final appUserState = context.watch<AppUserCubit>().state;
final user = appUserState.user;
// 获取用户信息(优先使用昵称,其次用户名)
final userName = user?.nickname ?? user?.username ?? '未知用户';
final userRole = user?.email ?? user?.phone ?? '未设置';
return Column(
children: [
// 用户信息卡片(延伸到状态栏)
@@ -96,7 +97,12 @@ class _MyPageContent extends StatelessWidget {
),
// 菜单列表(白色卡片)
Container(
margin: const EdgeInsets.only(top: 15, left: 16, right: 16, bottom: 16),
margin: const EdgeInsets.only(
top: 15,
left: 16,
right: 16,
bottom: 16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
@@ -147,9 +153,7 @@ class _MyPageContent extends StatelessWidget {
// 跳转到个人信息详情页
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfileDetailPage(),
),
MaterialPageRoute(builder: (context) => const ProfileDetailPage()),
);
debugPrint('✅ 已调用 Navigator.push');
break;
@@ -163,14 +167,12 @@ class _MyPageContent extends StatelessWidget {
// 跳转到系统设置页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
MaterialPageRoute(builder: (context) => const SystemSettingsPage()),
);
break;
default:
debugPrint('点击菜单: ${item.title}');
// TODO: 根据 ID 跳转到对应页面
// TODO: 根据 ID 跳转到对应页面
}
}
@@ -179,19 +181,25 @@ class _MyPageContent extends StatelessWidget {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('清除缓存'),
content: const Text('确定要清除所有缓存数据吗?'),
title: Text(
AppLocalizations.of(context).translate('my_v2.clear_cache'),
),
content: Text(
AppLocalizations.of(context).translate('my_v2.confirm_clear_cache'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
child: Text(AppLocalizations.of(context).translate('my_v2.cancel')),
),
TextButton(
onPressed: () {
Navigator.pop(context);
cubit.clearCache();
},
child: const Text('确定'),
child: Text(
AppLocalizations.of(context).translate('my_v2.confirm'),
),
),
],
),

View File

@@ -8,6 +8,7 @@ import 'package:maibu_satabot_v2/core/router/route_paths.dart';
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
/// 系统设置综合页面
class SystemSettingsPage extends StatefulWidget {
@@ -45,6 +46,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
// Tab 设置
_buildTabSettingsSection(),
const SizedBox(height: 12),
// 悬浮条设置
_buildFloatBarSection(),
const SizedBox(height: 12),
// 语言设置
_buildLanguageSection(),
const SizedBox(height: 12),
@@ -56,6 +60,65 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
);
}
/// 悬浮条设置
Widget _buildFloatBarSection() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).translate('my_v2.suspend_bar'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 4),
Text(
AppLocalizations.of(
context,
).translate('my_v2.suspend_bar_desc'),
style: const TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
),
),
],
),
),
ValueListenableBuilder<bool>(
valueListenable: FloatBarSettingService.settingNotifier,
builder: (context, isEnabled, child) {
return Switch(
value: isEnabled,
onChanged: (value) {
FloatBarSettingService.setEnabled(value);
},
activeColor: const Color(0xFF165DFF),
);
},
),
],
),
);
}
/// Tab 设置
Widget _buildTabSettingsSection() {
return Container(
@@ -74,9 +137,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tab 设置',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('my_v2.tab_settings'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
@@ -88,9 +151,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
if (state.runtimeType.toString() != 'TabConfigLoaded') {
return const Center(child: CircularProgressIndicator());
}
final tabs = state.config.items;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -164,14 +227,18 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
AppLocalizations.of(context).translate('my.chinese'),
'zh',
locale.languageCode == 'zh',
() => context.read<LocaleCubit>().setLocale(const Locale('zh', 'CN')),
() => context.read<LocaleCubit>().setLocale(
const Locale('zh', 'CN'),
),
),
const SizedBox(height: 12),
_buildLanguageOption(
AppLocalizations.of(context).translate('my.english'),
'en',
locale.languageCode == 'en',
() => context.read<LocaleCubit>().setLocale(const Locale('en', 'US')),
() => context.read<LocaleCubit>().setLocale(
const Locale('en', 'US'),
),
),
],
);
@@ -182,16 +249,25 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
);
}
Widget _buildLanguageOption(String label, String code, bool isSelected, VoidCallback onTap) {
Widget _buildLanguageOption(
String label,
String code,
bool isSelected,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF165DFF).withOpacity(0.1) : Colors.transparent,
color: isSelected
? const Color(0xFF165DFF).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB),
color: isSelected
? const Color(0xFF165DFF)
: const Color(0xFFE5E6EB),
width: 1,
),
),
@@ -202,7 +278,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
label,
style: TextStyle(
fontSize: 14,
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129),
color: isSelected
? const Color(0xFF165DFF)
: const Color(0xFF1D2129),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
@@ -258,11 +336,13 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).translate('login.logout')),
content: const Text('确定要退出登录吗?'),
content: Text(
AppLocalizations.of(context).translate('login.confirm_logout'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
child: Text(AppLocalizations.of(context).translate('my_v2.cancel')),
),
TextButton(
onPressed: () async {
@@ -275,7 +355,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFF53F3F),
),
child: const Text('确定'),
child: Text(
AppLocalizations.of(context).translate('my_v2.confirm'),
),
),
],
),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
/// 菜单项卡片组件(100% 还原设计稿)
@@ -24,9 +25,7 @@ class MenuItemCard extends StatelessWidget {
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 图标
@@ -39,21 +38,27 @@ class MenuItemCard extends StatelessWidget {
// 标题
Expanded(
child: Text(
item.title,
_getLocalizedTitle(context, item),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Color(0xFF1D2129),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// 副标题或开关
if (item.subTitle != null) ...[
Text(
item.subTitle!,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
Flexible(
child: Text(
item.subTitle!,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF86909C),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
@@ -65,7 +70,9 @@ class MenuItemCard extends StatelessWidget {
value: item.switchValue,
onChanged: onSwitchChanged,
activeColor: const Color(0xFF165DFF),
activeTrackColor: const Color(0xFF165DFF).withOpacity(0.3),
activeTrackColor: const Color(
0xFF165DFF,
).withOpacity(0.3),
inactiveThumbColor: const Color(0xFFC9CDD4),
inactiveTrackColor: const Color(0xFFE5E6EB),
),
@@ -115,4 +122,25 @@ class MenuItemCard extends StatelessWidget {
return Icons.label_outline;
}
}
String _getLocalizedTitle(BuildContext context, MenuItemEntity item) {
switch (item.id) {
case 'personal_info':
return AppLocalizations.of(context).translate('my_v2.personal_info');
case 'message_settings':
return AppLocalizations.of(context).translate('my_v2.message_settings');
case 'offline_cache':
return AppLocalizations.of(context).translate('my_v2.offline_cache');
case 'night_mode':
return AppLocalizations.of(context).translate('my_v2.night_mode');
case 'system_settings':
return AppLocalizations.of(context).translate('my_v2.system_settings');
case 'help_feedback':
return AppLocalizations.of(context).translate('my_v2.help_feedback');
case 'about_us':
return AppLocalizations.of(context).translate('my_v2.about_us');
default:
return item.title;
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart';
/// 快捷入口卡片组件(我的消息、我的收藏、我的工单)
@@ -36,32 +37,24 @@ class QuickActionCard extends StatelessWidget {
// 我的消息
_buildQuickActionItem(
icon: Icons.notifications_outlined,
label: '我的消息',
label: AppLocalizations.of(context).translate('my_v2.message'),
onTap: onMessageTap,
showBadge: true,
),
// 分割线
Container(
width: 1,
height: 40,
color: const Color(0xFFF0F0F0),
),
Container(width: 1, height: 40, color: const Color(0xFFF0F0F0)),
// 我的收藏
_buildQuickActionItem(
icon: Icons.favorite_border,
label: '我的收藏',
label: AppLocalizations.of(context).translate('my_v2.favorite'),
onTap: onFavoriteTap,
),
// 分割线
Container(
width: 1,
height: 40,
color: const Color(0xFFF0F0F0),
),
Container(width: 1, height: 40, color: const Color(0xFFF0F0F0)),
// 我的工单
_buildQuickActionItem(
icon: Icons.assignment_outlined,
label: '我的工单',
label: AppLocalizations.of(context).translate('my_v2.work_order'),
onTap: onWorkOrderTap,
),
],
@@ -79,18 +72,14 @@ class QuickActionCard extends StatelessWidget {
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 80,
width: 90,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Icon(
icon,
size: 24,
color: MyColors.primary,
),
Icon(icon, size: 24, color: MyColors.primary),
if (showBadge)
Positioned(
right: -4,
@@ -113,6 +102,9 @@ class QuickActionCard extends StatelessWidget {
fontSize: 13,
color: MyColors.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),

View File

@@ -27,26 +27,26 @@ class AppDimensions {
/// 上报类型枚举
enum ReportType {
deviceFault('设备故障', Icons.notifications_active),
patrolRecord('巡检记录', Icons.description),
hiddenDanger('隐患上报', Icons.shield),
defectReport('缺陷上报', Icons.build);
deviceFault('report.device_fault', Icons.notifications_active),
patrolRecord('report.patrol_record', Icons.description),
hiddenDanger('report.hidden_danger', Icons.shield),
defectReport('report.defect_report', Icons.build);
final String label;
final String labelKey;
final IconData icon;
const ReportType(this.label, this.icon);
const ReportType(this.labelKey, this.icon);
}
/// 问题等级枚举
enum ProblemLevel {
normal('一般', Color(0xFF86909C), Color(0xFFE5E6EB)),
important('重要', Color(0xFFFF7D00), Color(0xFFE5E6EB)),
urgent('紧急', Color(0xFFF53F3F), Color(0xFFF53F3F));
normal('report.normal', Color(0xFF86909C), Color(0xFFE5E6EB)),
important('report.important', Color(0xFFFF7D00), Color(0xFFE5E6EB)),
urgent('report.urgent', Color(0xFFF53F3F), Color(0xFFF53F3F));
final String label;
final String labelKey;
final Color textColor;
final Color borderColor;
const ProblemLevel(this.label, this.textColor, this.borderColor);
const ProblemLevel(this.labelKey, this.textColor, this.borderColor);
}

View File

@@ -9,18 +9,18 @@ class ReportCubit extends Cubit<ReportState> {
final SubmitReportUseCase submitReportUseCase;
ReportCubit({required this.submitReportUseCase})
: super(ReportFormState(report: ReportEntity()));
: super(ReportFormState(report: ReportEntity()));
/// 选择上报类型
void selectReportType(ReportType type) {
if (state is ReportFormState) {
final currentState = state as ReportFormState;
emit(currentState.copyWith(
selectedReportType: type,
report: currentState.report.copyWith(
reportType: type.label,
emit(
currentState.copyWith(
selectedReportType: type,
report: currentState.report.copyWith(reportType: type.labelKey),
),
));
);
}
}
@@ -28,10 +28,12 @@ class ReportCubit extends Cubit<ReportState> {
void selectDevice(String device) {
if (state is ReportFormState) {
final currentState = state as ReportFormState;
emit(currentState.copyWith(
selectedDevice: device,
report: currentState.report.copyWith(deviceId: device),
));
emit(
currentState.copyWith(
selectedDevice: device,
report: currentState.report.copyWith(deviceId: device),
),
);
}
}
@@ -39,10 +41,12 @@ class ReportCubit extends Cubit<ReportState> {
void updateLocation(String location) {
if (state is ReportFormState) {
final currentState = state as ReportFormState;
emit(currentState.copyWith(
location: location,
report: currentState.report.copyWith(location: location),
));
emit(
currentState.copyWith(
location: location,
report: currentState.report.copyWith(location: location),
),
);
}
}
@@ -50,9 +54,11 @@ class ReportCubit extends Cubit<ReportState> {
void updateDescription(String description) {
if (state is ReportFormState) {
final currentState = state as ReportFormState;
emit(currentState.copyWith(
report: currentState.report.copyWith(description: description),
));
emit(
currentState.copyWith(
report: currentState.report.copyWith(description: description),
),
);
}
}
@@ -60,12 +66,12 @@ class ReportCubit extends Cubit<ReportState> {
void selectProblemLevel(ProblemLevel level) {
if (state is ReportFormState) {
final currentState = state as ReportFormState;
emit(currentState.copyWith(
selectedProblemLevel: level,
report: currentState.report.copyWith(
problemLevel: level.label,
emit(
currentState.copyWith(
selectedProblemLevel: level,
report: currentState.report.copyWith(problemLevel: level.labelKey),
),
));
);
}
}
@@ -75,10 +81,12 @@ class ReportCubit extends Cubit<ReportState> {
final currentState = state as ReportFormState;
final updatedFiles = List<String>.from(currentState.mediaFiles)
..add(filePath);
emit(currentState.copyWith(
mediaFiles: updatedFiles,
report: currentState.report.copyWith(mediaUrls: updatedFiles),
));
emit(
currentState.copyWith(
mediaFiles: updatedFiles,
report: currentState.report.copyWith(mediaUrls: updatedFiles),
),
);
}
}
@@ -88,10 +96,12 @@ class ReportCubit extends Cubit<ReportState> {
final currentState = state as ReportFormState;
final updatedFiles = List<String>.from(currentState.mediaFiles)
..removeAt(index);
emit(currentState.copyWith(
mediaFiles: updatedFiles,
report: currentState.report.copyWith(mediaUrls: updatedFiles),
));
emit(
currentState.copyWith(
mediaFiles: updatedFiles,
report: currentState.report.copyWith(mediaUrls: updatedFiles),
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../cubit/report_cubit.dart';
import '../states/report_state.dart';
import '../constants/report_constants.dart';
@@ -46,9 +47,7 @@ class _ReportPageContent extends StatelessWidget {
builder: (context, state) {
if (state is ReportSubmitting) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.primary,
),
child: CircularProgressIndicator(color: AppColors.primary),
);
}
@@ -57,9 +56,7 @@ class _ReportPageContent extends StatelessWidget {
return Column(
children: [
_buildAppBar(context, state.location, cubit),
Expanded(
child: _buildFormContent(context, state),
),
Expanded(child: _buildFormContent(context, state)),
],
);
}
@@ -72,7 +69,11 @@ class _ReportPageContent extends StatelessWidget {
}
/// 构建导航栏
PreferredSizeWidget _buildAppBar(BuildContext context, String location, ReportCubit cubit) {
PreferredSizeWidget _buildAppBar(
BuildContext context,
String location,
ReportCubit cubit,
) {
return PreferredSize(
preferredSize: const Size.fromHeight(100),
child: Container(
@@ -89,9 +90,9 @@ class _ReportPageContent extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'现场上报',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('report.title'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
@@ -101,9 +102,9 @@ class _ReportPageContent extends StatelessWidget {
onTap: () {
context.read<ReportCubit>().submitReport();
},
child: const Text(
'提交',
style: TextStyle(
child: Text(
AppLocalizations.of(context).translate('report.submit'),
style: const TextStyle(
color: AppColors.primary,
fontSize: 18,
fontWeight: FontWeight.w600,
@@ -240,19 +241,12 @@ class _ReportPageContent extends StatelessWidget {
),
child: Row(
children: [
const Icon(
Icons.location_on,
color: AppColors.primary,
size: 20,
),
const Icon(Icons.location_on, color: AppColors.primary, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
location,
style: TextStyle(
fontSize: 16,
color: AppColors.textPrimary,
),
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
),
),
],
@@ -270,7 +264,7 @@ class _ReportPageContent extends StatelessWidget {
maxHeight: 1080,
imageQuality: 85,
);
if (image != null) {
cubit.addMediaFile(image.path);
}
@@ -287,7 +281,7 @@ class _ReportPageContent extends StatelessWidget {
source: ImageSource.camera,
maxDuration: const Duration(minutes: 5),
);
if (video != null) {
cubit.addMediaFile(video.path);
}
@@ -301,10 +295,8 @@ class _ReportPageContent extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MediaPreviewPage(
mediaFiles: mediaFiles,
initialIndex: index,
),
builder: (context) =>
MediaPreviewPage(mediaFiles: mediaFiles, initialIndex: index),
),
);
}
@@ -322,7 +314,9 @@ class _ReportPageContent extends StatelessWidget {
children: [
ListTile(
leading: const Icon(Icons.image),
title: const Text('选择图片'),
title: Text(
AppLocalizations.of(context).translate('report.select_image'),
),
onTap: () async {
Navigator.pop(context);
final XFile? image = await picker.pickImage(
@@ -338,7 +332,9 @@ class _ReportPageContent extends StatelessWidget {
),
ListTile(
leading: const Icon(Icons.video_library),
title: const Text('选择视频'),
title: Text(
AppLocalizations.of(context).translate('report.select_video'),
),
onTap: () async {
Navigator.pop(context);
final XFile? video = await picker.pickVideo(
@@ -360,19 +356,20 @@ class _ReportPageContent extends StatelessWidget {
/// 显示位置选择器
void _showLocationPicker(BuildContext context, ReportCubit cubit) {
final loc = AppLocalizations.of(context);
final locations = [
'主厂房A区',
'主厂房B区',
'锅炉房',
'汽机房',
'控制室',
'配电室',
loc.translate('report.main_building_a'),
loc.translate('report.main_building_b'),
loc.translate('report.boiler_room'),
loc.translate('report.turbine_room'),
loc.translate('report.control_room'),
loc.translate('report.power_distribution_room'),
];
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择位置'),
title: Text(loc.translate('report.select_location')),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -390,7 +387,7 @@ class _ReportPageContent extends StatelessWidget {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
child: Text(loc.translate('report.cancel')),
),
],
),
@@ -399,19 +396,20 @@ class _ReportPageContent extends StatelessWidget {
/// 显示设备选择器
void _showDevicePicker(BuildContext context, ReportCubit cubit) {
final loc = AppLocalizations.of(context);
final devices = [
'锅炉1号',
'锅炉2号',
'汽轮机1号',
'发电机1号',
'变压器1号',
'水泵1号',
loc.translate('report.boiler_1'),
loc.translate('report.boiler_2'),
loc.translate('report.turbine_1'),
loc.translate('report.generator_1'),
loc.translate('report.transformer_1'),
loc.translate('report.water_pump_1'),
];
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择设备'),
title: Text(loc.translate('report.select_device')),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -429,7 +427,7 @@ class _ReportPageContent extends StatelessWidget {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
child: Text(loc.translate('report.cancel')),
),
],
),
@@ -438,19 +436,20 @@ class _ReportPageContent extends StatelessWidget {
/// 显示成功对话框
void _showSuccessDialog(BuildContext context) {
final loc = AppLocalizations.of(context);
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('提交成功'),
content: const Text('您的上报已成功提交'),
title: Text(loc.translate('report.submit_success')),
content: Text(loc.translate('report.submit_success_message')),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context); // 返回上一页
},
child: const Text('确定'),
child: Text(loc.translate('report.confirm')),
),
],
),
@@ -459,15 +458,16 @@ class _ReportPageContent extends StatelessWidget {
/// 显示错误对话框
void _showErrorDialog(BuildContext context, String message) {
final loc = AppLocalizations.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('提交失败'),
title: Text(loc.translate('report.submit_failed')),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
child: Text(loc.translate('report.confirm')),
),
],
),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../constants/report_constants.dart';
/// 问题描述输入模块
@@ -14,6 +15,7 @@ class DescriptionInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppDimensions.horizontalPadding,
@@ -34,8 +36,8 @@ class DescriptionInput extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'问题描述',
style: TextStyle(
loc.translate('report.problem_description'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
@@ -47,35 +49,38 @@ class DescriptionInput extends StatelessWidget {
maxLength: 200,
onChanged: onChanged,
decoration: InputDecoration(
hintText: '请详细描述问题现象...',
hintStyle: TextStyle(color: AppColors.textHint),
hintText: loc.translate('report.description_hint'),
hintStyle: const TextStyle(color: AppColors.textHint),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
borderSide: BorderSide(color: AppColors.borderColor),
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
borderSide: const BorderSide(color: AppColors.borderColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
borderSide: BorderSide(color: AppColors.borderColor),
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
borderSide: const BorderSide(color: AppColors.borderColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
borderSide: BorderSide(color: AppColors.primary, width: 2),
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
borderSide: const BorderSide(
color: AppColors.primary,
width: 2,
),
),
counterText: '',
),
style: TextStyle(
fontSize: 14,
color: AppColors.textPrimary,
),
style: const TextStyle(fontSize: 14, color: AppColors.textPrimary),
),
Align(
alignment: Alignment.centerRight,
child: Text(
'${description?.length ?? 0}/200',
style: TextStyle(
fontSize: 12,
color: AppColors.textHint,
),
style: const TextStyle(fontSize: 12, color: AppColors.textHint),
),
),
],

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../constants/report_constants.dart';
/// 设备选择器
@@ -6,11 +7,7 @@ class DeviceSelector extends StatelessWidget {
final String? selectedDevice;
final VoidCallback onTap;
const DeviceSelector({
super.key,
this.selectedDevice,
required this.onTap,
});
const DeviceSelector({super.key, this.selectedDevice, required this.onTap});
@override
Widget build(BuildContext context) {
@@ -36,8 +33,8 @@ class DeviceSelector extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'设备选择',
style: TextStyle(
AppLocalizations.of(context).translate('report.select_device'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
@@ -46,7 +43,10 @@ class DeviceSelector extends StatelessWidget {
Row(
children: [
Text(
selectedDevice ?? '请选择设备',
selectedDevice ??
AppLocalizations.of(
context,
).translate('report.please_select_device'),
style: TextStyle(
fontSize: 14,
color: selectedDevice != null

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../constants/report_constants.dart';
/// 问题等级选择器
@@ -14,6 +15,7 @@ class LevelSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppDimensions.horizontalPadding,
@@ -34,8 +36,8 @@ class LevelSelector extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'问题等级',
style: TextStyle(
loc.translate('report.problem_level'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
@@ -55,19 +57,27 @@ class LevelSelector extends StatelessWidget {
color: isSelected && level == ProblemLevel.urgent
? AppColors.danger.withOpacity(0.1)
: AppColors.background,
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
border: Border.all(
color: isSelected ? level.borderColor : AppColors.borderColor,
color: isSelected
? level.borderColor
: AppColors.borderColor,
width: isSelected ? 2 : 1,
),
),
child: Center(
child: Text(
level.label,
loc.translate(level.labelKey),
style: TextStyle(
fontSize: 14,
color: isSelected ? level.textColor : level.textColor.withOpacity(0.6),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? level.textColor
: level.textColor.withOpacity(0.6),
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
),

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../constants/report_constants.dart';
/// 媒体上传模块
@@ -23,6 +24,7 @@ class MediaUploader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppDimensions.horizontalPadding,
@@ -43,8 +45,8 @@ class MediaUploader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'上传图片/视频',
style: TextStyle(
loc.translate('report.upload_media'),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
@@ -58,14 +60,14 @@ class MediaUploader extends StatelessWidget {
// 拍照按钮
_buildUploadButton(
icon: Icons.camera_alt,
label: '拍照',
label: loc.translate('report.take_photo'),
onTap: onCameraTap,
),
const SizedBox(width: 8),
// 录像按钮
_buildUploadButton(
icon: Icons.videocam,
label: '录像',
label: loc.translate('report.record_video'),
onTap: onVideoTap,
),
const SizedBox(width: 8),
@@ -74,84 +76,94 @@ class MediaUploader extends StatelessWidget {
final String filePath = entry.value;
final bool isNetworkImage = filePath.startsWith('http');
final bool isVideo = _isVideoFile(filePath);
return GestureDetector(
onTap: () => onTap?.call(entry.key),
child: Stack(
children: [
Container(
width: 80,
height: 80,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
color: AppColors.cardBackground,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
child: isVideo
? _buildVideoThumbnail(filePath)
: (isNetworkImage
? Image.network(
filePath,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholderIcon(Icons.broken_image);
},
)
: Image.file(
File(filePath),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholderIcon(Icons.broken_image);
},
)),
),
),
Positioned(
top: 4,
right: 12,
child: GestureDetector(
onTap: () => onRemove(entry.key),
child: Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
children: [
Container(
width: 80,
height: 80,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
child: const Icon(
Icons.close,
size: 14,
color: Colors.white,
color: AppColors.cardBackground,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
child: isVideo
? _buildVideoThumbnail(filePath)
: (isNetworkImage
? Image.network(
filePath,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return _buildPlaceholderIcon(
Icons.broken_image,
);
},
)
: Image.file(
File(filePath),
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return _buildPlaceholderIcon(
Icons.broken_image,
);
},
)),
),
),
),
// 视频播放图标
if (isVideo)
Positioned(
child: Container(
width: 80,
height: 80,
margin: const EdgeInsets.only(right: 8),
alignment: Alignment.center,
top: 4,
right: 12,
child: GestureDetector(
onTap: () => onRemove(entry.key),
child: Container(
padding: const EdgeInsets.all(8),
width: 20,
height: 20,
decoration: const BoxDecoration(
color: Colors.black38,
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
Icons.close,
size: 14,
color: Colors.white,
size: 24,
),
),
),
),
],
),
// 视频播放图标
if (isVideo)
Positioned(
child: Container(
width: 80,
height: 80,
margin: const EdgeInsets.only(right: 8),
alignment: Alignment.center,
child: Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.black38,
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
color: Colors.white,
size: 24,
),
),
),
),
],
),
);
}).toList(),
// 添加按钮
@@ -228,17 +240,9 @@ class MediaUploader extends StatelessWidget {
fit: StackFit.expand,
children: [
// 灰色背景
Container(
color: Colors.grey[300],
),
Container(color: Colors.grey[300]),
// 视频图标
const Center(
child: Icon(
Icons.videocam,
color: Colors.grey,
size: 32,
),
),
const Center(child: Icon(Icons.videocam, color: Colors.grey, size: 32)),
],
);
}
@@ -247,13 +251,7 @@ class MediaUploader extends StatelessWidget {
Widget _buildPlaceholderIcon(IconData icon) {
return Container(
color: Colors.grey[300],
child: Center(
child: Icon(
icon,
color: Colors.grey,
size: 32,
),
),
child: Center(child: Icon(icon, color: Colors.grey, size: 32)),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../constants/report_constants.dart';
/// 上报类型选择器
@@ -15,21 +16,32 @@ class ReportTypeSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: AppDimensions.horizontalPadding),
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.horizontalPadding,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: ReportType.values.map((type) {
final isSelected = selectedType == type;
return Expanded(
return Flexible(
flex: 1,
child: GestureDetector(
onTap: () => onSelected(type),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 12),
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 14,
),
decoration: BoxDecoration(
color: AppColors.background,
borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall),
borderRadius: BorderRadius.circular(
AppDimensions.borderRadiusSmall,
),
border: Border.all(
color: isSelected ? AppColors.primary : AppColors.borderColor,
color: isSelected
? AppColors.primary
: AppColors.borderColor,
width: isSelected ? 2 : 1,
),
boxShadow: [
@@ -45,17 +57,27 @@ class ReportTypeSelector extends StatelessWidget {
children: [
Icon(
type.icon,
color: isSelected ? AppColors.primary : AppColors.textHint,
size: 24,
color: isSelected
? AppColors.primary
: AppColors.textHint,
size: 26,
),
const SizedBox(height: 8),
const SizedBox(height: 6),
Text(
type.label,
AppLocalizations.of(context).translate(type.labelKey),
style: TextStyle(
fontSize: 12,
color: isSelected ? AppColors.primary : AppColors.textSecondary,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
fontSize: 11,
height: 1.2,
color: isSelected
? AppColors.primary
: AppColors.textSecondary,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
@@ -44,9 +45,7 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
child: BlocBuilder<AlarmCubit, AlarmState>(
builder: (context, state) {
if (state is AlarmLoading) {
return const Center(
child: CircularProgressIndicator(),
);
return const Center(child: CircularProgressIndicator());
}
if (state is AlarmError) {
@@ -64,7 +63,11 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _cubit.loadAlarms(),
child: const Text('重试'),
child: Text(
AppLocalizations.of(
context,
).translate('alarm_center.retry'),
),
),
],
),
@@ -96,22 +99,34 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
//const SizedBox(height: AlarmDimensions.moduleSpacing),
// 告警统计卡片
AlarmCountCard(count: state.count),
const SizedBox(height: AlarmDimensions.moduleSpacing),
const SizedBox(
height: AlarmDimensions.moduleSpacing,
),
// 告警列表
...state.alarms.map((alarm) {
return AlarmItemCard(
alarm: alarm,
onTap: () {
context.push('/alarm_center/detail/${alarm.id}');
context.push(
'/alarm_center/detail/${alarm.id}',
);
},
);
}).toList(),
const SizedBox(height: AlarmDimensions.moduleSpacing),
const SizedBox(
height: AlarmDimensions.moduleSpacing,
),
// AI 诊断建议(仅当有 AI 诊断内容时显示)
if (state.alarms.any((alarm) => alarm.aiDiagnosis != null))
if (state.alarms.any(
(alarm) => alarm.aiDiagnosis != null,
))
AiDiagnosisCard(
content: state.alarms
.firstWhere((alarm) => alarm.aiDiagnosis != null)
content:
state.alarms
.firstWhere(
(alarm) =>
alarm.aiDiagnosis != null,
)
.aiDiagnosis ??
'',
onViewDetails: () {
@@ -153,20 +168,61 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'筛选条件',
style: TextStyle(
Text(
AppLocalizations.of(
context,
).translate('alarm_center.filter_title'),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AlarmColors.textPrimary,
),
),
const SizedBox(height: 20),
_buildFilterOption('告警等级', ['高危', '中危', '低危', '提示']),
_buildFilterOption(
AppLocalizations.of(
context,
).translate('alarm_center.alarm_level'),
[
AppLocalizations.of(
context,
).translate('alarm_center.high_risk'),
AppLocalizations.of(
context,
).translate('alarm_center.medium_risk'),
AppLocalizations.of(
context,
).translate('alarm_center.low_risk'),
AppLocalizations.of(context).translate('alarm_center.hint'),
],
),
const SizedBox(height: 16),
_buildFilterOption('时间范围', ['今天', '本周', '本月', '全部']),
_buildFilterOption(
AppLocalizations.of(
context,
).translate('alarm_center.time_range'),
[
AppLocalizations.of(context).translate('alarm_center.today'),
AppLocalizations.of(
context,
).translate('alarm_center.this_week'),
AppLocalizations.of(
context,
).translate('alarm_center.this_month'),
AppLocalizations.of(context).translate('alarm_center.all'),
],
),
const SizedBox(height: 16),
_buildFilterOption('区域', ['A区', 'B区', 'C区', 'D区', '全部']),
_buildFilterOption(
AppLocalizations.of(context).translate('alarm_center.area'),
[
'A区',
'B区',
'C区',
'D区',
AppLocalizations.of(context).translate('alarm_center.all'),
],
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
@@ -181,9 +237,11 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'应用筛选',
style: TextStyle(
child: Text(
AppLocalizations.of(
context,
).translate('alarm_center.apply_filter'),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
@@ -204,9 +262,9 @@ class _AlarmCenterPageState extends State<AlarmCenterPage> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text(
'告警中心',
style: TextStyle(
Text(
AppLocalizations.of(context).translate('alarm_center.title'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),

View File

@@ -1,20 +1,20 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 告警统计卡片
class AlarmCountCard extends StatelessWidget {
const AlarmCountCard({
super.key,
required this.count,
});
const AlarmCountCard({super.key, required this.count});
final AlarmCountEntity count;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AlarmDimensions.horizontalPadding),
margin: const EdgeInsets.symmetric(
horizontal: AlarmDimensions.horizontalPadding,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AlarmColors.background,
@@ -25,7 +25,9 @@ class AlarmCountCard extends StatelessWidget {
children: [
Expanded(
child: _buildCountItem(
label: '未处理',
label: AppLocalizations.of(
context,
).translate('alarm_center.unprocessed'),
value: count.unprocessed,
color: AlarmColors.danger,
bgColor: AlarmColors.unprocessedBg,
@@ -51,7 +53,9 @@ class AlarmCountCard extends StatelessWidget {
_buildDivider(),
Expanded(
child: _buildCountItem(
label: '今日新增',
label: AppLocalizations.of(
context,
).translate('alarm_center.today_new'),
value: count.todayNew,
color: const Color(0xFF1D2129),
),

View File

@@ -1,14 +1,11 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 告警项卡片
class AlarmItemCard extends StatelessWidget {
const AlarmItemCard({
super.key,
required this.alarm,
this.onTap,
});
const AlarmItemCard({super.key, required this.alarm, this.onTap});
final AlarmEntity alarm;
final VoidCallback? onTap;
@@ -34,11 +31,7 @@ class AlarmItemCard extends StatelessWidget {
// 第一行:图标 + 标题 + 等级标签
Row(
children: [
Icon(
alarm.level.icon,
color: alarm.level.color,
size: 20,
),
Icon(alarm.level.icon, color: alarm.level.color, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
@@ -51,13 +44,16 @@ class AlarmItemCard extends StatelessWidget {
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: alarm.level.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
alarm.level.label,
_getLevelLabel(context, alarm.level),
style: TextStyle(
fontSize: 12,
color: alarm.level.color,
@@ -70,7 +66,7 @@ class AlarmItemCard extends StatelessWidget {
const SizedBox(height: 8),
// 第二行:区域和设备
Text(
'区域:${alarm.area} / 设备:${alarm.device}',
'${AppLocalizations.of(context).translate('alarm_center.area')}:${alarm.area} / ${AppLocalizations.of(context).translate('alarm_center.device')}:${alarm.device}',
style: const TextStyle(
fontSize: 13,
color: AlarmColors.textSecondary,
@@ -89,7 +85,7 @@ class AlarmItemCard extends StatelessWidget {
),
const Spacer(),
Text(
alarm.status.label,
_getStatusLabel(context, alarm.status),
style: TextStyle(
fontSize: 12,
color: alarm.status == AlarmStatus.unconfirmed
@@ -113,4 +109,32 @@ class AlarmItemCard extends StatelessWidget {
),
);
}
String _getLevelLabel(BuildContext context, AlarmLevel level) {
switch (level) {
case AlarmLevel.danger:
return AppLocalizations.of(context).translate('alarm_center.high_risk');
case AlarmLevel.warning:
return AppLocalizations.of(
context,
).translate('alarm_center.medium_risk');
case AlarmLevel.low:
return AppLocalizations.of(context).translate('alarm_center.low_risk');
case AlarmLevel.info:
return AppLocalizations.of(context).translate('alarm_center.hint');
}
}
String _getStatusLabel(BuildContext context, AlarmStatus status) {
switch (status) {
case AlarmStatus.unconfirmed:
return AppLocalizations.of(
context,
).translate('alarm_center.unconfirmed');
case AlarmStatus.confirmed:
return AppLocalizations.of(context).translate('alarm_center.confirmed');
case AlarmStatus.recovered:
return AppLocalizations.of(context).translate('alarm_center.recovered');
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart';
/// 告警筛选标签栏
@@ -35,11 +36,15 @@ class AlarmTabBar extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
filter.label,
_getFilterLabel(context, filter),
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? AlarmColors.primary : AlarmColors.textSecondary,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? AlarmColors.primary
: AlarmColors.textSecondary,
),
),
const SizedBox(height: 4),
@@ -47,7 +52,9 @@ class AlarmTabBar extends StatelessWidget {
height: 2,
width: 20,
decoration: BoxDecoration(
color: isSelected ? AlarmColors.primary : Colors.transparent,
color: isSelected
? AlarmColors.primary
: Colors.transparent,
borderRadius: BorderRadius.circular(1),
),
),
@@ -59,4 +66,19 @@ class AlarmTabBar extends StatelessWidget {
),
);
}
String _getFilterLabel(BuildContext context, AlarmFilterTab filter) {
switch (filter) {
case AlarmFilterTab.all:
return AppLocalizations.of(context).translate('alarm_center.all');
case AlarmFilterTab.unprocessed:
return AppLocalizations.of(
context,
).translate('alarm_center.unprocessed');
case AlarmFilterTab.confirmed:
return AppLocalizations.of(context).translate('alarm_center.confirmed');
case AlarmFilterTab.recovered:
return AppLocalizations.of(context).translate('alarm_center.recovered');
}
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../cubit/workorder_cubit.dart';
import '../widgets/workorder_tabbar.dart';
@@ -18,9 +19,15 @@ class WorkOrderPage extends StatelessWidget {
Widget build(BuildContext context) {
// 创建 Repository 和 UseCases
final remoteDataSource = WorkOrderRemoteDataSourceImpl();
final repository = WorkOrderRepositoryImpl(remoteDataSource: remoteDataSource);
final getWorkOrderListUseCase = GetWorkOrderListUseCase(repository: repository);
final getWorkOrderCountUseCase = GetWorkOrderCountUseCase(repository: repository);
final repository = WorkOrderRepositoryImpl(
remoteDataSource: remoteDataSource,
);
final getWorkOrderListUseCase = GetWorkOrderListUseCase(
repository: repository,
);
final getWorkOrderCountUseCase = GetWorkOrderCountUseCase(
repository: repository,
);
return BlocProvider(
create: (context) {
@@ -87,7 +94,9 @@ class WorkOrderPage extends StatelessWidget {
backgroundColor: AppConstants.primaryColor,
foregroundColor: Colors.white,
),
child: const Text('重试'),
child: Text(
AppLocalizations.of(context).translate('work_order_v2.retry'),
),
),
],
),
@@ -99,10 +108,10 @@ class WorkOrderPage extends StatelessWidget {
child: Column(
children: [
// 导航栏
_buildAppBar(),
_buildAppBar(context),
// 标签栏
_buildTabBar(context, state),
// 内容区域
Expanded(
child: RefreshIndicator(
@@ -114,16 +123,18 @@ class WorkOrderPage extends StatelessWidget {
// 统计卡片
if (state.count != null)
WorkOrderCountCard(count: state.count!),
// 工单列表
...state.workOrders.map((workOrder) => WorkOrderItemCard(
workOrder: workOrder,
onTap: () {
// TODO: 跳转到工单详情页
debugPrint('点击工单: ${workOrder.title}');
},
)),
...state.workOrders.map(
(workOrder) => WorkOrderItemCard(
workOrder: workOrder,
onTap: () {
// TODO: 跳转到工单详情页
debugPrint('点击工单: ${workOrder.title}');
},
),
),
const SizedBox(height: 20),
],
),
@@ -134,26 +145,22 @@ class WorkOrderPage extends StatelessWidget {
);
}
Widget _buildAppBar() {
Widget _buildAppBar(BuildContext context) {
return Container(
height: 44.0,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: const Row(
child: Row(
children: [
Text(
'工单任务',
style: TextStyle(
AppLocalizations.of(context).translate('work_order_v2.title'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
Spacer(),
Icon(
Icons.add_circle_outline,
size: 24,
color: Color(0xFF1D2129),
),
const Spacer(),
Icon(Icons.add_circle_outline, size: 24, color: Color(0xFF1D2129)),
],
),
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../../domain/entities/workorder_entity.dart';
@@ -6,10 +7,7 @@ import '../../domain/entities/workorder_entity.dart';
class WorkOrderCountCard extends StatelessWidget {
final WorkOrderCountEntity count;
const WorkOrderCountCard({
Key? key,
required this.count,
}) : super(key: key);
const WorkOrderCountCard({Key? key, required this.count}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -27,19 +25,25 @@ class WorkOrderCountCard extends StatelessWidget {
child: Row(
children: [
_buildCountItem(
label: '待处理',
label: AppLocalizations.of(
context,
).translate('work_order_v2.pending'),
value: count.pendingCount.toString(),
valueColor: WorkOrderColors.primaryText,
),
_buildDivider(),
_buildCountItem(
label: '执行中',
label: AppLocalizations.of(
context,
).translate('work_order_v2.executing'),
value: count.executingCount.toString(),
valueColor: WorkOrderColors.primaryText,
),
_buildDivider(),
_buildCountItem(
label: '今日完成',
label: AppLocalizations.of(
context,
).translate('work_order_v2.today_completed'),
value: count.todayCompletedCount.toString(),
valueColor: WorkOrderColors.primary,
),
@@ -78,10 +82,6 @@ class WorkOrderCountCard extends StatelessWidget {
}
Widget _buildDivider() {
return Container(
width: 1,
height: 40,
color: const Color(0xFFF0F0F0),
);
return Container(width: 1, height: 40, color: const Color(0xFFF0F0F0));
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/consts/workorder_consts.dart';
import '../../domain/entities/workorder_entity.dart';
@@ -7,11 +8,8 @@ class WorkOrderItemCard extends StatelessWidget {
final WorkOrderEntity workOrder;
final VoidCallback? onTap;
const WorkOrderItemCard({
Key? key,
required this.workOrder,
this.onTap,
}) : super(key: key);
const WorkOrderItemCard({Key? key, required this.workOrder, this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
@@ -47,48 +45,79 @@ class WorkOrderItemCard extends StatelessWidget {
),
),
),
_buildStatusTag(),
_buildStatusTag(context),
],
),
const SizedBox(height: 12),
// 详情信息
_buildDetailRow('工单号', workOrder.orderNo),
_buildDetailRow(
AppLocalizations.of(
context,
).translate('work_order_v2.order_no'),
workOrder.orderNo,
),
const SizedBox(height: 6),
// 优先级
_buildDetailRow(
'优先级',
_getPriorityText(workOrder.priority),
AppLocalizations.of(
context,
).translate('work_order_v2.priority'),
_getPriorityText(context, workOrder.priority),
valueColor: _getPriorityColor(workOrder.priority),
),
const SizedBox(height: 6),
// 根据状态显示不同信息
if (workOrder.status == WorkOrderStatus.pending) ...[
_buildDetailRow(
'创建时间',
AppLocalizations.of(
context,
).translate('work_order_v2.create_time'),
_formatDateTime(workOrder.createTime),
),
const SizedBox(height: 6),
_buildDetailRow('地点', workOrder.location),
_buildDetailRow(
AppLocalizations.of(
context,
).translate('work_order_v2.location'),
workOrder.location,
),
] else if (workOrder.status == WorkOrderStatus.executing) ...[
_buildDetailRow('执行人', workOrder.executor ?? '-'),
_buildDetailRow(
AppLocalizations.of(
context,
).translate('work_order_v2.executor'),
workOrder.executor ?? '-',
),
const SizedBox(height: 6),
_buildDetailRow('地点', workOrder.location),
_buildDetailRow(
AppLocalizations.of(
context,
).translate('work_order_v2.location'),
workOrder.location,
),
if (workOrder.progress != null) ...[
const SizedBox(height: 12),
_buildProgressBar(),
_buildProgressBar(context),
],
] else if (workOrder.status == WorkOrderStatus.completed) ...[
_buildDetailRow(
'完成时间',
AppLocalizations.of(
context,
).translate('work_order_v2.complete_time'),
workOrder.completeTime != null
? _formatDateTime(workOrder.completeTime!)
: '-',
),
const SizedBox(height: 6),
_buildDetailRow('地点', workOrder.location),
_buildDetailRow(
AppLocalizations.of(
context,
).translate('work_order_v2.location'),
workOrder.location,
),
],
],
),
@@ -113,26 +142,30 @@ class WorkOrderItemCard extends StatelessWidget {
);
}
Widget _buildStatusTag() {
Widget _buildStatusTag(BuildContext context) {
Color color;
String text;
switch (workOrder.status) {
case WorkOrderStatus.pending:
color = AppConstants.pendingColor;
text = '待处理';
text = AppLocalizations.of(context).translate('work_order_v2.pending');
break;
case WorkOrderStatus.executing:
color = AppConstants.executingColor;
text = '执行中';
text = AppLocalizations.of(
context,
).translate('work_order_v2.executing');
break;
case WorkOrderStatus.completed:
color = AppConstants.completedColor;
text = '已完成';
text = AppLocalizations.of(
context,
).translate('work_order_v2.completed');
break;
default:
color = AppConstants.auxiliaryTextColor;
text = '未知';
text = AppLocalizations.of(context).translate('common.unknown');
}
return Row(
@@ -144,13 +177,7 @@ class WorkOrderItemCard extends StatelessWidget {
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
text,
style: TextStyle(
fontSize: 12,
color: color,
),
),
child: Text(text, style: TextStyle(fontSize: 12, color: color)),
),
if (workOrder.status == WorkOrderStatus.executing &&
workOrder.progress != null)
@@ -169,39 +196,43 @@ class WorkOrderItemCard extends StatelessWidget {
);
}
Widget _buildDetailRow(
String label,
String value, {
Color? valueColor,
}) {
Widget _buildDetailRow(String label, String value, {Color? valueColor}) {
return Row(
children: [
Text(
'$label:',
style: TextStyle(
fontSize: 13,
color: AppConstants.auxiliaryTextColor,
Flexible(
child: Text(
'$label:',
style: TextStyle(
fontSize: 13,
color: AppConstants.auxiliaryTextColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
value,
style: TextStyle(
fontSize: 13,
color: valueColor ?? AppConstants.secondaryTextColor,
Flexible(
child: Text(
value,
style: TextStyle(
fontSize: 13,
color: valueColor ?? AppConstants.secondaryTextColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildProgressBar() {
Widget _buildProgressBar(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'进度:',
'${AppLocalizations.of(context).translate('work_order_v2.progress')}:',
style: TextStyle(
fontSize: 13,
color: AppConstants.auxiliaryTextColor,
@@ -224,14 +255,14 @@ class WorkOrderItemCard extends StatelessWidget {
);
}
String _getPriorityText(WorkOrderPriority priority) {
String _getPriorityText(BuildContext context, WorkOrderPriority priority) {
switch (priority) {
case WorkOrderPriority.high:
return '高';
return AppLocalizations.of(context).translate('work_order_v2.high');
case WorkOrderPriority.medium:
return '中';
return AppLocalizations.of(context).translate('work_order_v2.medium');
case WorkOrderPriority.low:
return '低';
return AppLocalizations.of(context).translate('work_order_v2.low');
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import '../../../../../core/consts/workorder_consts.dart';
/// 工单标签栏组件
@@ -15,10 +16,32 @@ class WorkOrderTabBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tabs = [
{'label': '待处理', 'status': WorkOrderStatus.pending, 'badge': false},
{'label': '执行中', 'status': WorkOrderStatus.executing, 'badge': true},
{'label': '已完成', 'status': WorkOrderStatus.completed, 'badge': false},
{'label': '全部', 'status': WorkOrderStatus.all, 'badge': true},
{
'label': AppLocalizations.of(
context,
).translate('work_order_v2.pending'),
'status': WorkOrderStatus.pending,
'badge': false,
},
{
'label': AppLocalizations.of(
context,
).translate('work_order_v2.executing'),
'status': WorkOrderStatus.executing,
'badge': true,
},
{
'label': AppLocalizations.of(
context,
).translate('work_order_v2.completed'),
'status': WorkOrderStatus.completed,
'badge': false,
},
{
'label': AppLocalizations.of(context).translate('work_order_v2.all'),
'status': WorkOrderStatus.all,
'badge': true,
},
];
return Container(
@@ -44,7 +67,9 @@ class WorkOrderTabBar extends StatelessWidget {
tab['label'] as String,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
color: isSelected
? AppConstants.primaryColor
: AppConstants.secondaryTextColor,

View File

@@ -167,6 +167,16 @@ class _LifecycleListenerState extends State<_LifecycleListener>
try {
final authCubit = context.read<AuthCubit>();
if (authCubit.state is! AuthAuthenticated) return;
// 🔥 关键修复:只有在用户已选择设备时才重连 TCP
// 否则创建一个无用的 TCP 连接,服务端会残留 session,
// 导致后续 connectBySwitch() 复用该连接时触发 have_logged_in
final remoteControlCubit = sl<RemoteControlCubit>();
if (remoteControlCubit.state.targetDevice == null) {
debugPrint('📱 [生命周期] 未选择设备,跳过 TCP 重连(避免创建无用连接)');
return;
}
await Future.delayed(const Duration(milliseconds: 500));
await authCubit.reconnectAfterResume();
} catch (e) {