330 lines
12 KiB
Dart
330 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:fpdart/fpdart.dart' hide State;
|
|
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
|
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
|
import 'package:maibu_satabot_v2/features/devices/domain/usecases/device_work_hostrirty_usecase.dart';
|
|
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_run_hostrity_entity.dart';
|
|
import 'package:maibu_satabot_v2/features/devices/domain/errors/device_failure.dart';
|
|
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
|
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
|
|
import '../../../../core/di/injection.dart';
|
|
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class WorkParamsCard extends StatefulWidget {
|
|
const WorkParamsCard({super.key});
|
|
|
|
@override
|
|
State<WorkParamsCard> createState() => _WorkParamsCardState();
|
|
}
|
|
|
|
class _WorkParamsCardState extends State<WorkParamsCard> {
|
|
List<DeviceRunStatisticsEntity>? _statistics;
|
|
String? _errorMessage;
|
|
bool _isLoading = false;
|
|
|
|
// 计时相关
|
|
DateTime? _firstTimestamp; // 第一次时间(开始时间)
|
|
int _totalWorkMinutes = 0; // 总作业分钟
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final deviceState = context.watch<DevicesCubit>().state;
|
|
final currentDevice = deviceState.selectedDevice;
|
|
final statusState = context.watch<DeviceStatusBloc>().state;
|
|
|
|
double? distance;
|
|
double? workArea;
|
|
|
|
if (statusState is DeviceStatusUpdated) {
|
|
workArea = double.tryParse(statusState.status.workingArea ?? '');
|
|
distance = workArea != null ? workArea / 0.7 : null;
|
|
|
|
// ====================== 计算作业时长(核心) ======================
|
|
_calculateWorkingTime(statusState.status.timestamp);
|
|
}
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 10, offset: const Offset(0, 4))],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 4,
|
|
height: 16,
|
|
decoration: BoxDecoration(color: Colors.blueAccent, borderRadius: BorderRadius.circular(2)),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
AppLocalizations.of(context).translate('home.work_params'),
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
GestureDetector(
|
|
onTap: () {
|
|
if (currentDevice != null) {
|
|
_handleInfoTap(currentDevice);
|
|
}
|
|
},
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.info, size: 14, color: Colors.grey[400]),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
AppLocalizations.of(context).translate('home.view_details'),
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
_buildParamItem(context, 'home.work_area', '${workArea?.toStringAsFixed(1) ?? '0'}', '㎡'),
|
|
_buildDivider(),
|
|
_buildParamItem(context, 'home.work_distance', '${distance?.toStringAsFixed(1) ?? '0'}', 'm'),
|
|
_buildDivider(),
|
|
// 显示实时计算的作业时长
|
|
_buildParamItem(context, 'home.work_duration', '${_totalWorkMinutes}', 'min'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ====================== 核心:根据时间戳计算时长 ======================
|
|
// 修复时间解析:适配 20-1-11-31 08:00:00 格式
|
|
void _calculateWorkingTime(String timestampStr) {
|
|
if (timestampStr.isEmpty) return;
|
|
|
|
try {
|
|
// 格式:年-月-日-星期 时:分:秒
|
|
// 我们只取 年-月-日 时:分:秒 来解析
|
|
List<String> parts = timestampStr.split(' ');
|
|
if (parts.length != 2) return;
|
|
|
|
String datePart = parts[0]; // 20-1-11-31
|
|
String timePart = parts[1]; // 08:00:00
|
|
|
|
List<String> dateSegments = datePart.split('-');
|
|
if (dateSegments.length < 3) return;
|
|
|
|
// 只取 年-月-日,丢弃星期
|
|
String cleanDate = '${dateSegments[0]}-${dateSegments[1]}-${dateSegments[2]}';
|
|
DateTime currentTime = DateFormat("yy-MM-dd HH:mm:ss").parse('$cleanDate $timePart');
|
|
|
|
// 记录第一次时间
|
|
if (_firstTimestamp == null) {
|
|
setState(() {
|
|
_firstTimestamp = currentTime;
|
|
});
|
|
}
|
|
|
|
// 计算分钟
|
|
final diff = currentTime.difference(_firstTimestamp!);
|
|
setState(() {
|
|
_totalWorkMinutes = diff.inMinutes;
|
|
});
|
|
} catch (e) {
|
|
debugPrint("时间解析失败: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> _handleInfoTap(DeviceEntity currentDevice) async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_errorMessage = null;
|
|
});
|
|
try {
|
|
final useCase = DeviceWorkHostrirty(sl());
|
|
final deviceId = currentDevice.deviceName.toString() ?? '';
|
|
final params = BindDeviceParams(deviceId, 'your_device_alias');
|
|
final result = await useCase(params);
|
|
result.fold(
|
|
(failure) {
|
|
setState(() {
|
|
_errorMessage = failure.message;
|
|
_isLoading = false;
|
|
});
|
|
},
|
|
(data) {
|
|
setState(() {
|
|
_statistics = data;
|
|
_isLoading = false;
|
|
});
|
|
_showDetailsDialog(context, _statistics!);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = '请求失败: $e';
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _showDetailsDialog(BuildContext context, List<DeviceRunStatisticsEntity> data) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useRootNavigator: true,
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
|
builder: (context) {
|
|
return Container(
|
|
height: MediaQuery.of(context).size.height * 0.75,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Center(child: Container(width: 40, height: 4)),
|
|
Text(
|
|
AppLocalizations.of(context).translate('home.history_details'),
|
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
IconButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
icon: const Icon(Icons.close, color: Colors.grey, size: 24),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Expanded(
|
|
child: data.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.description_outlined, size: 60, color: Colors.grey),
|
|
const SizedBox(height: 16),
|
|
Text(AppLocalizations.of(context).translate('home.no_history'), style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
|
],
|
|
),
|
|
)
|
|
: SingleChildScrollView(
|
|
child: Column(
|
|
children: data.map((item) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.blue.withOpacity(0.2), width: 1),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildDetailRow(context, Icons.calendar_today, 'home.start_time', item.startTime),
|
|
_buildDetailRow(context, Icons.calendar_today_outlined, 'home.end_time', item.endTime),
|
|
_buildDetailRow(context, Icons.square_foot, 'home.working_area', '${item.workArea} ㎡'),
|
|
if (item.distance != null && item.distance! > 0)
|
|
_buildDetailRow(context, Icons.directions_car, 'home.working_distance', '${item.distance!.toStringAsFixed(2)} m'),
|
|
_buildDetailRow(context, Icons.timer, 'home.time_spent', '${item.time} min'),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow(BuildContext context, IconData icon, String labelKey, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 16, color: Colors.blue),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
AppLocalizations.of(context).translate(labelKey) + ': ',
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87),
|
|
),
|
|
Expanded(
|
|
child: Text(value, style: const TextStyle(fontSize: 14, color: Colors.black54)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildParamItem(BuildContext context, String labelKey, String value, String unit) {
|
|
return Expanded(
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
labelKey == 'home.work_area'
|
|
? Icons.aspect_ratio
|
|
: labelKey == 'home.work_distance'
|
|
? Icons.local_shipping_outlined
|
|
: Icons.access_time,
|
|
size: 14,
|
|
color: Colors.black38,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(AppLocalizations.of(context).translate(labelKey), style: const TextStyle(fontSize: 12, color: Colors.black38)),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
RichText(
|
|
text: TextSpan(
|
|
children: [
|
|
TextSpan(
|
|
text: value,
|
|
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Colors.black, fontFamily: 'Inter'),
|
|
),
|
|
TextSpan(
|
|
text: ' $unit',
|
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.normal, color: Colors.black54),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDivider() {
|
|
return VerticalDivider(color: Colors.black.withOpacity(0.05), thickness: 1, indent: 10, endIndent: 10);
|
|
}
|
|
}
|