Files
feature-next-arch/lib/features/home/presentation/pages/running_status_page.dart
2026-03-04 11:08:50 +08:00

584 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../../core/di/injection.dart';
import '../../../../core/network/net_message_dispatcher.dart';
import '../../../../core/network/protocol_decoder.dart';
import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../../../devices/presentation/bloc/device_status_bloc.dart';
import '../../../devices/presentation/bloc/device_status_state.dart';
class RunningStatusPage extends StatefulWidget {
const RunningStatusPage({super.key});
@override
State<RunningStatusPage> createState() => _RunningStatusPageState();
}
class _RunningStatusPageState extends State<RunningStatusPage> {
bool _isCardView = true;
bool _chipGaugeMode = true;
// 图表历史数据
final List<FlSpot> _leftMeasureHistory = [];
final List<FlSpot> _rightMeasureHistory = [];
final List<FlSpot> _leftTargetHistory = [];
final List<FlSpot> _rightTargetHistory = [];
final List<FlSpot> _leftCurrentHistory = [];
final List<FlSpot> _rightCurrentHistory = [];
final List<FlSpot> _leftTempHistory = [];
final List<FlSpot> _rightTempHistory = [];
final List<FlSpot> _knifeHistory = [];
final List<FlSpot> _chipTempHistory = [];
final List<FlSpot> _voltageHistory = [];
double _timeIndex = 0;
// 限制列表长度,防止数据过多
void _limit(List<FlSpot> list, {int max = 30}) {
if (list.length > max) list.removeAt(0);
}
// 刷新数据方法(移到正确位置)
void _refreshDeviceData() {
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
if (currentDevice != null) {
// 触发Bloc刷新数据
// context.read<DeviceStatusBloc>().add(RefreshDeviceStatus(deviceId: currentDevice.id));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("数据刷新成功!"), duration: Duration(seconds: 1)));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("暂无设备,无法刷新"), duration: Duration(seconds: 1)));
}
}
// 解析定位质量数值为文字描述
String _parseLocationQuality(int locationValue) {
switch (locationValue) {
case 0:
return '无效';
case 1:
return 'GPS 单点定位';
case 2:
return 'DGPS 伪距差分或 SBAS';
case 4:
return 'RTK 固定解';
case 5:
return 'RTK 浮点解';
default:
return '未知($locationValue)';
}
}
// 追加图表数据
void _appendChartData(DeviceStatusUpdated state) {
final status = state.status;
// 添加速度数据
_leftTargetHistory.add(FlSpot(_timeIndex, status.leftTargetSpeed));
_rightTargetHistory.add(FlSpot(_timeIndex, status.rightTargetSpeed));
_leftMeasureHistory.add(FlSpot(_timeIndex, status.leftMeasureSpeed));
_rightMeasureHistory.add(FlSpot(_timeIndex, status.rightMeasureSpeed));
// 添加电流数据
_leftCurrentHistory.add(FlSpot(_timeIndex, status.leftCurrent));
_rightCurrentHistory.add(FlSpot(_timeIndex, status.rightCurrent));
// 添加温度数据
_leftTempHistory.add(FlSpot(_timeIndex, status.leftMotorTemp));
_rightTempHistory.add(FlSpot(_timeIndex, status.rightMotorTemp));
// 添加割刀速度
_knifeHistory.add(FlSpot(_timeIndex, double.tryParse(status.knifeCuttingSpeed) ?? 0));
// 添加芯片温度和电压
_chipTempHistory.add(FlSpot(_timeIndex, status.chipTemp));
_voltageHistory.add(FlSpot(_timeIndex, double.tryParse(status.battery) ?? 0));
_timeIndex += 1;
// 限制数据长度
_limit(_leftTargetHistory);
_limit(_rightTargetHistory);
_limit(_leftMeasureHistory);
_limit(_rightMeasureHistory);
_limit(_leftCurrentHistory);
_limit(_rightCurrentHistory);
_limit(_leftTempHistory);
_limit(_rightTempHistory);
_limit(_knifeHistory);
_limit(_chipTempHistory);
_limit(_voltageHistory);
}
// 构建间距组件
Widget _gap({double height = 20}) {
return SizedBox(height: height);
}
// 构建折线图
Widget _buildLineChart({required String title, required List<LineChartBarData> lines, double? minY, double? maxY}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
SizedBox(
height: 220,
child: LineChart(
LineChartData(
minY: minY,
maxY: maxY,
lineBarsData: lines,
gridData: const FlGridData(show: true),
borderData:FlBorderData(show: true),
titlesData: const FlTitlesData(
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)),
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
),
),
),
],
);
}
// 构建折线
LineChartBarData _line(List<FlSpot> data, Color color) {
return LineChartBarData(
spots: data,
color: color,
isCurved: true,
barWidth: 2,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
);
}
// 左右轮测量速度图表
Widget _buildSpeedMeasureChart() {
return _buildLineChart(
title: "左右轮测量速度 (rpm)",
minY: -3000,
maxY: 3000,
lines: [_line(_leftMeasureHistory, Colors.blue), _line(_rightMeasureHistory, Colors.red)],
);
}
// 左右轮目标速度图表
Widget _buildSpeedTargetChart() {
return _buildLineChart(
title: "左右轮目标速度 (rpm)",
minY: -3000,
maxY: 3000,
lines: [_line(_leftTargetHistory, Colors.green), _line(_rightTargetHistory, Colors.orange)],
);
}
// 电流图表
Widget _buildCurrentChart() {
return _buildLineChart(
title: "左右轮电流 (A)",
minY: 0,
maxY: 100,
lines: [_line(_leftCurrentHistory, Colors.purple), _line(_rightCurrentHistory, Colors.teal)],
);
}
// 电机温度图表
Widget _buildMotorTempChart() {
return _buildLineChart(
title: "左右轮电机温度 (°C)",
minY: 0,
maxY: 120,
lines: [_line(_leftTempHistory, Colors.deepOrange), _line(_rightTempHistory, Colors.brown)],
);
}
// 割刀速度图表
Widget _buildKnifeSpeedChart() {
return _buildLineChart(title: "割刀速度 (rpm)", minY: 0, maxY: 3000, lines: [_line(_knifeHistory, Colors.indigo)]);
}
// 芯片温度/电压面板
Widget _buildChipTempVoltagePanel(DeviceStatusUpdated state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("芯片温度 / 电压", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Switch(value: _chipGaugeMode, onChanged: (v) => setState(() => _chipGaugeMode = v)),
],
),
const SizedBox(height: 16),
_chipGaugeMode
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [_buildGauge(state.status.chipTemp, 0, 120, "芯片温度 (°C)"), _buildGauge(double.parse(state.status.battery), 0, 100, "电量 (%)")],
)
: _buildLineChart(title: "最近30次数据", lines: [_line(_chipTempHistory, Colors.red), _line(_voltageHistory, Colors.blue)]),
],
);
}
// 航向角仪表盘
Widget _buildHeadingGauge(DeviceStatusUpdated state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("航向角", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Center(child: _buildGauge(state.status.yaw, 0, 360, "航向角 (°)")),
],
);
}
// 构建仪表盘
Widget _buildGauge(double value, double min, double max, String label) {
return SizedBox(
width: 150,
height: 150,
child: SfRadialGauge(
title: GaugeTitle(text: label),
axes: [
RadialAxis(
minimum: min,
maximum: max,
pointers: [NeedlePointer(value: value)],
annotations: [
GaugeAnnotation(
widget: Text(value.toStringAsFixed(1), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
angle: 90,
positionFactor: 0.5,
),
],
),
],
),
);
}
// 构建选项卡
Widget _buildTab(String title, {required bool isActive}) {
return Text(
title,
style: TextStyle(
color: isActive ? const Color(0xFF1677FF) : const Color(0xFF666666),
fontSize: 16,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
decoration: isActive ? TextDecoration.underline : null,
decorationColor: const Color(0xFF1677FF),
decorationThickness: 2,
),
);
}
// 电机参数表格
Widget _buildMotorTable(DeviceStatusUpdated state) {
final status = state.status;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Text("电机参数", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
Table(
border: TableBorder.all(color: const Color(0xFFE5E5E5)),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
TableRow(
decoration: const BoxDecoration(color: Color(0xFFF5F5F5)),
children: [const SizedBox(), _buildTableCell("左轮", isHeader: true), _buildTableCell("右轮", isHeader: true)],
),
TableRow(
children: [
_buildTableCell("目标速度\n(rpm)"),
_buildTableCell(status.leftTargetSpeed.toStringAsFixed(2)),
_buildTableCell(status.rightTargetSpeed.toStringAsFixed(2)),
],
),
TableRow(
children: [
_buildTableCell("测量速度\n(rpm)"),
_buildTableCell(status.leftMeasureSpeed.toStringAsFixed(2)),
_buildTableCell(status.rightMeasureSpeed.toStringAsFixed(2)),
],
),
TableRow(
children: [
_buildTableCell("电流(A)"),
_buildTableCell(status.leftCurrent.toStringAsFixed(2)),
_buildTableCell(status.rightCurrent.toStringAsFixed(2)),
],
),
TableRow(
children: [
_buildTableCell("电机温度(°C)"),
_buildTableCell(status.leftMotorTemp.toStringAsFixed(2)),
_buildTableCell(status.rightMotorTemp.toStringAsFixed(2)),
],
),
],
),
],
);
}
// 其他参数表格
Widget _buildOtherParamsTable(DeviceStatusUpdated state) {
final status = state.status;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Text("其他参数", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
Table(
border: TableBorder.all(color: const Color(0xFFE5E5E5)),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
TableRow(
decoration: const BoxDecoration(color: Color(0xFFF5F5F5)),
children: [_buildTableCell("名称", isHeader: true), _buildTableCell("数值", isHeader: true)],
),
TableRow(children: [_buildTableCell("俯仰角(°)"), _buildTableCell(status.pitch.toStringAsFixed(2))]),
TableRow(children: [_buildTableCell("翻滚角(°)"), _buildTableCell(status.roll.toStringAsFixed(2))]),
TableRow(children: [_buildTableCell("航向角(°)"), _buildTableCell(status.yaw.toStringAsFixed(2))]),
TableRow(children: [_buildTableCell("电量(%)"), _buildTableCell(status.battery)]),
TableRow(children: [_buildTableCell("芯片温度(°C)"), _buildTableCell(status.chipTemp.toStringAsFixed(2))]),
TableRow(children: [_buildTableCell("割刀速度(rpm)"), _buildTableCell(status.knifeCuttingSpeed)]),
TableRow(children: [_buildTableCell("经度(°)"), _buildTableCell(status.longitude.toStringAsFixed(6))]),
TableRow(children: [_buildTableCell("纬度(°)"), _buildTableCell(status.latitude.toStringAsFixed(6))]),
],
),
],
);
}
// 表格单元格
Widget _buildTableCell(String text, {bool isHeader = false}) {
return TableCell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 6),
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: isHeader ? const Color(0xFF333333) : const Color(0xFF666666),
fontWeight: isHeader ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}
// 卡片视图
Widget _buildCardContentView(DeviceStatusState state) {
if (state is DeviceStatusUpdated) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(children: [_buildMotorTable(state), const SizedBox(height: 20), _buildOtherParamsTable(state)]),
);
} else if (state is DeviceStatusError) {
return const Center(
child: Text("数据加载失败", style: TextStyle(color: Colors.red, fontSize: 16)),
);
} else {
return const Center(child: CircularProgressIndicator());
}
}
// 图表视图
Widget _buildChartContentView(DeviceStatusState state) {
if (state is! DeviceStatusUpdated) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSpeedMeasureChart(),
_gap(),
_buildSpeedTargetChart(),
_gap(),
_buildCurrentChart(),
_gap(),
_buildMotorTempChart(),
_gap(),
_buildKnifeSpeedChart(),
_gap(),
_buildChipTempVoltagePanel(state),
_gap(),
_buildHeadingGauge(state),
],
),
);
}
@override
Widget build(BuildContext context) {
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
if (currentDevice == null) {
return const Scaffold(body: Center(child: Text("加载中...")));
}
return Scaffold(
backgroundColor: const Color(0xFFF7F7F7),
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
backgroundColor: const Color(0xFF1677FF),
pinned: true,
centerTitle: true,
elevation: 0,
title: const Text(
"设备数据监控",
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
// 顶部状态信息
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
String qual = '--';
String satelliteCnt = '--';
String headingStatus = "--";
if (state is DeviceStatusUpdated) {
headingStatus = state.status.headingStatus == 0 ? '未初始化' : '已初始化';
int qualValue = 0;
try {
qualValue = int.parse(state.status.qual.toString());
} catch (e) {
qualValue = 0;
}
qual = _parseLocationQuality(qualValue);
satelliteCnt = state.status.satelliteCnt.toString();
} else if (state is DeviceStatusError) {
qual = '-';
satelliteCnt = '-';
headingStatus = '-';
}
return SliverToBoxAdapter(
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 3,
child: Text("航向角状态:$headingStatus", style: const TextStyle(fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis),
),
Expanded(
flex: 4,
child: Text(
"定位质量:$qual",
style: const TextStyle(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
Expanded(
flex: 2,
child: Text(
"卫星数:$satelliteCnt",
style: const TextStyle(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
),
),
],
),
),
);
},
),
// 切换选项卡
SliverToBoxAdapter(
child: Container(
color: Colors.white,
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
GestureDetector(
onTap: () => setState(() => _isCardView = true),
child: _buildTab("卡片", isActive: _isCardView),
),
const SizedBox(width: 24),
GestureDetector(
onTap: () => setState(() => _isCardView = false),
child: _buildTab("图表", isActive: !_isCardView),
),
],
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1677FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
onPressed: _refreshDeviceData,
child: const Text("刷新数据", style: TextStyle(fontSize: 14)),
),
],
),
),
),
// 主要内容区域(修复Sliver嵌套问题)
SliverToBoxAdapter(
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
// 追加图表数据
if (state is DeviceStatusUpdated) {
_appendChartData(state);
}
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
child: _isCardView ? _buildCardContentView(state) : _buildChartContentView(state),
);
},
),
),
],
),
);
}
}