39 Commits

Author SHA1 Message Date
dce9718320 显示周边加氢站 2026-02-11 09:35:42 +08:00
4491aa9b91 ui调整 2026-02-10 16:35:02 +08:00
5364612a6f 更新样式 2026-02-10 13:37:24 +08:00
10867178fa 筛选框样式 2026-02-10 13:35:22 +08:00
a5e2a89e4f 预约列表样式 2026-02-10 11:51:47 +08:00
26c5f9d67a 消息样式修改 2026-02-09 17:57:00 +08:00
9cd87b0535 规则和历史 2026-02-09 17:28:12 +08:00
45e45d8160 积分兑换 2026-02-09 15:10:00 +08:00
87e890f97e 积分兑换首页 2026-02-06 17:37:43 +08:00
dcf925b8c1 商场页 2026-02-06 15:13:33 +08:00
c45863eda6 增加商城页面 2026-02-06 15:11:12 +08:00
756bf53cf5 司机预约时间调整 2026-02-06 14:16:26 +08:00
f68c2d0938 未车辆显示 2026-02-05 14:50:03 +08:00
211d0225e4 车辆图片动态 2026-02-05 13:54:10 +08:00
7d9b4d99e8 应用更新 2026-02-05 10:30:31 +08:00
3dd583a278 401增加节流 2026-02-03 10:59:05 +08:00
1278c38b7e 地图样式调整 2026-01-30 17:32:54 +08:00
171f556b40 viersion 1.2.3 2026-01-30 16:06:58 +08:00
55eade54b6 问题修改 2026-01-30 16:02:35 +08:00
bc99ffd691 增加参数 2026-01-30 13:01:50 +08:00
aa52a56bcf 地图相关的修改 2026-01-30 11:36:33 +08:00
73343ca297 调整 2026-01-30 10:01:31 +08:00
d09faac1d2 调整 2026-01-29 19:26:59 +08:00
1177be821a 样式调整 2026-01-29 17:01:21 +08:00
e59b89c225 Merge branch 'dev_feature' into dev
ui调整
# Conflicts:
#	ln_jq_app/lib/pages/b_page/reservation/controller.dart
#	ln_jq_app/lib/pages/b_page/site/controller.dart
#	ln_jq_app/lib/pages/b_page/site/view.dart
#	ln_jq_app/lib/pages/c_page/mine/view.dart
#	ln_jq_app/lib/pages/c_page/reservation/controller.dart
#	ln_jq_app/lib/pages/c_page/reservation/view.dart
#	ln_jq_app/lib/pages/login/view.dart
2026-01-29 11:45:17 +08:00
79fe3257b5 状态样式 2026-01-28 17:59:26 +08:00
55569839a7 搜索框 2026-01-28 16:27:37 +08:00
7112d70aba 站点样式 2026-01-28 15:00:30 +08:00
f8a8ecb0ed 司机 预约 样式修改 2026-01-27 17:35:37 +08:00
18c04272e2 车辆信息的部分 2026-01-27 13:57:14 +08:00
14e7fb3d78 首页ui 2026-01-27 13:15:53 +08:00
5ffaf81223 欢迎页 登录 2026-01-27 09:21:36 +08:00
907983a1d1 登录修改 2026-01-26 14:07:58 +08:00
9fdca9136d 导航栏 2026-01-23 17:00:17 +08:00
16bae6a1e9 车辆信息 ui 2026-01-23 15:02:16 +08:00
aabfbfae0c 车辆样式调整 2026-01-23 10:49:02 +08:00
5236670e7c ui修改 2026-01-23 09:22:07 +08:00
cf3ad579d3 新ui调整 2026-01-22 17:29:54 +08:00
70a752b6e5 协议同意 2026-01-21 14:29:03 +08:00
88 changed files with 6353 additions and 3378 deletions

View File

@@ -37,8 +37,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = 5
versionName = "1.2.2"
versionCode = 6
versionName = "1.2.3"
}
signingConfigs {

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -25,6 +25,41 @@
display: none !important;
}
/* 去除高德默认的 label 边框 and 背景 */
.amap-marker-label {
border: none !important;
background-color: transparent !important;
}
/* 自定义气泡样式 */
.custom-bubble {
position: relative;
background: rgba(51, 51, 51, 0.7);
/* #33333399 对应 rgba(51,51,51,0.7) */
color: #fff;
padding: 6px 15px;
border-radius: 20px;
/* 圆角 */
font-size: 13px;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
text-align: center;
line-height: 1.4;
}
/* 气泡下方的向下小箭头 */
.custom-bubble::after {
content: '';
position: absolute;
bottom: -6px;
/* 箭头高度 */
left: 50%;
transform: translateX(-50%);
border-width: 6px 6px 0 6px;
border-style: solid;
border-color: rgba(51, 51, 51, 0.7) transparent transparent transparent;
}
#panel .amap-call {
display: none;
}
@@ -32,7 +67,7 @@
/* --- 搜索栏样式 --- */
#search-box {
position: absolute;
top: 10px;
top: 40px;
left: 10px;
right: 10px;
z-index: 100;
@@ -63,7 +98,7 @@
button {
padding: 0 15px;
height: 38px;
background: #3366FF;
background: #017143FF;
color: #fff;
border: none;
border-radius: 4px;
@@ -74,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */
#panel {
position: fixed;
bottom: 0;
bottom: 75px;
left: 0;
width: 100%;
height: 35%;
@@ -94,7 +129,7 @@
#location-btn {
position: fixed;
right: 10px;
bottom: 50px;
bottom: 75px;
/* 默认位置 */
width: 44px;
height: 44px;
@@ -121,9 +156,24 @@
fill: #555;
}
/* --- 调整比例尺位置 --- */
.amap-scalecontrol {
/* 初始状态:避开底部的定位按钮或留出安全间距 */
bottom: 80px !important;
left: 10px !important;
transition: bottom 0.3s ease;
/* 增加平滑动画 */
}
/* --- 当路径规划面板显示时,比例尺自动上移 --- */
body.panel-active .amap-scalecontrol {
bottom: 38% !important;
/* 移动到面板上方 (面板高度35% + 3%间距) */
}
/* --- 关键:当 body 有 panel-active 类时,按钮上移 --- */
body.panel-active #location-btn {
bottom: 38%;
bottom: 45%;
/* 对应 #panel 的 height + 一点间距 */
}
</style>
@@ -167,16 +217,17 @@
</div>
<script>
var map, marker, driving, truckDriving, geocoder;
var map, marker, destMarker, driving, truckDriving, geocoder;
var currentLat, currentLng;
var isTruckMode = false;
var isInitialLocationSet = false;
var stationMarkers = []; // 存储所有站点的标记
function initMap() {
map = new AMap.Map('container', {
resizeEnable: true,
zoom: 15,
zoom: 17,
viewMode: '3D'
});
@@ -237,7 +288,7 @@
/**
* 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
*
*
*/
function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat);
@@ -263,6 +314,7 @@
angle: isNaN(rawAngle) ? 0 : rawAngle,
});
map.setCenter(position);
map.setZoom(13);
} else {
marker.moveTo(position, {
duration: 1000,
@@ -281,6 +333,12 @@
const addressComponent = regeo.addressComponent;
const pois = regeo.pois;
console.log("地理:" + JSON.stringify(result));
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
fetchStationInfoList(lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
@@ -295,8 +353,9 @@
}
// 策略2: 如果没有POI使用"道路+门牌号"
else if (addressComponent.street && addressComponent.streetNumber) {
shortAddress = addressComponent.street + addressComponent
.streetNumber;
shortAddress = addressComponent.district +
addressComponent.township +
addressComponent.street + addressComponent.streetNumber;
}
// 策略3: 如果还没有,使用"区+乡镇"
else if (addressComponent.district) {
@@ -330,7 +389,157 @@
}
/**
* 核心功能 2: 点击按钮回到当前位置
* 调用后端接口获取站点
*/
function fetchStationInfo(province, city, district, lat, lng) {
// 注意:某些直辖市在高德中 city 字段可能为空,需做兼容处理
console.log("JS->: 开始请求." + province + city + district);
var cityName = (typeof city === 'string' && city.length > 0) ? city : province;
fetch('https://beta-esg.api.lnh2e.com/appointment/station/getStationInfoByArea', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
province: province,
city: cityName,
district: district,
longitude: lng,
latitude: lat,
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json(); // 解析 JSON
})
.then(res => {
// 打印完整的返回结果,方便调试观察结构
console.log("JS->: 接口完整返回:", JSON.stringify(res));
// 安全校验:判断 res.data 是否存在
if (res.code === 0 && res.data) {
if (res.data.address) {
console.log("JS->: 找到地址:", res.data.address);
var destAddress = res.data.address;
document.getElementById('endInput').value = destAddress;
// 标记终点
markDestination(destAddress, res.data.name || "目的地",
res.data.longitude, res.data.latitude
);
} else {
console.log("JS->: 接口请求成功,但该区域暂无站点地址");
}
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.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: '<div class="custom-bubble">' + station.name + '</div>',
direction: 'top'
}
});
// 3. 绑定点击事件:选中即为目的地,并开始规划
sMarker.on('click', function() {
document.getElementById('endInput').value = station.address || station.name;
// 更新当前的 destMarker (如果需要)
if (destMarker) destMarker.setMap(null);
destMarker = sMarker;
startRouteSearch();
});
stationMarkers.push(sMarker);
});
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/**
* 地理编码并在地图标记终点
*/
function markDestination(address, name, longitude, latitude) {
// 1. 清除旧的终点标记
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径
imageSize: new AMap.Size(32, 32) // 图片在图标内拉伸的大小
});
// 3. 创建标记
destMarker = new AMap.Marker({
map: map,
position: [longitude, latitude],
icon: destIcon, // 使用自定义图标
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
content: '<div class="custom-bubble">' + name + '</div>',
direction: 'top'
}
});
console.log("JS->: 终点标记已添加", address);
}
/**
* 点击按钮回到当前位置
*/
function backToLocation() {
if (currentLng && currentLat) {
@@ -346,7 +555,7 @@
}
/**
* 核心功能 3: 路径规划
* 路径规划
*/
function startRouteSearch() {
// 获取输入框的文字
@@ -370,40 +579,34 @@
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
// --- 构造路径规划的点 ---
var points = [];
// 1. 处理起点逻辑
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
if (!startKw || startKw === '我的位置') {
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) {
if (!currentLng || !currentLat) {
// 如果还没获取到定位
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. 处理终点逻辑 (通常是关键词)
// 2. 处理终点逻辑
points.push({
keyword: endKw
});
// 3. 发起搜索
// points 数组里现在是一个起点对象和一个终点对象
driving.search(points, function (status, result) {
if (status === 'complete') {
console.log('JS: 规划成功');
@@ -438,4 +641,4 @@
</script>
</body>
</html>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

View File

@@ -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)
@@ -50,6 +52,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`)
@@ -80,6 +83,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:
@@ -111,6 +116,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

View File

@@ -70,5 +70,11 @@
<string>fetch</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>en</string>
</array>
</dict>
</plist>

View File

@@ -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<void> 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;
});
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:encrypt/encrypt.dart';
import 'package:flutter/material.dart' as ui;
class LoginUtil {
static final _keyString = '915eae87951a448c86c47796e44c1fcf';
@@ -26,5 +27,9 @@ class LoginUtil {
final decrypted = _encrypter.decrypt(encrypted);
return decrypted;
}
static ui.Image getAssImg(String imgName){
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'),fit: ui.BoxFit.cover,);
}
}

View File

@@ -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<String, dynamic> 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', // 默认全天
);
}
}

View File

@@ -5,7 +5,7 @@ class AppTheme {
static const String Font_YuYang = 'YuYang';
static const Color themeColor = Color(0xFF0c83c3);
static const Color themeColor = Color(0xFF017137);
//是否开放域名切换
static const bool is_show_host = true;

View File

@@ -1,15 +1,17 @@
import 'package:flutter/material.dart';
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';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'common/styles/theme.dart';
import 'pages/home/view.dart';
import 'pages/login/view.dart';
import 'pages/welcome/view.dart'; // 引入启动页
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -17,8 +19,10 @@ void main() async {
WidgetsBinding widgetsBinding = await init(
isDebug: true,
logTag: '小羚羚',
supportedLocales: [Locale('zh', 'CN')],
supportedLocales: [const Locale('zh', 'CN')],
);
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await GetStorage.init();
@@ -42,22 +46,17 @@ void main() async {
darkTheme: AppTheme.light,
// AppTitle
title: '小羚羚',
// 首页入口
home: HomePage(),
//组件国际化
fallbackLocale: Locale('zh', 'CN'),
supportedLocales: [Locale('zh', 'CN')],
// 将入口改为启动页
home: const WelcomePage(),
fallbackLocale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN')],
localizationsDelegates: const [
//pull_to_refresh
RefreshLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// Builder
builder: (context, widget) {
// do something....
return widget!;
},
),
@@ -67,20 +66,16 @@ void main() async {
void initHttpSet() {
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
// 设置基础 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 {
try {
final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 || baseModel.code == 200) {
return null;
} else if (baseModel.code == 401) {
await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage());
await AuthGuard.handle401(baseModel.message);
return baseModel.message;
} else {
return (baseModel.error.toString()).isEmpty

View File

@@ -1,22 +1,25 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/common/index.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/base_widgets/controller.dart';
import 'package:ln_jq_app/pages/b_page/reservation/view.dart';
import 'package:ln_jq_app/pages/b_page/site/view.dart';
class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
B_BaseWidgetsPage({super.key});
B_BaseWidgetsPage({super.key});
final PageController _pageController = PageController();
// 主视图
Widget _buildView() {
return PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(), // 禁止滑动
onPageChanged: (index) {
jumpTabAndPage(index);
},
children: _buildPages(), // 页面的列表
children: _buildPages(),
);
}
@@ -25,33 +28,59 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
controller.updateUi(); // 更新 UI
_pageController.jumpToPage(controller.pageIndex);
}
// 对应的页面
List<Widget> _buildPages() {
return [
SitePage(),
ReservationPage(),
];
return [SitePage(), ReservationPage()];
}
//导航栏
// 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() {
return NavigationX(
currentIndex: controller.pageIndex, // 当前选中的tab索引
onTap: (index) {
jumpTabAndPage(index);
}, // 切换tab事件
items: [
NavigationItemModel(
label: '加氢预约',
icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill,
return SafeArea(
child: Container(
height: 50.h,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
NavigationItemModel(
label: '站点信息',
icon: AntdIcon.car,
selectedIcon: AntdIcon.car_fill,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_h2_my@2x", "ic_h2_my_select@2x"),
],
),
],
),
);
}
// 构建单个导航项
Widget _buildNavItem(int index, String icon, String selectedIcon) {
bool isSelected = controller.pageIndex == index;
return GestureDetector(
onTap: () => jumpTabAndPage(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(horizontal: 50.w, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.transparent, // 选中时的深绿色背景
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
),
),
);
}
@@ -62,10 +91,10 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
id: 'b_baseWidgets',
builder: (_) {
return Scaffold(
extendBody: false,
extendBody: true,
resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()),
body: _buildView(),
);
},
);

View File

@@ -1,10 +1,14 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:intl/intl.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,24 +17,39 @@ class HistoryController extends GetxController {
final Rx<DateTime> 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<ReservationModel> historyList = <ReservationModel>[].obs;
final RxBool isLoading = true.obs;
final RxBool hasData = false.obs;
String get formattedStartDate => DateFormat('yyyy/MM/dd').format(startDate.value);
String get formattedEndDate => DateFormat('yyyy/MM/dd').format(endDate.value);
String stationName = "";
final Map<String, String> statusOptions = {
'': '全部',
'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<String, dynamic>;
stationName = args['stationName'] as String;
stationName = args['stationName'] as String? ?? "";
refreshData();
}
void refreshData() {
getAllOrderCounts();
fetchHistoryData();
}
@@ -38,51 +57,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<dynamic>.fromJson(response.data);
final dataMap = baseModel.data as Map<String, dynamic>;
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<void> 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 +108,6 @@ class HistoryController extends GetxController {
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
historyList.assignAll(
listFromServer
@@ -99,14 +116,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 +131,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

View File

@@ -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<HistoryController> {
const HistoryPage({Key? key}) : super(key: key);
const HistoryPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(HistoryController());
return GetBuilder<HistoryController>(
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<String>(
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<HistoryController> {
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),
),
);
}

View File

@@ -45,6 +45,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
customStartTime = DateTime.now();
customEndTime = customStartTime!.add(const Duration(days: 1));
renderData();
msgNotice(); // 红点消息
startAutoRefresh();
}
@@ -94,6 +95,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
String jobDetailsStr = "";
String jobId = "";
Timer? _refreshTimer;
bool isNotice = false;
Future<void> renderData() async {
showLoading("加载中");
@@ -168,7 +170,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
}
jobDetailsStr =
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
// 如果是处于非营运状态,自动回填开始和结束时间
// 假设 customStartTime 是现在customEndTime 是接口返回的结束时间
@@ -210,7 +212,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
var customerPriceTemp = result.data["customerPrice"];
customerPrice =
(customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty)
(customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty)
? "$customerPriceTemp"
: "暂无价格";
@@ -246,6 +248,27 @@ class ReservationController extends GetxController with BaseControllerMixin {
}
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void onOperationStatusChanged(String? newValue) {
if (newValue != null) {
selectedOperationStatus = newValue;

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter/services.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/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
class ReservationPage extends GetView<ReservationController> {
const ReservationPage({super.key});
// 定义主题色
static const kPrimaryColor = Color(0xFF006D35); // 效果图深绿色
static const kBgColor = Color(0xFFF5F7F9); // 背景灰
@override
Widget build(BuildContext context) {
return GetBuilder<ReservationController>(
@@ -13,21 +19,28 @@ class ReservationPage extends GetView<ReservationController> {
id: 'b_reservation',
builder: (_) {
return Scaffold(
backgroundColor: kBgColor,
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeaderCard(),
const SizedBox(height: 12),
_buildInfoFormCard(context),
const SizedBox(height: 12),
_buildTipsCard(),
const SizedBox(height: 12),
_buildLogoutButton(),
],
),
child: Column(
children: [
_buildTopSection(context),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
children: [
SizedBox(height: 16),
_buildBasicInfoCard(),
SizedBox(height: 16),
_buildOperationContentCard(context),
SizedBox(height: 16.h),
_buildSystemTips(),
SizedBox(height: 24),
_buildLogoutButton(),
SizedBox(height: 95.h),
],
),
),
],
),
),
);
@@ -35,158 +48,284 @@ class ReservationPage extends GetView<ReservationController> {
);
}
/// 构建顶部的站点信息头卡片
Widget _buildHeaderCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
/// 1. 顶部个人信息及统计栏
Widget _buildTopSection(BuildContext context) {
return Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(30)),
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 10,
left: 20,
right: 20,
bottom: 25,
),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40),
title: Text(
controller.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
subtitle: Text(controller.address),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(12),
Row(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
child: Text(
controller.selectedOperationStatus,
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 12,
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
controller.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
_buildStatusTag(),
],
),
const SizedBox(height: 4),
Text(
"站点:${controller.address}",
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
],
),
),
),
),
const Divider(height: 1, indent: 16, endIndent: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildHeaderStat(controller.customerPrice, '氢气价格'),
_buildHeaderStat(controller.timeStr, '营业时间'),
_buildHeaderStat('98%', '设备状态'),
],
),
),
],
),
);
}
IconButton(
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
/// 构建头部卡片中的单个统计项
Widget _buildHeaderStat(String value, String label) {
return Column(
children: [
Text(
value,
style: const TextStyle(
color: Colors.blue,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
/// 构建包含所有信息表单的卡片(增加 Tab 切换功能)
Widget _buildInfoFormCard(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
clipBehavior: Clip.antiAlias, // 确保 Tab 背景圆角生效
child: Column(
children: [
// Tab 切换栏
Obx(
() => Container(
color: Colors.grey[50],
child: Row(
children: [
_buildTabItem(0, Icons.business_outlined, '站点信息'),
_buildTabItem(1, Icons.campaign_outlined, '站点广播'),
],
),
),
),
const Divider(height: 1),
// 内容区域
Obx(
() => controller.selectedTabIndex.value == 0
? _buildStationInfo(context)
: _buildStationBroadcast(context),
),
],
),
);
}
/// 构建单个 Tab 项
Widget _buildTabItem(int index, IconData icon, String label) {
bool isSelected = controller.selectedTabIndex.value == index;
return Expanded(
child: InkWell(
onTap: () => controller.selectedTabIndex.value = index,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected ? Colors.blue : Colors.transparent,
width: 2,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 20, color: isSelected ? Colors.blue : Colors.grey[600]),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blue : Colors.grey[600],
},
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,
),
),
),
],
),
const SizedBox(height: 25),
Row(
children: [
_buildStatBox("氢气价格", "Hydrogen price", controller.customerPrice, "/kg"),
SizedBox(width: 4.w),
_buildStatBox("营业时间", "Opening time", controller.timeStr, ""),
SizedBox(width: 4.w),
_buildStatBox("设备状态", "Anlagenzustand", "98", "%"),
],
),
],
),
);
}
Widget _buildStatusTag() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFE1F5FE),
borderRadius: BorderRadius.circular(10),
),
child: Text(
controller.selectedOperationStatus,
style: TextStyle(
color: Color(0xFF03A9F4),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
);
}
/// 站点信息子视图
Widget _buildStationInfo(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
return Expanded(
child: Container(
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(
color: kBgColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(51, 51, 51, 0.8),
fontWeight: FontWeight.w400,
),
),
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
const SizedBox(width: 2),
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
),
],
),
),
);
}
/// 2. 站点基本信息
Widget _buildBasicInfoCard() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"站点基本信息",
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 15),
_buildInfoRow("站点名称", controller.name),
_buildInfoRow("运营企业", controller.operatingEnterprise),
_buildInfoRow("站点地址", controller.address),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
),
Text(
value,
style: TextStyle(
color: Color(0xFF333333),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
/// 3. 运营信息/站点广播 Tab 及内容
Widget _buildOperationContentCard(BuildContext context) {
return GestureDetector(
onTap: hideKeyboard,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
// 自定义 TabBar
Obx(
() => Padding(
padding: const EdgeInsets.only(left: 16, top: 16),
child: Row(
children: [
_buildTabTitle(0, "运营信息"),
const SizedBox(width: 30),
_buildTabTitle(1, "站点广播"),
],
),
),
),
Obx(
() => controller.selectedTabIndex.value == 0
? _buildOperatingForm(context)
: _buildBroadcastForm(),
),
],
),
),
);
}
Widget _buildTabTitle(int index, String title) {
bool isSelected = controller.selectedTabIndex.value == index;
return GestureDetector(
onTap: () => controller.selectedTabIndex.value = index,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.black87 : Colors.grey,
),
),
if (isSelected)
Container(
margin: const EdgeInsets.only(top: 4),
width: 25,
height: 3,
decoration: BoxDecoration(
color: const Color(0xFF00A870), // 效果图中的亮绿色横线
borderRadius: BorderRadius.circular(2),
),
),
],
),
);
}
Widget _buildOperatingForm(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('基本信息'),
_buildDisplayField(label: '站点名称', value: controller.name),
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise),
_buildDisplayField(label: '站点地址', value: controller.address),
const SizedBox(height: 16),
_buildSectionTitle('价格信息'),
_buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice),
const SizedBox(height: 16),
_buildSectionTitle('运营信息'),
Row(
children: [
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
Text(
'运营状态',
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
//加氢站未执行的状态修改任务
if (controller.jobTipStr.isNotEmpty)
GestureDetector(
@@ -204,157 +343,113 @@ class ReservationPage extends GetView<ReservationController> {
),
],
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: controller.selectedOperationStatus,
items: controller.operationStatusOptions.map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
const SizedBox(height: 12),
// 状态网格选择
Wrap(
spacing: 4,
runSpacing: 4,
children: controller.operationStatusOptions.map((status) {
bool isSelected = controller.selectedOperationStatus == status;
return GestureDetector(
onTap: () => controller.onOperationStatusChanged(status),
child: Container(
width: (Get.width - 80) / 2,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? kPrimaryColor : const Color(0xFFEBEBEB),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
status,
style: TextStyle(
fontSize: 13.sp,
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.w500,
),
),
),
);
}).toList(),
onChanged: controller.onOperationStatusChanged,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
),
),
const SizedBox(height: 16),
SizedBox(height: 12.h),
if (controller.selectedOperationStatus == "营运中")
_buildDisplayField(label: '营业时间', value: controller.timeStr)
else
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildClickField(
label: '开始时间',
value: controller.customStartTimeStr,
onTap: () => controller.pickDateTime(context, true),
_buildInputLabel("开始时间"),
_buildDateTimePicker(
controller.customStartTimeStr,
() => controller.pickDateTime(context, true),
),
_buildClickField(
label: '结束时间',
value: controller.customEndTimeStr,
onTap: () => controller.pickDateTime(context, false),
const SizedBox(height: 15),
_buildInputLabel("结束时间"),
_buildDateTimePicker(
controller.customEndTimeStr,
() => controller.pickDateTime(context, false),
),
const SizedBox(height: 15),
],
),
_buildDisplayField(label: '联系电话', value: controller.phone),
const SizedBox(height: 24),
ElevatedButton(
onPressed: controller.saveInfo,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('保存信息', style: TextStyle(fontSize: 16)),
),
],
),
);
}
/// 站点广播子视图
Widget _buildStationBroadcast(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 25),
Row(
children: [
const Icon(Icons.campaign, color: Colors.blue, size: 28),
const SizedBox(width: 10),
const Text(
'站点广播通知',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: () {
controller.renderData();
}, // 重置逻辑
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
),
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: controller.saveInfo,
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("保存设置", style: TextStyle(color: Colors.white)),
),
),
],
),
const SizedBox(height: 13),
_buildTextFieldLabel('通知标题'),
const SizedBox(height: 8),
SizedBox(
height: 45.h,
child: TextField(
controller: controller.broadcastTitleController,
maxLength: 30,
decoration: InputDecoration(
hintText: '例如:临时闭站通知',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
counterText: '', // 隐藏原生计数器,我们可以按需自定义
),
),
),
const SizedBox(height: 20),
_buildTextFieldLabel('通知内容'),
const SizedBox(height: 8),
TextField(
controller: controller.broadcastContentController,
maxLength: 150,
maxLines: 5,
decoration: InputDecoration(
hintText: '请输入通知内容...',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: controller.sendBroadcast,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
child: const Text(
'发送',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 20),
],
),
);
}
Widget _buildTextFieldLabel(String label) {
return Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
);
}
/// 构建带标题的表单区域
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
children: [
Container(width: 4, height: 16, color: Colors.blue),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
);
}
/// 构建一个“标签+纯文本”的显示行
Widget _buildDisplayField({required String label, required String value}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -374,118 +469,177 @@ class ReservationPage extends GetView<ReservationController> {
);
}
/// 构建一个“可点击”的选择行
Widget _buildClickField({
required String label,
required String value,
required VoidCallback onTap,
}) {
Widget _buildBroadcastForm() {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
const SizedBox(height: 8),
InkWell(
onTap: onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.blue.withOpacity(0.5)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
value,
style: const TextStyle(fontSize: 14, color: Colors.black87),
),
const Icon(Icons.calendar_month, size: 18, color: Colors.blue),
],
),
),
_buildInputLabel("通知标题"),
TextField(
controller: controller.broadcastTitleController,
decoration: _inputDecoration("例如:临时闭站通知"),
),
const SizedBox(height: 15),
_buildInputLabel("通知内容"),
TextField(
controller: controller.broadcastContentController,
maxLines: 4,
decoration: _inputDecoration("请输入通知内容..."),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: () {
controller.broadcastTitleController.clear();
controller.broadcastContentController.clear();
}, // 重置逻辑
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kPrimaryColor),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
),
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: controller.sendBroadcast,
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
);
}
/// 构建静态提示信息卡片
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
Widget _buildInputLabel(String label) {
return Padding(
padding: const EdgeInsets.only(left: 0, bottom: 8),
child: Text(
label,
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildDateTimePicker(String value, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF00A870).withOpacity(0.5)),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildInfoItem(Icons.info_outline, '请确保信息准确无误'),
const SizedBox(height: 10),
_buildInfoItem(Icons.help_outline, '价格信息将实时更新到用户端'),
const SizedBox(height: 10),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系技术支持: 400-021-1773'),
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: FutureBuilder<String>(
future: getVersion(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("");
}
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.black54,
);
}
return const Text("");
},
),
),
],
),
Text(value, style: const TextStyle(color: Colors.black87)),
const Icon(Icons.calendar_today_outlined, size: 18, color: Color(0xFF00A870)),
],
),
),
);
}
/// 构建退出登录按钮
Widget _buildLogoutButton() {
return ElevatedButton(
onPressed: controller.logout,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 2,
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
child: const Text(
'退出登录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
);
}
/// 4. 系统提醒
Widget _buildSystemTips() {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: const Color(0xFFF1F9F6), // 极浅绿色背景
borderRadius: BorderRadius.circular(10),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline, color: Color.fromRGBO(1, 113, 55, 1), size: 20),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"系统提醒",
style: TextStyle(
color: Color.fromRGBO(1, 113, 55, 1),
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
SizedBox(height: 6.h),
Text(
"请您确保所提供的信息准确无误,价格信息也将实时\n更新至用户端",
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
),
SizedBox(height: 6.h),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
),
],
),
],
),
);
}
/// 构建带图标的提示信息行
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
/// 5. 退出登录按钮
Widget _buildLogoutButton() {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.logout,
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromRGBO(204, 52, 46, 1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
elevation: 0,
),
],
child: const Text(
"退出登录",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}

View File

@@ -6,6 +6,7 @@ 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/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
enum ReservationStatus {
pending, // 待处理 ( addStatus: 0)
@@ -143,6 +144,8 @@ class SiteController extends GetxController with BaseControllerMixin {
Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController();
bool isNotice = false;
final RefreshController refreshController = RefreshController(initialRefresh: false);
@override
bool get listenLifecycleEvent => true;
@@ -167,7 +170,7 @@ class SiteController extends GetxController with BaseControllerMixin {
searchController.dispose();
super.onClose();
}
bool isNotice = false;
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
@@ -199,6 +202,8 @@ class SiteController extends GetxController with BaseControllerMixin {
});
}
void onRefresh() => renderData(isRefresh: true);
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
@@ -221,7 +226,7 @@ class SiteController extends GetxController with BaseControllerMixin {
'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条
'keyword': searchText, // 加氢站名称、手机号
'stationId': StorageService.to.userId
'stationId': StorageService.to.userId,
},
);
@@ -587,7 +592,7 @@ class SiteController extends GetxController with BaseControllerMixin {
String orderTotalAmount = "";
String orderUnfinishedAmount = "";
Future<void> renderData() async {
Future<void> renderData({bool isRefresh = false}) async {
try {
var responseData = await HttpService.to.get(
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
@@ -620,6 +625,10 @@ class SiteController extends GetxController with BaseControllerMixin {
} finally {
//加载列表数据
fetchReservationData();
if (isRefresh) {
refreshController.refreshCompleted();
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:getx_scaffold/common/index.dart';
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';
@@ -9,9 +11,10 @@ import 'package:ln_jq_app/pages/c_page/reservation/view.dart';
import 'index.dart';
class BaseWidgetsPage extends GetView<BaseWidgetsController> {
BaseWidgetsPage({super.key});
BaseWidgetsPage({super.key});
final PageController _pageController = PageController();
// 主视图
Widget _buildView() {
return PageView(
@@ -20,54 +23,70 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
onPageChanged: (index) {
jumpTabAndPage(index);
},
children: _buildPages(), // 页面的列表
children: _buildPages(),
);
}
void jumpTabAndPage(int index) {
controller.pageIndex = index; // 更新页面索引
controller.updateUi(); // 更新 UI
controller.pageIndex = index;
controller.updateUi();
_pageController.jumpToPage(controller.pageIndex);
}
// 对应的页面
List<Widget> _buildPages() {
return [
ReservationPage(),
MapPage(),
CarInfoPage(),
MinePage(),
];
return [ReservationPage(), MapPage(), MallPage(), CarInfoPage(), MinePage()];
}
//导航栏
// 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() {
return NavigationX(
currentIndex: controller.pageIndex, // 当前选中的tab索引
onTap: (index) {
jumpTabAndPage(index);
}, // 切换tab事件
items: [
NavigationItemModel(
label: '加氢预约',
icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill,
return SafeArea(
child: Container(
height: 50.h,
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
decoration: BoxDecoration(
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
NavigationItemModel(
label: '地图',
icon: AntdIcon.location,
selectedIcon: AntdIcon.location_fill,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
_buildNavItem(1, "ic_map_select@2x", "ic_map@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"),
],
),
NavigationItemModel(
label: '车辆信息',
icon: AntdIcon.car,
selectedIcon: AntdIcon.car_fill,
),
);
}
// 构建单个导航项
Widget _buildNavItem(int index, String icon, String selectedIcon) {
bool isSelected = controller.pageIndex == index;
return GestureDetector(
onTap: () => jumpTabAndPage(index),
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF006633) : Colors.transparent, // 选中时的深绿色背景
borderRadius: BorderRadius.circular(20),
),
NavigationItemModel(
label: '我的',
icon: AntdIcon.user,
selectedIcon: AntdIcon.user,
child: SizedBox(
height: 24,
width: 24,
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),
),
],
),
);
}
@@ -78,10 +97,10 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
id: 'baseWidgets',
builder: (_) {
return Scaffold(
extendBody: false,
extendBody: true, // 重要:让 body 延伸到导航栏后面
resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()),
body: _buildView(), // 移除 SafeArea 以获得更好的全屏沉浸感
);
},
);

View File

@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
@override
Widget build(BuildContext context) {
Get.put(AttachmentViewerController());
final fileName = controller.url.split('/').last;
// final fileName = controller.url.split('/').last;
return Scaffold(
appBar: AppBar(
title: Text(
fileName,
"证件详情",
style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis,
),

View File

@@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController with BaseControllerMixin{
class CertificateViewerController extends GetxController with BaseControllerMixin {
late final String title;
late final List<String> attachments;
@@ -78,18 +78,11 @@ class CertificateViewerController extends GetxController with BaseControllerMixi
return;
}
Get.to(
() => const AttachmentViewerPage(),
arguments: {
'url': url,
},
);
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
}

View File

@@ -2,10 +2,12 @@ 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/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart';
import 'package:path_provider/path_provider.dart';
import 'certificate_viewer_page.dart';
import 'dart:io';
class CarInfoController extends GetxController with BaseControllerMixin {
@override
@@ -13,20 +15,47 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// --- 车辆基本信息 ---
String plateNumber = "";
String vin = "未知";
String modelName = "未知";
String brandName = "未知";
String vin = "-";
String modelName = "-";
String brandName = "-";
// --- 证件附件列表 ---
final RxList<String> drivingAttachments = <String>[].obs;
final RxList<String> operationAttachments = <String>[].obs;
final RxList<String> hydrogenationAttachments = <String>[].obs;
final RxList<String> registerAttachments = <String>[].obs;
String color = "";
String hydrogenCapacity = "";
String rentFromCompany = "";
String address = "";
bool isNotice = false;
@override
void onInit() {
super.onInit();
getUserBindCarInfo();
msgNotice();
}
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
@override
@@ -96,6 +125,21 @@ class CarInfoController extends GetxController with BaseControllerMixin {
parseAttachments(data['hydrogenationAttachment']),
);
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
// 初始化时开始加载所有PDF
attachments = [
...drivingAttachments,
...operationAttachments,
...hydrogenationAttachments,
...registerAttachments,
];
color = data['color'].toString();
hydrogenCapacity = data['hydrogenCapacity'].toString();
rentFromCompany = data['rentFromCompany'].toString();
address = data['address'].toString();
loadAllPdfs();
}
}
updateUi();
@@ -117,4 +161,69 @@ class CarInfoController extends GetxController with BaseControllerMixin {
arguments: {'title': title, 'attachments': attachments},
);
}
/// 导航到通用的附件查看器页面
void openAttachment(String url) {
if (url.isEmpty) {
showErrorToast('附件链接无效');
return;
}
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
}
/// 检查 URL 是否为 PDF
bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf');
}
List<String> attachments = [];
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
/// 遍历所有附件如果是PDF则进行下载
void loadAllPdfs() {
for (var url in attachments) {
if (isPdf(url)) {
_downloadPdf(url);
}
}
}
/// 下载单个PDF文件
Future<void> _downloadPdf(String url) async {
if (url.isEmpty) return;
// 开始加载
isLoading[url] = true;
try {
final dio = Dio();
final Directory tempDir = await getTemporaryDirectory();
final String savePath = '${tempDir.path}/${url.split('/').last}';
// 检查文件是否已存在,避免重复下载
if (await File(savePath).exists()) {
localPdfPaths[url] = savePath;
isLoading[url] = false;
return;
}
await dio.download(url, savePath);
// 下载成功后,更新本地路径
localPdfPaths[url] = savePath;
} catch (e) {
print('PDF download error for $url: $e');
// 出错时也可以更新状态以便UI显示错误提示
} finally {
// 结束加载
isLoading[url] = false;
}
}
}

View File

@@ -1,9 +1,13 @@
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:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/qr_code/view.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';
import 'package:photo_view/photo_view.dart';
import '../../../common/styles/theme.dart';
import 'controller.dart';
class CarInfoPage extends GetView<CarInfoController> {
@@ -16,22 +20,26 @@ class CarInfoPage extends GetView<CarInfoController> {
id: 'car_info',
builder: (_) {
return Scaffold(
backgroundColor: Colors.grey[100],
backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildDriverInfoCard(),
const SizedBox(height: 5),
_buildCarBindingCard(),
const SizedBox(height: 5),
_buildCertificatesCard(),
const SizedBox(height: 5),
_buildTipsCard(),
],
),
child: Column(
children: [
_buildUserInfoCard(),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 16),
_buildCarInfoCard(),
_buildCertificatesCard(context),
const SizedBox(height: 12),
_buildSafetyReminderCard(),
SizedBox(height: 95.h),
],
),
),
],
),
),
);
@@ -39,74 +47,142 @@ class CarInfoPage extends GetView<CarInfoController> {
);
}
/// 构建顶部的司机信息卡片
Widget _buildDriverInfoCard() {
Widget _buildUserInfoCard() {
return Card(
elevation: 2,
elevation: 1,
color: Colors.white,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 50),
child: Row(
children: [
const CircleAvatar(
radius: 20,
backgroundColor: Colors.blue,
child: Icon(Icons.person, color: Colors.white, size: 34),
Stack(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
const SizedBox(width: 12),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
Row(
children: [
Text(
"${StorageService.to.name}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1),
border: Border.all(color: const Color(0xFFB7E19F)),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
SizedBox(width: 4),
Text(
"绿色先锋",
style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
"${StorageService.to.phone}",
style: TextStyle(color: Colors.grey, fontSize: 11),
"羚牛ID${StorageService.to.phone}",
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue, width: 0.5),
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),
),
child: const Row(
children: [
Icon(Icons.shield_outlined, color: Colors.blue, size: 14),
SizedBox(width: 4),
Text(
'已认证',
style: TextStyle(
color: Colors.blue,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
),
const Divider(height: 1, indent: 16, endIndent: 16),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem('156', '服务天数'),
_buildStatItem('4.9', '评分'),
_buildStatItem('98%', '准时率'),
_buildModernStatItem(
'本月里程数',
'Accumulated',
StorageService.to.hasVehicleInfo ? '2,852km' : '-',
'',
),
const SizedBox(width: 8),
_buildModernStatItem(
'总里程',
'Refuel Count',
StorageService.to.hasVehicleInfo ? "2.5W km" : '-',
'',
),
const SizedBox(width: 8),
_buildModernStatItem(
'服务评分',
'Driver rating',
StorageService.to.hasVehicleInfo ? "4.9分" : '-',
'',
),
],
),
),
@@ -115,207 +191,383 @@ class CarInfoPage extends GetView<CarInfoController> {
);
}
// 司机信息卡片中的小统计项
Widget _buildStatItem(String value, String label) {
return Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
/// 构建车辆绑定信息卡片
Widget _buildCarBindingCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
_buildInfoRow('车牌号: ${controller.plateNumber}', '扫码绑定'),
const SizedBox(height: 12),
_buildInfoRow('车架号:', '${controller.vin}'),
const SizedBox(height: 12),
_buildInfoRow('车辆型号:', '${controller.modelName}'),
const SizedBox(height: 12),
_buildInfoRow('车辆品牌:', '${controller.brandName}'),
],
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(width: 8),
Icon(Icons.propane_rounded, size: 50, color: Colors.blue.withOpacity(0.5)),
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
],
),
],
),
),
);
}
// 车辆绑定卡片中的信息行
Widget _buildInfoRow(String label, String value) {
bool isButton = value == '扫码绑定';
Widget _buildCarInfoCard() {
return Card(
elevation: 2,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
_buildDetailRow('车牌号', controller.plateNumber, isPlate: true),
const SizedBox(height: 11),
_buildDetailRow('车架号', controller.vin),
const SizedBox(height: 11),
_buildDetailRow('车辆型号', controller.modelName),
const SizedBox(height: 11),
_buildDetailRow('车辆品牌', controller.brandName),
const SizedBox(height: 10),
_buildH2LevelProgress(),
],
),
),
);
}
Widget _buildDetailRow(String label, String value, {bool isPlate = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 8),
isButton
? GestureDetector(
onTap: () async {
controller.doQrCode();
},
Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isPlate)
GestureDetector(
onTap: () => controller.doQrCode(),
child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue.shade300, width: 1),
borderRadius: BorderRadius.circular(5),
color: Colors.blue.withOpacity(0.05),
border: Border.all(color: const Color.fromRGBO(71, 174, 208, 1)),
borderRadius: BorderRadius.circular(12),
color: const Color.fromRGBO(235, 250, 255, 1),
),
child: Row(
mainAxisSize: MainAxisSize.min, // Keep the row compact
children: [
Icon(
StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search,
size: 13,
color: Colors.blue,
const Icon(
Icons.sync,
size: 12,
color: Color.fromRGBO(71, 174, 208, 1),
),
const SizedBox(width: 3),
const SizedBox(width: 4),
Text(
StorageService.to.hasVehicleInfo ? "换车牌" : value,
StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定",
style: const TextStyle(
color: Colors.blue,
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(71, 174, 208, 1),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
),
)
: Text(
value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
],
);
}
/// 3. 构建车辆证件卡片
Widget _buildCertificatesCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
children: [
_buildCertificateRow(
icon: Icons.credit_card_rounded,
title: '行驶证',
attachments: controller.drivingAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.article_rounded,
title: '营运证',
attachments: controller.operationAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.propane_tank_rounded,
title: '加氢证',
attachments: controller.hydrogenationAttachments,
),
const Divider(),
_buildCertificateRow(
icon: Icons.app_registration_rounded,
title: '登记证',
attachments: controller.registerAttachments,
),
],
),
),
);
}
/// 证件展示
Widget _buildCertificateRow({
required IconData icon,
required String title,
required List<String> attachments,
}) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 28),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
// 使用 Obx 响应式地显示附件数量
subtitle: Obx(
() => Text(
'${attachments.length} 个附件',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
),
trailing: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor),
),
// 更新 onTap 逻辑
onTap: () => controller.navigateToCertificateViewer(title, attachments),
);
}
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildTipItem(Icons.info_outline, '请确保车辆证件齐全有效'),
const SizedBox(height: 10),
_buildTipItem(Icons.rule, '定期检查车辆状态和证件有效期'),
const SizedBox(height: 10),
_buildTipItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
],
),
),
);
}
// 提示信息卡片中的列表项
Widget _buildTipItem(IconData icon, String text) {
return Row(
Widget _buildH2LevelProgress() {
return Column(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: StorageService.to.hasVehicleInfo ? 0.75 : 0,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
StorageService.to.hasVehicleInfo ? "75%" : "0%",
style: TextStyle(
fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
fontWeight: FontWeight.bold,
),
),
],
),
],
);
}
/// 3. 构建车辆证件卡片 (重构为 TabView 样式)
Widget _buildCertificatesCard(BuildContext context) {
return DefaultTabController(
length: 4,
child: Column(
children: [
TabBar(
isScrollable: false,
indicatorColor: Color.fromRGBO(16, 185, 129, 1),
labelColor: Color.fromRGBO(16, 185, 129, 1),
unselectedLabelColor: Colors.grey,
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
indicatorSize: TabBarIndicatorSize.label,
tabs: const [
Tab(text: '行驶证'),
Tab(text: '营运证'),
Tab(text: '加氢证'),
Tab(text: '登记证'),
],
),
const SizedBox(height: 9),
SizedBox(
height: 356.h, // 给定一个高度,或者使用别的方式布局
child: TabBarView(
children: [
_buildCertificateContent('行驶证', controller.drivingAttachments),
_buildCertificateContent('营运证', controller.operationAttachments),
_buildCertificateContent('加氢资格证', controller.hydrogenationAttachments),
_buildCertificateContent('登记证', controller.registerAttachments),
],
),
),
],
),
);
}
/// 构建单个证件的展示内容
Widget _buildCertificateContent(String title, RxList<String> attachments) {
return Obx(() {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: EdgeInsets.all(16.0),
child: attachments.isEmpty
? const Center(child: Text('暂无相关证件信息'))
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
//证件文字
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem(
'所属公司',
controller.rentFromCompany,
isFull: false,
),
_buildCertDetailItem('运营城市', controller.address),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCertDetailItem(
'车辆颜色',
controller.color,
valueColor: const Color(0xFF52C41A),
),
_buildCertDetailItem('氢瓶容量', controller.hydrogenCapacity),
],
),
const SizedBox(height: 16),
// 附件预览部分
GestureDetector(
onTap: () {
controller.navigateToCertificateViewer(title, attachments);
},
child: Container(
height: 184.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Color.fromRGBO(226, 232, 240, 1)),
color: Color.fromRGBO(248, 250, 252, 1),
),
child: Center(child: _buildAttachmentPreview(attachments[0])),
),
),
],
),
),
);
});
}
Widget _buildCertDetailItem(
String label,
String value, {
Color? valueColor,
bool isFull = false,
}) {
return Container(
width: isFull ? null : 140,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: valueColor ?? Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
/// 附件预览组件 (智能判断 PDF 或图片)
Widget _buildAttachmentPreview(String url) {
return AspectRatio(
aspectRatio: 4 / 3,
child: controller.isPdf(url)
? Obx(() {
final bool loading = controller.isLoading[url] ?? true;
final String? localPath = controller.localPdfPaths[url];
if (loading) {
return _buildLoadingIndicator();
} else if (localPath != null && localPath.isNotEmpty) {
return IgnorePointer(
ignoring: true, // 设置为 true 来忽略所有指针事件
child: PDFView(
backgroundColor: Color.fromRGBO(248, 250, 252, 1),
fitEachPage: true,
filePath: localPath,
fitPolicy: FitPolicy.WIDTH,
// 适配宽度
enableSwipe: false,
swipeHorizontal: false,
autoSpacing: false,
pageFling: false,
preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转
),
);
} else {
// PDF加载失败
return _buildErrorIndicator();
}
})
: Image.network(
url,
fit: BoxFit.contain,
// 图片加载时显示loading
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingIndicator();
},
// 图片加载失败时显示错误
errorBuilder: (context, error, stackTrace) {
return _buildErrorIndicator();
},
),
);
}
Widget _buildLoadingIndicator() {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
Widget _buildErrorIndicator() {
return const SizedBox(
height: 200,
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
);
}
/// 安全提醒卡片
Widget _buildSafetyReminderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 249, 248, 1),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: AppTheme.themeColor, size: 24),
const SizedBox(width: 8),
Text(
"安全提醒",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color.fromRGBO(1, 113, 55, 1),
),
),
],
),
const SizedBox(height: 12),
const Text(
"请确保车辆证件齐全有效,定期检查车辆状态和证件有效期,以确保运输作业合规安全。",
style: TextStyle(
fontSize: 13,
color: Color.fromRGBO(1, 113, 55, 0.8),
height: 1.5,
),
),
const SizedBox(height: 8),
const Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(fontSize: 13, color: Color.fromRGBO(1, 113, 55, 0.8)),
),
],
),
);
}
}

View File

@@ -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<GoodsModel?> goodsDetail = Rx<GoodsModel?>(null);
final addressController = TextEditingController();
final nameController = TextEditingController();
final phoneController = TextEditingController();
final formKey = GlobalKey<FormState>();
@override
void onInit() {
super.onInit();
goodsId = Get.arguments['goodsId'] as int;
getGoodsDetail();
}
@override
bool get listenLifecycleEvent => true;
Future<void> getGoodsDetail() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsDetail',
data: {'goodsId': goodsId},
);
if (response != null && response.data != null) {
var result = BaseModel<GoodsModel>.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<MallController>();
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();
}
}

View File

@@ -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<MallDetailController> {
const MallDetailPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MallDetailController>(
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,
),
),
),
);
}
}

View File

@@ -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"));
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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<GoodsModel> goodsList = <GoodsModel>[].obs;
final RxBool isLoading = true.obs;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([getUserScore(), getGoodsList()]);
isLoading.value = false;
updateUi();
}
/// 获取用户积分和签到状态
Future<void> getUserScore() async {
try {
var response = await HttpService.to.post('appointment/score/getUserScore');
if (response != null && response.data != null) {
var result = BaseModel<UserScore>.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<void> 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<void> getGoodsList() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsList',
data: {'categoryId': 0},
);
if (response != null && response.data != null) {
var result = BaseModel<List<GoodsModel>>.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());
}
}

View File

@@ -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<MallController> {
const MallPage({super.key});
@override
Widget build(BuildContext context) {
return GetX<MallController>(
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,
),
),
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -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<String, dynamic> 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<OrderModel> orderList = <OrderModel>[].obs;
final RxBool isLoading = true.obs;
int pageNum = 1;
final int pageSize = 50;
@override
void onInit() {
super.onInit();
getOrders();
}
Future<void> 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<Map<String, dynamic>>.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();
}
}
}

View File

@@ -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<MallOrdersController> {
const MallOrdersPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MallOrdersController>(
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),
),
],
),
],
),
);
}
}

View File

@@ -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:
'个人账户内累计的所有有效积分,可在平台积分商城中,用于兑换商城内上架的各类商品、权益或服务,兑换时将按照商品标注的积分值扣除对应积分,积分兑换后不支持撤销、退换,商品兑换规则以积分商城内公示为准。',
),
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),
),
),
],
),
);
}
}

View File

@@ -12,40 +12,63 @@ class MessagePage extends GetView<MessageController> {
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<MessageController> {
}
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<MessageController> {
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<MessageController> {
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<MessageController> {
},
);
}
}
}

View File

@@ -35,9 +35,9 @@ class MineController extends GetxController with BaseControllerMixin {
String historyBreakRules = "";
String vin = "";
String plateNumber = "";
String violationTotal = "0";
String violationScore = "0";
String violationDispose = "0";
String violationTotal = "0";//违章总数
String violationScore = "0";//扣分总数
String violationDispose = "0";//已处理
bool isNotice = false;
void renderData() async {

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/index.dart';
import 'package:getx_scaffold/common/widgets/index.dart';
import 'package:ln_jq_app/common/styles/theme.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';
import 'controller.dart';
@@ -17,25 +18,32 @@ class MinePage extends GetView<MineController> {
id: 'mine',
builder: (_) {
return Scaffold(
backgroundColor: Colors.grey[100],
backgroundColor: const Color.fromRGBO(247, 249, 251, 1),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUserInfoCard(),
const SizedBox(height: 5),
_buildDriverScoreCard(),
const SizedBox(height: 5),
_buildMonthlyRecordCard(),
const SizedBox(height: 5),
_buildTipsCard(),
const SizedBox(height: 20),
_buildLogoutButton(),
const SizedBox(height: 20),
],
),
child: Column(
children: [
_buildUserInfoCard(),
const SizedBox(height: 8),
// 新 UI 模块开始
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
children: [
_buildWalletCard(),
SizedBox(height: 16.h),
_buildGridMenu(),
SizedBox(height: 16.h),
_buildRecommendCard(context),
SizedBox(height: 8.h),
_buildSafetyReminderCard(),
SizedBox(height: 24.h),
_buildLogoutButton(),
SizedBox(height: 95.h),
],
),
),
// 新 UI 模块结束
],
),
),
);
@@ -43,82 +51,131 @@ class MinePage extends GetView<MineController> {
);
}
/// 1. 构建顶部用户信息卡片
/// 构建顶部用户信息卡片
Widget _buildUserInfoCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 1,
color: Colors.white,
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40),
// 增加了顶部 padding 适配状态栏
child: Row(
children: [
const CircleAvatar(
radius: 25,
backgroundColor: Colors.blue,
child: Icon(Icons.person, color: Colors.white, size: 40),
Stack(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
const SizedBox(width: 16),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
Row(
children: [
Text(
"${StorageService.to.name}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景
border: Border.all(color: const Color(0xFFB7E19F)), // 边框
borderRadius: BorderRadius.circular(12),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
// 叶子图标
SizedBox(width: 4),
Text(
"绿色先锋",
style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
),
],
),
),
],
),
const SizedBox(height: 4),
Text(
"${StorageService.to.phone}",
style: TextStyle(color: Colors.grey, fontSize: 11),
),
const SizedBox(height: 4),
Text(
StorageService.to.hasVehicleInfo ? "已绑定车辆" : '未绑定车辆',
style: TextStyle(color: Colors.orange, fontSize: 12),
"羚牛ID${StorageService.to.phone}",
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
IconButton(
onPressed: () async {
// 跳转消息中心
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
// 这里的 style 是为了模拟你图片里的灰色圆形背景
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
// label: Text('3'), // 如果你想显示数字,就加 label
smallSize: 8,
// 红点的大小
backgroundColor: controller.isNotice ? Colors.red : Colors.white,
// 红点颜色
child: Icon(
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 25,
size: 30,
),
),
),
],
),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatItem(controller.violationTotal, '违章总数'),
_buildStatItem(controller.violationScore, '扣分总数'),
_buildStatItem(controller.violationDispose, '已处理'),
_buildModernStatItem('服务天数', 'service days', '156', ''),
const SizedBox(width: 8),
_buildModernStatItem('准时率', 'Punctuality', controller.rate, ''),
const SizedBox(width: 8),
_buildModernStatItem('司机评分', 'Driver rating', controller.rating, ''),
],
),
),
@@ -127,242 +184,351 @@ class MinePage extends GetView<MineController> {
);
}
// 用户信息卡片中的小统计项
Widget _buildStatItem(String value, String label) {
// 统计项
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
],
),
],
),
),
);
}
/// 我的钱包卡片
Widget _buildWalletCard() {
return Card(
elevation: 1,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"我的钱包",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
Text("User wallet", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
Text(
"¥ 0,00元",
style: TextStyle(
color: Colors.green[700],
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
/// 2x2 功能网格菜单
Widget _buildGridMenu() {
return Column(
children: [
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
Row(
children: [
_buildGridItem(Icons.person_search_outlined, "客服评价", "3项可评"),
const SizedBox(width: 19),
_buildGridItem(
Icons.assignment_late_outlined,
"违章处理",
"${controller.historyBreakRules}项待办",
countColor: Colors.red,
),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildGridItem(Icons.book_outlined, "安全培训", "0个待看"),
const SizedBox(width: 19),
_buildGridItem(
Icons.verified_user_outlined,
"诚信加氢值",
"845",
isSpecial: true,
backgroundColor: const Color(0xFF006633),
),
],
),
],
);
}
/// 2. 构建驾驶得分卡片
Widget _buildDriverScoreCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
Widget _buildGridItem(
IconData icon,
String title,
String subtitle, {
Color? countColor,
bool isSpecial = false,
Color? backgroundColor,
}) {
return Expanded(
child: Container(
height: 100,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSpecial ? backgroundColor : Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'驾驶得分',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
Icon(icon, color: isSpecial ? Colors.white : Colors.black87, size: 28),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isSpecial ? Colors.white : Colors.black87,
),
),
const Text('本月表现', style: TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 100,
height: 100,
child: Stack(
fit: StackFit.expand,
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: isSpecial
? Colors.white.withOpacity(0.8)
: (countColor ?? Colors.grey),
),
),
],
),
),
);
}
/// 我要推荐卡片
Widget _buildRecommendCard(BuildContext context) {
return Card(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircularProgressIndicator(
value: (double.tryParse(controller.rating) ?? 0) / 10,
strokeWidth: 8,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
Text(
"我要推荐",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
Center(
child: Text(
controller.rating,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
Text("Recommend", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("累计奖励(积分)", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text(
"0,00",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
),
),
const SizedBox(height: 20),
_buildScoreDetailRow(Icons.directions_car, '安全驾驶', '无违章记录', true),
const Divider(),
_buildScoreDetailRow(Icons.timer, '准时率', '100%准时到达', true),
const Divider(),
_buildScoreDetailRow(Icons.thumb_up, '服务质量', '用户满意度高', true),
const Divider(),
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Row(
children: [
const Text(
'优秀驾驶员',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
child: const Text(
'A+',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
),
],
),
),
);
}
// 驾驶得分卡片中的评分项
Widget _buildScoreDetailRow(
IconData icon,
String title,
String subtitle,
bool isCompleted,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
if (isCompleted) const Icon(Icons.check_circle, color: Colors.blue),
],
),
);
}
/// 3. 构建本月记录卡
Widget _buildMonthlyRecordCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'本月记录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (GetPlatform.isIOS) {
// 跳转到 iOS 应用商店 (这里使用一个通用的应用商店链接模板,请确保替换为正式的 AppID)
openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815");
} else if (GetPlatform.isAndroid) {
// Android 弹出二维码图
_showAndroidDownloadDialog(context);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF006633),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 0,
),
child: const Text(
"下载推荐",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
_buildRecordRow(Icons.rate_review, '加氢预约践行率', controller.rate),
const Divider(),
_buildRecordRow(
Icons.report_problem_outlined,
'违章',
"${controller.historyBreakRules}",
),
const Divider(),
_buildRecordRow(Icons.car_crash_outlined, '交通事故', "${controller.accident}"),
],
),
),
);
}
// 本月记录中的列表项
Widget _buildRecordRow(IconData icon, String title, String value) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
/// Android 端下载二维码弹窗
void _showAndroidDownloadDialog(BuildContext context) {
Get.dialog(
Center(
child: Container(
width: 280.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Text(
"扫描二维码下载",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 20),
// 使用 LoginUtil.getAssImg 加载你的图片 android_apk_img.png
SizedBox(
width: 180.w,
height: 180.w,
child: LoginUtil.getAssImg('android_apk_img'),
),
const SizedBox(height: 16),
const Text(
"请让被推荐人扫描上方二维码进行下载安装",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: Colors.grey),
),
],
),
),
const Divider(height: 1),
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
child: const Text(
"确 定",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
),
title: Text(title, style: const TextStyle(fontSize: 14)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(value, style: const TextStyle(color: AppTheme.themeColor, fontSize: 14)),
],
),
onTap: () {
// TODO: 处理点击事件
},
);
}
/// 4. 构建提示信息卡片
Widget _buildTipsCard() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_buildInfoItem(Icons.info_outline, '保持良好的驾驶习惯,提高安全评分'),
const SizedBox(height: 10),
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'),
const SizedBox(height: 10),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
const SizedBox(height: 10),
Row(
children: [
Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: FutureBuilder<String>(
future: getVersion(),
builder: (context, snapshot) {
// 判断是否还在加载
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("");
}
// 如果加载完成且有数据
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.black54,
);
}
// 错误处理
/// 安全提醒卡片
Widget _buildSafetyReminderCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(242, 249, 248, 1), // 极浅绿色背景
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info_outline, color: Colors.green[700], size: 24),
const SizedBox(width: 8),
Text(
"安全提醒",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green[900],
),
),
],
),
const SizedBox(height: 12),
Text(
"请保持良好驾驶习惯,提高安全评分,遵守交通规则,避免违章扣分。",
style: TextStyle(fontSize: 13, color: Colors.green[800], height: 1.5),
),
const SizedBox(height: 8),
Text(
"如有疑问请联系客服400-021-1773",
style: TextStyle(fontSize: 13, color: Colors.green[800]),
),
Row(
children: [
Expanded(
child: FutureBuilder<String>(
future: getVersion(),
builder: (context, snapshot) {
// 判断是否还在加载
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("");
},
),
),
],
),
],
),
),
);
}
}
// 提示信息卡片中的列表项
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
),
],
// 如果加载完成且有数据
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.green[800],
);
}
// 错误处理
return const Text("");
},
),
),
],
),
],
),
);
}
@@ -372,7 +538,7 @@ class MinePage extends GetView<MineController> {
controller.logout();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[400],
backgroundColor: Color.fromRGBO(204, 52, 46, 1),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

View File

@@ -33,6 +33,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'reservation';
C_ReservationController();
final DateTime _now = DateTime.now();
// 计算当前时间属于哪个1小时区间
@@ -67,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<TimeSlot> 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);
}
// 用于存储上一次成功预约的信息
@@ -342,7 +139,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
return;
}*/
final reservationEndDateTime = DateTime(
DateTime reservationEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
@@ -350,7 +147,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
endTime.value.minute,
);
//判断预约区间的结束时间是否早于当前时间留出1分钟缓冲
// 如果结束的小时数小于开始的小时数,或者结束时间是 00:00说明是次日
if (endTime.value.hour < startTime.value.hour ||
(endTime.value.hour == 0 && endTime.value.minute == 0)) {
reservationEndDateTime = reservationEndDateTime.add(const Duration(days: 1));
}
// 执行时间检查
if (reservationEndDateTime.isBefore(
DateTime.now().subtract(const Duration(minutes: 1)),
)) {
@@ -363,10 +166,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
(s) => s.hydrogenId == selectedStationId.value,
);
if (selectedStation.siteStatusName != "营运中") {
/*if (selectedStation.siteStatusName != "营运中") {
showToast("该站点${selectedStation.siteStatusName},暂无法预约");
return;
}
}*/
showLoading("提交中");
@@ -528,14 +331,16 @@ 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;
//用来管理查看预约的弹窗
Worker? _sheetWorker;
@@ -551,16 +356,35 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
getUserBindCarInfo();
getSiteList();
startAutoRefresh();
msgNotice();
if (!init) {
_setupListener();
init = true;
}
}
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
bool isNotice = false;
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void startAutoRefresh() {
@@ -622,7 +446,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('服务暂不可用,请稍后');
@@ -631,9 +455,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data);
fillingWeight =
"${result.data["fillingWeight"]}${result.data["fillingWeightUnit"]}";
final value = double.tryParse(result.data["fillingWeight"]?.toString() ?? '0') ?? 0;
final String formatted = value.toStringAsFixed(2);
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
modeImage = result.data["modeImage"].toString();
updateUi();
} catch (e) {
@@ -644,9 +471,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
void getCatinfo() async {
try {
HttpService.to.setBaseUrl(AppTheme.car_service_url);
var responseData = await HttpService.to.post(
'VehicleData/getHydrogenInfoByPlateNumber',
'appointment/vehicle/getHydrogenInfoByPlateNumber',
data: {
'userName': "xll@lingniu",
'password': "4q%3!l6s0p",
@@ -660,8 +486,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);
@@ -671,15 +497,32 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
amountController.text = flooredDifference.toString();
}
if (maxHydrogen > 0) {
progressValue = leftHydrogenNum / maxHydrogen;
// 边界处理:确保值在 0 到 1 之间
if (progressValue > 1.0) progressValue = 1.0;
if (progressValue < 0.0) progressValue = 0.0;
}
updateUi();
} catch (e) {
} finally {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
}
} catch (e) {}
renderSliderTheme();
}
double current = 0.0;
double maxVal = 0.0;
void renderSliderTheme() {
current = double.tryParse(amountController.text) ?? 0.0;
maxVal = double.tryParse(difference) ?? 100.0;
if (maxVal <= 0) maxVal = 100.0;
updateUi();
}
void getSiteList() async {
if (StorageService.to.phone == "13344444444") {
if (StorageService.to.phone == "13888888888") {
//该账号给stationOptions手动添加一个数据
final testStation = StationModel(
hydrogenId: '1142167389150920704',
@@ -689,7 +532,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]);
@@ -735,7 +580,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (stationOptions.isEmpty) {
showToast('附近暂无可用加氢站');
} else {
showToast('站点列表已刷新');
// showToast('站点列表已刷新');
}
// 找到第一个可选的站点作为默认值
@@ -783,6 +628,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override
void onClose() {
super.onClose();
amountController.dispose();
plateNumberController.dispose();
if (_debounce != null) {
@@ -790,6 +636,5 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
}
_sheetWorker?.dispose();
stopAutoRefresh();
super.onClose();
}
}

View File

@@ -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<ReservationListBottomSheet>
@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<ReservationListBottomSheet>
);
}
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<String>(
value: _selectedStatus,
underline: const SizedBox.shrink(), // 隐藏下划线
items: _statusOptions.entries.map((entry) {
return DropdownMenuItem<String>(
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<ReservationListBottomSheet>
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<ReservationListBottomSheet>
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<ReservationListBottomSheet>
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<ReservationListBottomSheet>
},
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<ReservationListBottomSheet>
},
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<ReservationListBottomSheet>
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,
),
),
],
),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
class WebController extends GetxController {
late String title;
late String url;
final RxDouble progress = 0.0.obs;
InAppWebViewController? webViewController;
@override
void onInit() {
super.onInit();
// 从参数中获取标题和URL
title = Get.arguments['title'] ?? '详情';
url = Get.arguments['url'] ?? '';
}
void onWebViewCreated(InAppWebViewController controller) {
webViewController = controller;
}
void onProgressChanged(InAppWebViewController controller, int progressValue) {
progress.value = progressValue / 100;
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
import 'controller.dart';
class WebViewPage extends GetView<WebController> {
const WebViewPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(WebController());
return Scaffold(
appBar: AppBar(
title: Text(controller.title),
centerTitle: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(2.0),
child: Obx(
() => controller.progress.value < 1.0
? LinearProgressIndicator(
value: controller.progress.value,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor,
),
minHeight: 2.0,
)
: const SizedBox(height: 2.0),
),
),
),
body: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(controller.url)),
initialSettings: InAppWebViewSettings(
isInspectable: true,
javaScriptEnabled: true,
javaScriptCanOpenWindowsAutomatically: true,
useShouldOverrideUrlLoading: true,
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
),
onWebViewCreated: controller.onWebViewCreated,
onProgressChanged: controller.onProgressChanged,
),
);
}
}

View File

@@ -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<String, dynamic>;
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<void> 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<void> _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<void> _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<void> _onAndroidNotificationClickedWithNoAction(
Map<dynamic, dynamic> message,
) async {
Logger.d('onAndroidNotificationClickedWithNoAction ====> $message');
}
) async {}
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {
Logger.d('onAndroidNotificationReceivedInApp ====> $message');
}
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {}
Future<void> _onMessage(Map<dynamic, dynamic> message) async {
Logger.d('onMessage ====> $message');
}
Future<void> _onMessage(Map<dynamic, dynamic> message) async {}
Future<void> _onNotification(Map<dynamic, dynamic> message) async {
Logger.d('onNotification ====> $message');
}
Future<void> _onNotification(Map<dynamic, dynamic> message) async {}
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
Logger.d('onNotificationOpened ====> $message');
await Get.to(() => const MessagePage());
}
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {
Logger.d('onNotificationRemoved ====> $message');
}
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {}
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {
Logger.d('onIOSChannelOpened ====> $message');
}
Future<void> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {}
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {
Logger.d('onIOSRegisterDeviceTokenSuccess ====> $message');
}
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {}
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {
Logger.d('onIOSRegisterDeviceTokenFailed====> $message');
}
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {}
}

View File

@@ -5,18 +5,13 @@ import 'package:ln_jq_app/pages/home/controller.dart';
class HomePage extends GetView<HomeController> {
const HomePage({super.key});
// 主视图
Widget _buildView() {
return <Widget>[Text('主页面')].toColumn(mainAxisSize: MainAxisSize.min).center();
}
@override
Widget build(BuildContext context) {
return GetBuilder<HomeController>(
init: HomeController(),
id: 'home',
builder: (_) {
return controller.getHomePage();
return Scaffold(body: controller.getHomePage());
},
);
}

View File

@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
class LoginController extends GetxController with BaseControllerMixin {
@override
@@ -7,19 +10,73 @@ class LoginController extends GetxController with BaseControllerMixin {
LoginController();
// 控制输入框的 TextEditingController
final TextEditingController driverIdentityController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController codeController = TextEditingController();
final TextEditingController stationIdController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
// --- 倒计时逻辑 ---
final RxInt countdown = 0.obs;
Timer? _timer;
void startCountdown() async {
if (phoneController.text.isEmpty || !phoneController.text.isPhoneNumber) {
showToast("请输入正确的手机号");
return;
}
if (countdown.value > 0) return;
// 调用发送验证码接口
var responseData = await HttpService.to.post(
'appointment/login/sendCode',
data: {"mobile": phoneController.text},
);
if (responseData == null) {
showToast('验证码发送失败,请稍后重试');
return;
}
try {
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
showToast("验证码已发送");
countdown.value = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (countdown.value > 0) {
countdown.value--;
} else {
_timer?.cancel();
}
});
} catch (e) {
showToast('验证码服务异常,请稍后重试');
}
}
@override
void onInit() {
super.onInit();
}
@override
void onClose() {
_timer?.cancel();
phoneController.dispose();
codeController.dispose();
stationIdController.dispose();
passwordController.dispose();
super.onClose();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:dio/dio.dart' as dio;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
@@ -6,243 +7,156 @@ import 'package:image_picker/image_picker.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/storage_service.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:permission_handler/permission_handler.dart';
class QrCodeController extends GetxController
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
class QrCodeController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'qrcode';
// --- Animation ---
late final AnimationController animationController;
late final Animation<double> scanAnimation;
// --- 使用 MobileScanner 的控制器 ---
final MobileScannerController scannerController = MobileScannerController();
final RxBool isFlashOn = false.obs;
final RxBool isProcessingResult = false.obs;
final RxBool hasPermission = false.obs;
final RxBool hasCameraPermission = false.obs;
@override
void onInit() {
super.onInit();
requestPermission();
animationController = AnimationController(
duration: const Duration(milliseconds: 2500),
vsync: this,
);
scanAnimation =
Tween<double>(begin: 0, end: 1).animate(animationController);
animationController.repeat(reverse: false);
_checkPermission();
}
/// MobileScanner 的 onDetect 回调方法
void onDetect(BarcodeCapture capture) {
if (isProcessingResult.value) return;
final Barcode? barcode = capture.barcodes.firstOrNull;
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
isProcessingResult.value = true;
scannerController.stop();
animationController.stop();
renderResult(barcode.rawValue!);
}
/// 检查权限
void _checkPermission() async {
var status = await Permission.camera.status;
hasCameraPermission.value = status.isGranted;
}
/// 恢复扫描状态
void resumeScanner() {
isProcessingResult.value = false;
try {
scannerController.start();
animationController.repeat(reverse: false);
} catch (e) {
print("无法重启相机: $e");
}
}
/// 从相册选择图片并扫描二维码
void scanFromGallery() async {
try {
final XFile? imageFile =
await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) {
return;
}
scannerController.stop();
animationController.stop();
showLoading("正在识别...");
final BarcodeCapture? capture =
await scannerController.analyzeImage(imageFile.path);
dismissLoading();
final Barcode? firstBarcode = capture?.barcodes.firstOrNull;
if (firstBarcode?.rawValue != null &&
firstBarcode!.rawValue!.isNotEmpty) {
final String scanResult = firstBarcode.rawValue!;
print("相册识别到的内容: $scanResult");
renderResult(scanResult);
} else {
showErrorToast('未识别到二维码');
resumeScanner();
}
} catch (e, stackTrace) {
dismissLoading();
showErrorToast('从相册选择失败,请稍后重试');
print("scanFromGallery Error: $e\n$stackTrace");
resumeScanner();
}
}
/// 切换闪光灯
void toggleFlash() async {
try {
await scannerController.toggleTorch();
final currentTorchState = scannerController.value.torchState;
isFlashOn.value = currentTorchState == TorchState.on;
} catch (e) {
print("切换闪光灯失败: $e");
showErrorToast("无法打开闪光灯");
}
}
/// 翻转相机
void flipCamera() async {
await scannerController.switchCamera();
}
/// 请求相机权限
void requestPermission() async {
/// 1. 拍照并识别车牌流程
void takePhotoAndRecognize() async {
var status = await Permission.camera.request();
hasPermission.value = status.isGranted;
if (!status.isGranted) {
if (status.isPermanentlyDenied) {
showErrorToast('相机权限已被永久拒绝,请到系统设置中开启');
// 延迟一会再引导用户去设置
Future.delayed(const Duration(seconds: 2), () => openAppSettings());
} else {
showErrorToast('请授予相机权限以使用扫描功能');
}
if (status.isPermanentlyDenied) openAppSettings();
showErrorToast("需要相机权限才能拍照识别");
return;
}
}
final XFile? photo = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80, // 压缩图片质量以加快上传
);
if (photo == null) return;
void requestPhotoPermission() async {
var status = await Permission.photos.request();
if (status.isGranted) {
scanFromGallery();
} else if (status.isPermanentlyDenied) {
openAppSettings();
} else {
showErrorToast('需要相册权限才能从相册中选择图片');
}
}
/// 处理扫描结果
void renderResult(String resultStr, {plateNumber}) async {
showLoading("正在获取车辆信息...");
try {
final Map<String, dynamic> requestData = {
"code": resultStr,
"phone": StorageService.to.phone,
};
if (plateNumber != null && plateNumber.isNotEmpty) {
requestData['plateNumber'] = plateNumber;
}
var responseData = await HttpService.to.post(
"appointment/truck/bindTruck",
data: requestData,
);
if (responseData == null) {
dismissLoading();
showToast('无法获取车辆信息,请检查网络或稍后重试');
resumeScanner();
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
resumeScanner(); // 绑定失败也要恢复扫描
return;
}
if (result.data == null) {
dismissLoading();
showBindDialog(resultStr);
return;
}
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
await StorageService.to.saveVehicleInfo(vehicle);
dismissLoading();
Get.back(result: true);
} on DioException catch (_) {
showErrorToast("网络请求失败,请稍后重试");
resumeScanner();
} catch (e, _) {
showErrorToast("处理失败,请稍后重舍");
resumeScanner();
} finally {
if (Get.isDialogOpen ?? false) {
dismissLoading();
// 1.1 上传文件
String? imageUrl = await uploadFile(photo.path);
if (imageUrl != null) {
// 1.2 获取车牌号
String? plateNumber = await getPlateNumber(imageUrl);
if (plateNumber != null) {
// 1.3 弹窗确认
manualInputBind(plateNumber, 0);
}
}
}
/// 显示绑定确认对话框
void showBindDialog(String resultStr) {
final TextEditingController plateNumberController = TextEditingController();
// 使用 showConfirmDialog它有 onCancel 回调
/// 手动输入车牌绑定
void manualInputBind(String plateNumber, int source) {
final TextEditingController controller = TextEditingController(
text: plateNumber.toUpperCase() ?? '',
);
DialogX.to.showConfirmDialog(
title: '输入车牌',
barrierDismissible: false,
content: TextField(
controller: plateNumberController,
autofocus: false,
decoration: const InputDecoration(
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
title: source == 0 ? "识别结果" : '手动输入车牌',
content: SizedBox(
height: 40.h,
child: TextField(
textAlign: TextAlign.start,
controller: controller,
autofocus: plateNumber.isEmpty,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
contentPadding: EdgeInsets.only(left: 5),
hintText: '请输入完整的车牌号',
border: OutlineInputBorder(),
),
),
),
confirmText: '确认绑定',
cancelText: '取消', // showConfirmDialog 有 cancelText
onConfirm: () {
final String plateNumber = plateNumberController.text.trim();
if (plateNumber.isEmpty) {
showToast("请输入车牌号");
// 返回 false 可以阻止弹窗关闭,让用户继续输入
return false;
}
renderResult(resultStr, plateNumber: plateNumber);
//关闭弹窗
return true;
},
cancelText: source == 0 ? "重新拍摄" : '取消',
onCancel: () {
// 如果用户点击取消,恢复扫描
resumeScanner();
if (source == 0) {
takePhotoAndRecognize();
}
},
onConfirm: () {
final plate = controller.text.trim().toUpperCase();
if (plate.isNotEmpty) {
bindVehicleByPlate(plate);
}
},
);
}
@override
void onClose() {
scannerController.dispose();
animationController.dispose();
super.onClose();
/// 上传图片
Future<String?> uploadFile(String filePath) async {
showLoading("正在上传图片...");
try {
dio.FormData formData = dio.FormData.fromMap({
'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_plate.jpg'),
});
var response = await HttpService.to.post("appointment/ocr/upload", data: formData);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0) return result.data.toString();
showErrorToast(result.error);
}
} catch (e) {
showErrorToast("图片上传失败");
} finally {
dismissLoading();
}
return null;
}
/// OCR 识别
Future<String?> getPlateNumber(String imageUrl) async {
showLoading("正在识别车牌...");
try {
var response = await HttpService.to.get(
"appointment/ocr/getPlateNumber",
params: {'imageUrl': imageUrl},
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0) return result.data.toString();
showErrorToast(result.error);
}
} catch (e) {
showErrorToast("车牌识别失败");
} finally {
dismissLoading();
}
return null;
}
/// 核心绑定方法
void bindVehicleByPlate(String plateNumber) async {
showLoading("正在绑定车辆...");
try {
var responseData = await HttpService.to.post(
"appointment/truck/bindOcrTruck",
data: {"plateNumber": plateNumber, "phone": StorageService.to.phone},
);
var result = BaseModel.fromJson(responseData?.data);
if (result.code == 0 && result.data != null) {
await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(result.data));
dismissLoading();
showSuccessToast("绑定成功");
Get.back(result: true);
} else {
showErrorToast(result.error);
}
} catch (e) {
} finally {
dismissLoading();
}
}
}

View File

@@ -1,285 +1,82 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'controller.dart';
class QrCodePage extends GetView<QrCodeController> {
const QrCodePage({super.key});
const QrCodePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(QrCodeController());
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text('扫码', style: TextStyle(color: Colors.white)),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Get.back(),
),
actions: [
TextButton(
onPressed: controller.requestPhotoPermission,
child: const Text('相册', style: TextStyle(color: Colors.white, fontSize: 16)),
return GetBuilder<QrCodeController>(
init: QrCodeController(),
id: 'qrcode',
builder: (controller) {
return Scaffold(
appBar: AppBar(
title: const Text('绑定车辆'),
centerTitle: true,
elevation: 0,
),
],
),
body: Obx(() { // 1. 使用 Obx 包裹整个 body
// 根据权限状态来决定显示什么
if (controller.hasPermission.value) {
// 如果有权限,显示扫描器
return _buildScannerView(context);
} else {
// 如果没有权限,显示引导界面
return _buildPermissionDeniedView();
}
}),
);
}
Widget _buildScannerView(BuildContext context){
if (!controller.animationController.isAnimating) {
controller.animationController.repeat(reverse: false);
}
return Stack(
alignment: Alignment.center,
children: [
// 使用 MobileScanner 作为扫描视图
MobileScanner(
controller: controller.scannerController,
onDetect: controller.onDetect,
// 您可以自定义扫描框的样式
scanWindow: Rect.fromCenter(
center: Offset(
MediaQuery.of(context).size.width / 2,
MediaQuery.of(context).size.height / 2 - 50,
),
width: 250,
height: 250,
),
),
// 扫描动画和覆盖层
_buildScannerOverlay(context),
// 底部的功能按钮
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
],
);
}
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: SingleChildScrollView(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 24),
Icon(
Icons.directions_car_rounded,
size: 100,
color: Theme.of(context).primaryColor.withOpacity(0.1),
),
const SizedBox(height: 24),
const Text(
"请选择绑定方式",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"您可以拍摄照片自动识别,\n或手动输入车牌号进行绑定。",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 60),
Widget _buildPermissionDeniedView() {
// 确保动画是停止的
if (controller.animationController.isAnimating) {
controller.animationController.stop();
}
return Container(
color: Colors.black,
alignment: Alignment.center,
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.no_photography, color: Colors.white70, size: 64),
const SizedBox(height: 16),
const Text(
'需要相机权限',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'请授予相机权限以使用扫码功能。',
style: TextStyle(color: Colors.white70, fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: controller.requestPermission, // 点击按钮重新请求权限
child: const Text('授予权限'),
),
],
),
);
}
/// 构建扫描区域的覆盖层和动画
Widget _buildScannerOverlay(BuildContext context) {
// 模拟扫描框的位置和大小
const double scanAreaSize = 250.0;
return Stack(
children: [
// 半透明的覆盖层
ColorFiltered(
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.srcOut),
child: Stack(
children: [
Container(decoration: const BoxDecoration(color: Colors.transparent)),
Align(
alignment: Alignment.center,
child: Container(
margin: const EdgeInsets.only(bottom: 100), // 微调位置
width: scanAreaSize,
height: scanAreaSize,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
// 拍照识别按钮
ElevatedButton.icon(
onPressed: controller.takePhotoAndRecognize,
icon: const Icon(Icons.camera_alt_rounded),
label: const Text("拍照识别车牌"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
),
// 扫描动画
Align(
alignment: Alignment.center,
child: Container(
margin: const EdgeInsets.only(bottom: 100),
width: scanAreaSize,
height: scanAreaSize,
child: AnimatedBuilder(
animation: controller.scanAnimation,
builder: (context, child) {
return CustomPaint(
painter: ScannerAnimationPainter(
controller.scanAnimation.value,
AppTheme.themeColor,
),
);
},
),
),
),
],
);
}
const SizedBox(height: 20),
/// 构建底部的功能按钮(闪光灯、相册)
Widget _buildActionButtons() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'将二维码/条形码放入框内,即可自动扫描',
style: TextStyle(color: Colors.white, fontSize: 14),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 闪光灯按钮
_buildIconButton(
onPressed: controller.toggleFlash,
//闪光灯状态的变化
child: Obx(
() => IconButton(
icon: Icon(
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
color: Colors.white,
size: 28,
),
onPressed: controller.toggleFlash,
// 3. 手动输入按钮
OutlinedButton.icon(
onPressed: (){
controller.manualInputBind("",1);
},
icon: const Icon(Icons.edit_note_rounded),
label: const Text("手动输入车牌"),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.5),
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
// 翻转相机按钮
_buildIconButton(
onPressed: controller.flipCamera,
child: const Icon(
Icons.flip_camera_ios,
color: Colors.white,
size: 28,
),
),
],
const SizedBox(height: 100), // 底部留白
],
),),
),
],
);
}
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
return Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: onPressed,
icon: child,
iconSize: 32, // 增大点击区域
),
);
},
);
}
}
/// 扫描动画的绘制器
class ScannerAnimationPainter extends CustomPainter {
final double value;
final Color borderColor;
ScannerAnimationPainter(this.value, this.borderColor);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = borderColor
..strokeWidth = 3
..style = PaintingStyle.stroke;
final cornerLength = 20.0;
// 绘制四个角的边框
// Top-left
canvas.drawPath(
Path()
..moveTo(0, cornerLength)
..lineTo(0, 0)
..lineTo(cornerLength, 0),
paint,
);
// Top-right
canvas.drawPath(
Path()
..moveTo(size.width - cornerLength, 0)
..lineTo(size.width, 0)
..lineTo(size.width, cornerLength),
paint,
);
// Bottom-left
canvas.drawPath(
Path()
..moveTo(0, size.height - cornerLength)
..lineTo(0, size.height)
..lineTo(cornerLength, size.height),
paint,
);
// Bottom-right
canvas.drawPath(
Path()
..moveTo(size.width - cornerLength, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, size.height - cornerLength),
paint,
);
// 绘制扫描线
final linePaint = Paint()
..color = borderColor.withOpacity(0.8)
..strokeWidth = 2
..shader = LinearGradient(
colors: [borderColor.withOpacity(0), borderColor, borderColor.withOpacity(0)],
stops: const [0.0, 0.5, 1.0],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final y = size.height * value;
canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@@ -10,12 +10,14 @@ class UrlHostController extends GetxController {
// 预设的域名列表
final List<String> presetUrls = [
'https://beta-esg.api.lnh2e.com/', // 测试环境
'http://47.101.201.13:8443/api/', // 测试环境
'http://192.168.110.44:8080/', // 沈辰本地
'http://192.168.110.222:8080/', // 何斐本地
];
final List<String> urlNames = [
'测试环境',
'线上环境',
'沈辰本地环境',
'何斐本地环境',
];

View File

@@ -0,0 +1,32 @@
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/pages/home/view.dart';
import 'package:ln_jq_app/pages/login/view.dart';
import 'package:ln_jq_app/storage_service.dart';
class WelcomeController extends GetxController {
@override
void onReady() {
super.onReady();
// 移除原生闪屏页(如果有的话)
FlutterNativeSplash.remove();
_startTimer();
}
void _startTimer() {
// 1.5秒后执行跳转逻辑
Future.delayed(const Duration(milliseconds: 1500), () {
Get.offAll(() => const HomePage());
});
}
void _jumpToNextPage() {
if (StorageService.to.isLoggedIn) {
// 已登录,跳转到首页
Get.offAll(() => const HomePage());
} else {
// 未登录,跳转到登录页
Get.offAll(() => const LoginPage());
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'controller.dart';
class WelcomePage extends GetView<WelcomeController> {
const WelcomePage({super.key});
@override
Widget build(BuildContext context) {
// 初始化控制器
Get.put(WelcomeController());
return Scaffold(
backgroundColor: Colors.white,
body: SizedBox.expand(
child: Stack(
children: [
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: Image.asset(
'assets/images/welcome.png',
fit: BoxFit.cover
),
),
],
),
),
);
}
}

View File

@@ -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:

View File

@@ -16,7 +16,7 @@ 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.2+5
version: 1.2.3+6
environment:
sdk: ^3.9.0
@@ -52,7 +52,7 @@ dependencies:
geolocator: ^14.0.2 # 获取精确定位
aliyun_push_flutter: ^1.3.6
pull_to_refresh: ^2.0.0
flutter_app_update: ^3.2.2
dev_dependencies: