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>.fromJson(response.data); + if (result.code == 0 && result.data != null) { + var records = result.data!['records'] as List; + var list = records.map((e) => OrderModel.fromJson(e)).toList(); + if (isRefresh) { + orderList.assignAll(list); + } else { + orderList.addAll(list); + } + pageNum++; + } + } + } catch (e) { + log('获取订单列表失败: $e'); + } finally { + isLoading.value = false; + updateUi(); + } + } +} diff --git a/ln_jq_app/lib/pages/c_page/mall/orders/view.dart b/ln_jq_app/lib/pages/c_page/mall/orders/view.dart new file mode 100644 index 0000000..8de3674 --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/mall/orders/view.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:getx_scaffold/getx_scaffold.dart'; +import 'controller.dart'; + +class MallOrdersPage extends GetView { + const MallOrdersPage({super.key}); + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: MallOrdersController(), + id: 'mall_orders', + 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: RefreshIndicator( + onRefresh: () => controller.getOrders(isRefresh: true), + child: controller.isLoading.value && controller.orderList.isEmpty + ? const Center(child: CircularProgressIndicator()) + : _buildOrderList(), + ), + ); + }, + ); + } + + Widget _buildOrderList() { + if (controller.orderList.isEmpty) { + return const Center( + child: Text('暂无订单记录', style: TextStyle(color: Color(0xFF999999))), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: controller.orderList.length, + itemBuilder: (context, index) { + final order = controller.orderList[index]; + return _buildOrderItem(order); + }, + ); + } + + Widget _buildOrderItem(OrderModel order) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '兑换时间:${order.createTime}', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Color.fromRGBO(107, 114, 128, 1), + ), + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: order.goodsImage != null + ? Image.network( + order.goodsImage!, + width: 80.w, + height: 80.h, + fit: BoxFit.cover, + ) + : Container( + width: 80.w, + height: 80.h, + color: Colors.grey[200], + child: const Icon(Icons.image, color: Colors.grey), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.goodsName, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + order.score, + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF4CAF50), + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 4), + Text( + '积分', + style: TextStyle(fontSize: 11.sp, color: Color(0xFF4CAF50)), + ), + ], + ), + ], + ), + ), + Text( + 'x1', + style: TextStyle(color: Color(0xFFCCCCCC), fontSize: 16.sp), + ), + ], + ), + ], + ), + ); + } +} diff --git a/ln_jq_app/lib/pages/c_page/mall/rule/view.dart b/ln_jq_app/lib/pages/c_page/mall/rule/view.dart new file mode 100644 index 0000000..86761c8 --- /dev/null +++ b/ln_jq_app/lib/pages/c_page/mall/rule/view.dart @@ -0,0 +1,131 @@ +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 MallRulePage extends StatelessWidget { + const MallRulePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(64, 199, 154, 1), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white, size: 20), + onPressed: () => Get.back(), + ), + ), + body: Stack( + children: [ + // 顶部装饰图 + Positioned( + top: 30, + right: Get.width * 0.15, + child: LoginUtil.getAssImg("rule_bg@2x"), + ), + Container( + margin: const EdgeInsets.fromLTRB(20, 100, 20, 20), + padding: const EdgeInsets.all(24), + width: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/rule_bg_1@2x.png'), + fit: BoxFit.fill, + ), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '积分获取规则', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFF2C3E50), + ), + ), + const SizedBox(height: 30), + _buildRuleItem( + icon: 'tips_1@2x', + title: '每日首次签到积分规则', + content: '每日首签,立得 1 积分', + ), + _buildRuleItem( + icon: 'tips_2@2x', + title: '每日预约加氢积分规则', + content: '每日前 2 次预约加氢,各得 1 积分', + ), + _buildRuleItem( + icon: 'tips_3@2x', + title: '连续签到累计赠分规则', + content: '连续签到 3 天赠 2 积分,7 天赠 5 积分', + ), + _buildRuleItem( + icon: 'tips_4@2x', + title: '连续签到周期及断签重置规则', + content: '7 天为一个签到周期,中途断签则重新从第 1 天计算', + ), + _buildRuleItem( + icon: 'tips_5@2x', + title: '积分使用规则', + content: + '积分有效期3个月。个人账户内累计的所有有效积分,可在平台积分商城中,用于兑换商城内上架的各类商品、权益或服务,兑换时将按照商品标注的积分值扣除对应积分,积分兑换后不支持撤销、退换,商品兑换规则以积分商城内公示为准。', + ), + const SizedBox(height: 40), + const Center( + child: Text( + '本活动最终解释权归官方所有,如有疑问可咨询客服。', + style: TextStyle(color: Color(0xFF999999), fontSize: 12), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildRuleItem({ + required String icon, + required String title, + required String content, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LoginUtil.getAssImg(icon), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + ], + ), + const SizedBox(height: 6), + Padding( + padding: const EdgeInsets.only(left: 0), + child: Text( + content, + style: TextStyle(fontSize: 13.sp, color: Color(0xFF666666), height: 1.5), + ), + ), + ], + ), + ); + } +} diff --git a/ln_jq_app/lib/pages/c_page/message/view.dart b/ln_jq_app/lib/pages/c_page/message/view.dart index 18b376b..6105b3b 100644 --- a/ln_jq_app/lib/pages/c_page/message/view.dart +++ b/ln_jq_app/lib/pages/c_page/message/view.dart @@ -12,40 +12,63 @@ class MessagePage extends GetView { Get.put(MessageController()); return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: AppBar(title: const Text('消息通知'), centerTitle: true), - body: Column( + backgroundColor: const Color(0xFFF7F9FB), + appBar: AppBar( + title: const Text('消息通知'), + centerTitle: true, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, size: 20), + onPressed: () => Get.back(), + ), + ), + body: Stack( children: [ - Expanded( - child: Obx(() => SmartRefresher( - controller: controller.refreshController, - enablePullUp: true, - onRefresh: controller.onRefresh, - onLoading: controller.onLoading, - child: ListView.separated( - padding: const EdgeInsets.all(12), - itemCount: controller.messageList.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (context, index) { - return _buildMessageItem(context, controller.messageList[index]); - }, - ), - )), - ), + Obx(() => SmartRefresher( + controller: controller.refreshController, + enablePullUp: true, + onRefresh: controller.onRefresh, + onLoading: controller.onLoading, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + itemCount: controller.messageList.length, + itemBuilder: (context, index) { + return _buildMessageItem(context, controller.messageList[index]); + }, + ), + )), Obx(() => !controller.allRead.value - ? Container( - padding: const EdgeInsets.all(16), - color: Colors.white, - child: ElevatedButton( - onPressed: controller.markAllRead, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - minimumSize: const Size(double.infinity, 44), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: const Text('全部标为已读', style: TextStyle(fontSize: 16, color: Colors.white)), - ), - ) + ? Positioned( + right: 20, + bottom: 50, + child: GestureDetector( + onTap: controller.markAllRead, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), + decoration: BoxDecoration( + color: const Color(0xFF007A45), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Text( + '全部已读', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + ) : const SizedBox.shrink()), ], ), @@ -53,54 +76,92 @@ class MessagePage extends GetView { } Widget _buildMessageItem(BuildContext context, MessageModel item) { - return GestureDetector( - onTap: () { - controller.markRead(item); - _showMessageDialog(context, item); - }, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(top: 6, right: 12), - width: 8, - height: 8, - decoration: BoxDecoration( - color: item.isRead == 1 ? Colors.grey[300] : const Color(0xFFFAAD14), - shape: BoxShape.circle, + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 左侧时间轴线条和圆点 + SizedBox( + width: 40, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + width: 1.5, + color: const Color(0xFFD8E2EE), + ), + Positioned( + top: 25, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: item.isRead == 1 + ? const Color(0xFFAAB6C3) + : const Color(0xFF4CAF50), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ), + ), + // 右侧内容卡片 + Expanded( + child: GestureDetector( + onTap: () { + controller.markRead(item); + _showMessageDialog(context, item); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.02), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 10), + Text( + item.content, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + height: 1.4, + ), + ), + const SizedBox(height: 12), + Text( + item.createTime, + style: const TextStyle( + fontSize: 12, + color: Color(0xFFCCCCCC), + ), + ), + ], + ), ), ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87), - ), - const SizedBox(height: 8), - Text( - item.content, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 8), - Text( - item.createTime, - style: TextStyle(fontSize: 12, color: Colors.grey[400]), - ), - ], - ), - ), - ], - ), + ), + ], ), ); } @@ -111,7 +172,7 @@ class MessagePage extends GetView { barrierDismissible: true, builder: (context) { return Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( padding: const EdgeInsets.all(24), child: Column( @@ -122,22 +183,22 @@ class MessagePage extends GetView { item.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - const SizedBox(height: 12), + const SizedBox(height: 16), Text( item.content, - style: const TextStyle(fontSize: 15, height: 1.5, color: Colors.black87), + style: const TextStyle( + fontSize: 15, height: 1.5, color: Color(0xFF333333)), ), const SizedBox(height: 24), Align( alignment: Alignment.centerRight, - child: OutlinedButton( + child: TextButton( onPressed: () => Navigator.pop(context), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.blue), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF007A45), ), - child: const Text('确认', style: TextStyle(color: Colors.blue)), + child: const Text('确认', + style: TextStyle(fontWeight: FontWeight.bold)), ), ), ], @@ -147,4 +208,4 @@ class MessagePage extends GetView { }, ); } -} \ No newline at end of file +} diff --git a/ln_jq_app/lib/pages/c_page/reservation/controller.dart b/ln_jq_app/lib/pages/c_page/reservation/controller.dart index 501bc55..05bcf06 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/controller.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/controller.dart @@ -69,239 +69,34 @@ class C_ReservationController extends GetxController with BaseControllerMixin { String get formattedTimeSlot => '${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}'; - void pickDate(BuildContext context) { - DateTime tempDate = selectedDate.value; - - // 获取今天的日期 (不含时间) - final DateTime today = DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - ); - - //计算明天的日期 - final DateTime tomorrow = today.add(const Duration(days: 1)); - - Get.bottomSheet( - Container( - height: 300, - padding: const EdgeInsets.only(top: 6.0), - 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: [ - CupertinoButton( - onPressed: () => Get.back(), - child: const Text( - '取消', - style: TextStyle(color: CupertinoColors.systemGrey), - ), - ), - CupertinoButton( - onPressed: () { - final bool isChangingToToday = - tempDate.isAtSameMomentAs(today) && - !selectedDate.value.isAtSameMomentAs(today); - final bool isDateChanged = !tempDate.isAtSameMomentAs( - selectedDate.value, - ); - - // 更新选中的日期 - selectedDate.value = tempDate; - Get.back(); // 先关闭弹窗 - - // 如果日期发生了变化,则重置时间 - if (isDateChanged) { - resetTimeForSelectedDate(); - } - }, - child: const Text( - '确认', - style: TextStyle( - color: AppTheme.themeColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - const Divider(height: 1, color: Color(0xFFE5E5E5)), - Expanded( - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.date, - initialDateTime: selectedDate.value, - minimumDate: today, - // 最小可选日期为今天 - maximumDate: tomorrow, - // 最大可选日期为明天 - // --------------------- - onDateTimeChanged: (DateTime newDate) { - tempDate = newDate; - }, - ), - ), - ], - ), - ), - backgroundColor: Colors.transparent, - ); - } - void resetTimeForSelectedDate() { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); - // 判断新选择的日期是不是今天 + // 1. 获取当前站点 + final station = stationOptions.firstWhereOrNull( + (s) => s.hydrogenId == selectedStationId.value, + ); + if (station == null) return; + + // 2. 解析营业开始和结束的小时 + final bizStartHour = int.tryParse(station.startBusiness.split(':')[0]) ?? 0; + final bizEndHour = int.tryParse(station.endBusiness.split(':')[0]) ?? 23; + if (selectedDate.value.isAtSameMomentAs(today)) { - // 如果是今天,就将时间重置为当前时间所在的半小时区间 - startTime.value = _calculateInitialStartTime(now); - endTime.value = TimeOfDay.fromDateTime( - _getDateTimeFromTimeOfDay(startTime.value).add(const Duration(minutes: 30)), - ); + // 如果是今天:起始时间 = max(当前小时, 营业开始小时),且上限为营业结束小时 + int targetHour = now.hour; + if (targetHour < bizStartHour) targetHour = bizStartHour; + if (targetHour > bizEndHour) targetHour = bizEndHour; + + startTime.value = TimeOfDay(hour: targetHour, minute: 0); } else { - // 如果是明天(或其他未来日期),则可以将时间重置为一天的最早可用时间,例如 00:00 - startTime.value = const TimeOfDay(hour: 0, minute: 0); - endTime.value = const TimeOfDay(hour: 0, minute: 30); - } - } - - ///60 分钟为间隔 时间选择器 - void pickTime(BuildContext context) { - final now = DateTime.now(); - final isToday = - selectedDate.value.year == now.year && - selectedDate.value.month == now.month && - selectedDate.value.day == now.day; - - final List availableSlots = []; - for (int i = 0; i < 24; i++) { - // 每次增加 60 分钟 - final startMinutes = i * 60; - final endMinutes = startMinutes + 60; - - final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60); - // 注意:endMinutes % 60 始终为 0,因为间隔是整小时 - final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60); - - // 如果不是今天,所有时间段都有效 - if (!isToday) { - availableSlots.add(TimeSlot(startTime, endTime)); - } else { - // 如果是今天,需要判断该时间段是否可选 - // 创建时间段的结束时间对象 - final slotEndDateTime = DateTime( - selectedDate.value.year, - selectedDate.value.month, - selectedDate.value.day, - endTime.hour, - endTime.minute, - ); - - // 注意:如果是跨天的 00:00 (例如 23:00 - 00:00),需要将日期加一天,否则 isAfter 判断会出错 - // 但由于我们用的是 endTime.hour % 24,当变成 0 时,日期还是 selectedDate - // 这里做一个特殊处理:如果 endTime 是 00:00,意味着它实际上是明天的开始 - DateTime realEndDateTime = slotEndDateTime; - if (endTime.hour == 0 && endTime.minute == 0) { - realEndDateTime = slotEndDateTime.add(const Duration(days: 1)); - } - - // 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的 - if (realEndDateTime.isAfter(now)) { - availableSlots.add(TimeSlot(startTime, endTime)); - } - } + // 如果是明天:起始时间直接重置为营业开始小时 + startTime.value = TimeOfDay(hour: bizStartHour, minute: 0); } - if (availableSlots.isEmpty) { - showToast('今天已没有可预约的时间段'); - return; - } - - // 查找当前选中的时间对应的新列表中的索引 - int initialItem = availableSlots.indexWhere( - (slot) => slot.start.hour == startTime.value.hour, - ); - - if (initialItem == -1) { - initialItem = 0; - } - - TimeSlot tempSlot = availableSlots[initialItem]; - - final FixedExtentScrollController scrollController = FixedExtentScrollController( - initialItem: initialItem, - ); - - 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: [ - CupertinoButton( - onPressed: () => Get.back(), - child: const Text( - '取消', - style: TextStyle(color: CupertinoColors.systemGrey), - ), - ), - CupertinoButton( - onPressed: () { - startTime.value = tempSlot.start; - endTime.value = tempSlot.end; - Get.back(); - }, - child: const Text( - '确认', - style: TextStyle( - color: AppTheme.themeColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - const Divider(height: 1, color: Color(0xFFE5E5E5)), - Expanded( - child: CupertinoPicker( - scrollController: scrollController, - itemExtent: 40.0, - onSelectedItemChanged: (index) { - tempSlot = availableSlots[index]; - }, - children: availableSlots - .map((slot) => Center(child: Text(slot.display))) - .toList(), - ), - ), - ], - ), - ), - backgroundColor: Colors.transparent, - ); + // 结束时间默认顺延1小时 + endTime.value = TimeOfDay(hour: (startTime.value.hour + 1) % 24, minute: 0); } // 用于存储上一次成功预约的信息 @@ -426,8 +221,14 @@ class C_ReservationController extends GetxController with BaseControllerMixin { stateName: '', addStatus: '', addStatusName: '', - rejectReason: '', hasEdit: true, + rejectReason: '', + isTruckAttachment: 0, + hasHydrogenationAttachment: true, + hasDrivingAttachment: true, + isEdit: '', + drivingAttachments: [], + hydrogenationAttachments: [], gunNumber: '', ); //打开预约列表 @@ -458,7 +259,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { if (_debounce?.isActive ?? false) { return; } - _debounce = Timer(const Duration(seconds: 1), () {}); + _debounce = Timer(const Duration(milliseconds: 200), () {}); showLoading("加载中"); @@ -536,12 +337,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } } - String workEfficiency = "0"; - String fillingWeight = "0"; - String fillingTimes = "0"; + String workEfficiency = "-"; + String fillingWeight = "-"; + String fillingTimes = "-"; + String modeImage = ""; String plateNumber = ""; String vin = ""; - String leftHydrogen = "0"; + String leftHydrogen = "-"; num maxHydrogen = 0; String difference = ""; var progressValue = 0.0; @@ -597,7 +399,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { // 创建一个每1分钟执行一次的周期性定时器 _refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - getSiteList(); + getSiteList(showloading: false); }); } @@ -650,7 +452,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { try { HttpService.to.setBaseUrl(AppTheme.test_service_url); var responseData = await HttpService.to.get( - 'appointment/truck/history-filling-summary?vin=$vin', + 'appointment/truck/history-filling-summary?vin=$vin&plateNumber=$plateNumber', ); if (responseData == null || responseData.data == null) { showToast('服务暂不可用,请稍后'); @@ -664,6 +466,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { fillingWeight = "$formatted${result.data["fillingWeightUnit"]}"; fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}"; + modeImage = result.data["modeImage"].toString(); updateUi(); } catch (e) { @@ -689,8 +492,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin { var result = BaseModel.fromJson(responseData.data); - leftHydrogen = result.data["leftHydrogen"].toString(); - workEfficiency = result.data["workEfficiency"].toString(); + leftHydrogen = "${result.data["leftHydrogen"]}Kg"; + workEfficiency = "${result.data["workEfficiency"]}Kg"; final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0; difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2); @@ -724,7 +527,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin { updateUi(); } - void getSiteList() async { + void getSiteList({showloading = true}) async { if (StorageService.to.phone == "13344444444") { //该账号给stationOptions手动添加一个数据 final testStation = StationModel( @@ -735,7 +538,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin { // 价格 siteStatusName: '营运中', // 状态 - isSelect: 1, // 默认可选 + isSelect: 1, + startBusiness: '08:00:00', + endBusiness: '20:00:00', // 默认可选 ); // 使用 assignAll 可以确保列表只包含这个测试数据 stationOptions.assignAll([testStation]); @@ -747,7 +552,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin { } try { - showLoading("加氢站数据加载中"); + if (showloading) { + showLoading("加氢站数据加载中"); + } var responseData = await HttpService.to.get( "appointment/station/queryHydrogenSiteInfo", diff --git a/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart b/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart index f4d3844..055bce9 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/reservation_list_bottomsheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:getx_scaffold/common/index.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/pages/c_page/reservation/controller.dart'; import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart'; import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart'; @@ -36,19 +37,19 @@ class _ReservationListBottomSheetState extends State @override Widget build(BuildContext context) { return Container( - height: Get.height * 0.55, + height: Get.height * 0.6, decoration: const BoxDecoration( - color: Colors.white, + color: Color.fromRGBO(247, 249, 251, 1), borderRadius: BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ // 构建标题和下拉框 _buildHeader(), - const Divider(height: 1), // 下拉筛选框 _buildChoice(), // 构建列表(使用 Obx 监听数据变化) @@ -58,60 +59,64 @@ class _ReservationListBottomSheetState extends State ); } - Container _buildChoice() { - return Container( - padding: const EdgeInsets.fromLTRB(20, 8, 0, 0), - alignment: AlignmentGeometry.centerLeft, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(8), + Widget _buildChoice() { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: _statusOptions.entries.map((entry) { + bool isSelected = _selectedStatus == entry.key; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (_selectedStatus == entry.key) return; + + // 立即执行刷新逻辑 + _controller.getReservationList(addStatus: entry.key); + + // 先更新本地状态改变 UI 选中效果 + setState(() { + _selectedStatus = entry.key; + }); + }, + child: Container( + margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + decoration: BoxDecoration( + // 选中色使用深绿色,未选中保持纯白 + color: isSelected ? const Color(0xFF006633) : Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + entry.key == '' ? '全部' : entry.value, + style: TextStyle( + // 未选中文字颜色微调为图片中的灰蓝色 + color: isSelected ? Colors.white : const Color(0xFFAAB6C3), + fontSize: 14.sp, + fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, + ), + ), ), - child: DropdownButton( - value: _selectedStatus, - underline: const SizedBox.shrink(), // 隐藏下划线 - items: _statusOptions.entries.map((entry) { - return DropdownMenuItem( - value: entry.key, - child: Text(entry.value), - ); - }).toList(), - onChanged: (newValue) { - if (newValue != null) { - setState(() { - _selectedStatus = newValue; - }); - // 当选择新状态时,调用接口刷新数据 - _controller.getReservationList(addStatus: _selectedStatus); - } - }, - ), - ), - ); + ); + }).toList(), + ), + ); } /// 构建标题、关闭按钮和下拉筛选框 Widget _buildHeader() { return Container( - padding: const EdgeInsets.fromLTRB(20, 8, 8, 8), - child: Stack( - alignment: Alignment.center, - children: [ - Center(child: const Text('我的预约', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () => Get.back(), - style: ElevatedButton.styleFrom( - elevation: 0, - backgroundColor: Colors.grey[200], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - ), - child: const Text('关闭', style: TextStyle(color: Colors.black54)), - ), - ), - ], + margin: const EdgeInsets.fromLTRB(20, 20, 8, 8), + child: const Text( + '我的预约', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ); } @@ -134,8 +139,8 @@ class _ReservationListBottomSheetState extends State return Card( color: Colors.white, margin: const EdgeInsets.only(bottom: 12.0), - elevation: 1, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -151,17 +156,20 @@ class _ReservationListBottomSheetState extends State vertical: 5, ), decoration: BoxDecoration( - color: Colors.blue.shade50, // 淡蓝色背景 + color: reservation.state == "-1" + ? Color.fromRGBO(241, 67, 56, 0.1) + : Color.fromRGBO(230, 249, 243, 1), borderRadius: BorderRadius.circular(4), // 小圆角 // 可以选择去掉边框,或者用极淡的边框 - border: Border.all(color: Colors.blue.shade100), ), child: Text( "${reservation.stateName}-${reservation.addStatusName}", style: TextStyle( - color: Colors.blue.shade700, - fontSize: 12, - fontWeight: FontWeight.w500, + color: reservation.state == "-1" + ? Color.fromRGBO(241, 67, 56, 0.8) + : Color.fromRGBO(49, 186, 133, 1), + fontSize: 12.sp, + fontWeight: FontWeight.w600, ), ), ), @@ -171,12 +179,10 @@ class _ReservationListBottomSheetState extends State SizedBox( height: 28, // 限制按钮高度,显得精致 child: OutlinedButton( - onPressed: () async{ + onPressed: () async { var responseData = await HttpService.to.post( 'appointment/orderAddHyd/vehicle-cancel', - data: { - 'id': reservation.id, - }, + data: {'id': reservation.id}, ); if (responseData == null || responseData.data == null) { @@ -198,24 +204,27 @@ class _ReservationListBottomSheetState extends State }, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12), - side: BorderSide(color: Colors.grey.shade400), // 灰色边框 - shape: const StadiumBorder(), // 胶囊形状 + side: BorderSide(color: Colors.grey.shade400), + shape: const StadiumBorder(), ), child: Text( '取消预约', - style: TextStyle(color: Colors.grey.shade600, fontSize: 12), + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 11.sp, + ), ), ), ), - SizedBox(width: 10.w,), + SizedBox(width: 10.w), // 修改按钮 (仅在 hasEdit 为 true 时显示) if (reservation.hasEdit) SizedBox( height: 28, child: OutlinedButton( - onPressed: () async{ + onPressed: () async { var result = await Get.to( - () => ReservationEditPage(), + () => ReservationEditPage(), arguments: { 'reservation': reservation, 'difference': _controller.difference, @@ -233,21 +242,22 @@ class _ReservationListBottomSheetState extends State }, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12), - side: const BorderSide(color: Colors.blue), // 蓝色边框 + side: BorderSide(color: AppTheme.themeColor), shape: const StadiumBorder(), backgroundColor: Colors.white, ), - child: const Text( + child: Text( '修改', - style: TextStyle(color: Colors.blue, fontSize: 12), + style: TextStyle(color: AppTheme.themeColor, fontSize: 11.sp), ), - ),), + ), + ), ], ), const SizedBox(height: 12), _buildDetailRow('车牌号:', reservation.plateNumber), _buildDetailRow('预约日期:', reservation.date), - _buildDetailRow('预约氢量:', reservation.hydAmount), + _buildDetailRow('预约氢量:', "${reservation.hydAmount} KG"), _buildDetailRow('加氢站:', reservation.stationName), _buildDetailRow('开始时间:', reservation.startTime), _buildDetailRow('结束时间:', reservation.endTime), @@ -271,11 +281,26 @@ class _ReservationListBottomSheetState extends State return Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(label, style: const TextStyle(color: Colors.grey)), + Text( + label, + style: TextStyle( + color: Color.fromRGBO(51, 51, 51, 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), const SizedBox(width: 8), - Expanded( - child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), + Text( + value, + style: TextStyle( + color: label.contains("预约氢量:") + ? Color.fromRGBO(27, 168, 85, 1) + : Color.fromRGBO(51, 51, 51, 1), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), ), ], ), diff --git a/ln_jq_app/lib/pages/c_page/reservation/view.dart b/ln_jq_app/lib/pages/c_page/reservation/view.dart index 75a4608..9db425a 100644 --- a/ln_jq_app/lib/pages/c_page/reservation/view.dart +++ b/ln_jq_app/lib/pages/c_page/reservation/view.dart @@ -49,7 +49,7 @@ class ReservationPage extends GetView { Positioned( left: 20.w, right: 20.w, - bottom: 110.h, + bottom: Get.height * (Get.height < 826 ? 0.08 : 0.11), child: _buildReservationItem(context), ), ], @@ -148,9 +148,11 @@ class ReservationPage extends GetView { ), IconButton( onPressed: () async { + controller.stopAutoRefresh(); var scanResult = await Get.to(() => const MessagePage()); if (scanResult == null) { controller.msgNotice(); + controller.startAutoRefresh(); } }, icon: Badge( @@ -177,7 +179,12 @@ class ReservationPage extends GetView { const SizedBox(width: 8), _buildModernStatItem('总加氢次数', '', controller.fillingTimes, ''), const SizedBox(width: 8), - _buildModernStatItem('今日里程', '', "7kg", ''), + _buildModernStatItem( + '今日里程', + '', + StorageService.to.hasVehicleInfo ? "7kg" : "-", + '', + ), ], ), ), @@ -237,17 +244,33 @@ class ReservationPage extends GetView { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Expanded(flex: 4, child: LoginUtil.getAssImg('ic_car_bg@2x')), + Expanded( + flex: 4, + child: Image.network( + controller.modeImage, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) { + return Center(child: LoginUtil.getAssImg('ic_car_select@2x')); + }, + ), + ), const SizedBox(width: 16), Expanded( flex: 6, child: Column( children: [ - _buildCarDataItem('剩余电量', '36.8%'), + _buildCarDataItem( + '剩余电量', + StorageService.to.hasVehicleInfo ? '36.8%' : '-', + ), const SizedBox(height: 8), - _buildCarDataItem('剩余氢量', '${controller.leftHydrogen}Kg'), + _buildCarDataItem('剩余氢量', controller.leftHydrogen), const SizedBox(height: 8), - _buildCarDataItem('百公里氢耗', '${controller.workEfficiency}Kg'), + _buildCarDataItem('百公里氢耗', controller.workEfficiency), const SizedBox(height: 12), Column( children: [ @@ -275,7 +298,7 @@ class ReservationPage extends GetView { ), ), Text( - "${controller.leftHydrogen}Kg", + controller.leftHydrogen, style: const TextStyle( fontSize: 10, color: Color(0xFF006633), @@ -434,8 +457,34 @@ class ReservationPage extends GetView { /// 时间 Slider 选择器 Widget _buildTimeSlider(BuildContext context) { return Obx(() { - // 这里的逻辑对应 Controller 中的 24 小时可用 Slot - int currentIdx = controller.startTime.value.hour; + // 获取站点信息 + final station = controller.stationOptions.firstWhereOrNull( + (s) => s.hydrogenId == controller.selectedStationId.value, + ); + + // 如果没有站点数据,默认隐藏 + if (station == null) { + return const SizedBox(height: 100); + } + + // 解析营业范围 + final startParts = station.startBusiness.split(':'); + final endParts = station.endBusiness.split(':'); + int bizStartHour = int.tryParse(startParts[0]) ?? 0; + int bizEndHour = int.tryParse(endParts[0]) ?? 23; + int bizEndMinute = (endParts.length > 1) ? (int.tryParse(endParts[1]) ?? 0) : 0; + if (bizEndMinute == 0 && bizEndHour > bizStartHour) bizEndHour--; + + //确定当前滑块值 + int currentHour = controller.startTime.value.hour; + double sliderValue = currentHour.toDouble().clamp( + bizStartHour.toDouble(), + bizEndHour.toDouble(), + ); + + double minVal = bizStartHour.toDouble(); + double maxVal = bizEndHour.toDouble(); + if (minVal >= maxVal) maxVal = minVal + 1; return Column( children: [ @@ -484,23 +533,20 @@ class ReservationPage extends GetView { overlayColor: const Color(0xFF006633).withOpacity(0.1), ), child: Slider( - value: currentIdx.toDouble(), - min: 0, - max: 23, - divisions: 23, + value: sliderValue, + min: minVal, + max: maxVal, + // divisions: bizEndHour - bizStartHour > 0 ? bizEndHour - bizStartHour : 1, onChanged: (val) { int hour = val.toInt(); - // 模拟 Controller 中的 pickTime 逻辑校验 final now = DateTime.now(); final isToday = controller.selectedDate.value.year == now.year && controller.selectedDate.value.month == now.month && controller.selectedDate.value.day == now.day; - if (isToday && hour < now.hour) { - // 如果是今天且小时数小于当前,则忽略 - return; - } + // 如果是今天,判断不可选过去的时间点 + if (isToday && hour < now.hour) return; controller.startTime.value = TimeOfDay(hour: hour, minute: 0); controller.endTime.value = TimeOfDay(hour: (hour + 1) % 24, minute: 0); @@ -627,6 +673,7 @@ class ReservationPage extends GetView { onChanged: (value) { if (value != null) { controller.selectedStationId.value = value; + controller.resetTimeForSelectedDate(); } }, diff --git a/ln_jq_app/lib/pages/home/controller.dart b/ln_jq_app/lib/pages/home/controller.dart index 7c339cb..9588648 100644 --- a/ln_jq_app/lib/pages/home/controller.dart +++ b/ln_jq_app/lib/pages/home/controller.dart @@ -2,8 +2,11 @@ import 'dart:io'; import 'package:aliyun_push_flutter/aliyun_push_flutter.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_app_update/flutter_app_update.dart'; +import 'package:flutter_app_update/result_model.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:getx_scaffold/getx_scaffold.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/pages/b_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; @@ -20,107 +23,195 @@ class HomeController extends GetxController with BaseControllerMixin { final _aliyunPush = AliyunPushFlutter(); + @override + bool get listenLifecycleEvent => true; + @override void onInit() { super.onInit(); initAliyunPush(); addPushCallback(); FlutterNativeSplash.remove(); + log('page-init'); + + // 页面初始化后执行版本检查 + checkVersionInfo(); + } + + String downloadUrl = ""; + + /// 检查 App 更新信息,增加版本号比对逻辑 + void checkVersionInfo() async { + try { + final response = await HttpService.to.get('appointment/appConfig/get'); + if (response != null) { + final result = BaseModel.fromJson(response.data); + if (result.code == 0 && result.data != null) { + final data = result.data as Map; + + bool hasUpdate = data['hasUpdate']?.toString().toLowerCase() == "true"; + bool isForce = data['isForce']?.toString().toLowerCase() == "true"; + String versionName = data['versionName'] ?? "新版本"; + String updateContent = data['updateContent'] ?? "优化系统性能,提升用户体验"; + downloadUrl = data['downloadUrl'].toString(); + + // 获取服务器配置的目标构建号 + int serverVersionCode = + int.tryParse(data['versionCode']?.toString() ?? "0") ?? 0; + int serverIosBuildId = int.tryParse(data['iosBuildId']?.toString() ?? "0") ?? 0; + + // 获取本地当前的构建号 + String currentBuildStr = await getBuildNumber(); + int currentBuild = int.tryParse(currentBuildStr) ?? 0; + + bool needUpdate = false; + if (GetPlatform.isAndroid) { + needUpdate = currentBuild < serverVersionCode; + } else if (GetPlatform.isIOS) { + needUpdate = currentBuild < serverIosBuildId; + } + + // 如果服务器标记有更新,且本地版本号确实较低,则弹出更新 + if (hasUpdate && needUpdate) { + _showUpdateDialog("版本:$versionName\n\n更新内容:\n$updateContent", isForce); + } + } + } + } catch (e) { + Logger.d("版本检查失败: $e"); + } + } + + /// 显示更新弹窗 + void _showUpdateDialog(String content, bool isForce) { + DialogX.to.showConfirmDialog( + title: '升级提醒', + confirmText: '立即升级', + content: _buildDialogContent(content), + // 如果是强制更新,取消按钮显示为空,即隐藏 + cancelText: isForce ? "" : '以后再说', + // 设置为 false,禁止点击背景和物理返回键关闭 + barrierDismissible: false, + onConfirm: () { + jumpUpdateApp(); + + // ios如果是强制更新,点击后维持弹窗,防止用户进入 App + if (isForce && GetPlatform.isIOS) { + Future.delayed(const Duration(milliseconds: 500), () { + _showUpdateDialog(content, isForce); + }); + } + }, + ); + } + + Widget _buildDialogContent(String content) { + return PopScope( + canPop: false, // 关键:禁止 pop + child: TextX.bodyMedium(content).padding(bottom: 16.h), + ); + } + + void jumpUpdateApp() { + if (GetPlatform.isIOS) { + // 跳转到 iOS 应用商店网页 + openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815"); + } else if (GetPlatform.isAndroid) { + // Android 执行下载安装流程 + showAndroidDownloadDialog(); + } + } + + void showAndroidDownloadDialog() { + AzhonAppUpdate.listener((ResultModel model) { + if (model.type == ResultType.start) { + DialogX.to.showConfirmDialog( + content: PopScope( + canPop: false, + child: Center( + child: Column( + children: [ + TextX.bodyMedium('升级中...').padding(bottom: 45.h), + CircularProgressIndicator(), + ], + ), + ), + ), + confirmText: '', + cancelText: "", + barrierDismissible: false, + ); + } else if (model.type == ResultType.done) { + Get.back(); + } + }); + + UpdateModel model = UpdateModel(downloadUrl, "xll.apk", "logo", '正在下载最新版本...'); + AzhonAppUpdate.update(model); } // 根据登录状态和登录渠道返回不同的首页 Widget getHomePage() { requestPermission(); - //登录状态跳转 if (StorageService.to.isLoggedIn) { - // 如果已登录,再判断是哪个渠道 if (StorageService.to.loginChannel == LoginChannel.station) { - return B_BaseWidgetsPage(); // 站点首页 + return B_BaseWidgetsPage(); } else if (StorageService.to.loginChannel == LoginChannel.driver) { - return BaseWidgetsPage(); // 司机首页 + return BaseWidgetsPage(); } else { return LoginPage(); } } else { - // 未登录,直接去登录页 return LoginPage(); } } void requestPermission() async { PermissionStatus status = await Permission.notification.status; - if (status.isGranted) { - Logger.d("通知权限已开启"); - return; - } + if (status.isGranted) return; if (status.isDenied) { - // 建议此处增加一个应用内的 Rationale (解释说明) 弹窗 status = await Permission.notification.request(); } if (status.isGranted) { - // 授权成功 Logger.d('通知已开启'); } else if (status.isPermanentlyDenied) { Logger.d('通知权限已被拒绝,请到系统设置中开启'); - } else if (status.isDenied) { - Logger.d('请授予通知权限,以便接收加氢站通知'); } } - ///推送相关 + ///推送相关初始化 (保持原样) Future initAliyunPush() async { - // 1. 配置分离:建议将 Key 提取到外部或配置文件中 final String appKey = Platform.isIOS ? AppTheme.ios_key : AppTheme.android_key; final String appSecret = Platform.isIOS ? AppTheme.ios_appsecret : AppTheme.android_appsecret; try { - // 初始化推送 final result = await _aliyunPush.initPush(appKey: appKey, appSecret: appSecret); - - if (result['code'] != kAliyunPushSuccessCode) { - Logger.d('初始化推送失败: ${result['code']} - ${result['errorMsg']}'); - return; - } - - Logger.d('阿里云推送初始化成功'); - // 分平台配置 + if (result['code'] != kAliyunPushSuccessCode) return; if (Platform.isIOS) { await _setupIOSConfig(); } else if (Platform.isAndroid) { await _setupAndroidConfig(); } } catch (e) { - Logger.d('初始化过程中发生异常: $e'); + Logger.d('初始化异常: $e'); } } - /// iOS 专属配置 Future _setupIOSConfig() async { - final res = await _aliyunPush.showIOSNoticeWhenForeground(true); - if (res['code'] == kAliyunPushSuccessCode) { - Logger.d('iOS 前台通知展示已开启'); - } else { - Logger.d('iOS 前台通知开启失败: ${res['errorMsg']}'); - } + await _aliyunPush.showIOSNoticeWhenForeground(true); } - /// Android 专属配置 Future _setupAndroidConfig() async { await _aliyunPush.setNotificationInGroup(true); - final res = await _aliyunPush.createAndroidChannel( + await _aliyunPush.createAndroidChannel( "xll_push_android", '新消息通知', 4, '用于接收加氢站实时状态提醒', ); - if (res['code'] == kAliyunPushSuccessCode) { - Logger.d('Android 通知通道创建成功'); - } else { - Logger.d('Android 通道创建失败: ${res['code']} - ${res['errorMsg']}'); - } } void addPushCallback() { @@ -139,40 +230,23 @@ class HomeController extends GetxController with BaseControllerMixin { Future _onAndroidNotificationClickedWithNoAction( Map message, - ) async { - Logger.d('onAndroidNotificationClickedWithNoAction ====> $message'); - } + ) async {} - Future _onAndroidNotificationReceivedInApp(Map message) async { - Logger.d('onAndroidNotificationReceivedInApp ====> $message'); - } + Future _onAndroidNotificationReceivedInApp(Map message) async {} - Future _onMessage(Map message) async { - Logger.d('onMessage ====> $message'); - } + Future _onMessage(Map message) async {} - Future _onNotification(Map message) async { - Logger.d('onNotification ====> $message'); - } + Future _onNotification(Map message) async {} Future _onNotificationOpened(Map message) async { - Logger.d('onNotificationOpened ====> $message'); await Get.to(() => const MessagePage()); } - Future _onNotificationRemoved(Map message) async { - Logger.d('onNotificationRemoved ====> $message'); - } + Future _onNotificationRemoved(Map message) async {} - Future _onIOSChannelOpened(Map message) async { - Logger.d('onIOSChannelOpened ====> $message'); - } + Future _onIOSChannelOpened(Map message) async {} - Future _onIOSRegisterDeviceTokenSuccess(Map message) async { - Logger.d('onIOSRegisterDeviceTokenSuccess ====> $message'); - } + Future _onIOSRegisterDeviceTokenSuccess(Map message) async {} - Future _onIOSRegisterDeviceTokenFailed(Map message) async { - Logger.d('onIOSRegisterDeviceTokenFailed====> $message'); - } + Future _onIOSRegisterDeviceTokenFailed(Map message) async {} } diff --git a/ln_jq_app/lib/pages/home/view.dart b/ln_jq_app/lib/pages/home/view.dart index de19787..1edfc6b 100644 --- a/ln_jq_app/lib/pages/home/view.dart +++ b/ln_jq_app/lib/pages/home/view.dart @@ -5,18 +5,13 @@ import 'package:ln_jq_app/pages/home/controller.dart'; class HomePage extends GetView { const HomePage({super.key}); - // 主视图 - Widget _buildView() { - return [Text('主页面')].toColumn(mainAxisSize: MainAxisSize.min).center(); - } - @override Widget build(BuildContext context) { return GetBuilder( init: HomeController(), id: 'home', builder: (_) { - return controller.getHomePage(); + return Scaffold(body: controller.getHomePage()); }, ); } diff --git a/ln_jq_app/lib/pages/login/view.dart b/ln_jq_app/lib/pages/login/view.dart index f2865b8..ce47516 100644 --- a/ln_jq_app/lib/pages/login/view.dart +++ b/ln_jq_app/lib/pages/login/view.dart @@ -204,9 +204,9 @@ class _LoginPageState extends State with SingleTickerProviderStateMix Row( children: [ const Text( - "欢迎使用 ", + "欢迎使用小羚羚 ", style: TextStyle( - fontSize: 24, + fontSize: 22, fontWeight: FontWeight.w500, color: Color.fromRGBO(51, 51, 51, 1), ), diff --git a/ln_jq_app/lib/pages/welcome/view.dart b/ln_jq_app/lib/pages/welcome/view.dart index 3f67a90..9e11f12 100644 --- a/ln_jq_app/lib/pages/welcome/view.dart +++ b/ln_jq_app/lib/pages/welcome/view.dart @@ -23,7 +23,7 @@ class WelcomePage extends GetView { right: 0, child: Image.asset( 'assets/images/welcome.png', - fit: BoxFit.fill + fit: BoxFit.cover ), ), ], diff --git a/ln_jq_app/pubspec.lock b/ln_jq_app/pubspec.lock index d680450..558172d 100644 --- a/ln_jq_app/pubspec.lock +++ b/ln_jq_app/pubspec.lock @@ -302,6 +302,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_app_update: + dependency: "direct main" + description: + name: flutter_app_update + sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" flutter_easyloading: dependency: transitive description: @@ -790,7 +798,7 @@ packages: source: hosted version: "1.1.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -949,6 +957,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" + saver_gallery: + dependency: "direct main" + description: + name: saver_gallery + sha256: "1d942bd7f4fedc162d9a751e156ebac592e4b81fc2e757af82de9077f3437003" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" shared_preferences: dependency: transitive description: diff --git a/ln_jq_app/pubspec.yaml b/ln_jq_app/pubspec.yaml index 2416bf3..3c56192 100644 --- a/ln_jq_app/pubspec.yaml +++ b/ln_jq_app/pubspec.yaml @@ -16,12 +16,12 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.3+6 +version: 1.2.4+7 environment: sdk: ^3.9.0 -# Dependencies specify other packages that your package needs in order to work. +# Dependencies specify other packages tha。。。t your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to @@ -52,8 +52,9 @@ dependencies: geolocator: ^14.0.2 # 获取精确定位 aliyun_push_flutter: ^1.3.6 pull_to_refresh: ^2.0.0 - - + flutter_app_update: ^3.2.2 + saver_gallery: ^4.0.0 + path_provider: ^2.1.5 dev_dependencies: flutter_test: