From 0db64a96e74a1984d0ad4bbcd472140cb72ff191 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Thu, 2 Jul 2026 16:04:30 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BC=98=E5=8C=96=E8=A1=A5=E8=B6=B3=E4=BA=86?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E4=B8=8B=E6=8B=89=E5=88=B7=E6=96=B0=E4=B8=AD?= =?UTF-8?q?=E5=AF=B9=E8=8E=B7=E5=8F=96=E5=9C=BA=E7=AB=99=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E7=9A=84=E5=8A=A0=E5=85=A5=EF=BC=8C=E7=A1=AE=E4=BF=9D=E8=83=BD?= =?UTF-8?q?=E5=A4=9F=E5=AE=9E=E6=97=B6=E8=8E=B7=E5=8F=96=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=B9=B6=E6=B8=B2=E6=9F=93=E3=80=82=202.=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86tcp=E9=87=8D=E8=BF=9E=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=EF=BC=8C=E5=85=B7=E4=BD=93=E6=93=8D=E4=BD=9C=EF=BC=9A?= =?UTF-8?q?=E6=96=AD=E5=BC=80=E9=87=8D=E8=BF=9E=E6=97=B6=E5=80=99=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=BB=BA=E7=AB=8B=E6=96=B0=E9=93=BE=E6=8E=A5=20?= =?UTF-8?q?=E8=80=8C=E6=98=AF=E5=B0=86=E6=97=A7=E9=93=BE=E6=8E=A5=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E5=B9=B6=E9=87=8D=E6=96=B0=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E3=80=82=203.=E5=A4=A7=E9=9D=A2=E7=A7=AF?= =?UTF-8?q?=E7=9A=84=E7=BB=99=E9=A1=B9=E7=9B=AE=E4=B8=AD=E7=9A=84=E5=90=84?= =?UTF-8?q?=E4=B8=AA=E9=A1=B5=E9=9D=A2=20=E6=89=8B=E5=8A=A8=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E4=BA=86=E5=A4=9A=E8=AF=AD=E8=A8=80=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E3=80=82=E5=90=8C=E6=97=B6=E4=B9=9F=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=BA=86=E7=9B=B8=E5=BA=94=E5=B8=A6=E6=9D=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E8=8B=B1=E6=96=87=E9=95=BF=E5=BA=A6=E8=BE=83=E5=A4=A7?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=E5=8E=9F=E6=9C=89=E7=9A=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E5=B7=B2=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E4=BA=86=E5=AF=B9=E5=BA=94=E7=9A=84=E6=A0=B7=E5=BC=8F=E4=B8=8D?= =?UTF-8?q?=E5=92=8C=E8=B0=90=E9=97=AE=E9=A2=98=E3=80=82=E4=BF=9D=E8=AF=81?= =?UTF-8?q?=E4=BA=86=E5=A4=96=E8=A7=82=E7=9A=84=E7=BB=9F=E4=B8=80=E6=80=A7?= =?UTF-8?q?=E3=80=82=204.=E7=BB=99=E9=A1=B9=E7=9B=AE=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E4=B8=BB=E8=A6=81=E6=93=8D=E4=BD=9C=E5=8A=A0=E4=BA=86=E5=87=A0?= =?UTF-8?q?=E4=B8=AA=E6=A0=B8=E5=BF=83=E7=9A=84=E6=93=8D=E4=BD=9C=E6=AF=94?= =?UTF-8?q?=E5=A6=82=E8=BF=9C=E7=A8=8B=E9=81=A5=E6=8E=A7=E5=92=8C=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E8=A7=84=E5=88=92=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=8F=8C?= =?UTF-8?q?=E5=B1=82=E7=9A=84=E4=BF=9D=E6=8A=A4=EF=BC=8C=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=87=8C=E5=AE=89=E5=85=A8=E6=A8=A1=E5=BC=8F=EF=BC=8C=E5=8D=B3?= =?UTF-8?q?=E5=9C=A8=E6=AD=A4=E6=9C=9F=E9=97=B4=E5=AF=B9=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E6=8E=A8=E9=80=81=E7=9A=84=E9=87=8D=E5=A4=8D=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=93=8D=E4=BD=9C=E5=90=8E=E7=9A=84=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=80=80=E5=87=BA=E7=99=BB=E5=BD=95=E5=81=9A=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=20=E4=BF=9D=E8=AF=81=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E7=9A=84=E5=AE=89=E5=85=A8=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/languages/en-US.json | 184 +- assets/languages/zh-CN.json | 180 ++ lib/core/consts/http_api_consts.dart | 6 +- lib/core/di/injection.dart | 6 + .../managers/drone_task_state_manager.dart | 64 + lib/core/network/dio_client.dart | 1 + .../datasources/drone_osd_datasource.dart | 2 + lib/core/network/tcp/tcp_client.dart | 245 +- lib/core/router/app_router.dart | 2 + .../auth/presentation/bloc/auth_cubit.dart | 209 +- .../impl/device_http_datasource_impl.dart | 106 +- .../presentation/bloc/devices_cubit.dart | 35 +- .../presentation/pages/route_plan_page.dart | 23 +- .../pages/machine_details_page.dart | 4 +- .../presentation/main_wrapper.dart | 105 +- .../pages/custom_main_container.dart | 23 +- .../widgets/custom_bottom_nav_bar.dart | 32 +- .../bloc/remote_control_cubit.dart | 58 +- .../pages/remote_control_page.dart | 9 + .../datasources/device_remote_datasource.dart | 18 +- .../data/models/robot_data_model.dart | 21 +- .../presentation/bloc/device_status_bloc.dart | 2 +- .../bloc/device_status_state.dart | 2 +- .../presentation/bloc/drone_station_bloc.dart | 65 +- .../cubit/float_bar_setting_cubit.dart | 20 +- .../float_bar/manager/float_bar_manager.dart | 6 +- .../float_bar/view/float_bar_widget.dart | 2095 +++++++++++++++-- .../cleaning_weeding_robot_task_page.dart | 7 +- .../pages/device_status_page.dart | 175 +- .../pages/drone_mission_control_page.dart | 68 +- .../pages/drone_station_detail_page.dart | 27 +- .../pages/drone_station_status_page.dart | 504 ++-- .../pages/drone_video_control_page.dart | 54 +- .../presentation/pages/robot_list_page.dart | 426 ++-- .../widgets/device_bottom_nav_bar.dart | 13 +- .../widgets/device_item_widget.dart | 85 +- .../presentation/widgets/robot_item_card.dart | 43 +- .../datasources/site_datasource_impl.dart | 6 + .../v2/home/domain/entities/site_entity.dart | 2 +- .../home/presentation/bloc/home_v2_bloc.dart | 42 +- .../home/presentation/bloc/home_v2_state.dart | 2 +- .../home/presentation/pages/home_v2_page.dart | 75 +- .../widgets/plant_overview_card.dart | 103 +- .../home/presentation/widgets/power_card.dart | 23 +- .../widgets/power_trend_chart.dart | 27 +- .../widgets/quick_entry_card.dart | 42 +- .../home/presentation/widgets/stats_grid.dart | 45 +- .../presentation/widgets/work_order_card.dart | 82 +- .../v2/my/presentation/pages/my_page.dart | 42 +- .../pages/system_settings_page.dart | 110 +- .../presentation/widgets/menu_item_card.dart | 48 +- .../widgets/quick_action_card.dart | 30 +- .../constants/report_constants.dart | 22 +- .../presentation/cubit/report_cubit.dart | 70 +- .../presentation/pages/report_page.dart | 102 +- .../widgets/description_input.dart | 41 +- .../presentation/widgets/device_selector.dart | 16 +- .../presentation/widgets/level_selector.dart | 24 +- .../presentation/widgets/media_uploader.dart | 160 +- .../widgets/report_type_selector.dart | 48 +- .../presentation/pages/alarm_center_page.dart | 102 +- .../widgets/alarm_count_card.dart | 18 +- .../presentation/widgets/alarm_item_card.dart | 52 +- .../presentation/widgets/alarm_tab_bar.dart | 30 +- .../presentation/pages/workorder_page.dart | 57 +- .../widgets/workorder_count_card.dart | 24 +- .../widgets/workorder_item_card.dart | 135 +- .../widgets/workorder_tabbar.dart | 35 +- lib/main.dart | 10 + 69 files changed, 5018 insertions(+), 1532 deletions(-) create mode 100644 lib/core/managers/drone_task_state_manager.dart diff --git a/assets/languages/en-US.json b/assets/languages/en-US.json index 9b8c3fa8..c8a3c772 100644 --- a/assets/languages/en-US.json +++ b/assets/languages/en-US.json @@ -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", diff --git a/assets/languages/zh-CN.json b/assets/languages/zh-CN.json index a4abdc15..25411258 100644 --- a/assets/languages/zh-CN.json +++ b/assets/languages/zh-CN.json @@ -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": "产品亮点", diff --git a/lib/core/consts/http_api_consts.dart b/lib/core/consts/http_api_consts.dart index a0491bab..dc69d281 100644 --- a/lib/core/consts/http_api_consts.dart +++ b/lib/core/consts/http_api_consts.dart @@ -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"; } diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index b003e34c..28fa2f41 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -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 init() async { // 🔥 SiteCubit (全局共享,持久化选中场站) sl.registerLazySingleton(() => SiteCubit(sl())); + // 🔥 FloatBarSettingService (全局共享,控制悬浮条显示) + sl.registerLazySingleton( + () => FloatBarSettingService(sl()), + ); + // 🔥 语言管理 Cubit (单例) sl.registerLazySingleton(() => LocaleCubit()); diff --git a/lib/core/managers/drone_task_state_manager.dart b/lib/core/managers/drone_task_state_manager.dart new file mode 100644 index 00000000..321954b5 --- /dev/null +++ b/lib/core/managers/drone_task_state_manager.dart @@ -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? 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 _currentTaskInfo = + ValueNotifier(null); + + /// 监听无人机任务信息变化 + ValueListenable 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(); diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 3ba2dfd1..69824887 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -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().logout(); diff --git a/lib/core/network/mqtt/data/datasources/drone_osd_datasource.dart b/lib/core/network/mqtt/data/datasources/drone_osd_datasource.dart index 65cb96bf..4d86ec65 100644 --- a/lib/core/network/mqtt/data/datasources/drone_osd_datasource.dart +++ b/lib/core/network/mqtt/data/datasources/drone_osd_datasource.dart @@ -16,6 +16,8 @@ abstract class DroneOsdDataSource { }); Future stopListening(); + + void dispose(); } class DroneOsdDataSourceImpl implements DroneOsdDataSource { diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index 7f20dcda..551f515d 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -44,6 +44,52 @@ class TcpClient { int? _lastPort; bool isUserSwitch = false; // 用户是否切换设备 bool _isSwitching = false; + bool _connecting = false; // 🔥 是否正在建立连接(防止 connect() 和 connectBySwitch() 并发竞争) + Completer? _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 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(); + 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 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(); + try { - // 1. 如果已经连接,先断开 - if (_socket != null) { - _logger.logWithLevel('🔄 [TCP] 已有连接,先断开重新连接...', shouldLog: true); - disconnect(); - await Future.delayed(const Duration(milliseconds: 500)); - } - - // 2. 更新状态为连接中 + // 更新状态为连接中 try { GetIt.I().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().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().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 _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'); diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 33e639c0..656a4269 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -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) { diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 5a14f19d..39c3213b 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -39,6 +39,13 @@ class AuthCubit extends Cubit { final ILoggerService _logger = GetIt.I(); StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期 + + // 🔥 登录验证 Completer:用于等待登录阶段的 have_logged_in 推送 + Completer? _loginVerificationCompleter; + + // 🔥 安全模式标志位:保护关键操作阶段不被异地登录踢下线 + bool _isSafeMode = false; + Timer? _safeModeExitTimer; AuthCubit( this.storage, @@ -63,17 +70,14 @@ class AuthCubit extends Cubit { ); 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 { Future 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 { /// 退出登录 (主动或被动) Future logout() async { - //print("退出登录"); + print('>>> [AUTH] 🚨🚨🚨 logout() 被调用!时间: ${DateTime.now()}'); + print('>>> [AUTH] 调用栈:\n${StackTrace.current}'); // 🔥 关键修复:在断开TCP前,先清空所有业务状态,避免重连触发旧数据 _clearAllBusinessState(); @@ -108,6 +149,25 @@ class AuthCubit extends Cubit { 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 { } } + /// 🔥 等待登录验证结果(2.5 秒内看是否收到 have_logged_in) + Future _waitForLoginVerification() async { + _loginVerificationCompleter = Completer(); + + // 等待 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 { ); 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 { _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 close() { _kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听 + _safeModeExitTimer?.cancel(); // 清理倒计时 return super.close(); } diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index 9d12f252..096d644a 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -17,7 +17,6 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { final UserStorage _userStorage; // 🔥 新增字段 final ILoggerService _logger = GetIt.I(); - // 🔥 修改构造函数,注入 UserStorage DeviceHttpDatasourceImpl(this.dio, this._userStorage); @@ -32,22 +31,41 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { Future bindDevice(String deviceId, String deviceAlias) async { final logger = GetIt.I() 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().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().logout(); throw Exception(responseData['msg'] ?? '业务异常'); } @@ -96,10 +116,14 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { Future switchDevice(String platform, String deviceId) async { final logger = GetIt.I() 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 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 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'] ?? '业务异常'); diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index 6814017c..f5f0db1f 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -281,35 +281,14 @@ class DevicesCubit extends Cubit { }, // 成功处理:改为 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)); }, diff --git a/lib/features/home/presentation/pages/route_plan_page.dart b/lib/features/home/presentation/pages/route_plan_page.dart index 377ac068..e2c3f96b 100644 --- a/lib/features/home/presentation/pages/route_plan_page.dart +++ b/lib/features/home/presentation/pages/route_plan_page.dart @@ -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 createState() => _RoutePlanPageState(); +} + +class _RoutePlanPageState extends State { + @override + void initState() { + super.initState(); + // 🔥 进入安全模式:防止异地登录推送导致路径规划中断 + GetIt.I().enterSafeMode(); + } + + @override + void dispose() { + // 🔥 离开安全模式:启动 90 秒倒计时 + GetIt.I().exitSafeMode(); + super.dispose(); + } + @override Widget build(BuildContext context) { try { diff --git a/lib/features/machine_details/presentation/pages/machine_details_page.dart b/lib/features/machine_details/presentation/pages/machine_details_page.dart index a05a1e15..f98a5b9a 100644 --- a/lib/features/machine_details/presentation/pages/machine_details_page.dart +++ b/lib/features/machine_details/presentation/pages/machine_details_page.dart @@ -619,7 +619,7 @@ class _MachineDetailsPageState extends State { 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 { ); } - print("调用updateDeviceName,deviceId: $_deviceId, newDeviceName: $newDeviceName"); + //print("调用updateDeviceName,deviceId: $_deviceId, newDeviceName: $newDeviceName"); // 调用Cubit修改方法 await context.read().updateDeviceName(_deviceId, newDeviceName); diff --git a/lib/features/main_container/presentation/main_wrapper.dart b/lib/features/main_container/presentation/main_wrapper.dart index 9ea3572e..3e229759 100644 --- a/lib/features/main_container/presentation/main_wrapper.dart +++ b/lib/features/main_container/presentation/main_wrapper.dart @@ -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 { @override void initState() { super.initState(); + + // 🔥 确保 FloatBarSettingService 已初始化 + GetIt.I(); + WidgetsBinding.instance.addPostFrameCallback((_) { _updateCirclePosition(); }); @@ -38,7 +46,7 @@ class _MainWrapperState extends State { 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 { // 圆形应该在每个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 { switch (tab.id) { case 'home_v2': return BlocProvider( - create: (context) => sl(), + create: (_) => sl(), child: const HomeV2Page(), ); case 'home': @@ -120,7 +128,7 @@ class _MainWrapperState extends State { return const AiPage(); case 'warning': return BlocProvider( - create: (context) => sl(), + create: (_) => sl(), child: const AlarmCenterPage(), ); case 'workorder': @@ -141,18 +149,16 @@ class _MainWrapperState extends State { return BlocBuilder( 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 { 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( + 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 { 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 { 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, ), ), ], diff --git a/lib/features/main_container/presentation/pages/custom_main_container.dart b/lib/features/main_container/presentation/pages/custom_main_container.dart index 86c7a5a0..c1d15e45 100644 --- a/lib/features/main_container/presentation/pages/custom_main_container.dart +++ b/lib/features/main_container/presentation/pages/custom_main_container.dart @@ -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 with AutomaticKeepAliveClientMixin { @override - bool get wantKeepAlive => true; // 保持页面状态 + bool get wantKeepAlive => true; @override Widget build(BuildContext context) { @@ -33,6 +36,9 @@ class _CustomMainContainerState extends State debugPrint('🔍 [CustomMainContainer] build 被调用'); + // 🔥 确保 FloatBarSettingService 已初始化(懒加载单例第一次访问时创建) + GetIt.I(); + return BlocBuilder( builder: (context, state) { if (state is! TabConfigLoaded) { @@ -75,7 +81,7 @@ class _CustomMainContainerState extends State 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 ), ), ), + // 悬浮条 - 底部,位于导航栏上方 + ValueListenableBuilder( + valueListenable: FloatBarSettingService.settingNotifier, + builder: (context, isEnabled, child) { + return Visibility( + visible: isEnabled, + child: const Align( + alignment: Alignment.bottomCenter, + child: FloatBarWidget(), + ), + ); + }, + ), ], ), bottomNavigationBar: Column( diff --git a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart index 2000681c..f236e2cb 100644 --- a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart +++ b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart @@ -22,14 +22,8 @@ class _CustomBottomNavBarState extends State duration: const Duration(milliseconds: 300), vsync: this, ); - _animation = Tween( - begin: 0.0, - end: 0.0, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - ), + _animation = Tween(begin: 0.0, end: 0.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), ); } @@ -80,15 +74,16 @@ class _CustomBottomNavBarState extends State final beginOffset = _previousIndex * tabWidth + tabWidth / 2; final endOffset = selectedIndex * tabWidth + tabWidth / 2; - _animation = Tween( - begin: beginOffset - circleDiameter / 2, - end: endOffset - circleDiameter / 2, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutCubic, - ), - ); + _animation = + Tween( + 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 icon: _getIconData(tab.icon), label: tab.name, isSelected: isSelected, - onTap: () => context.read().selectTab(index), + onTap: () => + context.read().selectTab(index), ), ); }), diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart index 0f6d433e..b72a78df 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -419,7 +419,17 @@ class RemoteControlCubit extends Cubit { } // 情况 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 { 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] ========== 目标设备设置完成 =========='); } diff --git a/lib/features/remote_control/presentation/pages/remote_control_page.dart b/lib/features/remote_control/presentation/pages/remote_control_page.dart index 4231890b..871a9f75 100644 --- a/lib/features/remote_control/presentation/pages/remote_control_page.dart +++ b/lib/features/remote_control/presentation/pages/remote_control_page.dart @@ -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 { @override void initState() { super.initState(); + // 🔥 进入安全模式:防止异地登录推送导致控制中断 + GetIt.I().enterSafeMode(); + // 1. 锁定横屏 SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, @@ -121,6 +126,9 @@ class _RemoteControlPageState extends State { @override void dispose() { + // 🔥 离开安全模式:启动 90 秒倒计时 + GetIt.I().exitSafeMode(); + // 释放远程控制权限 _cubit.releasePermission("app"); //print("远程控制要推出啦"); @@ -566,4 +574,5 @@ class _RemoteControlPageState extends State { ), ); } + } diff --git a/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart b/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart index 7bc20edb..e74db63f 100644 --- a/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart +++ b/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart @@ -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(); } diff --git a/lib/features/v2/device_list/data/models/robot_data_model.dart b/lib/features/v2/device_list/data/models/robot_data_model.dart index 574ea938..ae8b5bd7 100644 --- a/lib/features/v2/device_list/data/models/robot_data_model.dart +++ b/lib/features/v2/device_list/data/models/robot_data_model.dart @@ -20,14 +20,16 @@ class RobotDataModel { /// 从 JSON 创建数据模型 factory RobotDataModel.fromJson(Map 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; + } } diff --git a/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart b/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart index b562ce7c..a604f0d0 100644 --- a/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart +++ b/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart @@ -49,7 +49,7 @@ class DeviceStatusBloc extends Bloc { try { final response = await getDeviceStatusDataUseCase.execute( siteId: currentState.siteId, - typeFilter: currentState.selectedType == '全部' + typeFilter: currentState.selectedType == 'all' ? null : currentState.selectedType, ); diff --git a/lib/features/v2/device_list/presentation/bloc/device_status_state.dart b/lib/features/v2/device_list/presentation/bloc/device_status_state.dart index a247ee8a..c3093619 100644 --- a/lib/features/v2/device_list/presentation/bloc/device_status_state.dart +++ b/lib/features/v2/device_list/presentation/bloc/device_status_state.dart @@ -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, }); diff --git a/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart b/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart index ad376db3..d5db026b 100644 --- a/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart +++ b/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart @@ -34,10 +34,12 @@ class DroneStationBloc extends Bloc { 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 { 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 { 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 { ); 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 { ); 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, + ), + ), ); } } diff --git a/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart b/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart index c9f91841..b102675e 100644 --- a/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart +++ b/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart @@ -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 get settingNotifier => _settingNotifier; - /// 获取当前是否启用(直接从静态变量读取) static bool get isEnabled => _isEnabled; /// 设置是否启用 static Future 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 toggle() async { await setEnabled(!_isEnabled); } diff --git a/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart b/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart index ddb1e579..fe30793f 100644 --- a/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart +++ b/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart @@ -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; } -} \ No newline at end of file +} diff --git a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart index 110fee43..699d8dff 100644 --- a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart +++ b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart @@ -1,318 +1,1919 @@ +import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc; import '../bloc/float_bar_bloc.dart'; import '../model/robot_status_model.dart'; import '../service/robot_status_service.dart'; import '../cubit/float_bar_setting_cubit.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 '../../../domain/entities/uav_video_stream_entity.dart'; +import '../../../../../../core/di/injection.dart'; +import '../../../../../../core/managers/drone_task_state_manager.dart'; +import '../../../../../../core/network/mqtt/domain/entities/drone_osd_entity.dart'; +import '../../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.dart'; -/// 悬浮条UI组件 -/// 内部独立管理Bloc,不依赖外部注入 +/// 可拖动的悬浮按钮组件 +/// 点击弹出底部弹窗显示设备状态详情 class FloatBarWidget extends StatefulWidget { - const FloatBarWidget({super.key}); + /// 全局 key,用于外部访问组件状态 + final GlobalKey? globalKey; + + const FloatBarWidget({super.key, this.globalKey}); @override - State createState() => _FloatBarWidgetState(); + FloatBarWidgetState createState() => FloatBarWidgetState(); } -class _FloatBarWidgetState extends State { +class FloatBarWidgetState extends State { late final FloatBarBloc _bloc; + /// 拖动相关状态 + Offset _position = const Offset(20, 300); + Offset _initialPosition = const Offset(20, 300); + Offset _offset = Offset.zero; + bool _isDragging = false; + double _screenWidth = 0; + double _screenHeight = 0; + double _buttonSize = 56; + + /// 无人机相关状态 + DroneTaskInfo? _droneTaskInfo; + DroneStationBloc? _droneStationBloc; + DroneOsdDataSource? _droneOsdDataSource; + StreamSubscription? _osdSubscription; + Timer? _simulationTimer; + List _trajectoryPoints = []; + LatLng? _currentPosition; + double? _currentHeading; + UavVideoStreamEntity? _videoStream; + CameraInfo? _currentCamera; + bool _isVideoLoading = false; + String? _videoError; + + // 火山引擎 RTC + volc.RTCEngine? _rtcEngine; + volc.RTCRoom? _rtcRoom; + volc.RTCViewContext? _remoteRenderContext; + String? _remoteUserId; + final volc.IRTCEngineEventHandler _engineEventHandler = + volc.IRTCEngineEventHandler(); + final volc.IRTCRoomEventHandler _roomEventHandler = + volc.IRTCRoomEventHandler(); + + MapController? _mapController; + bool _isUserDragging = false; + @override void initState() { super.initState(); - debugPrint('🔥 [FloatBarWidget] initState - 开始创建 Bloc'); - // 内部创建Bloc并启动服务(不需要FloatBarManager,因为我们使用Stack方式) _bloc = FloatBarBloc(RobotStatusService()); _bloc.startService(); - debugPrint('✅ [FloatBarWidget] Bloc 已创建并启动服务'); + + // 监听全局无人机任务状态变化 + // 支持从机场页面点击无人机卡片时实时同步 + droneTaskStateManager.currentTaskInfo.addListener(_onTaskInfoChanged); } @override void dispose() { - debugPrint('🔥 [FloatBarWidget] dispose - 关闭 Bloc'); _bloc.close(); + _droneStationBloc?.close(); + _osdSubscription?.cancel(); + _droneOsdDataSource?.dispose(); + _mapController?.dispose(); + _destroyRtcEngine(); + droneTaskStateManager.currentTaskInfo.removeListener(_onTaskInfoChanged); super.dispose(); } - @override - Widget build(BuildContext context) { - debugPrint('🔥 [FloatBarWidget] build - 渲染悬浮条'); - return BlocProvider.value(value: _bloc, child: const _FloatBarContent()); + /// 监听全局无人机任务状态变化 + void _onTaskInfoChanged() { + final newInfo = droneTaskStateManager.currentInfo; + if (newInfo != null && + (newInfo.droneSn != _droneTaskInfo?.droneSn || + newInfo.gatewaySn != _droneTaskInfo?.gatewaySn)) { + debugPrint('🛸 [FloatBarWidget] 检测到无人机任务信息变化,重新初始化'); + setDroneTaskInfo(newInfo); + + // 自动打开底部弹窗,让用户看到最新的无人机视频和轨迹 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _showBottomSheet(); + } + }); + } } -} -/// 悬浮条内容组件 -class _FloatBarContent extends StatelessWidget { - const _FloatBarContent(); + /// 设置当前无人机任务信息 + void setDroneTaskInfo(DroneTaskInfo info) { + // 先清理旧的资源 + _cleanupDroneComponents(); - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final isExpanded = state is FloatBarExpandedState; - final status = state is FloatBarCollapsedState - ? state.status - : (state as FloatBarExpandedState).status; + setState(() { + _droneTaskInfo = info; + _trajectoryPoints = []; + _currentPosition = null; + _currentHeading = null; + _videoStream = null; + _videoError = null; + _isVideoLoading = false; + }); + _initDroneComponents(); + } - debugPrint( - '🔥 [FloatBarContent] build - isExpanded: $isExpanded, status: ${status.taskName}', - ); + /// 清除无人机任务信息 + void clearDroneTaskInfo() { + _cleanupDroneComponents(); + setState(() { + _droneTaskInfo = null; + _trajectoryPoints = []; + _currentPosition = null; + _currentHeading = null; + _videoStream = null; + _videoError = null; + _isVideoLoading = false; + }); + } - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), // 底部留出 Tab 栏空间 - child: GestureDetector( - onTap: () { - debugPrint('🔥 [FloatBarContent] 点击悬浮条'); - context.read().add(ToggleExpandEvent()); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: isExpanded ? 180 : 56, - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xFFE8F5E9), // 浅绿色 - Color(0xFFFFFFFF), // 白色 - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.green.withOpacity(0.2), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 12, - offset: const Offset(0, 4), - spreadRadius: 2, - ), - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 1, - ), - BoxShadow( - color: Colors.white.withOpacity(0.6), - blurRadius: 8, - offset: const Offset(0, -2), - spreadRadius: -2, - ), - ], - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: isExpanded - ? _buildExpandedContent(status) - : _buildCollapsedContent(status), - ), - ), - ), - ); + /// 清理无人机相关资源 + void _cleanupDroneComponents() { + _simulationTimer?.cancel(); + _simulationTimer = null; + _osdSubscription?.cancel(); + _osdSubscription = null; + _droneOsdDataSource?.dispose(); + _droneOsdDataSource = null; + _droneStationBloc?.close(); + _droneStationBloc = null; + _mapController?.dispose(); + _mapController = null; + _destroyRtcEngine(); + } + + /// 初始化无人机相关组件 + void _initDroneComponents() { + if (_droneTaskInfo == null) return; + + debugPrint('🛸 [FloatBarWidget] 初始化无人机组件'); + debugPrint(' droneSn: ${_droneTaskInfo!.droneSn}'); + debugPrint(' gatewaySn: ${_droneTaskInfo!.gatewaySn}'); + debugPrint(' isOnline: ${_droneTaskInfo!.isOnline}'); + + // 初始化 DroneStationBloc + _droneStationBloc = sl(); + + // 监听状态变化 + _listenDroneStationBloc(); + + // 初始化地图控制器 + _mapController = MapController(); + + // 立即设置初始位置,确保飞机能立即显示 + setState(() { + _currentPosition = const LatLng(39.9042, 116.4074); + _currentHeading = 90; + _trajectoryPoints = [const LatLng(39.9042, 116.4074)]; + }); + + // 启动模拟轨迹数据(用于测试) + _startSimulationTrajectory(); + + // 只有在线时才启动 OSD 监听和视频流加载 + if (_droneTaskInfo!.isOnline) { + // 初始化 OSD 数据源 + _droneOsdDataSource = sl(); + _startOsdListening(); + + // 加载视频流 + _loadVideoStream(); + } else { + debugPrint('⚠️ [FloatBarWidget] 无人机离线,不启动 OSD 监听和视频流'); + setState(() { + _videoError = '无人机离线,无视频信号'; + }); + } + } + + /// 开始监听 OSD 数据 + void _startOsdListening() async { + if (_droneOsdDataSource == null || _droneTaskInfo == null) return; + + debugPrint('🛸 [FloatBarWidget] 开始监听 OSD 数据'); + + await _droneOsdDataSource!.startListening( + deviceSn: _droneTaskInfo!.droneSn, + gatewaySn: _droneTaskInfo!.gatewaySn, + ); + + _osdSubscription = _droneOsdDataSource!.droneOsdStream.listen( + (osd) { + if (!mounted) return; + _handleOsdUpdate(osd); + }, + onError: (error) { + debugPrint('❌ [FloatBarWidget] OSD 监听错误: $error'); }, ); } - /// 折叠态内容 - Widget _buildCollapsedContent(RobotStatusModel status) { - return Row( - children: [ - // 状态指示灯 - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: _parseColor(status.statusColor), - borderRadius: BorderRadius.circular(5), - ), - ), - const SizedBox(width: 12), - // 任务名称 - Expanded( - child: Text( - status.taskName, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - // 电量 - Row( - children: [ - Icon( - status.battery > 20 ? Icons.battery_full : Icons.battery_alert, - size: 18, - color: status.battery > 20 ? Colors.grey : Colors.red, - ), - const SizedBox(width: 4), - Text( - '${status.battery}%', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: status.battery > 20 ? Colors.black87 : Colors.red, - ), - ), - ], - ), - const SizedBox(width: 8), - // 展开箭头 - const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey), - const SizedBox(width: 8), - // 关闭按钮 - GestureDetector( - onTap: () { - debugPrint('🔥 [FloatBarContent] 点击关闭按钮'); - // 🔥 使用静态方法关闭悬浮条 - FloatBarSettingService.setEnabled(false); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.close, size: 16, color: Colors.grey), - ), - ), - ], + /// 处理 OSD 数据更新 + void _handleOsdUpdate(DroneOsdEntity osd) { + final rawData = osd.rawData; + + // 获取经纬度 + double? latitude = (rawData['latitude'] as num?)?.toDouble(); + double? longitude = (rawData['longitude'] as num?)?.toDouble(); + double? heading = (rawData['heading'] as num?)?.toDouble(); + + if (latitude != null && + longitude != null && + latitude != 0 && + longitude != 0) { + setState(() { + _currentPosition = LatLng(latitude, longitude); + _currentHeading = heading; + + // 添加到轨迹点 + if (_currentPosition != null) { + _trajectoryPoints.add(_currentPosition!); + // 限制轨迹点数量,避免内存问题 + if (_trajectoryPoints.length > 500) { + _trajectoryPoints.removeAt(0); + } + } + }); + + // 自动移动地图中心 + if (_mapController != null && _currentPosition != null) { + _mapController?.move(_currentPosition!, 17); + } + } + } + + /// 模拟轨迹数据(用于测试) + /// 简化为三个固定点,飞机在这三个点之间缓慢移动 + void _startSimulationTrajectory() { + debugPrint('🎯 [FloatBarWidget] 启动模拟轨迹数据(三点模式)'); + + // 如果已经有定时器在运行,先取消 + if (_simulationTimer != null && _simulationTimer!.isActive) { + _simulationTimer!.cancel(); + } + + // 三个点间距拉大,形成明显的三角形路径 + final waypoints = [ + const LatLng(39.9042, 116.4074), // 起点 + const LatLng(39.9060, 116.4110), // 东北方向 + const LatLng(39.9020, 116.4120), // 东南方向 + ]; + + int currentWaypoint = 0; + int nextWaypoint = 1; + double progress = 0; + const double speed = 0.02; // 每步移动2% + + // 初始化位置 + setState(() { + _currentPosition = waypoints[0]; + _currentHeading = 90; + _trajectoryPoints = [waypoints[0]]; + }); + + // 使用定时器模拟缓慢移动(500ms间隔,肉眼可见移动) + _simulationTimer = Timer.periodic(const Duration(milliseconds: 500), ( + timer, + ) { + if (!mounted) { + timer.cancel(); + return; + } + + // 首次触发时定位地图到起始区域 + if (progress == speed && _mapController != null) { + try { + _mapController!.move(waypoints[0], 16); + } catch (_) {} + } + + // 计算当前点到下一个点的移动 + final start = waypoints[currentWaypoint]; + final end = waypoints[nextWaypoint]; + + // 线性插值 + progress += speed; + if (progress >= 1.0) { + progress = 0; + currentWaypoint = nextWaypoint; + nextWaypoint = (currentWaypoint + 1) % waypoints.length; + } + + // 计算当前位置 + final lat = start.latitude + (end.latitude - start.latitude) * progress; + final lng = + start.longitude + (end.longitude - start.longitude) * progress; + + // 计算航向角 + final dx = end.longitude - start.longitude; + final dy = end.latitude - start.latitude; + final heading = atan2(dy, dx) * 180 / pi; + + setState(() { + _currentPosition = LatLng(lat, lng); + _currentHeading = heading; + + // 添加轨迹点 + if (_currentPosition != null) { + _trajectoryPoints.add(_currentPosition!); + if (_trajectoryPoints.length > 500) { + _trajectoryPoints.removeAt(0); + } + } + }); + + // 移动地图中心到无人机当前位置 + if (_currentPosition != null) { + _mapController?.move(_currentPosition!, 16); + } + + debugPrint( + '📍 模拟位置: lat=${lat.toStringAsFixed(6)}, lng=${lng.toStringAsFixed(6)}, heading=${heading.toStringAsFixed(1)}, progress=${progress.toStringAsFixed(2)}', + ); + }); + + debugPrint(' 模拟定时器已启动,三点循环模式(500ms间隔)'); + } + + /// 加载视频流 + void _loadVideoStream() { + if (_droneTaskInfo == null) return; + if (_droneTaskInfo!.cameraList == null || + _droneTaskInfo!.cameraList!.isEmpty) { + debugPrint('⚠️ [FloatBarWidget] 没有可用的摄像头'); + setState(() { + _videoError = '没有可用的摄像头'; + }); + return; + } + + setState(() { + _isVideoLoading = true; + _videoError = null; + _currentCamera = _droneTaskInfo!.cameraList!.first; + }); + + debugPrint('📺 [FloatBarWidget] 加载视频流'); + debugPrint(' droneSn: ${_droneTaskInfo!.droneSn}'); + debugPrint(' cameraIndex: ${_currentCamera?.cameraIndex}'); + + // 加载无人机视频流 + _droneStationBloc?.add( + UavVideoStreamLoad( + sn: _droneTaskInfo!.droneSn, + cameraIndex: _currentCamera?.cameraIndex ?? '0', + lensType: UavLensType.wide, + qualityType: VideoQualityType.adaptive, + videoExpire: 720000000, + ), ); } - /// 展开态内容 - Widget _buildExpandedContent(RobotStatusModel status) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 顶部:状态+任务名+折叠按钮 - Row( + /// 监听 DroneStationBloc 状态变化 + void _listenDroneStationBloc() { + if (_droneStationBloc == null) return; + + _droneStationBloc!.stream.listen((state) { + if (!mounted) return; + + if (state is UavVideoStreamLoading) { + setState(() { + _isVideoLoading = true; + _videoError = null; + }); + } else if (state is UavVideoStreamLoaded) { + setState(() { + _isVideoLoading = false; + _videoStream = state.videoStream; + _videoError = null; + }); + debugPrint('✅ [FloatBarWidget] 视频流加载成功'); + _initRtcEngine(state.videoStream); + } else if (state is UavVideoStreamError) { + setState(() { + _isVideoLoading = false; + _videoError = state.message; + }); + debugPrint('❌ [FloatBarWidget] 视频流加载失败: ${state.message}'); + } + }); + } + + @override + Widget build(BuildContext context) { + _screenWidth = MediaQuery.of(context).size.width; + _screenHeight = MediaQuery.of(context).size.height; + + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: _bloc), + if (_droneStationBloc != null) + BlocProvider.value(value: _droneStationBloc!), + ], + child: _buildDraggableButton(), + ); + } + + /// 构建可拖动的悬浮按钮 + Widget _buildDraggableButton() { + return Positioned( + left: _position.dx, + top: _position.dy, + child: GestureDetector( + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onTap: _isDragging ? null : _showBottomSheet, + child: _buildButtonContent(), + ), + ); + } + + /// 处理拖动开始 + void _onPanStart(DragStartDetails details) { + _isDragging = true; + _initialPosition = _position; + _offset = details.globalPosition - Offset(_position.dx, _position.dy); + } + + /// 处理拖动更新 + void _onPanUpdate(DragUpdateDetails details) { + setState(() { + double newX = details.globalPosition.dx - _offset.dx; + double newY = details.globalPosition.dy - _offset.dy; + + /// 限制在屏幕范围内 + newX = newX.clamp(0.0, _screenWidth - _buttonSize); + newY = newY.clamp(0.0, _screenHeight - _buttonSize - 100); + + _position = Offset(newX, newY); + }); + } + + /// 处理拖动结束 + void _onPanEnd(DragEndDetails details) { + _isDragging = false; + + /// 吸附到最近的边缘 + setState(() { + if (_position.dx < _screenWidth / 2) { + _position = Offset(16, _position.dy); + } else { + _position = Offset(_screenWidth - _buttonSize - 16, _position.dy); + } + }); + } + + /// 显示底部弹窗 + void _showBottomSheet() { + final status = _bloc.state is FloatBarCollapsedState + ? (_bloc.state as FloatBarCollapsedState).status + : (_bloc.state as FloatBarExpandedState).status; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.85, + ), + builder: (ctx) => _buildBottomSheetContent(status), + ); + } + + /// 构建底部弹窗内容 + Widget _buildBottomSheetContent(RobotStatusModel status) { + return DefaultTabController( + length: 3, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( children: [ + // 拖动条 Container( - width: 10, - height: 10, + margin: const EdgeInsets.only(top: 12), + width: 40, + height: 4, decoration: BoxDecoration( - color: _parseColor(status.statusColor), - borderRadius: BorderRadius.circular(5), + color: Colors.grey.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), ), ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: _parseColor(status.statusColor).withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - status.statusText, - style: TextStyle( - fontSize: 12, - color: _parseColor(status.statusColor), - fontWeight: FontWeight.w500, - ), + // 顶部:状态+任务名+关闭按钮 + Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: _parseColor(status.statusColor), + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: _parseColor(status.statusColor).withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + status.statusText, + style: TextStyle( + fontSize: 13, + color: _parseColor(status.statusColor), + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + status.taskName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.close, + size: 20, + color: Colors.grey, + ), + ), + ), + ], ), ), - const SizedBox(width: 12), + // 标签栏 + const TabBar( + tabs: [ + Tab(text: '无人机'), + Tab(text: '机器人'), + Tab(text: '其他'), + ], + labelColor: Color(0xFF165DFF), + unselectedLabelColor: Color(0xFF86909C), + labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + unselectedLabelStyle: TextStyle(fontSize: 14), + indicatorColor: Color(0xFF165DFF), + indicatorWeight: 3, + padding: EdgeInsets.symmetric(horizontal: 20), + ), + // 内容区域 Expanded( - child: Text( - status.taskName, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, + child: TabBarView( + children: [ + _buildDroneTab(status), + _buildRobotTab(status), + _buildOtherTab(status), + ], ), ), - const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey), - const SizedBox(width: 8), - // 关闭按钮 - GestureDetector( - onTap: () { - debugPrint('🔥 [FloatBarContent] 点击关闭按钮(展开态)'); - // 🔥 使用静态方法关闭悬浮条 - FloatBarSettingService.setEnabled(false); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.close, size: 16, color: Colors.grey), + SizedBox(height: MediaQuery.of(context).padding.bottom), + ], + ), + ), + ); + } + + /// 无人机标签页 + Widget _buildDroneTab(RobotStatusModel status) { + // 如果没有无人机任务信息,显示提示 + if (_droneTaskInfo == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.flight_takeoff, + size: 64, + color: Colors.grey.withOpacity(0.5), + ), + const SizedBox(height: 16), + Text( + '暂无无人机任务', + style: TextStyle( + fontSize: 16, + color: Colors.grey.withOpacity(0.7), + ), + ), + const SizedBox(height: 8), + Text( + '创建无人机任务后将显示视频和轨迹', + style: TextStyle( + fontSize: 13, + color: Colors.grey.withOpacity(0.5), ), ), ], ), + ); + } + + return ListView( + padding: const EdgeInsets.all(20), + children: [ + /// 机场和无人机名称 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.location_on, + size: 16, + color: Color(0xFF165DFF), + ), + const SizedBox(width: 8), + Text( + _droneTaskInfo!.stationName.isNotEmpty + ? _droneTaskInfo!.stationName + : '未知机场', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.flight, size: 16, color: Color(0xFF00B42A)), + const SizedBox(width: 8), + Text( + _droneTaskInfo!.droneName.isNotEmpty + ? _droneTaskInfo!.droneName + : '未知无人机', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: _droneTaskInfo!.isOnline + ? const Color(0xFF00B42A).withOpacity(0.1) + : const Color(0xFFF53F3F).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + _droneTaskInfo!.isOnline ? '在线' : '离线', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _droneTaskInfo!.isOnline + ? const Color(0xFF00B42A) + : const Color(0xFFF53F3F), + ), + ), + ), + ], + ), + ], + ), + ), const SizedBox(height: 16), - // 中间:详细信息网格 + + /// 视频播放器 + _buildVideoPlayer(), + const SizedBox(height: 16), + + /// 地图轨迹卡片 + _buildMapTrajectory(), + const SizedBox(height: 16), + + /// 电量+信号+速度 Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildInfoItem( status.battery > 20 ? Icons.battery_full : Icons.battery_alert, '电量', '${status.battery}%', - status.battery > 20 ? Colors.black87 : Colors.red, + status.battery > 20 ? const Color(0xFF165DFF) : Colors.red, + ), + Container( + width: 1, + height: 50, + color: Colors.grey.withOpacity(0.2), ), _buildInfoItem( Icons.signal_cellular_alt, '信号', '${status.signal}%', - Colors.black87, + const Color(0xFF165DFF), + ), + Container( + width: 1, + height: 50, + color: Colors.grey.withOpacity(0.2), ), _buildInfoItem( Icons.speed, '速度', '${status.speed.toStringAsFixed(1)}m/s', - Colors.black87, - ), - _buildInfoItem( - Icons.thermostat, - '温度', - '${status.temperature}°C', - Colors.black87, + const Color(0xFF165DFF), ), ], ), - const SizedBox(height: 12), - // 底部:位置+时间 - Row( - children: [ - const Icon(Icons.location_on, size: 14, color: Colors.grey), - const SizedBox(width: 4), - Expanded( - child: Text( - status.location, - style: const TextStyle(fontSize: 12, color: Colors.grey), - overflow: TextOverflow.ellipsis, + const SizedBox(height: 20), + + /// 位置信息 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.location_on, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + Expanded( + child: Text( + status.location, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), ), - ), - const SizedBox(width: 12), - const Icon(Icons.timer, size: 14, color: Colors.grey), - const SizedBox(width: 4), - Text( - status.runTime, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - ], + ], + ), + ), + const SizedBox(height: 20), + + /// 飞行高度 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.height, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '飞行高度', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + const Text( + '120m', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF165DFF), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 飞行模式 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.flight, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '飞行模式', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: const Text( + '自主飞行', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.green, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 运行时间 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.timer, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '运行时间', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + Text( + status.runTime, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF4E5969), + ), + ), + ], + ), ), ], ); } + /// 构建视频播放器组件 + Widget _buildVideoPlayer() { + return Container( + height: 220, + decoration: BoxDecoration( + color: const Color(0xFF1A1A1A), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + children: [ + // 视频区域 + if (_isVideoLoading) + const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + SizedBox(height: 12), + Text( + '正在加载视频流...', + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ], + ), + ) + else if (_videoError != null) + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.videocam_off, + color: Colors.white54, + size: 48, + ), + const SizedBox(height: 12), + Text( + _videoError!, + style: const TextStyle( + fontSize: 14, + color: Colors.white54, + ), + ), + ], + ), + ) + else if (_videoStream != null) + _buildVolcVideoView() + else if (_droneTaskInfo != null && !_droneTaskInfo!.isOnline) + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.flight_land, + color: Colors.white.withOpacity(0.7), + size: 48, + ), + const SizedBox(height: 12), + Text( + '无人机离线', + style: TextStyle( + fontSize: 16, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + '无视频', + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(0.6), + ), + ), + ], + ), + ) + else + const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.flight, color: Colors.white54, size: 64), + SizedBox(height: 12), + Text( + '等待视频流...', + style: TextStyle(fontSize: 14, color: Colors.white54), + ), + ], + ), + ), + + // 标签 + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _droneTaskInfo != null && !_droneTaskInfo!.isOnline + ? Colors.grey + : const Color(0xFF165DFF), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _droneTaskInfo != null && !_droneTaskInfo!.isOnline + ? Icons.videocam_off + : Icons.live_tv, + size: 12, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + _droneTaskInfo != null && !_droneTaskInfo!.isOnline + ? '离线' + : '实时视频', + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + void _initVolcEventHandlers() { + _engineEventHandler.onWarning = (volc.WarningCode code) { + debugPrint('⚠️ [FloatBarWidget] Volc Warning: $code'); + }; + + _engineEventHandler.onError = (volc.ErrorCode code) { + debugPrint('❌ [FloatBarWidget] Volc Error: $code'); + if (mounted) { + setState(() { + _videoError = '视频错误: $code'; + _isVideoLoading = false; + }); + } + }; + + _engineEventHandler.onFirstRemoteVideoFrameDecoded = + ( + String streamId, + volc.StreamInfo streamInfo, + volc.VideoFrameInfo frameInfo, + ) { + debugPrint('✅✅✅ [FloatBarWidget] Volc 第一帧视频解码完成!'); + debugPrint(' streamId: $streamId, userId: ${streamInfo.userId}'); + + if (streamInfo.userId.isNotEmpty && mounted) { + setState(() { + _remoteUserId = streamInfo.userId; + _remoteRenderContext = volc.RTCViewContext.remoteContext( + roomId: _videoStream?.roomId ?? '', + userId: streamInfo.userId, + streamId: streamId, + ); + _isVideoLoading = false; + }); + } + }; + + _roomEventHandler.onUserPublishStreamVideo = + (String userId, volc.StreamInfo streamInfo, bool isPublish) { + debugPrint('📹 [FloatBarWidget] Volc 远端用户 $userId 视频流状态: $isPublish'); + if (isPublish && mounted && _remoteUserId == null) { + debugPrint('⏳ [FloatBarWidget] 检测到 Volc 视频流推送,等待第一帧解码...'); + } + }; + + _roomEventHandler.onUserLeave = (String userId, int reason) { + debugPrint('👋 [FloatBarWidget] Volc 用户离开: $userId'); + if (userId == _remoteUserId && mounted) { + setState(() { + _remoteRenderContext = null; + _remoteUserId = null; + }); + } + }; + } + + Future _initRtcEngine(UavVideoStreamEntity stream) async { + final appId = stream.appId; + final roomId = stream.roomId; + final token = stream.token; + final userId = stream.userId.isNotEmpty + ? stream.userId + : 'user_${DateTime.now().millisecondsSinceEpoch}'; + + debugPrint('=== [FloatBarWidget] VolcEngine 参数 ==='); + debugPrint('AppId: "$appId"'); + debugPrint('RoomId: "$roomId"'); + debugPrint('UserId: "$userId"'); + + if (appId.isEmpty || roomId.isEmpty || token.isEmpty) { + setState(() { + _videoError = 'VolcEngine 参数缺失'; + _isVideoLoading = false; + }); + return; + } + + _initVolcEventHandlers(); + + try { + debugPrint('🚀 [FloatBarWidget] 创建 VolcEngine 引擎...'); + + _rtcEngine = await volc.RTCEngine.createRTCEngine( + volc.RTCVideoContext(appId: appId, eventHandler: _engineEventHandler), + ); + + if (_rtcEngine == null) { + setState(() { + _videoError = 'VolcEngine 引擎创建失败'; + _isVideoLoading = false; + }); + return; + } + debugPrint('✅ [FloatBarWidget] VolcEngine 引擎创建成功'); + + _rtcRoom = await _rtcEngine?.createRTCRoom(roomId); + if (_rtcRoom == null) { + setState(() { + _videoError = 'VolcEngine 房间创建失败'; + _isVideoLoading = false; + }); + return; + } + debugPrint('✅ [FloatBarWidget] VolcEngine 房间创建成功'); + + await _rtcRoom?.setRTCRoomEventHandler(_roomEventHandler); + + debugPrint('🔑 [FloatBarWidget] 加入 VolcEngine 房间...'); + + await _rtcRoom?.joinRoom( + token: token, + userInfo: volc.UserInfo(userId: userId, extraInfo: ''), + userVisibility: true, + roomConfig: volc.RoomConfig( + isPublishAudio: false, + isPublishVideo: false, + isAutoSubscribeAudio: false, + isAutoSubscribeVideo: true, + ), + ); + + debugPrint('✅ [FloatBarWidget] 成功加入 VolcEngine 房间: $roomId'); + debugPrint('⏳ [FloatBarWidget] 等待视频流推送...'); + } catch (e) { + debugPrint('❌ [FloatBarWidget] VolcEngine 初始化失败: $e'); + setState(() { + _videoError = 'VolcEngine 加入失败: $e'; + _isVideoLoading = false; + }); + } + } + + Future _destroyRtcEngine() async { + if (_rtcRoom != null) { + try { + await _rtcRoom?.leaveRoom(); + } catch (_) {} + _rtcRoom = null; + } + if (_rtcEngine != null) { + try { + _rtcEngine?.destroy(); + } catch (_) {} + _rtcEngine = null; + } + _remoteRenderContext = null; + _remoteUserId = null; + debugPrint('✅ [FloatBarWidget] RTC 引擎销毁完成'); + } + + /// 构建火山引擎视频视图 + Widget _buildVolcVideoView() { + if (_remoteRenderContext != null) { + return volc.RTCSurfaceView( + context: _remoteRenderContext!, + renderMode: volc.VideoRenderMode.fit, + ); + } + + if (_videoStream?.roomId != null && _videoStream?.userId != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.live_tv, color: Colors.green, size: 48), + const SizedBox(height: 8), + const Text( + 'RTC 视频流已连接', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + const SizedBox(height: 4), + Text( + 'Room: ${_videoStream?.roomId}', + style: const TextStyle(color: Colors.white54, fontSize: 11), + ), + ], + ), + ); + } + + // 如果有 HTTP 视频流 URL,显示播放器 + if (_videoStream?.url != null && _videoStream!.url!.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.play_circle_outline, + color: Colors.white, + size: 64, + ), + const SizedBox(height: 8), + const Text( + '视频流已就绪', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + const SizedBox(height: 4), + Text( + _videoStream!.url!.length > 30 + ? '${_videoStream!.url!.substring(0, 30)}...' + : _videoStream!.url!, + style: const TextStyle(color: Colors.white54, fontSize: 10), + ), + ], + ), + ); + } + + return const Center( + child: Text('视频流加载中...', style: TextStyle(color: Colors.white54)), + ); + } + + /// 构建地图轨迹组件 + Widget _buildMapTrajectory() { + debugPrint('🗺️ [FloatBarWidget] 构建地图轨迹组件'); + debugPrint(' currentPosition: $_currentPosition'); + debugPrint(' currentHeading: $_currentHeading'); + debugPrint(' trajectoryPoints.length: ${_trajectoryPoints.length}'); + + return Container( + height: 200, + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.withOpacity(0.2)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + children: [ + // 地图 + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: + _currentPosition ?? const LatLng(39.9042, 116.4074), // 默认北京 + initialZoom: 18, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + ), + onMapEvent: (event) { + if (event is MapEventMoveStart) { + setState(() { + _isUserDragging = true; + }); + } else if (event is MapEventMoveEnd) { + setState(() { + _isUserDragging = false; + }); + } + }, + ), + children: [ + // 高德地图瓦片(最底层) + TileLayer( + urlTemplate: + 'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', + subdomains: ['1', '2', '3', '4'], + userAgentPackageName: 'com.example.app', + ), + + // 轨迹线(中间层) + if (_trajectoryPoints.length >= 1) + PolylineLayer( + polylines: [ + Polyline( + points: _trajectoryPoints, + color: const Color(0xFF00B42A).withOpacity(0.9), + strokeWidth: 4, + borderColor: Colors.white, + borderStrokeWidth: 2, + ), + ], + ), + + // 无人机当前位置标记(最上层) + if (_currentPosition != null) + MarkerLayer( + markers: [ + Marker( + key: const ValueKey('drone_current_marker'), + point: _currentPosition!, + width: 60, + height: 60, + child: Transform.rotate( + angle: (_currentHeading ?? 0) * pi / 180, + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(2, 2), + ), + ], + ), + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon( + Icons.flight, + color: Color(0xFF165DFF), + size: 40, + ), + ), + ), + ), + ), + ], + ), + ], + ), + + // 调试信息 + if (_currentPosition == null) + const Positioned( + bottom: 8, + right: 8, + child: Text( + '等待定位...', + style: TextStyle(fontSize: 10, color: Colors.black54), + ), + ), + + // 离线状态提示 + if (_droneTaskInfo != null && !_droneTaskInfo!.isOnline) + Container( + color: Colors.black54, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.flight_land, + color: Colors.white.withOpacity(0.7), + size: 48, + ), + const SizedBox(height: 12), + Text( + '无人机离线', + style: TextStyle( + fontSize: 16, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + '无运动状态', + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(0.6), + ), + ), + ], + ), + ), + ), + + // 标签 + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _droneTaskInfo != null && !_droneTaskInfo!.isOnline + ? Colors.grey + : Colors.green, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + _droneTaskInfo != null && !_droneTaskInfo!.isOnline + ? '离线' + : 'GPS定位', + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ], + ), + ), + ), + + // 轨迹点数 + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${_trajectoryPoints.length} 个轨迹点', + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ), + ), + + // 中心位置按钮 + Positioned( + bottom: 8, + right: 8, + child: GestureDetector( + onTap: () { + if (_currentPosition != null) { + _mapController?.move(_currentPosition!, 17); + } + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.my_location, + size: 20, + color: Color(0xFF165DFF), + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// 机器人标签页 + Widget _buildRobotTab(RobotStatusModel status) { + return ListView( + padding: const EdgeInsets.all(20), + children: [ + /// 电量+信号+速度 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildInfoItem( + status.battery > 20 ? Icons.battery_full : Icons.battery_alert, + '电量', + '${status.battery}%', + status.battery > 20 ? const Color(0xFF165DFF) : Colors.red, + ), + Container( + width: 1, + height: 50, + color: Colors.grey.withOpacity(0.2), + ), + _buildInfoItem( + Icons.signal_cellular_alt, + '信号', + '${status.signal}%', + const Color(0xFF165DFF), + ), + Container( + width: 1, + height: 50, + color: Colors.grey.withOpacity(0.2), + ), + _buildInfoItem( + Icons.speed, + '速度', + '${status.speed.toStringAsFixed(1)}m/s', + const Color(0xFF165DFF), + ), + ], + ), + const SizedBox(height: 20), + + /// 位置信息 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.location_on, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + Expanded( + child: Text( + status.location, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 运行状态 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.play_circle, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '运行状态', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: const Text( + '巡检中', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.blue, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 当前任务 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: const [ + Icon(Icons.task, size: 16, color: Color(0xFF86909C)), + SizedBox(width: 6), + Text( + '当前任务', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + ], + ), + const SizedBox(height: 8), + Text( + status.taskName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 运行时间 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.timer, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '运行时间', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + Text( + status.runTime, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 温度信息 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.thermostat, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '设备温度', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + const Text( + '35°C', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + ], + ); + } + + /// 其他标签页 + Widget _buildOtherTab(RobotStatusModel status) { + return ListView( + padding: const EdgeInsets.all(20), + children: [ + /// 设备状态概览 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: _parseColor(status.statusColor), + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + status.statusText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: _parseColor(status.statusColor), + ), + ), + const SizedBox(height: 2), + const Text( + '设备运行正常', + style: TextStyle(fontSize: 12, color: Color(0xFF86909C)), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 设备信息卡片 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + '设备信息', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 12), + Row( + children: [ + Icon(Icons.device_hub, size: 14, color: Color(0xFF86909C)), + SizedBox(width: 8), + Text( + '设备型号', + style: TextStyle(fontSize: 12, color: Color(0xFF86909C)), + ), + Spacer(), + Text( + 'DR-2024', + style: TextStyle(fontSize: 13, color: Color(0xFF4E5969)), + ), + ], + ), + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.numbers, size: 14, color: Color(0xFF86909C)), + SizedBox(width: 8), + Text( + '序列号', + style: TextStyle(fontSize: 12, color: Color(0xFF86909C)), + ), + Spacer(), + Text( + 'SN-2024-0001', + style: TextStyle(fontSize: 13, color: Color(0xFF4E5969)), + ), + ], + ), + SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Color(0xFF86909C), + ), + SizedBox(width: 8), + Text( + '激活时间', + style: TextStyle(fontSize: 12, color: Color(0xFF86909C)), + ), + Spacer(), + Text( + '2024-01-15', + style: TextStyle(fontSize: 13, color: Color(0xFF4E5969)), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 运行时间 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.timer, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + const Text( + '运行时间', + style: TextStyle(fontSize: 13, color: Color(0xFF86909C)), + ), + const Spacer(), + Text( + status.runTime, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + + /// 位置信息 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.location_on, size: 16, color: Color(0xFF86909C)), + const SizedBox(width: 6), + Expanded( + child: Text( + status.location, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + ), + ], + ), + ), + ], + ); + } + + /// 构建按钮内容 + Widget _buildButtonContent() { + return BlocBuilder( + builder: (context, state) { + final status = state is FloatBarCollapsedState + ? state.status + : (state as FloatBarExpandedState).status; + + return Container( + width: _buttonSize, + height: _buttonSize, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + _parseColor(status.statusColor).withOpacity(0.9), + _parseColor(status.statusColor).withOpacity(0.7), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Stack( + children: [ + /// 状态指示灯 + Positioned( + top: 8, + right: 8, + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: status.battery > 20 ? Colors.white : Colors.red, + borderRadius: BorderRadius.circular(4), + boxShadow: const [ + BoxShadow(color: Colors.black26, blurRadius: 2), + ], + ), + ), + ), + + /// 电量图标 + const Center( + child: Icon(Icons.battery_full, size: 24, color: Colors.white), + ), + ], + ), + ); + }, + ); + } + /// 信息项组件 Widget _buildInfoItem( IconData icon, @@ -323,15 +1924,25 @@ class _FloatBarContent extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 20, color: Colors.grey), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: valueColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, size: 24, color: valueColor), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)), + ), const SizedBox(height: 2), Text( value, style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, + fontSize: 16, + fontWeight: FontWeight.bold, color: valueColor, ), ), @@ -344,7 +1955,7 @@ class _FloatBarContent extends StatelessWidget { try { return Color(int.parse(colorStr.replaceFirst('#', '0xFF'))); } catch (_) { - return Colors.grey; + return Colors.green; } } } diff --git a/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart b/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart index 70264dad..4ae47a3f 100644 --- a/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart +++ b/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart @@ -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), diff --git a/lib/features/v2/device_list/presentation/pages/device_status_page.dart b/lib/features/v2/device_list/presentation/pages/device_status_page.dart index 37640893..2b44e1b6 100644 --- a/lib/features/v2/device_list/presentation/pages/device_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart @@ -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( @@ -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().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)); // 🔥 判断是否为无人机机场(根据设备类型包含关键字) diff --git a/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart b/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart index fc0177a7..c1ada510 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart @@ -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 { 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 { } } + /// 加载无人机详情并设置给 FloatBarWidget + Future _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 jsonData = (response.data is String) + ? json.decode(response.data) + : Map.from(response.data); + + if (jsonData['code'] == 0 || jsonData['code'] == 200) { + final detailData = jsonData['data']; + final Map detailMap = (detailData is Map) + ? Map.from(detailData) + : {}; + + 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( diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart index 2a374af0..9b7c94df 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart @@ -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 { } 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, ), ), ); diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart index 8c9c8734..bc0cc9d9 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart @@ -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 createState() => _DroneStationStatusPageState(); +} + +class _DroneStationStatusPageState extends State { + late final DroneStationBloc _bloc; + UAVDetailEntity? _uavDetail; + + @override + void initState() { + super.initState(); + _bloc = sl(); + _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( + 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(0xFF00B42A)), + valueColor: AlwaysStoppedAnimation( + 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, ), ), diff --git a/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart b/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart index 3c7c1a31..0fadfe49 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart @@ -158,8 +158,10 @@ class _DroneVideoControlPageState extends State { // 根据是否使用备选视频源决定使用哪个 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 { 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 { 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 { 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 { 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 { 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( diff --git a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart index 9a943650..68c5693f 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -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()..add(RobotListLoadData(siteId: siteId)), + create: (_) => + sl()..add(RobotListLoadData(siteId: siteId)), child: RobotListView(siteId: siteId), ); } @@ -63,9 +65,7 @@ class _RobotListViewState extends State { 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 { } 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 { onTap: () { context.read().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 { 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 { onTap: () { context.read().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 { 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 { onTap: () { context.read().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 { 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 { ), 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 { 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 { // 获取第一个正在执行任务的机器人 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 { 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 { 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 { 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 { 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(); - 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(); - 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(); + 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(); + 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(); } } diff --git a/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart b/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart index df651c41..27660e30 100644 --- a/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart +++ b/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart @@ -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, ), ), diff --git a/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart b/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart index 3b2e3c68..213a3b83 100644 --- a/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart +++ b/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart @@ -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 = []; - // 根据设备类型显示不同的数据 - 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)), ), ], ); diff --git a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart index f915eb89..245109f5 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart @@ -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'; diff --git a/lib/features/v2/home/data/datasources/site_datasource_impl.dart b/lib/features/v2/home/data/datasources/site_datasource_impl.dart index a87cfee9..69a99061 100644 --- a/lib/features/v2/home/data/datasources/site_datasource_impl.dart +++ b/lib/features/v2/home/data/datasources/site_datasource_impl.dart @@ -64,6 +64,12 @@ class SiteDataSourceImpl implements SiteDataSource { } final List 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(); } } diff --git a/lib/features/v2/home/domain/entities/site_entity.dart b/lib/features/v2/home/domain/entities/site_entity.dart index d43d24f0..e4b53469 100644 --- a/lib/features/v2/home/domain/entities/site_entity.dart +++ b/lib/features/v2/home/domain/entities/site_entity.dart @@ -24,7 +24,7 @@ class SiteEntity extends Equatable { factory SiteEntity.fromJson(Map 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(), diff --git a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart index 5de16c7b..952c6b50 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart @@ -91,7 +91,18 @@ class HomeV2Bloc extends Bloc { 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 { shouldShowError: true, // 🔥 标记需要显示弹窗 )), (homeData) { + List 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, )); }, ); diff --git a/lib/features/v2/home/presentation/bloc/home_v2_state.dart b/lib/features/v2/home/presentation/bloc/home_v2_state.dart index 5aa42267..3b0ace39 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_state.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_state.dart @@ -25,7 +25,7 @@ class HomeV2Loaded extends HomeV2State { const HomeV2Loaded({ required this.homeData, - this.trendType = 'kW', + this.trendType = 'day', this.sites = const [], this.selectedSite, }); diff --git a/lib/features/v2/home/presentation/pages/home_v2_page.dart b/lib/features/v2/home/presentation/pages/home_v2_page.dart index 6df9e9a9..39854a41 100644 --- a/lib/features/v2/home/presentation/pages/home_v2_page.dart +++ b/lib/features/v2/home/presentation/pages/home_v2_page.dart @@ -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 { } }, 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 { sl().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 { 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 { 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 { ); } - return const SizedBox(); + // 🔥 Initial/Loading 状态显示加载指示器 + return const Center(child: CircularProgressIndicator(color: Color(0xFF165DFF))); }, ), ); @@ -256,9 +279,13 @@ class _HomeV2PageState extends State { 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 { 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 { 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 { builder: (BuildContext context) { return MultiBlocProvider( providers: [ + BlocProvider.value(value: context.read()), BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: GetIt.I(), // 🔥 注入 RemoteControlCubit + value: GetIt.I(), // 🔥 注入 RemoteControlCubit ), ], child: const DeviceStatusModal(), diff --git a/lib/features/v2/home/presentation/widgets/plant_overview_card.dart b/lib/features/v2/home/presentation/widgets/plant_overview_card.dart index 04aa4970..22f18895 100644 --- a/lib/features/v2/home/presentation/widgets/plant_overview_card.dart +++ b/lib/features/v2/home/presentation/widgets/plant_overview_card.dart @@ -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, ), - ), - ], + ], + ), ), ], ), diff --git a/lib/features/v2/home/presentation/widgets/power_card.dart b/lib/features/v2/home/presentation/widgets/power_card.dart index 40d7be4c..d8a076f6 100644 --- a/lib/features/v2/home/presentation/widgets/power_card.dart +++ b/lib/features/v2/home/presentation/widgets/power_card.dart @@ -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(), diff --git a/lib/features/v2/home/presentation/widgets/power_trend_chart.dart b/lib/features/v2/home/presentation/widgets/power_trend_chart.dart index cfd22d6c..010903d1 100644 --- a/lib/features/v2/home/presentation/widgets/power_trend_chart.dart +++ b/lib/features/v2/home/presentation/widgets/power_trend_chart.dart @@ -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, diff --git a/lib/features/v2/home/presentation/widgets/quick_entry_card.dart b/lib/features/v2/home/presentation/widgets/quick_entry_card.dart index 7190a0b6..680f17fe 100644 --- a/lib/features/v2/home/presentation/widgets/quick_entry_card.dart +++ b/lib/features/v2/home/presentation/widgets/quick_entry_card.dart @@ -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, diff --git a/lib/features/v2/home/presentation/widgets/stats_grid.dart b/lib/features/v2/home/presentation/widgets/stats_grid.dart index 25690499..03900793 100644 --- a/lib/features/v2/home/presentation/widgets/stats_grid.dart +++ b/lib/features/v2/home/presentation/widgets/stats_grid.dart @@ -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( diff --git a/lib/features/v2/home/presentation/widgets/work_order_card.dart b/lib/features/v2/home/presentation/widgets/work_order_card.dart index d42cda59..6e729222 100644 --- a/lib/features/v2/home/presentation/widgets/work_order_card.dart +++ b/lib/features/v2/home/presentation/widgets/work_order_card.dart @@ -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), + ), + ], ), ); } diff --git a/lib/features/v2/my/presentation/pages/my_page.dart b/lib/features/v2/my/presentation/pages/my_page.dart index 500e728c..a137fade 100644 --- a/lib/features/v2/my/presentation/pages/my_page.dart +++ b/lib/features/v2/my/presentation/pages/my_page.dart @@ -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( 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(); final appUserState = context.watch().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'), + ), ), ], ), diff --git a/lib/features/v2/my/presentation/pages/system_settings_page.dart b/lib/features/v2/my/presentation/pages/system_settings_page.dart index 95c7faeb..6ffa8bd4 100644 --- a/lib/features/v2/my/presentation/pages/system_settings_page.dart +++ b/lib/features/v2/my/presentation/pages/system_settings_page.dart @@ -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 { // Tab 设置 _buildTabSettingsSection(), const SizedBox(height: 12), + // 悬浮条设置 + _buildFloatBarSection(), + const SizedBox(height: 12), // 语言设置 _buildLanguageSection(), const SizedBox(height: 12), @@ -56,6 +60,65 @@ class _SystemSettingsPageState extends State { ); } + /// 悬浮条设置 + 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( + 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 { 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 { 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 { AppLocalizations.of(context).translate('my.chinese'), 'zh', locale.languageCode == 'zh', - () => context.read().setLocale(const Locale('zh', 'CN')), + () => context.read().setLocale( + const Locale('zh', 'CN'), + ), ), const SizedBox(height: 12), _buildLanguageOption( AppLocalizations.of(context).translate('my.english'), 'en', locale.languageCode == 'en', - () => context.read().setLocale(const Locale('en', 'US')), + () => context.read().setLocale( + const Locale('en', 'US'), + ), ), ], ); @@ -182,16 +249,25 @@ class _SystemSettingsPageState extends State { ); } - 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 { 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 { 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 { style: TextButton.styleFrom( foregroundColor: const Color(0xFFF53F3F), ), - child: const Text('确定'), + child: Text( + AppLocalizations.of(context).translate('my_v2.confirm'), + ), ), ], ), diff --git a/lib/features/v2/my/presentation/widgets/menu_item_card.dart b/lib/features/v2/my/presentation/widgets/menu_item_card.dart index 9bf3b20d..97f1e960 100644 --- a/lib/features/v2/my/presentation/widgets/menu_item_card.dart +++ b/lib/features/v2/my/presentation/widgets/menu_item_card.dart @@ -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; + } + } } diff --git a/lib/features/v2/my/presentation/widgets/quick_action_card.dart b/lib/features/v2/my/presentation/widgets/quick_action_card.dart index fb426d0a..9f12ed0d 100644 --- a/lib/features/v2/my/presentation/widgets/quick_action_card.dart +++ b/lib/features/v2/my/presentation/widgets/quick_action_card.dart @@ -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, ), ], ), diff --git a/lib/features/v2/report/presentation/constants/report_constants.dart b/lib/features/v2/report/presentation/constants/report_constants.dart index 76f6c1cc..557a4c54 100644 --- a/lib/features/v2/report/presentation/constants/report_constants.dart +++ b/lib/features/v2/report/presentation/constants/report_constants.dart @@ -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); } diff --git a/lib/features/v2/report/presentation/cubit/report_cubit.dart b/lib/features/v2/report/presentation/cubit/report_cubit.dart index 95ae5ada..973f00aa 100644 --- a/lib/features/v2/report/presentation/cubit/report_cubit.dart +++ b/lib/features/v2/report/presentation/cubit/report_cubit.dart @@ -9,18 +9,18 @@ class ReportCubit extends Cubit { 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 { 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 { 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 { 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 { 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 { final currentState = state as ReportFormState; final updatedFiles = List.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 { final currentState = state as ReportFormState; final updatedFiles = List.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), + ), + ); } } diff --git a/lib/features/v2/report/presentation/pages/report_page.dart b/lib/features/v2/report/presentation/pages/report_page.dart index 2a6f03f6..5b4fc39c 100644 --- a/lib/features/v2/report/presentation/pages/report_page.dart +++ b/lib/features/v2/report/presentation/pages/report_page.dart @@ -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().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')), ), ], ), diff --git a/lib/features/v2/report/presentation/widgets/description_input.dart b/lib/features/v2/report/presentation/widgets/description_input.dart index 839f223b..ac1775d0 100644 --- a/lib/features/v2/report/presentation/widgets/description_input.dart +++ b/lib/features/v2/report/presentation/widgets/description_input.dart @@ -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), ), ), ], diff --git a/lib/features/v2/report/presentation/widgets/device_selector.dart b/lib/features/v2/report/presentation/widgets/device_selector.dart index dd40ea52..e70aebff 100644 --- a/lib/features/v2/report/presentation/widgets/device_selector.dart +++ b/lib/features/v2/report/presentation/widgets/device_selector.dart @@ -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 diff --git a/lib/features/v2/report/presentation/widgets/level_selector.dart b/lib/features/v2/report/presentation/widgets/level_selector.dart index 8e05e7bc..162e850a 100644 --- a/lib/features/v2/report/presentation/widgets/level_selector.dart +++ b/lib/features/v2/report/presentation/widgets/level_selector.dart @@ -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, ), ), ), diff --git a/lib/features/v2/report/presentation/widgets/media_uploader.dart b/lib/features/v2/report/presentation/widgets/media_uploader.dart index b6cb7303..73e3ab0b 100644 --- a/lib/features/v2/report/presentation/widgets/media_uploader.dart +++ b/lib/features/v2/report/presentation/widgets/media_uploader.dart @@ -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)), ); } } diff --git a/lib/features/v2/report/presentation/widgets/report_type_selector.dart b/lib/features/v2/report/presentation/widgets/report_type_selector.dart index e262f21a..1d158837 100644 --- a/lib/features/v2/report/presentation/widgets/report_type_selector.dart +++ b/lib/features/v2/report/presentation/widgets/report_type_selector.dart @@ -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, ), ], ), diff --git a/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart b/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart index 5e1f204e..84fadf0a 100644 --- a/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart +++ b/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart @@ -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 { child: BlocBuilder( 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 { 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 { //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 { 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 { 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 { 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), diff --git a/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart b/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart index 5c7bedfe..74956cc9 100644 --- a/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart +++ b/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart @@ -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), ), diff --git a/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart b/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart index 1c11a5a5..cc3ff72c 100644 --- a/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart +++ b/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart @@ -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'); + } + } } diff --git a/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart b/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart index 7828bafe..18e40189 100644 --- a/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart +++ b/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart @@ -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'); + } + } } diff --git a/lib/features/v2/workorder/presentation/pages/workorder_page.dart b/lib/features/v2/workorder/presentation/pages/workorder_page.dart index cc7d2a5a..99e24398 100644 --- a/lib/features/v2/workorder/presentation/pages/workorder_page.dart +++ b/lib/features/v2/workorder/presentation/pages/workorder_page.dart @@ -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)), ], ), ); diff --git a/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart b/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart index 6a4f9237..eaba2f72 100644 --- a/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart +++ b/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart @@ -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)); } } diff --git a/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart b/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart index 26589db3..c679f0e2 100644 --- a/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart +++ b/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart @@ -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'); } } diff --git a/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart b/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart index 92561e18..78dc3443 100644 --- a/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart +++ b/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart @@ -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, diff --git a/lib/main.dart b/lib/main.dart index 45787878..090ff792 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -167,6 +167,16 @@ class _LifecycleListenerState extends State<_LifecycleListener> try { final authCubit = context.read(); if (authCubit.state is! AuthAuthenticated) return; + + // 🔥 关键修复:只有在用户已选择设备时才重连 TCP + // 否则创建一个无用的 TCP 连接,服务端会残留 session, + // 导致后续 connectBySwitch() 复用该连接时触发 have_logged_in + final remoteControlCubit = sl(); + if (remoteControlCubit.state.targetDevice == null) { + debugPrint('📱 [生命周期] 未选择设备,跳过 TCP 重连(避免创建无用连接)'); + return; + } + await Future.delayed(const Duration(milliseconds: 500)); await authCubit.reconnectAfterResume(); } catch (e) {