diff --git a/ln_jq_app/android/app/build.gradle.kts b/ln_jq_app/android/app/build.gradle.kts
index 2539bdf..65f652d 100644
--- a/ln_jq_app/android/app/build.gradle.kts
+++ b/ln_jq_app/android/app/build.gradle.kts
@@ -37,8 +37,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
- versionCode = 6
- versionName = "1.2.3"
+ versionCode = 7
+ versionName = "1.2.4"
}
signingConfigs {
diff --git a/ln_jq_app/android/app/src/main/AndroidManifest.xml b/ln_jq_app/android/app/src/main/AndroidManifest.xml
index 5392736..c23b807 100644
--- a/ln_jq_app/android/app/src/main/AndroidManifest.xml
+++ b/ln_jq_app/android/app/src/main/AndroidManifest.xml
@@ -1,47 +1,50 @@
-
-
-
-
-
-
-
+
+
+
+
+ android:icon="@mipmap/logo"
+ android:label="小羚羚">
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
-
+
+
@@ -99,6 +125,7 @@
android:exported="true">
+
@@ -117,8 +144,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
-
-
+
+
diff --git a/ln_jq_app/assets/html/map.html b/ln_jq_app/assets/html/map.html
index 10f4855..a4d16d3 100644
--- a/ln_jq_app/assets/html/map.html
+++ b/ln_jq_app/assets/html/map.html
@@ -25,7 +25,7 @@
display: none !important;
}
- /* 去除高德默认的 label 边框和背景 */
+ /* 去除高德默认的 label 边框 and 背景 */
.amap-marker-label {
border: none !important;
background-color: transparent !important;
@@ -109,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */
#panel {
position: fixed;
- bottom: 75px;
+ bottom: 95px;
left: 0;
width: 100%;
height: 35%;
@@ -129,7 +129,7 @@
#location-btn {
position: fixed;
right: 10px;
- bottom: 75px;
+ bottom: 105px;
/* 默认位置 */
width: 44px;
height: 44px;
@@ -159,7 +159,7 @@
/* --- 调整比例尺位置 --- */
.amap-scalecontrol {
/* 初始状态:避开底部的定位按钮或留出安全间距 */
- bottom: 80px !important;
+ bottom: 110px !important;
left: 10px !important;
transition: bottom 0.3s ease;
/* 增加平滑动画 */
@@ -195,10 +195,10 @@
@@ -221,6 +221,7 @@
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
+ var stationMarkers = []; // 存储所有站点的标记
function initMap() {
@@ -243,6 +244,11 @@
}
});
+ // 点击地图空白处重置状态
+ map.on('click', function() {
+ resetSearchState();
+ });
+
// 添加基础控件
map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({
@@ -284,6 +290,19 @@
});
}
+ /**
+ * 重置搜索状态,隐藏面板和路线
+ */
+ function resetSearchState() {
+ if (document.body.classList.contains('panel-active')) {
+ console.log("JS->: 重置地图状态");
+ document.body.classList.remove('panel-active');
+ var panel = document.getElementById('panel');
+ panel.style.display = 'none';
+ if (driving) driving.clear();
+ }
+ }
+
/**
* 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
@@ -336,6 +355,8 @@
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
+ fetchStationInfoList(lat, lng);
+
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
@@ -397,7 +418,6 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
- // "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
},
body: JSON.stringify({
province: province,
@@ -437,6 +457,79 @@
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
+ /**
+ * 获取站点列表
+ */
+ function fetchStationInfoList(lat, lng) {
+ fetch('https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ longitude: lng,
+ latitude: lat,
+ })
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('网络响应错误: ' + response.status);
+ }
+ return response.json(); // 解析 JSON
+ })
+ .then(res => {
+ console.log("JS->:2 接口完整返回:", JSON.stringify(res));
+ if (res.code === 0 && res.data && Array.isArray(res.data)) {
+ // 1. 清除旧的站点标记
+ stationMarkers.forEach(m => m.setMap(null));
+ stationMarkers = [];
+
+ // 2. 循环标记所有加氢站
+ res.data.forEach(station => {
+ var stationIcon = new AMap.Icon({
+ size: new AMap.Size(32, 32),
+ image: 'ic_tag.png',
+ imageSize: new AMap.Size(32, 32)
+ });
+
+ var sMarker = new AMap.Marker({
+ map: map,
+ position: [station.longitude, station.latitude],
+ icon: stationIcon,
+ offset: new AMap.Pixel(-16, -32),
+ title: station.name,
+ label: {
+ content: '' + station.name +
+ '
',
+ direction: 'top'
+ }
+ });
+
+ // 3. 绑定点击事件:选中即为目的地,并开始规划
+ sMarker.on('click', function () {
+ var stationName = station.name || "目的地";
+ document.getElementById('endInput').value = station.address ||
+ stationName;
+
+ // 更新当前的 destMarker
+ if (destMarker && destMarker !== sMarker) destMarker.setMap(null);
+ destMarker = sMarker;
+
+ // 直接传入坐标对象,避免关键字搜索失败
+ var loc = new AMap.LngLat(station.longitude, station.latitude);
+ startRouteSearch(loc);
+ });
+
+ stationMarkers.push(sMarker);
+ });
+
+ } else {
+ console.log("JS->: 业务报错或无数据:", res.message);
+ }
+ })
+ .catch(err => console.error('JS->:获取站点信息失败:', err));
+ }
+
/**
* 地理编码并在地图标记终点
*/
@@ -447,7 +540,6 @@
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
- // 假设图标大小为 32x32,你可以根据实际图片尺寸调整 Size
var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径
@@ -459,8 +551,6 @@
map: map,
position: [longitude, latitude],
icon: destIcon, // 使用自定义图标
- // 偏移量:如果图标底部中心是尖角,offset 设为宽的一半的负数,高度的负数
- // 这样能确保图片的底部尖端指向地图上的精确位置
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
@@ -469,17 +559,7 @@
}
});
- // 4. 打印调试信息
- console.log("JS->: 终点标记已添加", address, loc.toString());
-
- // 5. 自动调整视野包含起点和终点
- // if (marker) {
- // // 如果起点标志已存在,缩放地图以展示两者
- // map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
- // } else {
- // // 如果没有起点,直接跳到终点
- // map.setCenter(loc);
- // }
+ console.log("JS->: 终点标记已添加", address);
}
/**
@@ -498,11 +578,12 @@
}
}
+
/**
- * 路径规划
+ * 路径规划
+ * @param {AMap.LngLat} [destLoc] 可选的终点坐标
*/
- function startRouteSearch() {
- // 获取输入框的文字
+ function startRouteSearch(destLoc) {
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
@@ -510,63 +591,59 @@
alert("请输入起点");
return;
}
-
if (!endKw) {
alert("请输入终点");
return;
}
- // 清除旧路线
if (driving) driving.clear();
-
- // 收起键盘
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
- // --- 构造路径规划的点 (使用数组方式,更灵活) ---
var points = [];
- // 1. 处理起点逻辑
- // 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
- if (!startKw || startKw === '我的位置') {
+ // 1. 起点逻辑
+ if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
if (!currentLng || !currentLat) {
- // 如果还没获取到定位
- if (window.flutter_inappwebview) {
- window.flutter_inappwebview.callHandler('requestLocation');
- }
+ if (window.flutter_inappwebview) window.flutter_inappwebview.callHandler('requestLocation');
alert("正在获取定位,请稍后...");
return;
}
- // 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
points.push({
- keyword: '我的位置', // 用于显示的名字
- location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
+ keyword: '我的位置',
+ location: new AMap.LngLat(currentLng, currentLat)
});
} else {
- // 如果用户手动输入了地点 (例如 "北京南站")
- // 直接存入关键词,让高德自己去搜
points.push({
keyword: startKw
});
}
- // 2. 处理终点逻辑 (通常是关键词)
- points.push({
- keyword: endKw
- });
+ // 2. 终点逻辑:如果有传入坐标,则直接使用坐标导航,成功率最高
+ if (destLoc) {
+ points.push({
+ keyword: endKw,
+ location: destLoc // 关键:使用精确坐标
+ });
+ } else {
+ points.push({
+ keyword: endKw
+ });
+ }
// 3. 发起搜索
- // points 数组里现在是一个起点对象和一个终点对象
driving.search(points, function (status, result) {
if (status === 'complete') {
console.log('JS: 规划成功');
var panel = document.getElementById('panel');
panel.style.display = 'block';
document.body.classList.add('panel-active');
- } else {
- console.log('JS: 规划失败', result);
- alert("规划失败,请检查起终点名称");
}
+ // else {
+ // console.error('JS: 规划失败', result);
+ // // 如果坐标规划都失败了,通常是由于起终点距离过近或政策限制(如货车禁行)
+ // alert("路径规划未成功,请尝试微调起终点");
+ // }
});
}
diff --git a/ln_jq_app/assets/images/history_bg.png b/ln_jq_app/assets/images/history_bg.png
new file mode 100644
index 0000000..e06d5e1
Binary files /dev/null and b/ln_jq_app/assets/images/history_bg.png differ
diff --git a/ln_jq_app/assets/images/ic_attention@2x.png b/ln_jq_app/assets/images/ic_attention@2x.png
new file mode 100644
index 0000000..81c56dc
Binary files /dev/null and b/ln_jq_app/assets/images/ic_attention@2x.png differ
diff --git a/ln_jq_app/assets/images/ic_close@2x.png b/ln_jq_app/assets/images/ic_close@2x.png
new file mode 100644
index 0000000..a4c782e
Binary files /dev/null and b/ln_jq_app/assets/images/ic_close@2x.png differ
diff --git a/ln_jq_app/assets/images/ic_ex_menu@2x.png b/ln_jq_app/assets/images/ic_ex_menu@2x.png
new file mode 100644
index 0000000..c63c6b0
Binary files /dev/null and b/ln_jq_app/assets/images/ic_ex_menu@2x.png differ
diff --git a/ln_jq_app/assets/images/ic_h2_my@2x.png b/ln_jq_app/assets/images/ic_h2_my@2x.png
index a3bde44..a2a9e44 100644
Binary files a/ln_jq_app/assets/images/ic_h2_my@2x.png and b/ln_jq_app/assets/images/ic_h2_my@2x.png differ
diff --git a/ln_jq_app/assets/images/ic_h2_my_select@2x.png b/ln_jq_app/assets/images/ic_h2_my_select@2x.png
index 74cb09a..625e2af 100644
Binary files a/ln_jq_app/assets/images/ic_h2_my_select@2x.png and b/ln_jq_app/assets/images/ic_h2_my_select@2x.png differ
diff --git a/ln_jq_app/assets/images/ic_serch@2x.png b/ln_jq_app/assets/images/ic_serch@2x.png
new file mode 100644
index 0000000..cd6ae6a
Binary files /dev/null and b/ln_jq_app/assets/images/ic_serch@2x.png differ
diff --git a/ln_jq_app/assets/images/ic_upload@2x.png b/ln_jq_app/assets/images/ic_upload@2x.png
new file mode 100644
index 0000000..cb0583c
Binary files /dev/null and b/ln_jq_app/assets/images/ic_upload@2x.png differ
diff --git a/ln_jq_app/assets/images/mall_bar@2x.png b/ln_jq_app/assets/images/mall_bar@2x.png
new file mode 100644
index 0000000..651cb20
Binary files /dev/null and b/ln_jq_app/assets/images/mall_bar@2x.png differ
diff --git a/ln_jq_app/assets/images/mall_pay_success@2x.png b/ln_jq_app/assets/images/mall_pay_success@2x.png
new file mode 100644
index 0000000..5398a16
Binary files /dev/null and b/ln_jq_app/assets/images/mall_pay_success@2x.png differ
diff --git a/ln_jq_app/assets/images/rule_bg@2x.png b/ln_jq_app/assets/images/rule_bg@2x.png
new file mode 100644
index 0000000..016c043
Binary files /dev/null and b/ln_jq_app/assets/images/rule_bg@2x.png differ
diff --git a/ln_jq_app/assets/images/rule_bg_1@2x.png b/ln_jq_app/assets/images/rule_bg_1@2x.png
new file mode 100644
index 0000000..30fed05
Binary files /dev/null and b/ln_jq_app/assets/images/rule_bg_1@2x.png differ
diff --git a/ln_jq_app/assets/images/tips_1@2x.png b/ln_jq_app/assets/images/tips_1@2x.png
new file mode 100644
index 0000000..c7cbb9b
Binary files /dev/null and b/ln_jq_app/assets/images/tips_1@2x.png differ
diff --git a/ln_jq_app/assets/images/tips_2@2x.png b/ln_jq_app/assets/images/tips_2@2x.png
new file mode 100644
index 0000000..0f0b9a9
Binary files /dev/null and b/ln_jq_app/assets/images/tips_2@2x.png differ
diff --git a/ln_jq_app/assets/images/tips_3@2x.png b/ln_jq_app/assets/images/tips_3@2x.png
new file mode 100644
index 0000000..36157fb
Binary files /dev/null and b/ln_jq_app/assets/images/tips_3@2x.png differ
diff --git a/ln_jq_app/assets/images/tips_4@2x.png b/ln_jq_app/assets/images/tips_4@2x.png
new file mode 100644
index 0000000..82df4b1
Binary files /dev/null and b/ln_jq_app/assets/images/tips_4@2x.png differ
diff --git a/ln_jq_app/assets/images/tips_5@2x.png b/ln_jq_app/assets/images/tips_5@2x.png
new file mode 100644
index 0000000..b05d6bd
Binary files /dev/null and b/ln_jq_app/assets/images/tips_5@2x.png differ
diff --git a/ln_jq_app/assets/images/welcome.png b/ln_jq_app/assets/images/welcome.png
index 3a2d95e..3ea8db5 100644
Binary files a/ln_jq_app/assets/images/welcome.png and b/ln_jq_app/assets/images/welcome.png differ
diff --git a/ln_jq_app/ios/Podfile.lock b/ln_jq_app/ios/Podfile.lock
index 27b161b..a09f840 100644
--- a/ln_jq_app/ios/Podfile.lock
+++ b/ln_jq_app/ios/Podfile.lock
@@ -12,6 +12,8 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
+ - flutter_app_update (0.0.1):
+ - Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
@@ -39,6 +41,8 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
+ - saver_gallery (0.0.1):
+ - Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -50,6 +54,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
+ - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
@@ -59,6 +64,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
+ - saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -80,6 +86,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
+ flutter_app_update:
+ :path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash:
@@ -98,6 +106,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
+ saver_gallery:
+ :path: ".symlinks/plugins/saver_gallery/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
@@ -111,6 +121,7 @@ SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
@@ -121,6 +132,7 @@ SPEC CHECKSUMS:
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
+ saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
diff --git a/ln_jq_app/ios/Runner/Info.plist b/ln_jq_app/ios/Runner/Info.plist
index 0126237..629759d 100644
--- a/ln_jq_app/ios/Runner/Info.plist
+++ b/ln_jq_app/ios/Runner/Info.plist
@@ -76,5 +76,11 @@
en
+UIFileSharingEnabled
+
+
+LSSupportsOpeningDocumentsInPlace
+
+
diff --git a/ln_jq_app/lib/common/AuthGuard.dart b/ln_jq_app/lib/common/AuthGuard.dart
new file mode 100644
index 0000000..e0e269c
--- /dev/null
+++ b/ln_jq_app/lib/common/AuthGuard.dart
@@ -0,0 +1,22 @@
+import 'package:getx_scaffold/common/index.dart';
+import 'package:ln_jq_app/pages/login/view.dart';
+import 'package:ln_jq_app/storage_service.dart';
+
+class AuthGuard {
+ static bool _handling401 = false;
+
+ static Future handle401(String? message) async {
+ if (_handling401) return;
+ _handling401 = true;
+
+ try {
+ await StorageService.to.clearLoginInfo();
+ Get.offAll(() => const LoginPage());
+ } finally {
+ // 防止意外卡死,可视情况是否延迟重置
+ Future.delayed(const Duration(seconds: 1), () {
+ _handling401 = false;
+ });
+ }
+ }
+}
diff --git a/ln_jq_app/lib/common/model/station_model.dart b/ln_jq_app/lib/common/model/station_model.dart
index 1292f07..8291298 100644
--- a/ln_jq_app/lib/common/model/station_model.dart
+++ b/ln_jq_app/lib/common/model/station_model.dart
@@ -4,7 +4,9 @@ class StationModel {
final String address;
final String price;
final String siteStatusName; // 例如 "维修中"
- final int isSelect; // 新增字段 1是可用 0是不可用
+ final int isSelect; // 1是可用 0是不可用
+ final String startBusiness; // 新增:可预约最早开始时间,如 "06:00:00"
+ final String endBusiness; // 新增:可预约最晚结束时间,如 "22:00:00"
StationModel({
required this.hydrogenId,
@@ -13,9 +15,10 @@ class StationModel {
required this.price,
required this.siteStatusName,
required this.isSelect,
+ required this.startBusiness,
+ required this.endBusiness,
});
- // 从 JSON map 创建对象的工厂构造函数
factory StationModel.fromJson(Map json) {
return StationModel(
hydrogenId: json['hydrogenId'] ?? '',
@@ -23,7 +26,9 @@ class StationModel {
address: json['address'] ?? '地址未知',
price: json['price']?.toString() ?? '0.00',
siteStatusName: json['siteStatusName'] ?? '',
- isSelect: json['isSelect'] as int? ?? 0, // 新增字段的解析,默认为 0
+ isSelect: json['isSelect'] as int? ?? 0,
+ startBusiness: json['startBusiness'] ?? '00:00:00', // 默认全天
+ endBusiness: json['endBusiness'] ?? '23:59:59', // 默认全天
);
}
}
diff --git a/ln_jq_app/lib/main.dart b/ln_jq_app/lib/main.dart
index b2273f1..1ebe100 100644
--- a/ln_jq_app/lib/main.dart
+++ b/ln_jq_app/lib/main.dart
@@ -3,6 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:ln_jq_app/common/AuthGuard.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/token_interceptor.dart';
import 'package:ln_jq_app/storage_service.dart';
@@ -20,7 +21,7 @@ void main() async {
logTag: '小羚羚',
supportedLocales: [const Locale('zh', 'CN')],
);
-
+
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -65,6 +66,7 @@ void main() async {
void initHttpSet() {
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
+ HttpService.to.init(timeout: 15);
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
HttpService.to.setOnResponseHandler((response) async {
@@ -76,8 +78,7 @@ void initHttpSet() {
if (baseModel.code == 0 || baseModel.code == 200) {
return null;
} else if (baseModel.code == 401) {
- await StorageService.to.clearLoginInfo();
- Get.offAll(() => const LoginPage());
+ await AuthGuard.handle401(baseModel.message);
return baseModel.message;
} else {
return (baseModel.error.toString()).isEmpty
diff --git a/ln_jq_app/lib/pages/b_page/history/controller.dart b/ln_jq_app/lib/pages/b_page/history/controller.dart
index 63874b8..ef4c9dc 100644
--- a/ln_jq_app/lib/pages/b_page/history/controller.dart
+++ b/ln_jq_app/lib/pages/b_page/history/controller.dart
@@ -1,10 +1,11 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
-class HistoryController extends GetxController {
+class HistoryController extends GetxController with BaseControllerMixin {
+ @override
+ String get builderId => 'history';
+
// --- 定义 API 需要的日期格式化器 ---
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
@@ -13,8 +14,8 @@ class HistoryController extends GetxController {
final Rx endDate = DateTime.now().obs;
final TextEditingController plateNumberController = TextEditingController();
- final RxString totalHydrogen = '0 kg'.obs;
- final RxString totalCompletions = '0 次'.obs;
+ final RxString totalHydrogen = '0'.obs;
+ final RxString totalCompletions = '0'.obs;
final RxList historyList = [].obs;
final RxBool isLoading = true.obs;
@@ -23,14 +24,31 @@ class HistoryController extends GetxController {
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
+
String stationName = "";
+ final Map statusOptions = {
+ '': '全部',
+ '100': '未预约加氢',
+ '0': '待加氢',
+ '1': '已加氢',
+ '2': '未加氢',
+ '5': '拒绝加氢',
+ };
+
+ final RxString selectedStatus = ''.obs;
+ final RxString selectedDateType = ''.obs; // week, month, three_month
+
@override
void onInit() {
super.onInit();
-
final args = Get.arguments as Map;
- stationName = args['stationName'] as String;
+ stationName = args['stationName'] as String? ?? "";
+ refreshData();
+ }
+
+ void refreshData() {
+ getAllOrderCounts();
fetchHistoryData();
}
@@ -38,51 +56,50 @@ class HistoryController extends GetxController {
var response = await HttpService.to.post(
"appointment/orderAddHyd/getAllOrderCounts",
data: {
- // --- 直接使用 DateFormat 来格式化日期 ---
- 'startTime': _apiDateFormat.format(startDate.value),
- 'endTime': _apiDateFormat.format(endDate.value),
+ /*'startTime': _apiDateFormat.format(startDate.value),
+ 'endTime': _apiDateFormat.format(endDate.value),*/
'plateNumber': plateNumberController.text,
- 'stationName': stationName, // 加氢站名称
+ 'stationName': stationName,
+ "status": selectedStatus.value,
+ "dateType": selectedDateType.value,
},
);
if (response == null || response.data == null) {
- totalHydrogen.value = '0 kg';
- totalCompletions.value = '0 次';
+ totalHydrogen.value = '0';
+ totalCompletions.value = '0';
return;
}
try {
final baseModel = BaseModel.fromJson(response.data);
final dataMap = baseModel.data as Map;
- totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0} kg';
- totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0} 次';
+ totalHydrogen.value = '${dataMap['totalAddAmount'] ?? 0}';
+ totalCompletions.value = '${dataMap['orderCompleteCount'] ?? 0}';
} catch (e) {
- totalHydrogen.value = '0 kg';
- totalCompletions.value = '0 次';
+ totalHydrogen.value = '0';
+ totalCompletions.value = '0';
}
}
Future fetchHistoryData() async {
isLoading.value = true;
-
- //获取数据
- getAllOrderCounts();
+ updateUi();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
- // --- 直接使用 DateFormat 来格式化日期 ---
- 'startTime': _apiDateFormat.format(startDate.value),
- 'endTime': _apiDateFormat.format(endDate.value),
+ /*'startTime': _apiDateFormat.format(startDate.value),
+ 'endTime': _apiDateFormat.format(endDate.value),*/
'plateNumber': plateNumberController.text,
'pageNum': 1,
'pageSize': 50,
- 'stationName': stationName, // 加氢站名称
+ 'stationName': stationName,
+ "status": selectedStatus.value,
+ "dateType": selectedDateType.value,
},
);
if (response == null || response.data == null) {
- showToast('无法获取历史记录');
_resetData();
return;
}
@@ -90,7 +107,6 @@ class HistoryController extends GetxController {
final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map;
-
final List listFromServer = dataMap['records'] ?? [];
historyList.assignAll(
listFromServer
@@ -99,14 +115,13 @@ class HistoryController extends GetxController {
);
hasData.value = historyList.isNotEmpty;
} else {
- showToast(baseModel.message);
_resetData();
}
} catch (e) {
- showToast('获取历史记录失败: $e');
_resetData();
} finally {
isLoading.value = false;
+ updateUi();
}
}
@@ -115,97 +130,15 @@ class HistoryController extends GetxController {
hasData.value = false;
}
- void pickDate(BuildContext context, bool isStartDate) {
- // 确定当前操作的日期和临时存储变量
- final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
- DateTime tempDate = initialDate;
+ void onStatusSelected(String status) {
+ if (selectedStatus.value == status) return;
+ selectedStatus.value = status;
+ refreshData();
+ }
- // 定义全局的最早可选日期
- final DateTime globalMinimumDate = DateTime(2025, 12, 1);
-
- // 动态计算当前选择器的最小/最大日期范围
- DateTime minimumDate;
- DateTime? maximumDate; // 声明为可空,因为两个日期都可能没有最大限制
-
- if (isStartDate) {
- // 当选择【开始日期】时 它的最小日期就是全局最小日期
- minimumDate = globalMinimumDate;
- // 最大日期没有限制
- maximumDate = null;
- } else {
- // 当选择【结束日期】时 它的最小日期不能早于当前的开始日期
- minimumDate = startDate.value;
- // 确认结束日期没有最大限制 ---
- //最大日期没有限制
- maximumDate = null;
- }
-
- Get.bottomSheet(
- Container(
- height: 300,
- decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.only(
- topLeft: Radius.circular(16),
- topRight: Radius.circular(16),
- ),
- ),
- child: Column(
- children: [
- // 顶部的取消和确认按钮
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- TextButton(
- onPressed: () => Get.back(),
- child: const Text('取消', style: TextStyle(color: Colors.grey)),
- ),
- TextButton(
- onPressed: () {
- // 4. 确认后,更新对应的日期变量
- if (isStartDate) {
- startDate.value = tempDate;
- // 如果新的开始日期晚于结束日期,自动将结束日期调整为同一天
- if (tempDate.isAfter(endDate.value)) {
- endDate.value = tempDate;
- }
- } else {
- endDate.value = tempDate;
- }
- Get.back();
-
- // 选择日期后自动刷新数据
- fetchHistoryData();
- },
- child: const Text(
- '确认',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- ),
- ],
- ),
- ),
- const Divider(height: 1),
- // 日期选择器
- Expanded(
- child: CupertinoDatePicker(
- mode: CupertinoDatePickerMode.date,
- initialDateTime: initialDate,
- // 应用动态计算好的最小/最大日期
- minimumDate: minimumDate,
- maximumDate: maximumDate,
- onDateTimeChanged: (DateTime newDate) {
- tempDate = newDate;
- },
- ),
- ),
- ],
- ),
- ),
- backgroundColor: Colors.transparent, // 使底部工作表外的区域透明
- );
+ void onDateTypeSelected(String type) {
+ selectedDateType.value = type;
+ refreshData();
}
@override
diff --git a/ln_jq_app/lib/pages/b_page/history/view.dart b/ln_jq_app/lib/pages/b_page/history/view.dart
index 7832f5c..e472210 100644
--- a/ln_jq_app/lib/pages/b_page/history/view.dart
+++ b/ln_jq_app/lib/pages/b_page/history/view.dart
@@ -1,107 +1,173 @@
import 'package:flutter/material.dart';
-import 'package:get/get.dart';
-import 'package:ln_jq_app/common/styles/theme.dart';
+import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/b_page/history/controller.dart';
-import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
+import 'package:ln_jq_app/pages/b_page/site/controller.dart';
class HistoryPage extends GetView {
- const HistoryPage({Key? key}) : super(key: key);
+ const HistoryPage({super.key});
@override
Widget build(BuildContext context) {
- Get.put(HistoryController());
+ return GetBuilder(
+ init: HistoryController(),
+ id: 'history',
+ builder: (_) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF7F8FA),
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20),
+ onPressed: () => Get.back(),
+ ),
+ title: _buildSearchBox(),
+ ),
+ body: Column(
+ children: [
+ _buildFilterBar(),
+ _buildSummaryCard(),
+ Expanded(child: _buildHistoryList()),
+ ],
+ ),
+ );
+ },
+ );
+ }
- return Scaffold(
- appBar: AppBar(title: const Text('历史记录'), centerTitle: true),
- body: Padding(
- padding: const EdgeInsets.all(12.0),
- child: Column(
- children: [
- _buildFilterCard(context),
- const SizedBox(height: 12),
- _buildSummaryCard(),
- const SizedBox(height: 12),
- _buildListHeader(),
- Expanded(child: _buildHistoryList()),
- ],
+ Widget _buildSearchBox() {
+ return Container(
+ height: 36,
+ decoration: BoxDecoration(
+ color: const Color(0xFFF2F3F5),
+ borderRadius: BorderRadius.circular(18),
+ ),
+ child: TextField(
+ controller: controller.plateNumberController,
+ onSubmitted: (v) => controller.refreshData(),
+ decoration: const InputDecoration(
+ hintText: '搜索车牌号',
+ hintStyle: TextStyle(color: Color(0xFFBBBBBB), fontSize: 14),
+ prefixIcon: Icon(Icons.search_sharp, color: Color(0xFFBBBBBB), size: 20),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.only(bottom: 12),
),
),
);
}
- Widget _buildFilterCard(BuildContext context) {
- return Card(
- elevation: 2,
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text('时间范围', style: TextStyle(fontSize: 14, color: Colors.grey)),
- const SizedBox(height: 8),
- Row(
- children: [
- Expanded(child: _buildDateField(context, true)),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 8.0),
- child: Text('至'),
- ),
- Expanded(child: _buildDateField(context, false)),
- ],
- ),
- const SizedBox(height: 16),
- const Text('车牌号', style: TextStyle(fontSize: 14, color: Colors.grey)),
- const SizedBox(height: 8),
- SizedBox(
- height: 44,
- child: TextField(
- controller: controller.plateNumberController,
- decoration: InputDecoration(
- hintText: '请输入车牌号',
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
- contentPadding: const EdgeInsets.symmetric(horizontal: 12),
- ),
+ Widget _buildFilterBar() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Row(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: controller.statusOptions.entries.map((entry) {
+ return Obx(() {
+ bool isSelected = controller.selectedStatus.value == entry.key;
+ return GestureDetector(
+ onTap: () => controller.onStatusSelected(entry.key),
+ child: Container(
+ margin: const EdgeInsets.only(right: 12),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ decoration: BoxDecoration(
+ color: isSelected ? const Color(0xFF006633) : Colors.white,
+ borderRadius: BorderRadius.circular(15),
+ ),
+ child: Text(
+ entry.value,
+ style: TextStyle(
+ color: isSelected
+ ? Colors.white
+ : Color.fromRGBO(148, 163, 184, 1),
+ fontSize: 12.sp,
+ fontWeight: isSelected ? FontWeight.bold : FontWeight.w600,
+ ),
+ ),
+ ),
+ );
+ });
+ }).toList(),
),
),
- const SizedBox(height: 16),
- ElevatedButton.icon(
- onPressed: () {
- FocusScope.of(context).unfocus(); // Hide keyboard
- controller.fetchHistoryData();
- },
- icon: const Icon(Icons.search, size: 20),
- label: const Text('查询'),
- style: ElevatedButton.styleFrom(
- minimumSize: const Size(double.infinity, 44),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
- ),
- ),
- ],
- ),
+ ),
+ _buildTimeFilterIcon(),
+ ],
),
);
}
+ Widget _buildTimeFilterIcon() {
+ return PopupMenuButton(
+ icon: LoginUtil.getAssImg("ic_ex_menu@2x"),
+ onSelected: controller.onDateTypeSelected,
+ itemBuilder: (context) => [
+ const PopupMenuItem(value: 'week', child: Text('最近一周')),
+ const PopupMenuItem(value: 'month', child: Text('最近一月')),
+ const PopupMenuItem(value: 'three_month', child: Text('最近三月')),
+ ],
+ );
+ }
+
Widget _buildSummaryCard() {
- return Card(
- elevation: 2,
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 20.0),
- child: Obx(
- () => Row(
- mainAxisAlignment: MainAxisAlignment.spaceAround,
- children: [
- _buildSummaryItem('实际加氢总量', controller.totalHydrogen.value, Colors.blue),
- const SizedBox(width: 1, height: 40, child: VerticalDivider()),
- _buildSummaryItem(
- '预约完成次数',
- controller.totalCompletions.value,
- Colors.green,
- ),
- ],
- ),
+ return Container(
+ margin: const EdgeInsets.only(left: 16, right: 16,bottom: 12),
+ padding: const EdgeInsets.all(20),
+ height: 160,
+ width: double.infinity,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(24),
+ image: const DecorationImage(
+ image: AssetImage('assets/images/history_bg.png'),
+ fit: BoxFit.cover,
),
),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text('加氢站', style: TextStyle(color: Colors.white70, fontSize: 12)),
+ Text(
+ controller.stationName,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const Spacer(),
+ Obx(
+ () => Row(
+ children: [
+ _buildSummaryItem('实际加氢量', '${controller.totalHydrogen.value} Kg'),
+ const SizedBox(width: 40),
+ _buildSummaryItem('预约完成次数', '${controller.totalCompletions.value} 次'),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSummaryItem(String label, String value) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: const TextStyle(color: Colors.white60, fontSize: 12)),
+ const SizedBox(height: 4),
+ Text(
+ value,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ],
);
}
@@ -110,143 +176,138 @@ class HistoryPage extends GetView {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
- if (!controller.hasData.value) {
- return const Center(child: Text('没有找到相关记录'));
+ if (controller.historyList.isEmpty) {
+ return const Center(
+ child: Text('暂无相关记录', style: TextStyle(color: Color(0xFF999999))),
+ );
}
return ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: controller.historyList.length,
itemBuilder: (context, index) {
- final ReservationModel item = controller.historyList[index];
- return Card(
- margin: const EdgeInsets.only(bottom: 8),
- child: ListTile(
- title: Text('车牌号: ${item.plateNumber}'),
- subtitle: Text.rich(
- TextSpan(
- children: [
- TextSpan(
- text: '加氢站: ${item.stationName}\n',
- style: TextStyle(fontSize: 16),
- ),
- TextSpan(
- text: '时间: ${item.time}\n',
- style: TextStyle(fontSize: 16),
- ),
- TextSpan(
- text: '加氢量:',
- ),
- TextSpan(
- text: '${item.amount}',
- style: TextStyle(fontSize: 16, color: AppTheme.themeColor),
- ),
- ],
- ),
- )
- ,
- trailing:
- // 状态标签
- _buildStatusChip(item.status),
- ),
- );
+ return _buildHistoryItem(controller.historyList[index]);
},
);
});
}
- Widget _buildStatusChip(ReservationStatus status) {
- String text;
- Color color;
+ Widget _buildHistoryItem(ReservationModel item) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '车牌号',
+ style: TextStyle(
+ color: Color.fromRGBO(148, 163, 184, 1),
+ fontSize: 12.sp,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ item.plateNumber,
+ style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ ],
+ ),
+ _buildStatusBadge(item.status),
+ ],
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ _buildInfoColumn('加氢时间:', item.time),
+ _buildInfoColumn('加氢量', '${item.amount} Kg', isRight: true),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildInfoColumn(String label, String value, {bool isRight = false}) {
+ return Expanded(
+ child: Column(
+ crossAxisAlignment: isRight ? CrossAxisAlignment.end : CrossAxisAlignment.start,
+ children: [
+ Text(
+ label,
+ style: TextStyle(color: Color.fromRGBO(148, 163, 184, 1), fontSize: 12.sp),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ value,
+ style: TextStyle(
+ fontSize: isRight ? 16 : 13,
+ fontWeight: isRight ? FontWeight.bold : FontWeight.normal,
+ color: const Color(0xFF333333),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildStatusBadge(ReservationStatus status) {
+ String text = '未知';
+ Color bgColor = Colors.grey.shade100;
+ Color textColor = Colors.grey;
+
switch (status) {
case ReservationStatus.pending:
text = '待加氢';
- color = Colors.orange;
+ bgColor = const Color(0xFFFFF7E8);
+ textColor = const Color(0xFFFF9800);
break;
case ReservationStatus.completed:
text = '已加氢';
- color = Colors.greenAccent;
+ bgColor = const Color(0xFFE8F5E9);
+ textColor = const Color(0xFF4CAF50);
break;
case ReservationStatus.rejected:
text = '拒绝加氢';
- color = Colors.red;
+ bgColor = const Color(0xFFFFEBEE);
+ textColor = const Color(0xFFF44336);
break;
case ReservationStatus.unadded:
text = '未加氢';
- color = Colors.red;
+ bgColor = const Color(0xFFFFEBEE);
+ textColor = const Color(0xFFF44336);
break;
case ReservationStatus.cancel:
text = '已取消';
- color = Colors.red;
+ bgColor = const Color(0xFFFFEBEE);
+ textColor = const Color(0xFFF44336);
break;
default:
text = '未知状态';
- color = Colors.grey;
+ bgColor = Colors.grey;
+ textColor = Colors.grey;
break;
}
+
return Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
- color: color.withOpacity(0.1),
- borderRadius: BorderRadius.circular(12),
+ color: bgColor,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: textColor.withOpacity(0.3)),
),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.circle, color: color, size: 8),
- const SizedBox(width: 4),
- Text(
- text,
- style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
- ),
- ],
- ),
- );
- }
-
- Widget _buildDateField(BuildContext context, bool isStart) {
- return Obx(
- () => InkWell(
- onTap: () => controller.pickDate(context, isStart),
- child: Container(
- height: 44,
- padding: const EdgeInsets.symmetric(horizontal: 12),
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey.shade400),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(isStart ? controller.formattedStartDate : controller.formattedEndDate),
- const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
- ],
- ),
- ),
- ),
- );
- }
-
- Widget _buildSummaryItem(String label, String value, Color color) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
- const SizedBox(height: 8),
- Text(
- value,
- style: TextStyle(color: color, fontSize: 22, fontWeight: FontWeight.bold),
- ),
- ],
- );
- }
-
- Widget _buildListHeader() {
- return const Padding(
- padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 14.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text('加氢明细', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
- ],
+ child: Text(
+ text,
+ style: TextStyle(color: textColor, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}
diff --git a/ln_jq_app/lib/pages/b_page/reservation/view.dart b/ln_jq_app/lib/pages/b_page/reservation/view.dart
index 2689dd9..ae6d7a5 100644
--- a/ln_jq_app/lib/pages/b_page/reservation/view.dart
+++ b/ln_jq_app/lib/pages/b_page/reservation/view.dart
@@ -36,7 +36,7 @@ class ReservationPage extends GetView {
_buildSystemTips(),
SizedBox(height: 24),
_buildLogoutButton(),
- SizedBox(height: 75.h),
+ SizedBox(height: 95.h),
],
),
),
diff --git a/ln_jq_app/lib/pages/b_page/site/controller.dart b/ln_jq_app/lib/pages/b_page/site/controller.dart
index 93ac439..c01d166 100644
--- a/ln_jq_app/lib/pages/b_page/site/controller.dart
+++ b/ln_jq_app/lib/pages/b_page/site/controller.dart
@@ -1,12 +1,20 @@
import 'dart:async';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_pdfview/flutter_pdfview.dart';
+import 'package:getx_scaffold/getx_scaffold.dart' as dio;
import 'package:getx_scaffold/getx_scaffold.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:path_provider/path_provider.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';
+import 'package:saver_gallery/saver_gallery.dart';
enum ReservationStatus {
pending, // 待处理 ( addStatus: 0)
@@ -40,6 +48,15 @@ class ReservationModel {
final String addStatus;
final String addStatusName;
bool hasEdit;
+ final String isEdit; // "1" 表示可以修改信息
+ final String gunNumber;
+
+ // 新增附件相关字段
+ final int isTruckAttachment; // 1为有证件数据 0为缺少
+ final bool hasDrivingAttachment; // 是否有行驶证
+ final bool hasHydrogenationAttachment; // 是否有加氢证
+ final List drivingAttachments; // 行驶证图片列表
+ final List hydrogenationAttachments; // 加氢证图片列表
ReservationModel({
required this.id,
@@ -63,6 +80,13 @@ class ReservationModel {
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,
+ required this.gunNumber,
});
/// 工厂构造函数,用于从JSON创建ReservationModel实例
@@ -101,15 +125,21 @@ class ReservationModel {
? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm
: '时间未定';
+ // 解析附件信息
+ Map 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',
+ plateNumber: json['plateNumber']?.toString() ?? '---',
+ amount: '${json['hydAmount']?.toString() ?? '0'}',
time: timeRange,
- contactPerson: json['contacts']?.toString() ?? '无联系人',
- contactPhone: json['phone']?.toString() ?? '无联系电话',
+ contactPerson: json['contacts']?.toString() ?? '',
+ contactPhone: json['phone']?.toString() ?? '',
status: currentStatus,
// 新增的完整字段
@@ -126,6 +156,13 @@ class ReservationModel {
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(),
+ gunNumber: json['gunNumber']?.toString() ?? '',
);
}
}
@@ -147,6 +184,11 @@ class SiteController extends GetxController with BaseControllerMixin {
bool isNotice = false;
final RefreshController refreshController = RefreshController(initialRefresh: false);
+ // 加氢枪列表
+ final RxList gasGunList = [].obs;
+ final RxMap> gasGunMap =
+ >{}.obs;
+
@override
bool get listenLifecycleEvent => true;
@@ -156,6 +198,7 @@ class SiteController extends GetxController with BaseControllerMixin {
renderData();
msgNotice();
startAutoRefresh();
+ fetchGasGunList();
}
@override
@@ -192,11 +235,9 @@ class SiteController extends GetxController with BaseControllerMixin {
}
}
+ /// 创建一个每5分钟执行一次的周期性定时器
void startAutoRefresh() {
- // 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
-
- // 创建一个每5分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
renderData();
});
@@ -204,12 +245,38 @@ class SiteController extends GetxController with BaseControllerMixin {
void onRefresh() => renderData(isRefresh: true);
- ///停止定时器的方法
+ ///停止定时器
void stopAutoRefresh() {
- // 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
- _refreshTimer = null; // 置为null,方便判断
- print("【自动刷新】定时器已停止。");
+ _refreshTimer = null;
+ }
+
+ /// 获取加氢枪列表
+ Future 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.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.from(item);
+ }
+ }
+ }
+ } catch (e) {
+ Logger.d("获取加氢枪列表失败: $e");
+ }
}
/// 获取预约数据的方法
@@ -272,138 +339,724 @@ class SiteController extends GetxController with BaseControllerMixin {
}
}
- /// 确认预约
- Future confirmReservation(String id) async {
- final item = reservationList.firstWhere(
- (item) => item.id == id,
- orElse: () => throw Exception('Reservation not found'),
+ /// 确认预约弹窗
+ Future 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: [], gunNumber: '',
+ );
+ }
+
+ //车牌输入
+ final TextEditingController plateController = TextEditingController(
+ text: item.plateNumber == "---" ? "" : item.plateNumber,
);
+
+ // 加氢量保留3位小数
+ double initialAmount = double.tryParse(item.hydAmount) ?? 0.0;
final TextEditingController amountController = TextEditingController(
- text: item.hydAmount,
+ text: initialAmount.toStringAsFixed(3),
);
+
+ //枪号回填
+ String initialGun = '';
+ if (item.gunNumber.isNotEmpty && gasGunList.contains(item.gunNumber)) {
+ initialGun = item.gunNumber; // 如果接口有返回且在列表中,则反显
+ } else if (gasGunList.isNotEmpty) {
+ initialGun = gasGunList.first; // 否则默认选第一个
+ }
+ final RxString selectedGun = initialGun.obs;
+
+
+
+ final RxBool isOfflineChecked = false.obs;
+
Get.dialog(
Dialog(
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), // 圆角
- child: Padding(
- padding: const EdgeInsets.only(top: 24.0),
- child: Column(
- mainAxisSize: MainAxisSize.min, // 高度自适应
- children: [
- const Text(
- '确认加氢状态',
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 5),
- // content 部分
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24),
- child: Column(
+ 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: [
- Text(
- '车牌号 ${item.plateNumber}',
- style: const TextStyle(
- fontSize: 16,
- color: AppTheme.themeColor,
- fontWeight: FontWeight.bold,
+ Container(
+ width: 80.w,
+ child: TextField(
+ enabled: !isEdit,
+ 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(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Text(
- '加氢量',
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
- ),
- const SizedBox(width: 16),
- SizedBox(
- width: 100,
- child: TextField(
- controller: amountController,
- textAlign: TextAlign.center,
- keyboardType: TextInputType.number,
- inputFormatters: [
- FilteringTextInputFormatter.digitsOnly, // 只允许数字输入
- ],
- style: TextStyle(
- fontSize: 22,
- fontWeight: FontWeight.bold,
- color: Get.theme.primaryColor,
- ),
- decoration: const InputDecoration(
- suffixText: 'kg',
- suffixStyle: TextStyle(fontSize: 16, color: Colors.grey),
- enabledBorder: UnderlineInputBorder(
- borderSide: BorderSide(color: Colors.grey),
+ 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,
),
- focusedBorder: UnderlineInputBorder(
- borderSide: BorderSide(color: Colors.grey, width: 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,
+ ),
),
),
),
- ),
- ],
- ),
- const SizedBox(height: 12),
- const Text(
- '请选择本次加氢的实际状态\n用于更新预约记录。',
- textAlign: TextAlign.center,
- style: TextStyle(fontSize: 14, color: Colors.grey),
- ),
+ SizedBox(width: 16.w),
+ if (item.plateNumber != "---" && item.hasDrivingAttachment)
+ buildInfoTag('行驶证', item.drivingAttachments),
+ if (item.plateNumber != "---" && item.hasHydrogenationAttachment)
+ buildInfoTag('加氢证', item.hydrogenationAttachments),
],
),
- ),
- const SizedBox(height: 24),
- // actions 部分 (按钮)
- Padding(
- padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
- child: Column(
+
+ 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(
+ value: selectedGun.value.isEmpty ? null : selectedGun.value,
+ isExpanded: true,
+ hint: const Text('请选择加氢枪号'),
+ items: gasGunList.map((String gun) {
+ return DropdownMenuItem(value: gun, child: Text(gun));
+ }).toList(),
+ onChanged: (v) => selectedGun.value = v ?? '',
+ ),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 24),
+
+ // 按钮
+ Row(
children: [
- ElevatedButton(
- onPressed: () {
- Get.back(); // 关闭弹窗
- final num addHydAmount = num.tryParse(amountController.text) ?? 0;
- upDataService(id, 0, 1, addHydAmount, "", item);
- },
- style: ElevatedButton.styleFrom(
- minimumSize: const Size(double.infinity, 48),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(5),
+ 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,
+ );
+ 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,
+ );
+ return;
+ }
+ //有预约订单确认
+ 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),
),
),
- child: const Text('加氢完成', style: TextStyle(fontSize: 16)),
),
- const SizedBox(height: 12),
- ElevatedButton(
- onPressed: () {
- Get.back(); // 关闭弹窗
- upDataService(id, 0, 2, 0, "", item);
- },
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.orange,
- minimumSize: const Size(double.infinity, 48),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(5),
+ const SizedBox(width: 12),
+ Expanded(
+ flex: 1,
+ child: OutlinedButton(
+ onPressed: () {
+ if (!isEdit && !isAdd) {
+ upDataService(
+ id,
+ 0,
+ 2,
+ 0,
+ "",
+ item!,
+ gunNumber: selectedGun.value,
+ plateNumber: plateController.text,
+ );
+ } else {
+ Get.back();
+ }
+ },
+ 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),
),
- ),
- child: const Text('未加氢', style: TextStyle(fontSize: 16)),
- ),
- const SizedBox(height: 12),
- TextButton(
- onPressed: () => Get.back(), // 只关闭弹窗
- child: const Text(
- '暂不处理',
- style: TextStyle(color: Colors.grey, fontSize: 14),
),
),
],
),
+
+ 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 saveFileToLocal(String url) async {
+ try {
+ // 权限请求
+ if (Platform.isAndroid) {
+ dio.PermissionStatus status;
+
+ final deviceInfo = await DeviceInfoPlugin().androidInfo;
+ final sdkInt = deviceInfo.version.sdkInt;
+
+ if (sdkInt <= 32) {
+ status = await Permission.storage.request();
+ } else {
+ status = await Permission.photos.request();
+ }
+
+ if (!status.isGranted) {
+ showErrorToast("请在系统设置中开启存储权限");
+ return;
+ }
+ } else {
+ var status = await Permission.photos.request();
+ if (!status.isGranted) {
+ showErrorToast("请在系统设置中开启存储权限");
+ return;
+ }
+ }
+
+ showLoading("正在保存...");
+
+ // 下载文件
+ var response = await Dio().get(
+ url,
+ options: Options(responseType: ResponseType.bytes),
+ );
+
+ final Uint8List bytes = Uint8List.fromList(response.data);
+
+ if (url.toLowerCase().endsWith('.pdf')) {
+ String? savePath;
+
+ if (Platform.isAndroid) {
+ final directory = Directory('/storage/emulated/0/Download');
+ if (!await directory.exists()) {
+ await directory.create(recursive: true);
+ }
+ final String fileName = "certificate_${DateTime.now().millisecondsSinceEpoch}.pdf";
+ savePath = "${directory.path}/$fileName";
+ } else {
+ // iOS: 保存到文档目录
+ final directory = await getApplicationDocumentsDirectory();
+ final String fileName = "certificate_${DateTime.now().millisecondsSinceEpoch}.pdf";
+ savePath = "${directory.path}/$fileName";
+ }
+
+ final File file = File(savePath);
+ await file.writeAsBytes(bytes);
+
+ dismissLoading();
+ showSuccessToast(Platform.isAndroid ? "PDF已保存至系统下载目录" : "PDF已保存,请在'文件'App中查看");
+ } else {
+ // 保存图片到相册
+ final result = await SaverGallery.saveImage(
+ bytes,
+ quality: 100,
+ fileName: "certificate_${DateTime.now().millisecondsSinceEpoch}",
+ skipIfExists: false,
+ );
+ dismissLoading();
+ if (result.isSuccess) {
+ showSuccessToast("图片已保存至相册");
+ } else {
+ showErrorToast("保存失败");
+ }
+ }
+ } catch (e) {
+ dismissLoading();
+ showErrorToast("保存异常");
+ }
+ }
+
+ Widget buildInfoTag(String label, List 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 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) {
+ final String url = images[index];
+ final bool isPdf = url.toLowerCase().endsWith('.pdf');
+
+ if (isPdf) {
+ return PhotoViewGalleryPageOptions.customChild(
+ child: GestureDetector(
+ onTap: (){
+ _showSaveMenu(url);
+ },
+ child: _buildPdfPreview(url),),
+ initialScale: PhotoViewComputedScale.contained,
+ heroAttributes: PhotoViewHeroAttributes(tag: url),
+ onTapDown: (context, details, controllerValue) {
+ _showSaveMenu(url);
+ },
+ );
+ }
+
+ return PhotoViewGalleryPageOptions(
+ imageProvider: NetworkImage(url),
+ initialScale: PhotoViewComputedScale.contained,
+ heroAttributes: PhotoViewHeroAttributes(tag: url),
+ onTapDown: (context, details, controllerValue) {
+ _showSaveMenu(url);
+ },
+ );
+ },
+ 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,
+ );
+ }
+
+ /// PDF 预览小部件
+ Widget _buildPdfPreview(String url) {
+ return FutureBuilder(
+ future: _downloadPdf(url),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return const Center(child: CircularProgressIndicator(color: Colors.white));
+ }
+ if (snapshot.hasError || snapshot.data == null) {
+ return const Center(child: Text("PDF 加载失败", style: TextStyle(color: Colors.white)));
+ }
+ return PDFView(
+ filePath: snapshot.data!,
+ enableSwipe: false,
+ swipeHorizontal: false,
+ autoSpacing: false,
+ pageFling: false,
+ );
+ },
+ );
+ }
+
+ Future _downloadPdf(String url) async {
+ final file = File('${(await getTemporaryDirectory()).path}/${url.hashCode}.pdf');
+ if (await file.exists()) return file.path;
+ var response = await Dio().get(url, options: Options(responseType: ResponseType.bytes));
+ await file.writeAsBytes(response.data);
+ return file.path;
+ }
+
+ void _showSaveMenu(String url) {
+ final bool isPdf = url.toLowerCase().endsWith('.pdf');
+ Get.bottomSheet(
+ Container(
+ color: Colors.white,
+ child: SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ leading: const Icon(Icons.download),
+ title: Text(isPdf ? '保存 PDF 文件' : '保存图片到相册'),
+ onTap: () {
+ Get.back();
+ saveFileToLocal(url);
+ },
+ ),
+ const Divider(height: 1),
+ ListTile(
+ title: const Text('取消', textAlign: TextAlign.center),
+ onTap: () => Get.back(),
),
],
),
),
),
- barrierDismissible: false, // 点击外部不关闭弹窗
);
}
@@ -502,8 +1155,15 @@ class SiteController extends GetxController with BaseControllerMixin {
return;
}
- Get.back(); // 关闭弹窗
- upDataService(id, 1, -1, 0, finalReason, item);
+ upDataService(
+ id,
+ 1,
+ -1,
+ 0,
+ finalReason,
+ item,
+ plateNumber: item.plateNumber,
+ );
},
child: const Text('确认拒绝', style: TextStyle(color: Colors.red)),
),
@@ -533,19 +1193,44 @@ class SiteController extends GetxController with BaseControllerMixin {
int addStatus,
num addHydAmount,
String rejectReason,
- ReservationModel item,
- ) async {
+ ReservationModel item, {
+ String? gunNumber,
+ String? plateNumber,
+ bool isEdit = false,
+ bool isAdd = false,
+ }) async {
showLoading("确认中");
try {
- var responseData;
- if (addStatus == -1) {
+ dio.Response? 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 {
@@ -553,21 +1238,26 @@ class SiteController extends GetxController with BaseControllerMixin {
'appointment/orderAddHyd/completeOrder',
data: {
'id': id,
- 'addStatus': addStatus, //完成使用 完成1,未加2
+ 'addStatus': addStatus, //完成使用 完成1,未加2`
"addHydAmount": addHydAmount,
+ "plateNumber": plateNumber,
+ if (gunNumber != null && gunNumber.isNotEmpty) "gunNumber": gunNumber,
},
);
}
- if (responseData == null && responseData!.data == null) {
+ if (responseData == null || responseData.data == null) {
dismissLoading();
- showToast('服务暂不可用,请稍后');
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("操作成功");
+ } else {
+ showToast(result.message);
}
+
+ Get.back();
dismissLoading();
//1完成 2未加 -1拒绝
@@ -585,6 +1275,110 @@ class SiteController extends GetxController with BaseControllerMixin {
}
}
+ //车牌&加氢量 识别
+ Future 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 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 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 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 = "";
@@ -598,7 +1392,7 @@ class SiteController extends GetxController with BaseControllerMixin {
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
);
- if (responseData == null && responseData!.data == null) {
+ if (responseData == null || responseData.data == null) {
showToast('暂时无法获取站点信息');
return;
}
diff --git a/ln_jq_app/lib/pages/b_page/site/view.dart b/ln_jq_app/lib/pages/b_page/site/view.dart
index 1df6c42..db8d624 100644
--- a/ln_jq_app/lib/pages/b_page/site/view.dart
+++ b/ln_jq_app/lib/pages/b_page/site/view.dart
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
-import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
@@ -57,17 +56,33 @@ class SitePage extends GetView {
),
GestureDetector(
onTap: () {
- Get.to(
- () => HistoryPage(),
- arguments: {'stationName': controller.name},
- );
+ // 手动录入
+ controller.confirmReservation("", isAdd: true);
},
- child: Text(
- '历史记录',
- style: TextStyle(
- fontSize: 14.sp,
- fontWeight: FontWeight.bold,
- color: Color.fromRGBO(156, 163, 175, 1),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: const Color(0xFFEEEEEE)),
+ ),
+ child: Row(
+ children: [
+ const Icon(
+ Icons.add_circle_outline,
+ size: 18,
+ color: Color(0xFF666666),
+ ),
+ const SizedBox(width: 4),
+ Text(
+ "无预约车辆加氢",
+ style: TextStyle(
+ color: Color.fromRGBO(51, 51, 51, 1),
+ fontSize: 13.sp,
+ fontWeight: FontWeight.w400
+ ),
+ ),
+ ],
),
),
),
@@ -78,12 +93,12 @@ class SitePage extends GetView {
Column(
children: [
_buildSearchView(),
+ SizedBox(height: 15.h),
controller.hasReservationData
? _buildReservationListView()
: _buildEmptyReservationView(),
],
),
-
SizedBox(height: 35.h),
//第三部分
Container(
@@ -136,7 +151,7 @@ class SitePage extends GetView {
],
),
),
- SizedBox(height: 75.h),
+ SizedBox(height: 105.h),
],
);
}
@@ -185,27 +200,7 @@ class SitePage extends GetView {
],
),
),
- IconButton(
- onPressed: () async {
- var scanResult = await Get.to(() => const MessagePage());
- if (scanResult == null) {
- controller.msgNotice();
- }
- },
- style: IconButton.styleFrom(
- backgroundColor: Colors.grey[100],
- padding: const EdgeInsets.all(8),
- ),
- icon: Badge(
- smallSize: 8,
- backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
- child: const Icon(
- Icons.notifications_outlined,
- color: Colors.black87,
- size: 30,
- ),
- ),
- ),
+ _buildDropdownMenu(),
],
),
const SizedBox(height: 25),
@@ -233,6 +228,40 @@ class SitePage extends GetView {
);
}
+ Widget _buildDropdownMenu() {
+ return PopupMenuButton(
+ icon: Container(child: const Icon(Icons.grid_view_rounded, size: 24)),
+ onSelected: (value) async {
+ if (value == 'message') {
+ var scanResult = await Get.to(() => const MessagePage());
+ if (scanResult == null) {
+ controller.msgNotice();
+ }
+ } else if (value == 'history') {
+ Get.to(() => const HistoryPage(), arguments: {'stationName': controller.name});
+ }
+ },
+ itemBuilder: (context) => [
+ const PopupMenuItem(
+ value: 'message',
+ child: Row(
+ children: [
+ Icon(Icons.notifications_none, size: 20),
+ SizedBox(width: 8),
+ Text('消息中心'),
+ ],
+ ),
+ ),
+ const PopupMenuItem(
+ value: 'history',
+ child: Row(
+ children: [Icon(Icons.history, size: 20), SizedBox(width: 8), Text('加氢历史')],
+ ),
+ ),
+ ],
+ );
+ }
+
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Container(
@@ -391,8 +420,9 @@ class SitePage extends GetView {
/// 构建“有预约数据”的列表视图
Widget _buildReservationListView() {
- return ListView.separated(
+ return ListView.builder(
shrinkWrap: true,
+ padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length,
@@ -401,7 +431,6 @@ class SitePage extends GetView {
// 调用新的方法来构建每一项
return _buildReservationItem(index, item);
},
- separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
);
}
@@ -466,7 +495,7 @@ class SitePage extends GetView {
/// 右侧具体数据卡片
Widget _buildInfoCard(ReservationModel item) {
return Container(
- padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
+ padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
@@ -508,59 +537,82 @@ class SitePage extends GetView {
),
const SizedBox(height: 8),
// 联系信息
- Column(
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- Text(
- "${item.contactPerson} | ${item.contactPhone}",
- style: TextStyle(
- color: Color(0xFF999999),
- fontSize: 13.sp,
- fontWeight: FontWeight.w400,
- ),
- ),
- ],
+ Text(
+ item.contactPerson.isEmpty || item.contactPhone.isEmpty
+ ? ""
+ : "${item.contactPerson} | ${item.contactPhone}",
+ style: TextStyle(
+ color: Color(0xFF999999),
+ fontSize: 13.sp,
+ fontWeight: FontWeight.w400,
+ ),
),
-
- //操作按钮(仅在待处理状态显示)
- if (item.status == ReservationStatus.pending) ...[
- const SizedBox(height: 15),
- const Divider(height: 1, color: Color(0xFFF5F5F5)),
- const SizedBox(height: 12),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- _buildSmallButton(
- "拒绝",
- isOutline: true,
- onTap: () {
- controller.rejectReservation(item.id);
- },
+ SizedBox(height: 6.h),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ if (item.hasDrivingAttachment)
+ controller.buildInfoTag('行驶证',item.drivingAttachments),
+ if (item.hasHydrogenationAttachment) ...[
+ SizedBox(width: 8.w),
+ controller.buildInfoTag('加氢证',item.hydrogenationAttachments)
+ ],
+ Spacer(),
+ if (item.isEdit == "1") ...[
+ const SizedBox(height: 15),
+ const Divider(height: 1, color: Color(0xFFF5F5F5)),
+ const SizedBox(height: 12),
+ Align(
+ alignment: Alignment.centerRight,
+ child: _buildSmallButton(
+ "修改信息",
+ isOutline: true,
+ onTap: () {
+ controller.confirmReservation(item.id, isEdit: true);
+ },
+ ),
),
- const SizedBox(width: 12),
- _buildSmallButton(
- "确认",
- isOutline: false,
- onTap: () {
- controller.confirmReservation(item.id);
- },
+ ] else if (item.status == ReservationStatus.pending) ...[
+ const SizedBox(height: 15),
+ const Divider(height: 1, color: Color(0xFFF5F5F5)),
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ _buildSmallButton(
+ "拒绝",
+ isOutline: true,
+ onTap: () {
+ controller.rejectReservation(item.id);
+ },
+ ),
+ const SizedBox(width: 12),
+ _buildSmallButton(
+ "确认",
+ isOutline: false,
+ onTap: () {
+ controller.confirmReservation(item.id);
+ },
+ ),
+ ],
),
],
- ),
- ],
+ ],
+ ),
],
),
);
}
- /// 通用小按钮
+
+
Widget _buildSmallButton(
String text, {
required bool isOutline,
required VoidCallback onTap,
}) {
const kPrimaryGreen = Color(0xFF006D35);
- const kDangerRed = Color(0xFFFF7D7D);
+ var kDangerRed = text.contains('修改') ? Colors.red : Color.fromRGBO(255, 142, 98, 1);
return GestureDetector(
onTap: onTap,
@@ -634,139 +686,4 @@ class SitePage extends GetView {
),
);
}
-
- /// 右侧操作按钮(拒绝/确认)
- Widget _buildActionButtons(ReservationModel item) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- // 拒绝按钮(空心)
- GestureDetector(
- onTap: () => controller.rejectReservation(item.id),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(10),
- border: Border.all(color: const Color(0xFFFF7D7D)),
- ),
- child: const Text(
- "拒绝",
- style: TextStyle(
- color: Color(0xFFFF7D7D),
- fontSize: 14,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- const SizedBox(width: 8),
- // 确认按钮(实心深绿)
- GestureDetector(
- onTap: () => controller.confirmReservation(item.id),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
- decoration: BoxDecoration(
- color: const Color(0xFF006D35),
- borderRadius: BorderRadius.circular(10),
- ),
- child: const Text(
- "确认",
- style: TextStyle(
- color: Colors.white,
- fontSize: 14,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- ],
- );
- }
-
- /// 构建状态标签
- Widget _buildStatusChip(ReservationStatus status) {
- String text;
- Color color;
- switch (status) {
- case ReservationStatus.pending:
- text = '待加氢';
- color = Colors.orange;
- break;
- case ReservationStatus.completed:
- text = '已加氢';
- color = Colors.greenAccent;
- break;
- case ReservationStatus.rejected:
- text = '拒绝加氢';
- color = Colors.red;
- break;
- case ReservationStatus.unadded:
- text = '未加氢';
- color = Colors.red;
- break;
- case ReservationStatus.cancel:
- text = '已取消';
- color = Colors.red;
- break;
- default:
- text = '未知状态';
- color = Colors.grey;
- break;
- }
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(
- color: color.withOpacity(0.1),
- borderRadius: BorderRadius.circular(12),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(Icons.circle, color: color, size: 8),
- const SizedBox(width: 4),
- Text(
- text,
- style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
- ),
- ],
- ),
- );
- }
-
- /// 构建信息详情行
- Widget _buildDetailRow(
- IconData icon,
- String label,
- String value, {
- Color valueColor = Colors.black87,
- }) {
- return Row(
- children: [
- Icon(icon, color: Colors.grey, size: 20),
- const SizedBox(width: 8),
- Text('$label: ', style: const TextStyle(fontSize: 14, color: Colors.grey)),
- Expanded(
- child: Text(
- value,
- style: TextStyle(
- fontSize: 14,
- color: valueColor,
- fontWeight: FontWeight.w500,
- ),
- ),
- ),
- ],
- );
- }
-
- /// 底部构建带图标的提示信息行
- Widget _buildInfoItem(IconData icon, String text) {
- return Row(
- children: [
- Icon(icon, color: Colors.blue, size: 20),
- const SizedBox(width: 8),
- Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
- ],
- );
- }
}
diff --git a/ln_jq_app/lib/pages/c_page/base_widgets/view.dart b/ln_jq_app/lib/pages/c_page/base_widgets/view.dart
index 55a3fcb..314ba7b 100644
--- a/ln_jq_app/lib/pages/c_page/base_widgets/view.dart
+++ b/ln_jq_app/lib/pages/c_page/base_widgets/view.dart
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
+import 'package:ln_jq_app/pages/c_page/mall/mall_view.dart';
import 'package:ln_jq_app/pages/c_page/map/view.dart';
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
@@ -33,14 +34,14 @@ class BaseWidgetsPage extends GetView {
}
List _buildPages() {
- return [ReservationPage(), MapPage(), CarInfoPage(), MinePage()];
+ return [ReservationPage(), MapPage(), MallPage(), CarInfoPage(), MinePage()];
}
// 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() {
return SafeArea(
child: Container(
- height: 50.h,
+ height: Get.height * 0.05,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
@@ -58,8 +59,9 @@ class BaseWidgetsPage extends GetView {
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
- _buildNavItem(2, "ic_car_select@2x", "ic_car@2x"),
- _buildNavItem(3, "ic_user_select@2x", "ic_user@2x"),
+ _buildNavItem(2, "ic_mall_select@2x", "ic_mall@2x"),
+ _buildNavItem(3, "ic_car_select@2x", "ic_car@2x"),
+ _buildNavItem(4, "ic_user_select@2x", "ic_user@2x"),
],
),
),
@@ -82,7 +84,8 @@ class BaseWidgetsPage extends GetView {
child: SizedBox(
height: 24,
width: 24,
- child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),),
+ child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
+ ),
),
);
}
diff --git a/ln_jq_app/lib/pages/c_page/car_info/controller.dart b/ln_jq_app/lib/pages/c_page/car_info/controller.dart
index ee2045c..ada54aa 100644
--- a/ln_jq_app/lib/pages/c_page/car_info/controller.dart
+++ b/ln_jq_app/lib/pages/c_page/car_info/controller.dart
@@ -1,5 +1,8 @@
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:getx_scaffold/getx_scaffold.dart' as dio;
+import 'package:image_picker/image_picker.dart';
+import 'package:image_picker_platform_interface/src/types/image_source.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
@@ -15,9 +18,9 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// --- 车辆基本信息 ---
String plateNumber = "";
- String vin = "未知";
- String modelName = "未知";
- String brandName = "未知";
+ String vin = "-";
+ String modelName = "-";
+ String brandName = "-";
// --- 证件附件列表 ---
final RxList drivingAttachments = [].obs;
@@ -89,6 +92,113 @@ class CarInfoController extends GetxController with BaseControllerMixin {
}
}
+ //上传证件照
+ void pickImage(String title, ImageSource source) async {
+ if (source == ImageSource.camera) {
+ takePhotoAndRecognize(title);
+ } else if (source == ImageSource.gallery) {
+ //相册选择逻辑
+ var status = await Permission.photos.request();
+ if (!status.isGranted) {
+ if (status.isPermanentlyDenied) openAppSettings();
+ showErrorToast("需要相册权限才能拍照上传");
+ return;
+ }
+ final XFile? image = await ImagePicker().pickImage(
+ source: ImageSource.gallery,
+ imageQuality: 80,
+ );
+ if (image != null) {
+ _uploadAndSaveCertificate(title, image.path);
+ }
+ }
+ }
+
+ void takePhotoAndRecognize(String title) 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;
+
+ _uploadAndSaveCertificate(title, photo.path);
+ }
+
+ /// 提取共用的上传与关联证件逻辑
+ void _uploadAndSaveCertificate(String title, String filePath) async {
+ // 上传文件
+ String? imageUrl = await uploadFile(filePath);
+ if (imageUrl == null) return;
+
+ // 根据标题映射业务类型
+ String type = "";
+ switch (title) {
+ case "行驶证":
+ type = "9";
+ break;
+ case "营运证":
+ type = "10";
+ break;
+ case "加氢证":
+ type = "12";
+ break;
+ case "登记证":
+ type = "13";
+ break;
+ default:
+ return;
+ }
+
+ // 调用后台接口关联证件
+ var response = await HttpService.to.post(
+ "appointment/truck/uploadCertificatePic",
+ data: {
+ "plateNumber": plateNumber,
+ "imageUrl": imageUrl,
+ "type": type,
+ },
+ );
+
+ if (response != null) {
+ final result = BaseModel.fromJson(response.data);
+ if (result.code == 0) {
+ showSuccessToast("上传成功");
+ getUserBindCarInfo(); // 重新拉取数据更新UI
+ } else {
+ showErrorToast(result.error);
+ }
+ }
+ }
+
+ /// 上传图片
+ Future uploadFile(String filePath) async {
+ showLoading("正在上传...");
+ try {
+ dio.FormData formData = dio.FormData.fromMap({
+ 'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_identity.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;
+ }
+
void getUserBindCarInfo() async {
if (StorageService.to.hasVehicleInfo) {
VehicleInfo? bean = StorageService.to.vehicleInfo;
@@ -102,7 +212,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// 获取证件信息
final response = await HttpService.to.get(
- 'appointment/vehicle/getPicInfoByVin?vin=$vin',
+ 'appointment/vehicle/getPicInfoByVin?vin=$vin&plateNumber=$plateNumber',
);
if (response != null && response.data != null) {
@@ -134,10 +244,10 @@ class CarInfoController extends GetxController with BaseControllerMixin {
...registerAttachments,
];
- color = data['color'].toString();
- hydrogenCapacity = data['hydrogenCapacity'].toString();
- rentFromCompany = data['rentFromCompany'].toString();
- address = data['address'].toString();
+ color = data['color'].toString();
+ hydrogenCapacity = data['hydrogenCapacity'].toString();
+ rentFromCompany = data['rentFromCompany'].toString();
+ address = data['address'].toString();
loadAllPdfs();
}
diff --git a/ln_jq_app/lib/pages/c_page/car_info/view.dart b/ln_jq_app/lib/pages/c_page/car_info/view.dart
index 0811bc4..ee0d8b3 100644
--- a/ln_jq_app/lib/pages/c_page/car_info/view.dart
+++ b/ln_jq_app/lib/pages/c_page/car_info/view.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:image_picker/image_picker.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart';
@@ -133,7 +134,7 @@ class CarInfoPage extends GetView {
),
),
IconButton(
- onPressed: () async{
+ onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
@@ -163,11 +164,26 @@ class CarInfoPage extends GetView {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- _buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
+ _buildModernStatItem(
+ '本月里程数',
+ 'Accumulated',
+ StorageService.to.hasVehicleInfo ? '2,852km' : '-',
+ '',
+ ),
const SizedBox(width: 8),
- _buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
+ _buildModernStatItem(
+ '总里程',
+ 'Refuel Count',
+ StorageService.to.hasVehicleInfo ? "2.5W km" : '-',
+ '',
+ ),
const SizedBox(width: 8),
- _buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
+ _buildModernStatItem(
+ '服务评分',
+ 'Driver rating',
+ StorageService.to.hasVehicleInfo ? "4.9分" : '-',
+ '',
+ ),
],
),
),
@@ -300,20 +316,20 @@ class CarInfoPage extends GetView {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
- child: const LinearProgressIndicator(
- value: 0.75,
+ child: LinearProgressIndicator(
+ value: StorageService.to.hasVehicleInfo ? 0.75 : 0,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
- const Row(
+ Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
- "75%",
+ StorageService.to.hasVehicleInfo ? "75%" : "0%",
style: TextStyle(
fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
@@ -353,7 +369,7 @@ class CarInfoPage extends GetView {
children: [
_buildCertificateContent('行驶证', controller.drivingAttachments),
_buildCertificateContent('营运证', controller.operationAttachments),
- _buildCertificateContent('加氢资格证', controller.hydrogenationAttachments),
+ _buildCertificateContent('加氢证', controller.hydrogenationAttachments),
_buildCertificateContent('登记证', controller.registerAttachments),
],
),
@@ -373,7 +389,7 @@ class CarInfoPage extends GetView {
child: Padding(
padding: EdgeInsets.all(16.0),
child: attachments.isEmpty
- ? const Center(child: Text('暂无相关证件信息'))
+ ? _buildEmptyCertificateState(title)
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@@ -382,7 +398,11 @@ class CarInfoPage extends GetView {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- _buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
+ _buildCertDetailItem(
+ '所属公司',
+ controller.rentFromCompany,
+ isFull: false,
+ ),
_buildCertDetailItem('运营城市', controller.address),
],
),
@@ -421,6 +441,158 @@ class CarInfoPage extends GetView {
});
}
+ Widget _buildEmptyCertificateState(String title) {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Image.asset(
+ 'assets/images/ic_attention@2x.png', // 请替换为您的实际图片路径
+ width: 120,
+ height: 120,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ '您未上传“$title”',
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ color: Color.fromRGBO(51, 51, 51, 1),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '上传后可提前通知加氢站报备\n大幅减少加氢站等待时间',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 12.sp,
+ color: Color.fromRGBO(156, 163, 175, 1),
+ height: 1.5,
+ ),
+ ),
+ const SizedBox(height: 24),
+ SizedBox(
+ width: 200,
+ height: 44,
+ child: ElevatedButton.icon(
+ onPressed: () {
+ _showUploadDialog(title);
+ },
+ icon: const Text(
+ '立即上传',
+ style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
+ ),
+ label: Image.asset(
+ 'assets/images/ic_upload@2x.png',
+ height: 20.h,
+ width: 20.w,
+ ),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF017137),
+ foregroundColor: Colors.white,
+ shape: StadiumBorder(),
+ elevation: 0,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _showUploadDialog(String title) {
+ Get.dialog(
+ Dialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+ child: Container(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '上传$title',
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: Colors.black,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ '请确保拍摄证件清晰可见,关键文字无反光遮挡,这将有助于快速通过审核',
+ style: TextStyle(fontSize: 13, color: Colors.grey[400], height: 1.5),
+ ),
+ const SizedBox(height: 24),
+ Row(
+ children: [
+ Expanded(
+ child: _buildUploadOption(
+ icon: Icons.camera_alt_outlined,
+ label: '拍照上传',
+ onTap: () {
+ controller.pickImage(title, ImageSource.camera);
+ Get.back();
+ },
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: _buildUploadOption(
+ icon: Icons.image_outlined,
+ label: '相册上传',
+ onTap: () {
+ controller.pickImage(title, ImageSource.gallery);
+ Get.back();
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ // 构建弹窗内的选择按钮
+ Widget _buildUploadOption({
+ required IconData icon,
+ required String label,
+ required VoidCallback onTap,
+ }) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 24),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF2F9F7), // 浅绿色背景
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: const BoxDecoration(
+ color: Color(0xFF017137), // 深绿色圆圈
+ shape: BoxShape.circle,
+ ),
+ child: Icon(icon, color: Colors.white, size: 28),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ label,
+ style: const TextStyle(
+ fontSize: 14,
+ color: Color(0xFF333333),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
Widget _buildCertDetailItem(
String label,
String value, {
diff --git a/ln_jq_app/lib/pages/c_page/mall/detail/controller.dart b/ln_jq_app/lib/pages/c_page/mall/detail/controller.dart
new file mode 100644
index 0000000..74eb736
--- /dev/null
+++ b/ln_jq_app/lib/pages/c_page/mall/detail/controller.dart
@@ -0,0 +1,113 @@
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:ln_jq_app/common/model/base_model.dart';
+import 'package:ln_jq_app/pages/c_page/mall/exchange_success/view.dart';
+
+import '../mall_controller.dart';
+
+class MallDetailController extends GetxController with BaseControllerMixin {
+ @override
+ String get builderId => 'mall_detail';
+
+ late final int goodsId;
+ final Rx goodsDetail = Rx(null);
+
+ final addressController = TextEditingController();
+ final nameController = TextEditingController();
+ final phoneController = TextEditingController();
+
+ final formKey = GlobalKey();
+
+ @override
+ void onInit() {
+ super.onInit();
+ goodsId = Get.arguments['goodsId'] as int;
+ getGoodsDetail();
+ }
+
+ @override
+ bool get listenLifecycleEvent => true;
+
+ Future getGoodsDetail() async {
+ try {
+ var response = await HttpService.to.post(
+ 'appointment/score/getScoreGoodsDetail',
+ data: {'goodsId': goodsId},
+ );
+ if (response != null && response.data != null) {
+ var result = BaseModel.fromJson(
+ response.data,
+ dataBuilder: (dataJson) => GoodsModel.fromJson(dataJson),
+ );
+ if (result.code == 0 && result.data != null) {
+ goodsDetail.value = result.data;
+ } else {
+ showErrorToast('加载失败: ${result.message}');
+ Get.back();
+ }
+ }
+ } catch (e) {
+ log('获取商品详情失败: $e');
+ showErrorToast('网络异常,请稍后重试');
+ Get.back();
+ } finally {
+ updateUi();
+ }
+ }
+
+ /// 兑换商品
+ void exchange() async {
+ if (!formKey.currentState!.validate()) {
+ return;
+ }
+
+ /*
+ final mallController = Get.find();
+ if (mallController.userScore.value < (goodsDetail.value?.score ?? 0)) {
+ showWarningToast('积分不足');
+ return;
+ }*/
+
+ // 接口调用预留
+ showLoading('兑换中...');
+
+ final goods = goodsDetail.value;
+ if (goods == null) {
+ showErrorToast('兑换失败,请稍后重试');
+ return;
+ }
+ try {
+ var response = await HttpService.to.post(
+ 'appointment/score/scoreExchange',
+ data: {
+ "goodsId": goods.id,
+ "address": addressController.text,
+ "name": nameController.text,
+ "phone": phoneController.text,
+ },
+ );
+ if (response != null && response.data != null) {
+ var result = BaseModel.fromJson(response.data);
+ if (result.code == 0) {
+ Get.off(() => MallExchangeSuccessPage());
+ }
+ }
+ } catch (e) {
+ log('兑换失败: $e');
+ showErrorToast('兑换失败,请稍后重试');
+ } finally {
+ dismissLoading();
+ }
+ }
+
+ @override
+ void onClose() {
+ addressController.dispose();
+ nameController.dispose();
+ phoneController.dispose();
+ super.onClose();
+ }
+}
diff --git a/ln_jq_app/lib/pages/c_page/mall/detail/view.dart b/ln_jq_app/lib/pages/c_page/mall/detail/view.dart
new file mode 100644
index 0000000..d67db55
--- /dev/null
+++ b/ln_jq_app/lib/pages/c_page/mall/detail/view.dart
@@ -0,0 +1,281 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:getx_scaffold/getx_scaffold.dart';
+import 'controller.dart';
+
+class MallDetailPage extends GetView {
+ const MallDetailPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return GetBuilder(
+ init: MallDetailController(),
+ id: 'mall_detail',
+ builder: (_) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF7F8FA),
+ appBar: AppBar(
+ title: const Text('商品兑换'),
+ backgroundColor: Colors.white,
+ foregroundColor: Colors.black,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios, size: 20),
+ onPressed: () => Get.back(),
+ ),
+ ),
+ body: GestureDetector(
+ onTap: () {
+ hideKeyboard();
+ },
+ child: _buildBody(),
+ ),
+ bottomNavigationBar: _buildBottomButton(),
+ );
+ },
+ );
+ }
+
+ Widget _buildBody() {
+ final goods = controller.goodsDetail.value;
+ if (goods == null) return const Center(child: Text('商品信息不存在'));
+
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(16),
+ child: Form(
+ key: controller.formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildGoodsInfoCard(goods),
+ const SizedBox(height: 24),
+ _buildSectionTitle('填写收货信息'),
+ const SizedBox(height: 16),
+ _buildInputLabel('详细地址'),
+ _buildTextField(
+ controller: controller.addressController,
+ hint: '请输入完整的收货地址',
+ icon: Icons.location_on_outlined,
+ ),
+ const SizedBox(height: 16),
+ _buildInputLabel('收货人姓名'),
+ _buildTextField(
+ controller: controller.nameController,
+ hint: '请输入收货人姓名',
+ icon: Icons.person_outline,
+ ),
+ const SizedBox(height: 16),
+ _buildInputLabel('联系电话'),
+ _buildTextField(
+ controller: controller.phoneController,
+ hint: '请输入手机号码',
+ icon: Icons.phone_android_outlined,
+ keyboardType: TextInputType.phone,
+ ),
+ const SizedBox(height: 40),
+ Center(
+ child: Text(
+ '兑换成功后,商品会在3个工作日内邮寄\n请注意查收',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Color(0xFF999999),
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildGoodsInfoCard(goods) {
+ return Container(
+ padding: const EdgeInsets.all(17),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.03),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: goods.goodsImage != null
+ ? Image.network(
+ goods.goodsImage!,
+ width: 94.w,
+ height: 94.h,
+ fit: BoxFit.cover,
+ )
+ : Container(
+ width: 80,
+ height: 80,
+ color: Colors.grey[200],
+ child: const Icon(Icons.image, color: Colors.grey),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ goods.goodsName,
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ SizedBox(height: 8.h),
+ Row(
+ children: [
+ Text(
+ '${goods.score}',
+ style: TextStyle(
+ fontSize: 20.sp,
+ color: Color(0xFF4CAF50),
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Text(
+ '积分',
+ style: TextStyle(
+ fontSize: 10.sp,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF999999),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 10.h),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF2F3F5),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ '数量: 1',
+ style: TextStyle(
+ fontSize: 10.sp,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF666666),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSectionTitle(String title) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 6.w,
+ height: 16.h,
+ decoration: BoxDecoration(
+ color: const Color(0xFF4CAF50),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ color: Color.fromRGBO(148, 163, 184, 1),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildInputLabel(String label) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Text(
+ label,
+ style: TextStyle(
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w600,
+ color: Color.fromRGBO(100, 116, 139, 1),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildTextField({
+ required TextEditingController controller,
+ required String hint,
+ required IconData icon,
+ TextInputType? keyboardType,
+ }) {
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TextFormField(
+ controller: controller,
+ keyboardType: keyboardType,
+ textAlign: TextAlign.start,
+ decoration: InputDecoration(
+ hintText: hint,
+ hintStyle: TextStyle(
+ color: Color.fromRGBO(134, 144, 156, 1),
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ ),
+ prefixIcon: Icon(icon, color: const Color(0xFF999999), size: 20),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.symmetric(vertical: 12),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return '内容不能为空';
+ }
+ return null;
+ },
+ ),
+ );
+ }
+
+ Widget _buildBottomButton() {
+ return Container(
+ padding: const EdgeInsets.fromLTRB(16, 10, 16, 30),
+ child: ElevatedButton(
+ onPressed: controller.exchange,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF007A45),
+ minimumSize: const Size(double.infinity, 50),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
+ elevation: 0,
+ ),
+ child: const Text(
+ '兑换商品',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/ln_jq_app/lib/pages/c_page/mall/exchange_success/view.dart b/ln_jq_app/lib/pages/c_page/mall/exchange_success/view.dart
new file mode 100644
index 0000000..bcf0464
--- /dev/null
+++ b/ln_jq_app/lib/pages/c_page/mall/exchange_success/view.dart
@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:getx_scaffold/common/index.dart';
+import 'package:ln_jq_app/common/login_util.dart';
+
+class MallExchangeSuccessPage extends StatelessWidget {
+ const MallExchangeSuccessPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios, size: 20, color: Colors.black),
+ onPressed: () => Get.back(), // 返回首页
+ ),
+ title: const Text('商品兑换', style: TextStyle(color: Colors.black, fontSize: 18)),
+ ),
+ body: Center(
+ child: Column(
+ children: [
+ SizedBox(height: 114.h),
+ _buildSuccessIcon(),
+ const SizedBox(height: 24),
+ Text(
+ '兑换成功',
+ style: TextStyle(
+ fontSize: 24.sp,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '预计 3 日内发货\n请留意查收',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 14.sp, color: Color(0xFF999999)),
+ ),
+ const SizedBox(height: 60),
+ ElevatedButton(
+ onPressed: () => Get.back(),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF007A45),
+ minimumSize: const Size(140, 50),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
+ ),
+ child: Text(
+ '返回首页',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.w400,
+ fontSize: 16.sp,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSuccessIcon() {
+ return Container(child: LoginUtil.getAssImg("mall_pay_success@2x"));
+ }
+}
diff --git a/ln_jq_app/lib/pages/c_page/mall/mall_controller.dart b/ln_jq_app/lib/pages/c_page/mall/mall_controller.dart
new file mode 100644
index 0000000..32384e8
--- /dev/null
+++ b/ln_jq_app/lib/pages/c_page/mall/mall_controller.dart
@@ -0,0 +1,160 @@
+
+import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:ln_jq_app/common/model/base_model.dart';
+import 'package:ln_jq_app/pages/c_page/mall/detail/view.dart';
+import 'package:ln_jq_app/pages/c_page/mall/orders/view.dart';
+import 'package:ln_jq_app/pages/c_page/mall/rule/view.dart';
+
+class GoodsModel {
+ final int id;
+ final String categoryId;
+ final String goodsName;
+ final String? goodsImage;
+ final int originalScore;
+ final int score;
+ final int stock;
+ final int status;
+
+ GoodsModel({
+ required this.id,
+ required this.categoryId,
+ required this.goodsName,
+ this.goodsImage,
+ required this.originalScore,
+ required this.score,
+ required this.stock,
+ required this.status,
+ });
+
+ factory GoodsModel.fromJson(Map json) {
+ return GoodsModel(
+ id: json['id'] as int,
+ categoryId: json['categoryId']?.toString() ?? '',
+ goodsName: json['goodsName']?.toString() ?? '',
+ goodsImage: json['goodsImage'],
+ originalScore: json['originalScore'] as int? ?? 0,
+ score: json['score'] as int? ?? 0,
+ stock: json['stock'] as int? ?? 0,
+ status: json['status'] as int? ?? 1,
+ );
+ }
+}
+
+class UserScore {
+ final int score;
+ final int todaySign;
+
+ UserScore({required this.score, required this.todaySign});
+
+ factory UserScore.fromJson(Map json) {
+ return UserScore(
+ score: json['score'] as int? ?? 0,
+ todaySign: json['todaySign'] as int? ?? 1,
+ );
+ }
+}
+
+class MallController extends GetxController with BaseControllerMixin {
+ @override
+ String get builderId => 'mall';
+
+ final RxInt userScore = 0.obs;
+ final RxInt todaySign = 1.obs; // 0可签到,1已签到
+ final RxList goodsList = [].obs;
+ final RxBool isLoading = true.obs;
+
+ @override
+ void onInit() {
+ super.onInit();
+ refreshData();
+ }
+
+ Future refreshData() async {
+ isLoading.value = true;
+ await Future.wait([getUserScore(), getGoodsList()]);
+ isLoading.value = false;
+ updateUi();
+ }
+
+ /// 获取用户积分和签到状态
+ Future getUserScore() async {
+ try {
+ var response = await HttpService.to.post('appointment/score/getUserScore');
+ if (response != null && response.data != null) {
+ var result = BaseModel.fromJson(
+ response.data,
+ dataBuilder: (dataJson) => UserScore.fromJson(dataJson),
+ );
+ if (result.code == 0 && result.data != null) {
+ userScore.value = result.data!.score;
+ todaySign.value = result.data!.todaySign;
+ }
+ }
+ } catch (e) {
+ log('获取积分失败: $e');
+ }
+ }
+
+ /// 签到逻辑
+ Future signAction() async {
+ if (todaySign.value == 1) return;
+
+ showLoading('签到中...');
+ try {
+ var response = await HttpService.to.post('appointment/score/sign');
+ dismissLoading();
+ if (response != null && response.data != null) {
+ var result = BaseModel.fromJson(response.data);
+ if (result.code == 0) {
+ showSuccessToast('签到成功');
+ getUserScore(); // 签到成功后刷新积分
+ } else {
+ showErrorToast(result.message);
+ }
+ }
+ } catch (e) {
+ dismissLoading();
+ showErrorToast('签到异常');
+ }
+ }
+
+ /// 获取商品列表
+ Future getGoodsList() async {
+ try {
+ var response = await HttpService.to.post(
+ 'appointment/score/getScoreGoodsList',
+ data: {'categoryId': 0},
+ );
+ if (response != null && response.data != null) {
+ var result = BaseModel>.fromJson(
+ response.data,
+ dataBuilder: (dataJson) {
+ var list = dataJson as List;
+ return list.map((e) => GoodsModel.fromJson(e)).toList();
+ },
+ );
+ if (result.code == 0 && result.data != null) {
+ goodsList.assignAll(result.data!);
+ }
+ }
+ } catch (e) {
+ log('获取商品列表失败: $e');
+ }
+ }
+
+ /// 兑换商品
+ void exchangeGoods(GoodsModel goods) {
+ Get.to(() => const MallDetailPage(), arguments: {'goodsId': goods.id})
+ ?.then((_) => refreshData());
+ }
+
+ ///规则说明
+ void toRuleDes() {
+ Get.to(() => const MallRulePage());
+ }
+
+ ///历史订单
+ void toOrders() {
+ Get.to(() => const MallOrdersPage());
+ }
+}
diff --git a/ln_jq_app/lib/pages/c_page/mall/mall_view.dart b/ln_jq_app/lib/pages/c_page/mall/mall_view.dart
new file mode 100644
index 0000000..49f249a
--- /dev/null
+++ b/ln_jq_app/lib/pages/c_page/mall/mall_view.dart
@@ -0,0 +1,345 @@
+import 'package:flutter/material.dart';
+import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:ln_jq_app/common/login_util.dart';
+
+import 'mall_controller.dart';
+
+class MallPage extends GetView {
+ const MallPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return GetX(
+ init: MallController(),
+ builder: (_) {
+ return Scaffold(
+ backgroundColor: Color.fromRGBO(247, 249, 251, 1),
+ body: RefreshIndicator(
+ onRefresh: controller.refreshData,
+ child: CustomScrollView(
+ slivers: [
+ SliverToBoxAdapter(
+ child: Container(
+ padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20.h),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.only(
+ bottomLeft: Radius.circular(20),
+ bottomRight: Radius.circular(20),
+ ),
+ ),
+ child: Column(children: [_buildAppBar(), _buildScoreCard()]),
+ ),
+ ),
+ _buildSectionHeader(),
+ _buildGoodsGrid(),
+ SliverToBoxAdapter(child: SizedBox(height: 120.h)),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildAppBar() {
+ return Container(
+ padding: EdgeInsets.only(top: MediaQuery.of(Get.context!).padding.top, bottom: 0),
+ child: Row(
+ children: [
+ Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: const Color(0xFF4CAF50),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: LoginUtil.getAssImg("mall_bar@2x"),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ '氢能商城',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF333333),
+ ),
+ ),
+ Row(
+ children: [
+ const Text(
+ 'Hydrogen Energy Mall',
+ style: TextStyle(fontSize: 12, color: Color(0xFF999999)),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ /*IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.notifications_none, color: Color(0xFF333333)),
+ ),*/
+ ],
+ ),
+ );
+ }
+
+ Widget _buildScoreCard() {
+ return Container(
+ margin: EdgeInsets.only(top: 22.h),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color.fromRGBO(2, 27, 31, 1), Color.fromRGBO(11, 67, 67, 1)],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ '我的可用积分',
+ style: TextStyle(color: Colors.white70, fontSize: 14.sp),
+ ),
+ const SizedBox(width: 4),
+ GestureDetector(
+ onTap: (){
+ controller.toRuleDes();
+ },
+ child: const Icon(Icons.help_outline, color: Colors.white70, size: 14),
+ )
+
+ ],
+ ),
+ Text(
+ 'Available points',
+ style: TextStyle(color: Colors.white38, fontSize: 11.sp),
+ ),
+ ],
+ ),
+ GestureDetector(
+ onTap: controller.signAction,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
+ decoration: BoxDecoration(
+ color: controller.todaySign.value == 0
+ ? Color.fromRGBO(78, 184, 49, 1)
+ : Colors.grey,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Text(
+ controller.todaySign.value == 0 ? '立即签到' : '已签到',
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ controller.userScore.value.toString(),
+ style: TextStyle(
+ color: Color.fromRGBO(236, 236, 236, 1),
+ fontSize: 18.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ controller.toOrders();
+ },
+ child: Text(
+ '历史订单',
+ style: TextStyle(
+ color: Color.fromRGBO(148, 163, 184, 1),
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSectionHeader() {
+ return SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ child: Row(
+ children: [
+ Container(
+ width: 4.w,
+ height: 16.h,
+ decoration: BoxDecoration(
+ color: const Color(0xFF4CAF50),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ '热门商品',
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ color: Color.fromRGBO(78, 89, 105, 1),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildGoodsGrid() {
+ if (controller.goodsList.isEmpty) {
+ return const SliverToBoxAdapter(
+ child: Center(
+ child: Padding(
+ padding: EdgeInsets.only(top: 50),
+ child: Text('暂无商品', style: TextStyle(color: Color(0xFF999999))),
+ ),
+ ),
+ );
+ }
+
+ return SliverPadding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ sliver: SliverGrid(
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 2,
+ mainAxisSpacing: 14.h,
+ crossAxisSpacing: 19.w,
+ childAspectRatio: 0.75,
+ ),
+ delegate: SliverChildBuilderDelegate((context, index) {
+ final goods = controller.goodsList[index];
+ return _buildGoodsItem(goods);
+ }, childCount: controller.goodsList.length),
+ ),
+ );
+ }
+
+ Widget _buildGoodsItem(GoodsModel goods) {
+ return Container(
+ padding: EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(10),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.05),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: ClipRRect(
+ borderRadius: const BorderRadius.all(Radius.circular(12)),
+ child: goods.goodsImage != null
+ ? Image.network(
+ goods.goodsImage!,
+ fit: BoxFit.cover,
+ width: double.infinity,
+ )
+ : Container(
+ color: const Color(0xFFEEEEEE),
+ child: const Center(
+ child: Icon(Icons.image, color: Colors.grey, size: 40),
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ goods.goodsName,
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ color: Colors.black,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Text(
+ '${goods.score}',
+ style: TextStyle(
+ fontSize: 16.sp,
+ color: Color(0xFF4CAF50),
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(width: 4),
+ Text(
+ '积分',
+ style: TextStyle(
+ fontSize: 12.sp,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF4CAF50),
+ ),
+ ),
+ ],
+ ),
+ GestureDetector(
+ onTap: () => controller.exchangeGoods(goods),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
+ decoration: BoxDecoration(
+ color: const Color(0xFFE8F5E9),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Text(
+ '兑换',
+ style: TextStyle(
+ color: Color(0xFF4CAF50),
+ fontWeight: FontWeight.bold,
+ fontSize: 13.sp,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/ln_jq_app/lib/pages/c_page/mall/orders/controller.dart b/ln_jq_app/lib/pages/c_page/mall/orders/controller.dart
new file mode 100644
index 0000000..9d49911
--- /dev/null
+++ b/ln_jq_app/lib/pages/c_page/mall/orders/controller.dart
@@ -0,0 +1,94 @@
+
+import 'dart:developer';
+import 'package:get/get.dart';
+import 'package:getx_scaffold/getx_scaffold.dart';
+import 'package:ln_jq_app/common/model/base_model.dart';
+
+class OrderModel {
+ final int id;
+ final int scoreGoodsId;
+ final String goodsName;
+ final String? goodsImage;
+ final String? goodsContent;
+ final String address;
+ final String createTime;
+ final String score;
+
+ OrderModel({
+ required this.id,
+ required this.scoreGoodsId,
+ required this.goodsName,
+ this.goodsImage,
+ this.goodsContent,
+ required this.address,
+ required this.createTime,
+ required this.score,
+ });
+
+ factory OrderModel.fromJson(Map json) {
+ return OrderModel(
+ id: json['id'] as int,
+ scoreGoodsId: json['scoreGoodsId'] as int,
+ goodsName: json['goodsName']?.toString() ?? '',
+ goodsImage: json['goodsImage'],
+ goodsContent: json['goodsContent'],
+ address: json['address']?.toString() ?? '',
+ createTime: json['createTime']?.toString() ?? '',
+ score: json['score']?.toString() ?? '',
+ );
+ }
+}
+
+class MallOrdersController extends GetxController with BaseControllerMixin {
+ @override
+ String get builderId => 'mall_orders';
+
+ final RxList orderList = [].obs;
+ final RxBool isLoading = true.obs;
+ int pageNum = 1;
+ final int pageSize = 50;
+
+ @override
+ void onInit() {
+ super.onInit();
+ getOrders();
+ }
+
+ Future getOrders({bool isRefresh = true}) async {
+ if (isRefresh) {
+ pageNum = 1;
+ isLoading.value = true;
+ updateUi();
+ }
+
+ try {
+ var response = await HttpService.to.post(
+ 'appointment/score/getScoreExchangeList',
+ data: {
+ "status": "",
+ "pageNum": pageNum.toString(),
+ "pageSize": pageSize.toString()
+ },
+ );
+
+ if (response != null && response.data != null) {
+ var result = BaseModel