|
|
@@ -0,0 +1,277 @@
|
|
|
+import 'package:chicken_farm/core/utils/logger.dart';
|
|
|
+import 'package:chicken_farm/routes/app_routes.dart';
|
|
|
+import 'package:dio/dio.dart';
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:chicken_farm/components/vb_app_bar.dart';
|
|
|
+import 'package:chicken_farm/apis/device/_inspection_rule.dart';
|
|
|
+import 'package:chicken_farm/modes/device/inspection_rule/checkin_log.dart';
|
|
|
+import 'package:chicken_farm/core/utils/toast.dart';
|
|
|
+import 'package:chicken_farm/core/utils/datetime_util.dart';
|
|
|
+import 'package:chicken_farm/modes/device/inspection_rule/inspection_rule.dart';
|
|
|
+import 'package:chicken_farm/components/watermarked_camera.dart';
|
|
|
+
|
|
|
+import 'package:go_router/go_router.dart';
|
|
|
+
|
|
|
+class CheckinRecordPage extends StatefulWidget {
|
|
|
+ final String id;
|
|
|
+
|
|
|
+ const CheckinRecordPage({super.key, required this.id});
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<CheckinRecordPage> createState() => _CheckinRecordPageState();
|
|
|
+}
|
|
|
+
|
|
|
+class _CheckinRecordPageState extends State<CheckinRecordPage> {
|
|
|
+ List<CheckinLogModel> _records = [];
|
|
|
+ InspectionRuleModel? _rule;
|
|
|
+ bool _isLoading = true;
|
|
|
+
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ super.initState();
|
|
|
+ // 将 _loadData 调用推迟到下一帧,避免在构建过程中修改状态
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
+ _loadData();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _loadData() async {
|
|
|
+ // 在更新状态前检查组件是否仍然挂载
|
|
|
+ if (!mounted) return;
|
|
|
+
|
|
|
+ setState(() {
|
|
|
+ _isLoading = true;
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ logger.w('查询设备签到记录: ${widget.id}');
|
|
|
+
|
|
|
+ final ruleFuture = InspectionRuleApi().queryRule(widget.id);
|
|
|
+ final recordsFuture = InspectionRuleApi().queryCheckinList(widget.id);
|
|
|
+
|
|
|
+ final results = await Future.wait([ruleFuture, recordsFuture]);
|
|
|
+
|
|
|
+ setState(() {
|
|
|
+ _rule = results[0] as InspectionRuleModel;
|
|
|
+ _records = results[1] as List<CheckinLogModel>;
|
|
|
+ _isLoading = false;
|
|
|
+ });
|
|
|
+ } catch (e, stackTrace) {
|
|
|
+ logger.e('加载记录失败: $e, error: e, stackTrace: $stackTrace');
|
|
|
+
|
|
|
+ setState(() {
|
|
|
+ _isLoading = false;
|
|
|
+ });
|
|
|
+ ToastUtil.error('加载记录失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> _performCheckin() async {
|
|
|
+ // 打开拍照界面
|
|
|
+ await Navigator.of(context).push(
|
|
|
+ MaterialPageRoute(
|
|
|
+ builder: (context) => WatermarkedCamera(
|
|
|
+ onImageCaptured: (imageFile) async {
|
|
|
+ try {
|
|
|
+ // 创建 FormData 对象用于文件上传
|
|
|
+ final formData = FormData.fromMap({
|
|
|
+ 'phone': await MultipartFile.fromFile(
|
|
|
+ imageFile.path,
|
|
|
+ filename:
|
|
|
+ 'isCheckin_${widget.id}_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
|
|
+ ),
|
|
|
+ });
|
|
|
+
|
|
|
+ await InspectionRuleApi().checkInWithPhoto(widget.id, formData);
|
|
|
+ if (mounted) {
|
|
|
+ ToastUtil.success('签到成功');
|
|
|
+ _loadData(); // 刷新数据
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ logger.e('签到失败: $e');
|
|
|
+ if (mounted) {
|
|
|
+ ToastUtil.error('签到失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Scaffold(
|
|
|
+ appBar: const VberAppBar(
|
|
|
+ title: '签到记录',
|
|
|
+ showLeftButton: true,
|
|
|
+ alwaysGoToHome: true,
|
|
|
+ ),
|
|
|
+ body: _buildBody(),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildBody() {
|
|
|
+ if (_isLoading) {
|
|
|
+ return const Center(child: CircularProgressIndicator());
|
|
|
+ }
|
|
|
+
|
|
|
+ return SingleChildScrollView(
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ if (_rule != null) _buildRuleInfo(),
|
|
|
+ const Divider(),
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.all(16.0),
|
|
|
+ child: Text('签到记录', style: Theme.of(context).textTheme.titleLarge),
|
|
|
+ ),
|
|
|
+ if (_records.isEmpty)
|
|
|
+ const Center(
|
|
|
+ child: Padding(
|
|
|
+ padding: EdgeInsets.all(32.0),
|
|
|
+ child: Text(
|
|
|
+ '暂无签到记录',
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.grey),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ else
|
|
|
+ ListView.builder(
|
|
|
+ shrinkWrap: true,
|
|
|
+ physics: const NeverScrollableScrollPhysics(),
|
|
|
+ itemCount: _records.length,
|
|
|
+ itemBuilder: (context, index) {
|
|
|
+ final record = _records[index];
|
|
|
+ return CheckinRecordItem(record: record);
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildRuleInfo() {
|
|
|
+ return Container(
|
|
|
+ margin: const EdgeInsets.all(16),
|
|
|
+ width: double.infinity,
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ SizedBox(
|
|
|
+ width: double.infinity,
|
|
|
+ child: Card(
|
|
|
+ child: Padding(
|
|
|
+ padding: const EdgeInsets.all(16.0),
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ _rule!.taskName,
|
|
|
+ style: Theme.of(context).textTheme.titleLarge,
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 8),
|
|
|
+ Text('地点: ${_rule!.location}'),
|
|
|
+ const SizedBox(height: 4),
|
|
|
+ Text('周期: ${_rule!.cycleHours}小时'),
|
|
|
+ const SizedBox(height: 4),
|
|
|
+ Text('执行人: ${_rule!.executorName}'),
|
|
|
+ const SizedBox(height: 4),
|
|
|
+ Text(
|
|
|
+ '开始时间: ${DateTimeUtil.toStandardString(_rule!.startTime)}',
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 4),
|
|
|
+ Text(
|
|
|
+ '结束时间: ${DateTimeUtil.toStandardString(_rule!.endTime)}',
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 16),
|
|
|
+ Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ Expanded(
|
|
|
+ child: SizedBox(
|
|
|
+ height: 48,
|
|
|
+ child: ElevatedButton.icon(
|
|
|
+ onPressed: _performCheckin,
|
|
|
+ style: ElevatedButton.styleFrom(
|
|
|
+ textStyle: const TextStyle(
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: FontWeight.bold,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ icon: const Icon(Icons.check, size: 20),
|
|
|
+ label: const Text('立即签到'),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 16),
|
|
|
+ Expanded(
|
|
|
+ child: SizedBox(
|
|
|
+ height: 48,
|
|
|
+ child: ElevatedButton.icon(
|
|
|
+ onPressed: () {
|
|
|
+ context.pushNamed(AppRouteNames.checkin);
|
|
|
+ },
|
|
|
+ style: ElevatedButton.styleFrom(
|
|
|
+ textStyle: const TextStyle(
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: FontWeight.bold,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ icon: const Icon(Icons.qr_code_scanner, size: 20),
|
|
|
+ label: const Text('扫一扫'),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class CheckinRecordItem extends StatelessWidget {
|
|
|
+ final CheckinLogModel record;
|
|
|
+
|
|
|
+ const CheckinRecordItem({super.key, required this.record});
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Card(
|
|
|
+ margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
|
+ child: ListTile(
|
|
|
+ title: Text('${record.plannedSequence}. ${record.inspectorName}'),
|
|
|
+ subtitle: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ Text('计划时间: ${DateTimeUtil.toStandardString(record.planTime)}'),
|
|
|
+ const SizedBox(height: 4),
|
|
|
+ Text(_getExecuteTimeText()),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ trailing: Chip(
|
|
|
+ label: Text(
|
|
|
+ record.checkinStatus == 1 ? '已签到' : '未签到',
|
|
|
+ style: TextStyle(
|
|
|
+ color: record.checkinStatus == 1 ? Colors.green : Colors.red,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ String _getExecuteTimeText() {
|
|
|
+ if (record.executeTime != null) {
|
|
|
+ return '签到时间: ${DateTimeUtil.toStandardString(record.executeTime)}';
|
|
|
+ } else {
|
|
|
+ return '签到时间: 未签到';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|