Files
flutterApp/lib/features/pv_control/presentation/widgets/pv_info_column.dart

378 lines
12 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 '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,
),
),
],
],
),
],
),
),
],
),
);
}
}