1330 lines
46 KiB
Dart
1330 lines
46 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:getx_scaffold/getx_scaffold.dart' as dio;
|
||
import 'package:getx_scaffold/getx_scaffold.dart';
|
||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
import 'package:ln_jq_app/common/model/base_model.dart';
|
||
import 'package:ln_jq_app/common/styles/theme.dart';
|
||
import 'package:ln_jq_app/storage_service.dart';
|
||
import 'package:photo_view/photo_view.dart';
|
||
import 'package:photo_view/photo_view_gallery.dart';
|
||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||
|
||
enum ReservationStatus {
|
||
pending, // 待处理 ( addStatus: 0)
|
||
completed, // 完成 ( addStatus: 1)
|
||
rejected, // 拒绝 ( -1)
|
||
unadded, // 未加 ( 2)
|
||
cancel, // 取消预约
|
||
unknown, // 未知状态
|
||
}
|
||
|
||
class ReservationModel {
|
||
final String id;
|
||
final String stationId;
|
||
final String plateNumber;
|
||
String amount;
|
||
final String time;
|
||
final String contactPerson;
|
||
final String contactPhone;
|
||
ReservationStatus status; // 状态是可变的
|
||
|
||
final String contacts;
|
||
final String phone;
|
||
final String rejectReason;
|
||
final String stationName;
|
||
final String startTime;
|
||
final String endTime;
|
||
final String date;
|
||
final String hydAmount;
|
||
final String state;
|
||
final String stateName;
|
||
final String addStatus;
|
||
final String addStatusName;
|
||
bool hasEdit;
|
||
final String isEdit; // "1" 表示可以修改信息
|
||
|
||
// 新增附件相关字段
|
||
final int isTruckAttachment; // 1为有证件数据 0为缺少
|
||
final bool hasDrivingAttachment; // 是否有行驶证
|
||
final bool hasHydrogenationAttachment; // 是否有加氢证
|
||
final List<String> drivingAttachments; // 行驶证图片列表
|
||
final List<String> hydrogenationAttachments; // 加氢证图片列表
|
||
|
||
ReservationModel({
|
||
required this.id,
|
||
required this.stationId,
|
||
required this.plateNumber,
|
||
required this.amount,
|
||
required this.time,
|
||
required this.contactPerson,
|
||
required this.contactPhone,
|
||
required this.hasEdit,
|
||
this.status = ReservationStatus.pending,
|
||
required this.contacts,
|
||
required this.phone,
|
||
required this.stationName,
|
||
required this.startTime,
|
||
required this.endTime,
|
||
required this.date,
|
||
required this.hydAmount,
|
||
required this.state,
|
||
required this.stateName,
|
||
required this.addStatus,
|
||
required this.addStatusName,
|
||
required this.rejectReason,
|
||
required this.isTruckAttachment,
|
||
required this.hasDrivingAttachment,
|
||
required this.hasHydrogenationAttachment,
|
||
required this.isEdit,
|
||
required this.drivingAttachments,
|
||
required this.hydrogenationAttachments,
|
||
});
|
||
|
||
/// 工厂构造函数,用于从JSON创建ReservationModel实例
|
||
factory ReservationModel.fromJson(Map<String, dynamic> json) {
|
||
// 1完成 2未加 -1拒绝 0是待加氢
|
||
ReservationStatus currentStatus;
|
||
int statusFromServer = json['addStatus'] as int? ?? 0;
|
||
int state = json['state'] as int? ?? 0;
|
||
if (state == -1) {
|
||
currentStatus = ReservationStatus.rejected;
|
||
} else {
|
||
switch (statusFromServer) {
|
||
case 0:
|
||
currentStatus = ReservationStatus.pending;
|
||
break;
|
||
case 1:
|
||
currentStatus = ReservationStatus.completed;
|
||
break;
|
||
case 2:
|
||
currentStatus = ReservationStatus.unadded;
|
||
break;
|
||
case 6:
|
||
currentStatus = ReservationStatus.cancel;
|
||
break;
|
||
default:
|
||
currentStatus = ReservationStatus.unknown;
|
||
}
|
||
}
|
||
|
||
// 格式化时间显示
|
||
String startTimeStr = json['startTime']?.toString() ?? '';
|
||
String endTimeStr = json['endTime']?.toString() ?? '';
|
||
String dateStr = json['date']?.toString() ?? '';
|
||
String timeRange =
|
||
(startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty)
|
||
? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm
|
||
: '时间未定';
|
||
|
||
// 解析附件信息
|
||
Map<String, dynamic> attachmentVo = json['truckAttachmentVo'] ?? {};
|
||
int isTruckAttachment = attachmentVo['isTruckAttachment'] as int? ?? 0;
|
||
List drivingList = attachmentVo['drivingAttachment'] ?? [];
|
||
List hydrogenationList = attachmentVo['hydrogenationAttachment'] ?? [];
|
||
|
||
return ReservationModel(
|
||
// 原始字段,用于UI兼容
|
||
id: json['id']?.toString() ?? '',
|
||
stationId: json['stationId']?.toString() ?? '',
|
||
plateNumber: json['plateNumber']?.toString() ?? '---',
|
||
amount: '${json['hydAmount']?.toString() ?? '0'}kg',
|
||
time: timeRange,
|
||
contactPerson: json['contacts']?.toString() ?? '',
|
||
contactPhone: json['phone']?.toString() ?? '',
|
||
status: currentStatus,
|
||
|
||
// 新增的完整字段
|
||
contacts: json['contacts']?.toString() ?? '',
|
||
phone: json['phone']?.toString() ?? '',
|
||
stationName: json['stationName']?.toString() ?? '',
|
||
startTime: startTimeStr,
|
||
endTime: endTimeStr,
|
||
date: dateStr,
|
||
hydAmount: json['hydAmount']?.toString() ?? '0',
|
||
state: json['state']?.toString() ?? '',
|
||
addStatus: statusFromServer.toString(),
|
||
addStatusName: json['addStatusName']?.toString() ?? '',
|
||
stateName: json['stateName']?.toString() ?? '',
|
||
rejectReason: json['rejectReason']?.toString() ?? '',
|
||
hasEdit: true,
|
||
isTruckAttachment: isTruckAttachment,
|
||
hasDrivingAttachment: drivingList.isNotEmpty,
|
||
hasHydrogenationAttachment: hydrogenationList.isNotEmpty,
|
||
isEdit: json['isEdit']?.toString() ?? '0',
|
||
drivingAttachments: drivingList.map((e) => e.toString()).toList(),
|
||
hydrogenationAttachments: hydrogenationList.map((e) => e.toString()).toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class SiteController extends GetxController with BaseControllerMixin {
|
||
@override
|
||
String get builderId => 'site';
|
||
|
||
SiteController();
|
||
|
||
/// 状态变量:是否有预约数据
|
||
bool hasReservationData = false;
|
||
|
||
// 新增预约数据列表
|
||
List<ReservationModel> reservationList = [];
|
||
Timer? _refreshTimer;
|
||
|
||
final TextEditingController searchController = TextEditingController();
|
||
bool isNotice = false;
|
||
final RefreshController refreshController = RefreshController(initialRefresh: false);
|
||
|
||
// 加氢枪列表
|
||
final RxList<String> gasGunList = <String>[].obs;
|
||
final RxMap<String, Map<String, dynamic>> gasGunMap =
|
||
<String, Map<String, dynamic>>{}.obs;
|
||
|
||
@override
|
||
bool get listenLifecycleEvent => true;
|
||
|
||
@override
|
||
void onInit() {
|
||
super.onInit();
|
||
renderData();
|
||
msgNotice();
|
||
startAutoRefresh();
|
||
fetchGasGunList();
|
||
}
|
||
|
||
@override
|
||
void onPaused() {
|
||
stopAutoRefresh();
|
||
super.onPaused();
|
||
}
|
||
|
||
@override
|
||
void onClose() {
|
||
stopAutoRefresh();
|
||
searchController.dispose();
|
||
super.onClose();
|
||
}
|
||
|
||
Future<void> msgNotice() async {
|
||
final Map<String, dynamic> requestData = {
|
||
'appFlag': 1,
|
||
'isRead': 1,
|
||
'pageNum': 1,
|
||
'pageSize': 5,
|
||
};
|
||
final response = await HttpService.to.get(
|
||
'appointment/unread_notice/page',
|
||
params: requestData,
|
||
);
|
||
if (response != null) {
|
||
final result = BaseModel.fromJson(response.data);
|
||
if (result.code == 0 && result.data != null) {
|
||
String total = result.data["total"].toString();
|
||
isNotice = int.parse(total) > 0;
|
||
updateUi();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 创建一个每5分钟执行一次的周期性定时器
|
||
void startAutoRefresh() {
|
||
stopAutoRefresh();
|
||
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
||
renderData();
|
||
});
|
||
}
|
||
|
||
void onRefresh() => renderData(isRefresh: true);
|
||
|
||
///停止定时器
|
||
void stopAutoRefresh() {
|
||
_refreshTimer?.cancel();
|
||
_refreshTimer = null;
|
||
}
|
||
|
||
/// 获取加氢枪列表
|
||
Future<void> fetchGasGunList() async {
|
||
try {
|
||
var response = await HttpService.to.get(
|
||
'appointment/station/getGasGunList?hydrogenId=${StorageService.to.userId}',
|
||
);
|
||
if (response != null && response.data != null) {
|
||
final result = BaseModel<dynamic>.fromJson(response.data);
|
||
if (result.code == 0 && result.data != null) {
|
||
List dataList = result.data as List;
|
||
|
||
gasGunList.clear();
|
||
gasGunMap.clear();
|
||
|
||
for (var item in dataList) {
|
||
String name = item['deviceName'].toString();
|
||
// 将名称加入列表供 Dropdown/Picker 使用
|
||
gasGunList.add(name);
|
||
// 将完整对象存入 Map,方便后续通过 name 获取 sign
|
||
gasGunMap[name] = Map<String, dynamic>.from(item);
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
Logger.d("获取加氢枪列表失败: $e");
|
||
}
|
||
}
|
||
|
||
/// 获取预约数据的方法
|
||
Future<void> fetchReservationData() async {
|
||
showLoading("加载中");
|
||
|
||
final String searchText = searchController.text.trim();
|
||
|
||
try {
|
||
var response = await HttpService.to.post(
|
||
"appointment/orderAddHyd/sitOrderPage",
|
||
data: {
|
||
'stationName': name, // 使用从 renderData 中获取到的 name
|
||
'pageNum': 1,
|
||
'pageSize': 50, // 暂时不考虑分页,一次获取30条
|
||
'keyword': searchText, // 加氢站名称、手机号
|
||
'stationId': StorageService.to.userId,
|
||
},
|
||
);
|
||
|
||
// 安全校验
|
||
if (response == null || response.data == null) {
|
||
showToast('暂时无法获取预约数据');
|
||
hasReservationData = false;
|
||
reservationList = [];
|
||
dismissLoading();
|
||
return;
|
||
}
|
||
|
||
final baseModel = BaseModel<dynamic>.fromJson(response.data);
|
||
|
||
if (baseModel.code == 0 && baseModel.data != null) {
|
||
// 【核心修改】处理接口返回的列表数据
|
||
final dataMap = baseModel.data as Map<String, dynamic>;
|
||
|
||
final List<dynamic> listFromServer = dataMap['records'] ?? [];
|
||
|
||
// 使用 .map() 遍历列表,将每个 item 转换为一个 ReservationModel 对象
|
||
reservationList = listFromServer.map((item) {
|
||
return ReservationModel.fromJson(item as Map<String, dynamic>);
|
||
}).toList();
|
||
|
||
// 根据列表是否为空来更新 hasReservationData 状态
|
||
hasReservationData = reservationList.isNotEmpty;
|
||
} else {
|
||
// 接口返回业务错误
|
||
showToast(baseModel.message);
|
||
hasReservationData = false;
|
||
reservationList = []; // 清空列表
|
||
}
|
||
} catch (e) {
|
||
// 捕获网络或解析异常
|
||
showToast('获取预约数据失败');
|
||
hasReservationData = false;
|
||
reservationList = []; // 清空列表
|
||
} finally {
|
||
// 无论成功失败,最后都要关闭加载动画并更新UI
|
||
dismissLoading();
|
||
updateUi();
|
||
}
|
||
}
|
||
|
||
/// 确认预约弹窗
|
||
Future<void> confirmReservation(
|
||
String id, {
|
||
bool isEdit = false,
|
||
bool isAdd = false,
|
||
}) async {
|
||
ReservationModel item;
|
||
//处理是否是无预约车辆加氢数据
|
||
if (!isAdd) {
|
||
item = reservationList.firstWhere(
|
||
(item) => item.id == id,
|
||
orElse: () => throw Exception('Reservation not found'),
|
||
);
|
||
} else {
|
||
// 如果是无预约车辆加氢,创建一个临时 model
|
||
item = ReservationModel(
|
||
id: "",
|
||
stationId: StorageService.to.userId ?? "",
|
||
plateNumber: "---",
|
||
amount: "0.000",
|
||
time: "",
|
||
contactPerson: "",
|
||
contactPhone: "",
|
||
hasEdit: true,
|
||
contacts: "",
|
||
phone: "",
|
||
stationName: name,
|
||
startTime: "",
|
||
endTime: "",
|
||
date: "",
|
||
hydAmount: "0.000",
|
||
state: "",
|
||
stateName: "",
|
||
addStatus: "",
|
||
addStatusName: "",
|
||
rejectReason: "",
|
||
isTruckAttachment: 0,
|
||
hasDrivingAttachment: false,
|
||
hasHydrogenationAttachment: false,
|
||
isEdit: "0",
|
||
drivingAttachments: [],
|
||
hydrogenationAttachments: [],
|
||
);
|
||
}
|
||
|
||
//车牌输入
|
||
final TextEditingController plateController = TextEditingController(
|
||
text: item.plateNumber == "---" ? "" : item.plateNumber,
|
||
);
|
||
|
||
// 加氢量保留3位小数
|
||
double initialAmount = double.tryParse(item.hydAmount) ?? 0.0;
|
||
final TextEditingController amountController = TextEditingController(
|
||
text: initialAmount.toStringAsFixed(3),
|
||
);
|
||
|
||
final RxString selectedGun = (gasGunList.isNotEmpty ? gasGunList.first : '').obs;
|
||
final RxBool isOfflineChecked = false.obs;
|
||
|
||
Get.dialog(
|
||
Dialog(
|
||
insetPadding: EdgeInsets.only(left: 20.w, right: 20.w),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
child: SingleChildScrollView(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20.0),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
isAdd ? "无预约车辆加氢" : (isEdit ? '修改加氢量' : '确认加氢状态'),
|
||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 车牌号及标签
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 80.w,
|
||
child: TextField(
|
||
controller: plateController,
|
||
style: TextStyle(
|
||
color: const Color.fromRGBO(51, 51, 51, 1),
|
||
fontSize: 14.sp,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
decoration: InputDecoration(
|
||
hintText: item.plateNumber == "---" ? '_ _ _ _ _ _' : '修正车牌',
|
||
hintStyle: TextStyle(
|
||
color: const Color.fromRGBO(51, 51, 51, 1),
|
||
fontSize: 13.sp,
|
||
),
|
||
border: InputBorder.none,
|
||
isDense: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
isEdit
|
||
? SizedBox()
|
||
: GestureDetector(
|
||
onTap: () async {
|
||
String? temp = await takePhotoAndRecognize(true);
|
||
if (temp != null && temp.isNotEmpty) {
|
||
plateController.text = temp;
|
||
}
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8,
|
||
vertical: 2,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Color.fromRGBO(232, 243, 255, 1),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
item.plateNumber == "---" ? '车牌号识别' : '重新识别',
|
||
style: TextStyle(
|
||
color: Color.fromRGBO(22, 93, 255, 1),
|
||
fontSize: 13.sp,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
SizedBox(width: 16.w),
|
||
if (item.plateNumber != "---" && item.hasDrivingAttachment)
|
||
buildInfoTag('行驶证', item.drivingAttachments),
|
||
if (item.plateNumber != "---" && item.hasHydrogenationAttachment)
|
||
buildInfoTag('加氢证', item.hydrogenationAttachments),
|
||
],
|
||
),
|
||
|
||
SizedBox(height: 6.h),
|
||
|
||
// 提示逻辑
|
||
if (isEdit)
|
||
Text(
|
||
'每个订单只能修改一次,请确认加氢量准确无误',
|
||
style: TextStyle(
|
||
color: Colors.red,
|
||
fontSize: 12.sp,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
)
|
||
else if (item.plateNumber == "---" || item.isTruckAttachment == 0)
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'车辆未上传加氢证,请完成线下登记',
|
||
style: TextStyle(
|
||
color: Colors.red,
|
||
fontSize: 12.sp,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
),
|
||
),
|
||
Obx(
|
||
() => Checkbox(
|
||
value: isOfflineChecked.value,
|
||
onChanged: (v) => isOfflineChecked.value = v ?? false,
|
||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
activeColor: AppTheme.themeColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
SizedBox(height: 6.h),
|
||
|
||
// 预定加氢量输入区
|
||
Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF7F8FA),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'预定加氢量',
|
||
style: TextStyle(
|
||
color: Color.fromRGBO(51, 51, 51, 1),
|
||
fontSize: 14.sp,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||
textBaseline: TextBaseline.alphabetic,
|
||
children: [
|
||
IntrinsicWidth(
|
||
child: TextField(
|
||
controller: amountController,
|
||
keyboardType: const TextInputType.numberWithOptions(
|
||
decimal: true,
|
||
),
|
||
inputFormatters: [
|
||
// 限制最多输入3位小数
|
||
FilteringTextInputFormatter.allow(
|
||
RegExp(r'^\d+\.?\d{0,3}'),
|
||
),
|
||
],
|
||
style: const TextStyle(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.bold,
|
||
color: Color(0xFF017143),
|
||
),
|
||
decoration: const InputDecoration(
|
||
enabledBorder: UnderlineInputBorder(
|
||
borderSide: BorderSide(color: Color(0xFF017143)),
|
||
),
|
||
focusedBorder: const UnderlineInputBorder(
|
||
borderSide: BorderSide(color: Color(0xFF017143)),
|
||
),
|
||
isDense: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
),
|
||
const Text(
|
||
' KG',
|
||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
GestureDetector(
|
||
onTap: () async {
|
||
String? temp = await takePhotoAndRecognize(
|
||
false,
|
||
deviceName: selectedGun.value,
|
||
sign: getSignByDeviceName(selectedGun.value),
|
||
);
|
||
if (temp != null && temp.isNotEmpty) {
|
||
amountController.text = temp;
|
||
}
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 8,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF017143),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Row(
|
||
children: [
|
||
Icon(
|
||
Icons.camera_alt_outlined,
|
||
color: Colors.white,
|
||
size: 18,
|
||
),
|
||
SizedBox(width: 4),
|
||
Text(
|
||
'识别',
|
||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 加氢枪号选择
|
||
const Text('请选择加氢枪号', style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||
const SizedBox(height: 8),
|
||
Obx(
|
||
() => Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: Colors.grey.shade300),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: DropdownButtonHideUnderline(
|
||
child: DropdownButton<String>(
|
||
value: selectedGun.value.isEmpty ? null : selectedGun.value,
|
||
isExpanded: true,
|
||
hint: const Text('请选择加氢枪号'),
|
||
items: gasGunList.map((String gun) {
|
||
return DropdownMenuItem<String>(value: gun, child: Text(gun));
|
||
}).toList(),
|
||
onChanged: (v) => selectedGun.value = v ?? '',
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
// 按钮
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 2,
|
||
child: ElevatedButton(
|
||
onPressed: () {
|
||
//加氢后 订单编辑
|
||
if (isEdit) {
|
||
final num addHydAmount =
|
||
num.tryParse(amountController.text) ?? 0;
|
||
upDataService(
|
||
id,
|
||
0,
|
||
1,
|
||
addHydAmount,
|
||
"",
|
||
item!,
|
||
gunNumber: selectedGun.value,
|
||
plateNumber: item.plateNumber,
|
||
isEdit: true,
|
||
);
|
||
Get.back();
|
||
return;
|
||
}
|
||
//订单确认
|
||
if (!isEdit &&
|
||
(item!.plateNumber == "---" ||
|
||
item.isTruckAttachment == 0) &&
|
||
!isOfflineChecked.value) {
|
||
showToast("车辆未上传加氢证 , 请确保线下登记后点击确认");
|
||
return;
|
||
}
|
||
if (selectedGun.value.isEmpty) {
|
||
showToast("请选择加氢枪号");
|
||
return;
|
||
}
|
||
//无预约订单
|
||
if (isAdd) {
|
||
final num addHydAmount =
|
||
num.tryParse(amountController.text) ?? 0;
|
||
upDataService(
|
||
id,
|
||
0,
|
||
1,
|
||
addHydAmount,
|
||
"",
|
||
item,
|
||
gunNumber: selectedGun.value,
|
||
plateNumber: plateController.text,
|
||
isAdd: true,
|
||
);
|
||
Get.back();
|
||
return;
|
||
}
|
||
//有预约订单确认
|
||
Get.back();
|
||
final num addHydAmount =
|
||
num.tryParse(amountController.text) ?? 0;
|
||
upDataService(
|
||
id,
|
||
0,
|
||
1,
|
||
addHydAmount,
|
||
"",
|
||
item,
|
||
gunNumber: selectedGun.value,
|
||
plateNumber: plateController.text,
|
||
);
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF017143),
|
||
minimumSize: const Size(double.infinity, 48),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
elevation: 0,
|
||
),
|
||
child: Text(
|
||
isEdit ? '确认修改' : '确认加氢',
|
||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
flex: 1,
|
||
child: OutlinedButton(
|
||
onPressed: () {
|
||
Get.back();
|
||
if (!isEdit && !isAdd) {
|
||
upDataService(
|
||
id,
|
||
0,
|
||
2,
|
||
0,
|
||
"",
|
||
item!,
|
||
gunNumber: selectedGun.value,
|
||
plateNumber: plateController.text,
|
||
);
|
||
}
|
||
},
|
||
style: OutlinedButton.styleFrom(
|
||
minimumSize: const Size(double.infinity, 48),
|
||
side: BorderSide(color: Colors.grey.shade300),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
),
|
||
child: Text(
|
||
isEdit || isAdd ? '取消' : '未加氢',
|
||
style: const TextStyle(color: Colors.grey, fontSize: 16),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
const Expanded(
|
||
child: Divider(color: Color(0xFFEEEEEE), thickness: 1),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
child: GestureDetector(
|
||
onTap: () => Get.back(),
|
||
child: Text(
|
||
'暂不处理',
|
||
style: TextStyle(
|
||
color: Color.fromRGBO(16, 185, 129, 1),
|
||
fontSize: 14.sp,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const Expanded(
|
||
child: Divider(color: Color(0xFFEEEEEE), thickness: 1),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
barrierDismissible: false,
|
||
);
|
||
}
|
||
|
||
/// 保存图片到相册
|
||
Future<void> saveImageToLocal(String url) async {
|
||
try {
|
||
// 1. 权限请求
|
||
if (Platform.isAndroid) {
|
||
var status = await Permission.storage.request();
|
||
if (!status.isGranted) {
|
||
showErrorToast("请在系统设置中开启存储权限");
|
||
return;
|
||
}
|
||
} else {
|
||
var status = await Permission.photos.request();
|
||
if (!status.isGranted) {
|
||
showErrorToast("请在系统设置中开启相册权限");
|
||
return;
|
||
}
|
||
}
|
||
|
||
showLoading("正在保存...");
|
||
|
||
// 2. 下载图片
|
||
var response = await Dio().get(
|
||
url,
|
||
options: Options(responseType: ResponseType.bytes),
|
||
);
|
||
|
||
// 3. 保存到相册
|
||
final result = await ImageGallerySaver.saveImage(
|
||
Uint8List.fromList(response.data),
|
||
quality: 100,
|
||
name: "certificate_${DateTime.now().millisecondsSinceEpoch}",
|
||
);
|
||
|
||
dismissLoading();
|
||
|
||
if (result != null && result['isSuccess'] == true) {
|
||
showSuccessToast("图片已保存至相册");
|
||
} else {
|
||
showErrorToast("保存失败");
|
||
}
|
||
} catch (e) {
|
||
dismissLoading();
|
||
showErrorToast("保存异常");
|
||
}
|
||
}
|
||
|
||
Widget buildInfoTag(String label, List<String> images) {
|
||
return GestureDetector(
|
||
onTap: () {
|
||
showImagePreview(images);
|
||
},
|
||
child: Container(
|
||
margin: const EdgeInsets.only(left: 4),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF2F3F5),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(color: Color(0xFF999999), fontSize: 11.sp),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 显示图片预览弹窗
|
||
void showImagePreview(List<String> images) {
|
||
if (images.isEmpty) return;
|
||
|
||
final RxInt currentIndex = 0.obs;
|
||
final PageController pageController = PageController();
|
||
|
||
Get.dialog(
|
||
GestureDetector(
|
||
onTap: () => Get.back(),
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 图片翻页
|
||
SizedBox(
|
||
height: Get.height * 0.5,
|
||
child: PhotoViewGallery.builder(
|
||
scrollPhysics: const BouncingScrollPhysics(),
|
||
builder: (BuildContext context, int index) {
|
||
return PhotoViewGalleryPageOptions(
|
||
imageProvider: NetworkImage(images[index]),
|
||
initialScale: PhotoViewComputedScale.contained,
|
||
heroAttributes: PhotoViewHeroAttributes(tag: images[index]),
|
||
onTapDown: (context, details, controllerValue) {
|
||
_showSaveImageMenu(images[index]);
|
||
},
|
||
);
|
||
},
|
||
itemCount: images.length,
|
||
loadingBuilder: (context, event) => const Center(
|
||
child: CircularProgressIndicator(color: Colors.white),
|
||
),
|
||
backgroundDecoration: const BoxDecoration(
|
||
color: Colors.transparent,
|
||
),
|
||
pageController: pageController,
|
||
onPageChanged: (index) => currentIndex.value = index,
|
||
),
|
||
),
|
||
SizedBox(height: 10.h),
|
||
// 页码指示器
|
||
Center(
|
||
child: Text(
|
||
"${currentIndex.value + 1} / ${images.length}",
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16,
|
||
decoration: TextDecoration.none,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 关闭按钮
|
||
Positioned(
|
||
top: 150.h,
|
||
right: 20,
|
||
child: IconButton(
|
||
icon: const Icon(Icons.close, color: Colors.white, size: 30),
|
||
onPressed: () => Get.back(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
useSafeArea: false,
|
||
);
|
||
}
|
||
|
||
void _showSaveImageMenu(String url) {
|
||
Get.bottomSheet(
|
||
Container(
|
||
color: Colors.white,
|
||
child: SafeArea(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
ListTile(
|
||
leading: const Icon(Icons.download),
|
||
title: const Text('保存图片到相册'),
|
||
onTap: () {
|
||
Get.back();
|
||
saveImageToLocal(url);
|
||
},
|
||
),
|
||
const Divider(height: 1),
|
||
ListTile(
|
||
title: const Text('取消', textAlign: TextAlign.center),
|
||
onTap: () => Get.back(),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> rejectReservation(String id) async {
|
||
final item = reservationList.firstWhere((item) => item.id == id);
|
||
final TextEditingController reasonController = TextEditingController();
|
||
final RxString selectedReason = ''.obs;
|
||
|
||
// 预设的拒绝原因列表
|
||
final List<String> presetReasons = ['车辆未到场', '司机联系不上', '设备故障无法加氢'];
|
||
|
||
Get.dialog(
|
||
Dialog(
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
child: SingleChildScrollView(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24.0),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
const Text(
|
||
'拒绝预约',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'正在处理车牌号 ${item.plateNumber} 的预约',
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(fontSize: 16, color: AppTheme.themeColor),
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
// 4. 预设原因选择区域
|
||
const Text('选择或填写拒绝原因:', style: TextStyle(color: Colors.grey)),
|
||
const SizedBox(height: 8),
|
||
Obx(
|
||
() => Wrap(
|
||
// 使用 Wrap 自动换行
|
||
spacing: 8.0, // 水平间距
|
||
children: presetReasons.map((reason) {
|
||
final isSelected = selectedReason.value == reason;
|
||
return ChoiceChip(
|
||
label: Text(reason),
|
||
selected: isSelected,
|
||
onSelected: (selected) {
|
||
if (selected) {
|
||
selectedReason.value = reason;
|
||
reasonController.clear(); // 选择预设原因时,清空自定义输入
|
||
}
|
||
},
|
||
selectedColor: Get.theme.primaryColor.withOpacity(0.2),
|
||
labelStyle: TextStyle(
|
||
color: isSelected ? Get.theme.primaryColor : Colors.black,
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 5. 自定义原因输入框
|
||
TextField(
|
||
controller: reasonController,
|
||
maxLines: 2,
|
||
decoration: InputDecoration(
|
||
hintText: '输入其它原因',
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||
),
|
||
),
|
||
onChanged: (text) {
|
||
if (text.isNotEmpty) {
|
||
selectedReason.value = ''; // 输入自定义原因时,取消预设选择
|
||
}
|
||
},
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
// 6. 按钮区域
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextButton(
|
||
onPressed: () {
|
||
// 获取最终的拒绝原因
|
||
String finalReason = reasonController.text.trim();
|
||
if (finalReason.isEmpty) {
|
||
finalReason = selectedReason.value;
|
||
}
|
||
|
||
if (finalReason.isEmpty) {
|
||
showToast('请选择或填写一个拒绝原因');
|
||
return;
|
||
}
|
||
|
||
Get.back(); // 关闭弹窗
|
||
upDataService(
|
||
id,
|
||
1,
|
||
-1,
|
||
0,
|
||
finalReason,
|
||
item,
|
||
plateNumber: item.plateNumber,
|
||
);
|
||
},
|
||
child: const Text('确认拒绝', style: TextStyle(color: Colors.red)),
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: TextButton(
|
||
onPressed: () => Get.back(),
|
||
child: const Text('暂不处理', style: TextStyle(color: Colors.grey)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
barrierDismissible: false,
|
||
);
|
||
}
|
||
|
||
//addStatus 1完成 2未加 -1拒绝
|
||
void upDataService(
|
||
String id,
|
||
int status,
|
||
int addStatus,
|
||
num addHydAmount,
|
||
String rejectReason,
|
||
ReservationModel item, {
|
||
String? gunNumber,
|
||
String? plateNumber,
|
||
bool isEdit = false,
|
||
bool isAdd = false,
|
||
}) async {
|
||
showLoading("确认中");
|
||
|
||
try {
|
||
var responseData;
|
||
if (isAdd) {
|
||
responseData = await HttpService.to.post(
|
||
'appointment/orderAddHyd/addOfflineOrder',
|
||
data: {
|
||
"addHydAmount": addHydAmount,
|
||
"plateNumber": plateNumber,
|
||
if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber,
|
||
},
|
||
);
|
||
} else if (isEdit) {
|
||
responseData = await HttpService.to.post(
|
||
'appointment/orderAddHyd/modifyOrder',
|
||
data: {
|
||
'id': id,
|
||
"addHydAmount": addHydAmount,
|
||
"plateNumber": plateNumber,
|
||
if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber,
|
||
},
|
||
);
|
||
} else if (addStatus == -1) {
|
||
responseData = await HttpService.to.post(
|
||
'appointment/orderAddHyd/rejectOrder',
|
||
data: {
|
||
'id': id,
|
||
'state': -1, //拒绝使用
|
||
"rejectReason": rejectReason,
|
||
"plateNumber": plateNumber,
|
||
if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber,
|
||
},
|
||
);
|
||
} else {
|
||
responseData = await HttpService.to.post(
|
||
'appointment/orderAddHyd/completeOrder',
|
||
data: {
|
||
'id': id,
|
||
'addStatus': addStatus, //完成使用 完成1,未加2
|
||
"addHydAmount": addHydAmount,
|
||
"plateNumber": plateNumber,
|
||
if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber,
|
||
},
|
||
);
|
||
}
|
||
|
||
if (responseData == null || responseData.data == null) {
|
||
dismissLoading();
|
||
return;
|
||
}
|
||
var result = BaseModel.fromJson(responseData.data);
|
||
if (result.code == 0) {
|
||
showSuccessToast("操作成功");
|
||
} else {
|
||
showToast(result.message);
|
||
}
|
||
dismissLoading();
|
||
|
||
//1完成 2未加 -1拒绝
|
||
if (addStatus == 1) {
|
||
item.status = ReservationStatus.completed;
|
||
item.amount = "${addHydAmount}kg";
|
||
} else if (addStatus == -1) {
|
||
item.status = ReservationStatus.rejected;
|
||
} else if (addStatus == 2) {
|
||
item.status = ReservationStatus.unadded;
|
||
}
|
||
renderData();
|
||
} catch (e) {
|
||
dismissLoading();
|
||
}
|
||
}
|
||
|
||
//车牌&加氢量 识别
|
||
Future<String?> takePhotoAndRecognize(
|
||
bool isPlate, {
|
||
String deviceName = "",
|
||
String sign = "",
|
||
}) async {
|
||
var status = await Permission.camera.request();
|
||
if (!status.isGranted) {
|
||
if (status.isPermanentlyDenied) openAppSettings();
|
||
showErrorToast("需要相机权限才能拍照识别");
|
||
return "";
|
||
}
|
||
|
||
final XFile? photo = await ImagePicker().pickImage(
|
||
source: ImageSource.camera,
|
||
imageQuality: 80, // 压缩图片质量以加快上传
|
||
);
|
||
if (photo == null) {
|
||
return "";
|
||
}
|
||
|
||
//上传文件
|
||
String? imageUrl = await uploadFile(photo.path);
|
||
String? ocrStr = "";
|
||
if (imageUrl != null) {
|
||
// 获取车牌号
|
||
if (isPlate) {
|
||
ocrStr = await getPlateNumber(imageUrl);
|
||
} else {
|
||
ocrStr = await getHyd(imageUrl, deviceName, sign);
|
||
}
|
||
return ocrStr;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
String getSignByDeviceName(String deviceName) {
|
||
return gasGunMap[deviceName]?['sign']?.toString() ?? '';
|
||
}
|
||
|
||
/// 上传图片
|
||
Future<String?> uploadFile(String filePath) async {
|
||
showLoading("正在上传图片...");
|
||
try {
|
||
dio.FormData formData = dio.FormData.fromMap({
|
||
'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_plate.jpg'),
|
||
});
|
||
|
||
var response = await HttpService.to.post("appointment/ocr/upload", data: formData);
|
||
if (response != null) {
|
||
final result = BaseModel.fromJson(response.data);
|
||
if (result.code == 0) return result.data.toString();
|
||
showErrorToast(result.error);
|
||
}
|
||
} catch (e) {
|
||
showErrorToast("图片上传失败");
|
||
} finally {
|
||
dismissLoading();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// OCR 识别
|
||
Future<String?> getPlateNumber(String imageUrl) async {
|
||
showLoading("正在识别车牌...");
|
||
try {
|
||
var response = await HttpService.to.get(
|
||
"appointment/ocr/getPlateNumber",
|
||
params: {'imageUrl': imageUrl},
|
||
);
|
||
if (response != null) {
|
||
final result = BaseModel.fromJson(response.data);
|
||
if (result.code == 0) return result.data.toString();
|
||
showErrorToast(result.error);
|
||
}
|
||
} catch (e) {
|
||
showErrorToast("车牌识别失败");
|
||
} finally {
|
||
dismissLoading();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
//加氢量识别 (加油枪列表接口返回的deviceName) (加油枪列表接口返回的sign)
|
||
Future<String?> getHyd(String imageUrl, String deviceName, String sign) async {
|
||
showLoading("正在识别加氢量...");
|
||
try {
|
||
var response = await HttpService.to.post(
|
||
"appointment/hyd-ocr/get-info",
|
||
data: {"url": imageUrl, "deviceName": deviceName, "sign": sign},
|
||
);
|
||
if (response != null) {
|
||
final result = BaseModel.fromJson(response.data);
|
||
if (result.code == 0) return result.data["mass"].toString();
|
||
showErrorToast(result.error);
|
||
}
|
||
} catch (e) {
|
||
showErrorToast("车牌识别失败");
|
||
} finally {
|
||
dismissLoading();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
String leftHydrogen = "";
|
||
String orderAmount = "";
|
||
String completedAmount = "";
|
||
String name = "";
|
||
String orderTotalAmount = "";
|
||
String orderUnfinishedAmount = "";
|
||
|
||
Future<void> renderData({bool isRefresh = false}) async {
|
||
try {
|
||
var responseData = await HttpService.to.get(
|
||
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
|
||
);
|
||
|
||
if (responseData == null || responseData.data == null) {
|
||
showToast('暂时无法获取站点信息');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
var result = BaseModel.fromJson(responseData.data);
|
||
|
||
leftHydrogen = result.data["leftHydrogen"] ?? "";
|
||
orderAmount = result.data["orderAmount"].toString();
|
||
completedAmount = result.data["completedAmount"].toString();
|
||
name = result.data["name"].toString();
|
||
orderTotalAmount = result.data["orderTotalAmount"].toString();
|
||
orderUnfinishedAmount = result.data["orderUnfinishedAmount"].toString();
|
||
|
||
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
|
||
orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString();
|
||
orderUnfinishedAmount = orderUnfinishedAmount.isEmpty
|
||
? "统计中"
|
||
: orderUnfinishedAmount.toString();
|
||
} catch (e) {
|
||
showToast('数据异常');
|
||
}
|
||
} catch (e) {
|
||
} finally {
|
||
//加载列表数据
|
||
fetchReservationData();
|
||
|
||
if (isRefresh) {
|
||
refreshController.refreshCompleted();
|
||
}
|
||
}
|
||
}
|
||
}
|