378 lines
12 KiB
Dart
378 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||
|
||
import '../models/pv_telemetry.dart';
|
||
import '../theme/pv_colors.dart';
|
||
|
||
/// 第一行右侧信息列:设备概览 / 驾驶模式 / 速度·里程 / 网络·温度。
|
||
class PvInfoColumn extends StatelessWidget {
|
||
const PvInfoColumn({
|
||
super.key,
|
||
required this.telemetry,
|
||
this.onSwitchMode,
|
||
});
|
||
|
||
final PvTelemetry telemetry;
|
||
final VoidCallback? onSwitchMode;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 收窄上下间距 + 卡片内边距,尽量让全部内容在一屏内可见(不触发滚动);
|
||
// 仍保留 SingleChildScrollView 作为极小屏的安全兜底。
|
||
return SingleChildScrollView(
|
||
child: Column(
|
||
children: [
|
||
_RobotHeader(telemetry: telemetry),
|
||
const SizedBox(height: 6),
|
||
_DriveModeCard(
|
||
telemetry: telemetry,
|
||
onSwitchMode: onSwitchMode,
|
||
),
|
||
const SizedBox(height: 6),
|
||
Row(
|
||
children: [
|
||
Expanded(child: _MetricCard.icon(
|
||
icon: Icons.speed,
|
||
color: PvColors.info,
|
||
label: AppLocalizations.of(
|
||
context,
|
||
).translate('pv_control.current_speed'),
|
||
value: telemetry.speed.toStringAsFixed(1),
|
||
unit: 'm/s',
|
||
)),
|
||
const SizedBox(width: 6),
|
||
Expanded(child: _MetricCard.icon(
|
||
icon: Icons.timeline,
|
||
color: PvColors.accent,
|
||
label: AppLocalizations.of(
|
||
context,
|
||
).translate('pv_control.patrol_mileage'),
|
||
value: telemetry.mileage == null
|
||
? AppLocalizations.of(context).translate(
|
||
'pv_control.unknown',
|
||
)
|
||
: telemetry.mileage!.toStringAsFixed(1),
|
||
unit: telemetry.mileage == null ? '' : 'km',
|
||
)),
|
||
],
|
||
),
|
||
const SizedBox(height: 6),
|
||
Row(
|
||
children: [
|
||
Expanded(child: _MetricCard.icon(
|
||
icon: Icons.signal_cellular_alt,
|
||
color: PvColors.online,
|
||
label: AppLocalizations.of(
|
||
context,
|
||
).translate('pv_control.network_status'),
|
||
value: telemetry.networkNormal
|
||
? AppLocalizations.of(context).translate(
|
||
'pv_control.status_normal',
|
||
)
|
||
: AppLocalizations.of(context).translate(
|
||
'pv_control.status_abnormal',
|
||
),
|
||
unit: '',
|
||
valueIsText: true,
|
||
)),
|
||
const SizedBox(width: 6),
|
||
Expanded(child: _MetricCard.icon(
|
||
icon: Icons.thermostat,
|
||
color: PvColors.info,
|
||
label: AppLocalizations.of(
|
||
context,
|
||
).translate('pv_control.device_temperature'),
|
||
value: telemetry.temperature == null
|
||
? AppLocalizations.of(context).translate(
|
||
'pv_control.unknown',
|
||
)
|
||
: telemetry.temperature!.toStringAsFixed(1),
|
||
unit: telemetry.temperature == null ? '' : '℃',
|
||
)),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RobotHeader extends StatelessWidget {
|
||
const _RobotHeader({required this.telemetry});
|
||
final PvTelemetry telemetry;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: PvColors.panel(),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: PvColors.cardInner,
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: const Icon(
|
||
Icons.smart_toy_outlined,
|
||
color: PvColors.accentBright,
|
||
size: 22,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Flexible(
|
||
child: Text(
|
||
telemetry.deviceName,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: PvColors.textPrimary,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
_OnlineBadge(isOnline: telemetry.isOnline),
|
||
],
|
||
),
|
||
const SizedBox(height: 3),
|
||
Row(
|
||
children: [
|
||
Text(
|
||
'SN:${telemetry.serialNumber}',
|
||
style: const TextStyle(
|
||
color: PvColors.textSecondary,
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Icon(
|
||
Icons.battery_std,
|
||
size: 12,
|
||
color: telemetry.battery == null
|
||
? PvColors.textTertiary
|
||
: (telemetry.battery! > 20
|
||
? PvColors.online
|
||
: PvColors.danger),
|
||
),
|
||
Text(
|
||
telemetry.battery == null
|
||
? AppLocalizations.of(context).translate(
|
||
'pv_control.battery_unknown',
|
||
)
|
||
: AppLocalizations.of(context).translate(
|
||
'pv_control.battery_percent',
|
||
).replaceAll(
|
||
'{n}',
|
||
telemetry.battery!.toStringAsFixed(0),
|
||
),
|
||
style: const TextStyle(
|
||
color: PvColors.textSecondary,
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _OnlineBadge extends StatelessWidget {
|
||
const _OnlineBadge({required this.isOnline});
|
||
final bool isOnline;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final color = isOnline ? PvColors.online : PvColors.textTertiary;
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: color.withValues(alpha: 0.15),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: color.withValues(alpha: 0.6)),
|
||
),
|
||
child: Text(
|
||
isOnline
|
||
? AppLocalizations.of(context).translate('pv_control.online')
|
||
: AppLocalizations.of(context).translate('pv_control.offline'),
|
||
style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.w700),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _DriveModeCard extends StatelessWidget {
|
||
const _DriveModeCard({required this.telemetry, this.onSwitchMode});
|
||
final PvTelemetry telemetry;
|
||
final VoidCallback? onSwitchMode;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: PvColors.panel(),
|
||
child: Row(
|
||
children: [
|
||
const Icon(
|
||
Icons.local_shipping_outlined,
|
||
color: PvColors.accent,
|
||
size: 22,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
telemetry.isAutoDriving
|
||
? AppLocalizations.of(context).translate(
|
||
'pv_control.auto_driving',
|
||
)
|
||
: AppLocalizations.of(context).translate(
|
||
'pv_control.manual_controlling',
|
||
),
|
||
style: const TextStyle(
|
||
color: PvColors.textPrimary,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
const SizedBox(height: 1),
|
||
Text(
|
||
// 遥测未带任务状态时,UI 层用当前语言兜底
|
||
telemetry.taskStatus.isEmpty
|
||
? AppLocalizations.of(
|
||
context,
|
||
).translate('pv_control.task_patrol_running')
|
||
: telemetry.taskStatus,
|
||
style: const TextStyle(
|
||
color: PvColors.textSecondary,
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: onSwitchMode,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||
decoration: BoxDecoration(
|
||
color: PvColors.cardInner,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: PvColors.cardBorder),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
AppLocalizations.of(context).translate(
|
||
'pv_control.switch_mode',
|
||
),
|
||
style: TextStyle(
|
||
color: PvColors.textPrimary,
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
SizedBox(width: 4),
|
||
Icon(Icons.swap_horiz, size: 14, color: PvColors.accent),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _MetricCard extends StatelessWidget {
|
||
const _MetricCard.icon({
|
||
required this.icon,
|
||
required this.color,
|
||
required this.label,
|
||
required this.value,
|
||
required this.unit,
|
||
this.valueIsText = false,
|
||
});
|
||
|
||
final IconData icon;
|
||
final Color color;
|
||
final String label;
|
||
final String value;
|
||
final String unit;
|
||
final bool valueIsText;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||
decoration: PvColors.panel(),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, color: color, size: 18),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: const TextStyle(
|
||
color: PvColors.textSecondary,
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
const SizedBox(height: 1),
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||
textBaseline: TextBaseline.alphabetic,
|
||
children: [
|
||
Flexible(
|
||
child: Text(
|
||
value,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: PvColors.textPrimary,
|
||
fontSize: valueIsText ? 12 : 14,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
),
|
||
if (unit.isNotEmpty) ...[
|
||
const SizedBox(width: 3),
|
||
Text(
|
||
unit,
|
||
style: const TextStyle(
|
||
color: PvColors.textSecondary,
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|