584 lines
20 KiB
Dart
584 lines
20 KiB
Dart
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),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|