图标样式
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -25,7 +26,6 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
bool _isCardView = true;
|
||||
bool _chipGaugeMode = true;
|
||||
|
||||
// 图表历史数据
|
||||
final List<FlSpot> _leftMeasureHistory = [];
|
||||
final List<FlSpot> _rightMeasureHistory = [];
|
||||
final List<FlSpot> _leftTargetHistory = [];
|
||||
@@ -40,26 +40,29 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
|
||||
double _timeIndex = 0;
|
||||
|
||||
// 限制列表长度,防止数据过多
|
||||
// 兼容低版本的 takeLast 方法
|
||||
List<T> _takeLast<T>(List<T> list, int count) {
|
||||
if (list.length <= count) {
|
||||
return List.from(list);
|
||||
}
|
||||
return list.sublist(list.length - count);
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -77,34 +80,23 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// 追加图表数据
|
||||
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);
|
||||
@@ -118,33 +110,142 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
_limit(_voltageHistory);
|
||||
}
|
||||
|
||||
// 构建间距组件
|
||||
Widget _gap({double height = 20}) {
|
||||
return SizedBox(height: height);
|
||||
}
|
||||
|
||||
// 构建折线图
|
||||
Widget _buildLineChart({required String title, required List<LineChartBarData> lines, double? minY, double? maxY}) {
|
||||
// ====================== 图表卡片(统一样式) ======================
|
||||
Widget _chartCard({required String title, required Widget child}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2))],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1677FF)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== 半圆仪表盘(电压/芯片温度) ======================
|
||||
Widget _semiCircleGauge({
|
||||
required double value,
|
||||
required double maxValue,
|
||||
required String unit,
|
||||
required String label,
|
||||
List<double> majorTicks = const [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250],
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
painter: SemiCircleGaugePainter(value: value / maxValue, maxValue: maxValue, majorTicks: majorTicks),
|
||||
size: const Size(280, 140),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
child: Text(
|
||||
"${value.toStringAsFixed(2)} $unit",
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== 罗盘式航向角仪表盘 ======================
|
||||
Widget _compassGauge({required double yaw}) {
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CustomPaint(
|
||||
painter: CompassGaugePainter(yaw: yaw),
|
||||
size: const Size(280, 280),
|
||||
),
|
||||
Text(
|
||||
"${yaw.toStringAsFixed(0)}°",
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== 折线图(统一样式) ======================
|
||||
Widget _styledLineChart({
|
||||
required String title,
|
||||
required List<LineChartBarData> lines,
|
||||
required double yAxisMax,
|
||||
double yAxisMin = 0,
|
||||
List<String> bottomLabels = const ["t", "t+1", "t+2", "t+3", "t+4", "t+5"],
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
if (lines.length > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_legendItem(color: const Color(0xFF3F51B5), label: "左轮"),
|
||||
const SizedBox(width: 24),
|
||||
_legendItem(color: const Color(0xFF8BC34A), label: "右轮"),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
height: 200,
|
||||
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)),
|
||||
minY: yAxisMin,
|
||||
maxY: yAxisMax,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
horizontalInterval: (yAxisMax - yAxisMin) / 6,
|
||||
getDrawingHorizontalLine: (value) => const FlLine(color: Color(0xFFE5E5E5), strokeWidth: 1),
|
||||
drawVerticalLine: false,
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: (yAxisMax - yAxisMin) / 6,
|
||||
getTitlesWidget: (value, meta) => Text(value.toStringAsFixed(0), style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
getTitlesWidget: (value, meta) {
|
||||
int idx = value.toInt();
|
||||
if (idx >= 0 && idx < bottomLabels.length) {
|
||||
return Text(bottomLabels[idx], style: const TextStyle(fontSize: 12, color: Colors.grey));
|
||||
}
|
||||
return const Text("");
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: lines,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -152,142 +253,156 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 构建折线
|
||||
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,
|
||||
Widget _legendItem({required Color color, required String label}) {
|
||||
return Row(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("芯片温度 / 电压", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
Switch(value: _chipGaugeMode, onChanged: (v) => setState(() => _chipGaugeMode = v)),
|
||||
],
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(color: color, width: 2),
|
||||
),
|
||||
),
|
||||
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)]),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 14, color: Colors.black87)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 航向角仪表盘
|
||||
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, "航向角 (°)")),
|
||||
],
|
||||
LineChartBarData _lineData(List<FlSpot> data, Color color) {
|
||||
// 替换 takeLast 为自定义的 _takeLast 方法
|
||||
return LineChartBarData(
|
||||
spots: _takeLast(data, 6), // 兼容低版本 Dart
|
||||
isCurved: false,
|
||||
color: color,
|
||||
barWidth: 2,
|
||||
isStrokeCapRound: true,
|
||||
dotData: FlDotData(show: true, getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(radius: 4, color: Colors.white, strokeWidth: 2)),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建仪表盘
|
||||
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 _buildChartContentView(DeviceStatusState state) {
|
||||
if (state is! DeviceStatusUpdated) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final status = state.status;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// 电压仪表盘
|
||||
_chartCard(
|
||||
title: "电压 (V)",
|
||||
child: _semiCircleGauge(
|
||||
value: status.voltage,
|
||||
maxValue: 250,
|
||||
unit: "V",
|
||||
label: "电压",
|
||||
majorTicks: const [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250],
|
||||
),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 航向角仪表盘
|
||||
_chartCard(
|
||||
title: "航向角 (°)",
|
||||
child: _compassGauge(yaw: status.yaw),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 芯片温度仪表盘
|
||||
_chartCard(
|
||||
title: "芯片温度 (°C)",
|
||||
child: _semiCircleGauge(
|
||||
value: status.chipTemp,
|
||||
maxValue: 100,
|
||||
unit: "°C",
|
||||
label: "芯片温度",
|
||||
majorTicks: const [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
||||
),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 割刀速度折线图
|
||||
_chartCard(
|
||||
title: "割刀速度 (rpm)",
|
||||
child: _styledLineChart(title: "割刀速度", lines: [_lineData(_knifeHistory, const Color(0xFF4CAF50))], yAxisMax: 3000, yAxisMin: -3000),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 电流对比折线图
|
||||
_chartCard(
|
||||
title: "电流对比 (A)",
|
||||
child: _styledLineChart(
|
||||
title: "电流对比",
|
||||
lines: [_lineData(_leftCurrentHistory, const Color(0xFF3F51B5)), _lineData(_rightCurrentHistory, const Color(0xFF8BC34A))],
|
||||
yAxisMax: 100,
|
||||
),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 电机温度对比折线图
|
||||
_chartCard(
|
||||
title: "电机温度对比 (°C)",
|
||||
child: _styledLineChart(
|
||||
title: "电机温度对比",
|
||||
lines: [_lineData(_leftTempHistory, const Color(0xFF3F51B5)), _lineData(_rightTempHistory, const Color(0xFF8BC34A))],
|
||||
yAxisMax: 100,
|
||||
),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 右轮速度对比折线图
|
||||
_chartCard(
|
||||
title: "右轮速度对比 (rpm)",
|
||||
child: _styledLineChart(
|
||||
title: "右轮速度对比",
|
||||
lines: [_lineData(_rightTargetHistory, const Color(0xFF3F51B5)), _lineData(_rightMeasureHistory, const Color(0xFF8BC34A))],
|
||||
yAxisMax: 3000,
|
||||
yAxisMin: -3000,
|
||||
),
|
||||
),
|
||||
_gap(),
|
||||
|
||||
// 左轮速度对比折线图
|
||||
_chartCard(
|
||||
title: "左轮速度对比 (rpm)",
|
||||
child: _styledLineChart(
|
||||
title: "左轮速度对比",
|
||||
lines: [_lineData(_leftTargetHistory, const Color(0xFF3F51B5)), _lineData(_leftMeasureHistory, const Color(0xFF8BC34A))],
|
||||
yAxisMax: 3000,
|
||||
yAxisMin: -3000,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建选项卡
|
||||
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 _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 _buildMotorTable(DeviceStatusUpdated state) {
|
||||
final status = state.status;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -337,10 +452,8 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 其他参数表格
|
||||
Widget _buildOtherParamsTable(DeviceStatusUpdated state) {
|
||||
final status = state.status;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -370,7 +483,6 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 表格单元格
|
||||
Widget _buildTableCell(String text, {bool isHeader = false}) {
|
||||
return TableCell(
|
||||
child: Padding(
|
||||
@@ -388,46 +500,16 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 卡片视图
|
||||
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),
|
||||
],
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -461,7 +543,6 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// 顶部状态信息
|
||||
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
builder: (context, state) {
|
||||
String qual = '--';
|
||||
@@ -522,7 +603,6 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
},
|
||||
),
|
||||
|
||||
// 切换选项卡
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
@@ -559,15 +639,12 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// 主要内容区域(修复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)),
|
||||
@@ -581,3 +658,182 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 半圆仪表盘绘制器 ======================
|
||||
class SemiCircleGaugePainter extends CustomPainter {
|
||||
final double value;
|
||||
final double maxValue;
|
||||
final List<double> majorTicks;
|
||||
|
||||
SemiCircleGaugePainter({required this.value, required this.maxValue, required this.majorTicks});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height);
|
||||
final radius = size.width / 2 - 20;
|
||||
|
||||
// 背景弧
|
||||
final bgPaint = Paint()
|
||||
..color = const Color(0xFFE3F2FD)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 20
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), math.pi, math.pi, false, bgPaint);
|
||||
|
||||
// 进度弧
|
||||
final progressPaint = Paint()
|
||||
..color = const Color(0xFFBBDEFB)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 20
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), math.pi, math.pi * value, false, progressPaint);
|
||||
|
||||
// 刻度
|
||||
final tickPaint = Paint()
|
||||
..color = Colors.black54
|
||||
..strokeWidth = 2;
|
||||
|
||||
for (int i = 0; i <= 40; i++) {
|
||||
final angle = math.pi + (math.pi / 40) * i;
|
||||
final x1 = center.dx + (radius - 15) * math.cos(angle);
|
||||
final y1 = center.dy + (radius - 15) * math.sin(angle);
|
||||
final x2 = center.dx + radius * math.cos(angle);
|
||||
final y2 = center.dy + radius * math.sin(angle);
|
||||
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), tickPaint);
|
||||
|
||||
// 主刻度
|
||||
if (majorTicks.contains(i * (maxValue / 40))) {
|
||||
final x3 = center.dx + (radius - 25) * math.cos(angle);
|
||||
final y3 = center.dy + (radius - 25) * math.sin(angle);
|
||||
canvas.drawLine(Offset(x1, y1), Offset(x3, y3), tickPaint..strokeWidth = 4);
|
||||
|
||||
// 刻度值
|
||||
final textValue = (i * (maxValue / 40)).toStringAsFixed(0);
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: textValue,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
final textX = center.dx + (radius - 40) * math.cos(angle) - textPainter.width / 2;
|
||||
final textY = center.dy + (radius - 40) * math.sin(angle) - textPainter.height / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
}
|
||||
}
|
||||
|
||||
// 指针
|
||||
final needleAngle = math.pi + math.pi * value;
|
||||
final needlePaint = Paint()
|
||||
..color = const Color(0xFF90A4AE)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final needlePath = Path()
|
||||
..moveTo(center.dx, center.dy)
|
||||
..lineTo(center.dx + radius * 0.8 * math.cos(needleAngle - 0.05), center.dy + radius * 0.8 * math.sin(needleAngle - 0.05))
|
||||
..lineTo(center.dx + radius * 0.8 * math.cos(needleAngle + 0.05), center.dy + radius * 0.8 * math.sin(needleAngle + 0.05))
|
||||
..close();
|
||||
|
||||
canvas.drawPath(needlePath, needlePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
// ====================== 罗盘式航向角仪表盘绘制器 ======================
|
||||
class CompassGaugePainter extends CustomPainter {
|
||||
final double yaw;
|
||||
|
||||
CompassGaugePainter({required this.yaw});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final radius = size.width / 2 - 20;
|
||||
|
||||
// 外圈
|
||||
final outerPaint = Paint()
|
||||
..color = const Color(0xFF1677FF)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 12;
|
||||
|
||||
canvas.drawCircle(center, radius, outerPaint);
|
||||
|
||||
// 刻度
|
||||
final tickPaint = Paint()
|
||||
..color = Colors.grey
|
||||
..strokeWidth = 2;
|
||||
|
||||
for (int i = 0; i < 36; i++) {
|
||||
final angle = (i * 10 - 90) * math.pi / 180;
|
||||
final x1 = center.dx + (radius - 10) * math.cos(angle);
|
||||
final y1 = center.dy + (radius - 10) * math.sin(angle);
|
||||
final x2 = center.dx + radius * math.cos(angle);
|
||||
final y2 = center.dy + radius * math.sin(angle);
|
||||
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), tickPaint);
|
||||
|
||||
// 主刻度(45°间隔)
|
||||
if (i % 9 == 0) {
|
||||
final x3 = center.dx + (radius - 20) * math.cos(angle);
|
||||
final y3 = center.dy + (radius - 20) * math.sin(angle);
|
||||
canvas.drawLine(Offset(x1, y1), Offset(x3, y3), tickPaint..strokeWidth = 4);
|
||||
|
||||
// 方向文字
|
||||
final direction = i == 0
|
||||
? "N"
|
||||
: i == 9
|
||||
? "E"
|
||||
: i == 18
|
||||
? "S"
|
||||
: i == 27
|
||||
? "W"
|
||||
: "";
|
||||
if (direction.isNotEmpty) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: direction,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
final textX = center.dx + (radius - 40) * math.cos(angle) - textPainter.width / 2;
|
||||
final textY = center.dy + (radius - 40) * math.sin(angle) - textPainter.height / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
}
|
||||
|
||||
// 角度值
|
||||
final angleValue = (i * 10).toString();
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: angleValue,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.black54),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
final textX = center.dx + (radius - 60) * math.cos(angle) - textPainter.width / 2;
|
||||
final textY = center.dy + (radius - 60) * math.sin(angle) - textPainter.height / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
}
|
||||
}
|
||||
|
||||
// 指针
|
||||
final needleAngle = (yaw - 90) * math.pi / 180;
|
||||
final needlePaint = Paint()
|
||||
..color = Colors.red
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final needlePath = Path()
|
||||
..moveTo(center.dx, center.dy)
|
||||
..lineTo(center.dx + radius * 0.7 * math.cos(needleAngle - 0.03), center.dy + radius * 0.7 * math.sin(needleAngle - 0.03))
|
||||
..lineTo(center.dx + radius * 0.7 * math.cos(needleAngle + 0.03), center.dy + radius * 0.7 * math.sin(needleAngle + 0.03))
|
||||
..close();
|
||||
|
||||
canvas.drawPath(needlePath, needlePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user