61 Commits

Author SHA1 Message Date
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
5722e3ace0 ios build 2026-01-16 15:00:45 +08:00
d41b21654a 改用账号推送 2026-01-16 14:35:11 +08:00
2eb059defd 定时器黄字2位小数 2026-01-16 13:35:38 +08:00
fbcc85af2a 站点增加消息入口 2026-01-16 13:06:35 +08:00
9a97b56505 定时器显示调整 2026-01-15 17:55:07 +08:00
8302d7c179 优化问题修改 2026-01-15 16:28:59 +08:00
e7a9e4483a 优化定时器 弹窗 2026-01-15 13:29:02 +08:00
9b64fdfa52 优化定时器 2026-01-15 13:24:02 +08:00
d8f335eb4e 优化 2026-01-15 09:30:47 +08:00
d1b7a9eb76 v1.2.2 version 2026-01-14 13:21:54 +08:00
f25feaa55a 联调修改结构 2026-01-14 13:18:22 +08:00
16639e2384 站点增加广播 2026-01-12 17:52:08 +08:00
20ef495571 非营业状态 增加时间选择 2026-01-12 16:41:36 +08:00
285a20f070 地址切换 2026-01-12 09:13:37 +08:00
baee5dba83 消息中心,待测试 2026-01-08 15:33:40 +08:00
7d9c879a4e 消息中心 2026-01-07 17:43:22 +08:00
953e5e773c 消息入口 2026-01-07 16:02:10 +08:00
8a4bc1d1ab 取消预约按钮 2026-01-07 09:59:12 +08:00
c57c849073 样式交互修改 2026-01-06 17:57:56 +08:00
6cc123f272 更换导航栏位置 2026-01-05 14:09:15 +08:00
5168b23609 增加请求头 2026-01-04 16:59:08 +08:00
c5299dd655 增加版本号 2026-01-04 10:27:39 +08:00
4bedd8c04b 优化 2025-12-31 17:37:07 +08:00
295b71c819 推送配置,测试 2025-12-31 17:22:13 +08:00
6629c8047f Merge branch 'dev' 2025-12-25 10:41:04 +08:00
bfa615a7f4 扫码无权限优化,司机预约多弹窗 2025-12-25 10:40:26 +08:00
288d629f99 bugfix 2025-12-24 10:53:34 +08:00
15fdbc7043 v1.2.1 2025-12-23 10:43:12 +08:00
f2f2348b54 逆地理编码 2025-12-23 10:18:44 +08:00
9ba152b3c3 v1.2.1 待发布 2025-12-22 16:15:32 +08:00
21a528d6d1 调整和修改 2025-12-19 13:18:04 +08:00
62ca3888d3 历史统计数据,错误提示 2025-12-18 15:20:54 +08:00
3ec56a925c 修改搜索条件 2025-12-18 10:33:24 +08:00
95c08818cb 取消dark主题 2025-12-18 09:03:08 +08:00
42355bd1ef 站点-新增历史记录 2025-12-17 17:40:02 +08:00
fe2ce75cec 修改绑定车辆接口
登录后查询车辆信息
2025-12-17 09:18:34 +08:00
98cac8a0a5 预约列表增加筛选条件 2025-12-16 16:18:28 +08:00
9ce46a0c7d 加氢站默认选择,演示账号数据
附件默认显示,详情
2025-12-16 11:58:00 +08:00
82 changed files with 7136 additions and 2640 deletions

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -5,6 +8,14 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { input ->
keystoreProperties.load(input.reader(Charsets.UTF_8))
}
}
android { android {
namespace = "com.lnkj.ln_jq_app" namespace = "com.lnkj.ln_jq_app"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -26,15 +37,30 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = 6
versionName = flutter.versionName versionName = "1.2.3"
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
val storeFilePath = keystoreProperties["storeFile"] as String?
storeFile = if (storeFilePath != null) file(storeFilePath) else null
storePassword = keystoreProperties["storePassword"] as String?
}
} }
buildTypes { buildTypes {
release { getByName("release") {
// TODO: Add your own signing config for the release build. // 使用上面定义的 release 签名
// Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("release")
signingConfig = signingConfigs.getByName("debug")
// 修复混淆规则引用语法
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
} }

View File

@@ -0,0 +1,61 @@
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
# 忽略 Google Play Core 相关的缺失警告(解决你目前的报错)
-dontwarn com.google.android.play.core.**
# Flutter 基础规则
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn com.huawei.android.os.BuildEx$VERSION
-dontwarn com.huawei.hianalytics.process.HiAnalyticsConfig$Builder
-dontwarn com.huawei.hianalytics.process.HiAnalyticsConfig
-dontwarn com.huawei.hianalytics.process.HiAnalyticsInstance$Builder
-dontwarn com.huawei.hianalytics.process.HiAnalyticsInstance
-dontwarn com.huawei.hianalytics.process.HiAnalyticsManager
-dontwarn com.huawei.hianalytics.util.HiAnalyticTools
-dontwarn com.huawei.hms.availableupdate.UpdateAdapterMgr
-dontwarn com.huawei.libcore.io.ExternalStorageFile
-dontwarn com.huawei.libcore.io.ExternalStorageFileInputStream
-dontwarn com.huawei.libcore.io.ExternalStorageFileOutputStream
-dontwarn com.huawei.libcore.io.ExternalStorageRandomAccessFile
-dontwarn org.android.netutil.PingEntry
-dontwarn org.android.netutil.PingResponse
-dontwarn org.android.netutil.PingTask
-dontwarn org.bouncycastle.crypto.BlockCipher
-dontwarn org.bouncycastle.crypto.engines.AESEngine
-dontwarn org.bouncycastle.crypto.prng.SP800SecureRandom
-dontwarn org.bouncycastle.crypto.prng.SP800SecureRandomBuilder
-keepclasseswithmembernames class ** {
native <methods>;
}
-keepattributes Signature
-keep class sun.misc.Unsafe { *; }
-keep class com.taobao.** {*;}
-keep class com.alibaba.** {*;}
-keep class com.alipay.** {*;}
-keep class com.ut.** {*;}
-keep class com.ta.** {*;}
-keep class anet.**{*;}
-keep class anetwork.**{*;}
-keep class org.android.spdy.**{*;}
-keep class org.android.agoo.**{*;}
-keep class android.os.**{*;}
-keep class org.json.**{*;}
-dontwarn com.taobao.**
-dontwarn com.alibaba.**
-dontwarn com.alipay.**
-dontwarn anet.**
-dontwarn org.android.spdy.**
-dontwarn org.android.agoo.**
-dontwarn anetwork.**
-dontwarn com.ut.**
-dontwarn com.ta.**

View File

@@ -15,6 +15,8 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
android:label="小羚羚" android:label="小羚羚"
@@ -48,6 +50,64 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- 请填写你自己的- appKey -->
<meta-data android:name="com.alibaba.app.appkey" android:value="335642645"/>
<!-- 请填写你自己的appSecret -->
<meta-data android:name="com.alibaba.app.appsecret" android:value="39628204345a4240b5b645b68a5896c7"/>
<!-- 华为通道的参数appid -->
<meta-data android:name="com.huawei.hms.client.appid" android:value="" />
<!-- vivo通道的参数api_key为appkey -->
<meta-data android:name="com.vivo.push.api_key" android:value="" />
<meta-data android:name="com.vivo.push.app_id" android:value="" />
<!-- honor通道的参数-->
<meta-data android:name="com.hihonor.push.app_id" android:value="" />
<!-- oppo -->
<meta-data android:name="com.oppo.push.key" android:value="" />
<meta-data android:name="com.oppo.push.secret" android:value="" />
<!-- 小米-->
<meta-data android:name="com.xiaomi.push.id" android:value="id=2222222222222222222" />
<meta-data android:name="com.xiaomi.push.key" android:value="id=5555555555555" />
<!-- 魅族-->
<meta-data android:name="com.meizu.push.id" android:value="" />
<meta-data android:name="com.meizu.push.key" android:value="" />
<!-- 接收推送消息 -->
<receiver
android:name="com.aliyun.ams.push.AliyunPushMessageReceiver"
android:exported="false"> <!-- 为保证receiver安全建议设置不可导出如需对其他应用开放可通过androidpermission进行限制 -->
<intent-filter>
<action android:name="com.alibaba.push2.action.NOTIFICATION_OPENED" />
</intent-filter>
<intent-filter>
<action android:name="com.alibaba.push2.action.NOTIFICATION_REMOVED" />
</intent-filter>
<intent-filter>
<action android:name="com.alibaba.sdk.android.push.RECEIVE" />
</intent-filter>
</receiver>
<!-- 辅助弹窗Activity -->
<activity
android:name="com.aliyun.ams.push.PushPopupActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${applicationId}"
android:path="/thirdpush"
android:scheme="agoo" />
</intent-filter>
</activity>
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:

View File

@@ -3,6 +3,17 @@ allprojects {
// 使用阿里云镜像 // 使用阿里云镜像
maven("https://maven.aliyun.com/repository/public") maven("https://maven.aliyun.com/repository/public")
maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/google")
maven(
"https://maven.aliyun.com/nexus/content/repositories/releases/"
)
// 集成华为通道需要配置 HMS Core SDK 的 Maven地址
maven(
"https://developer.huawei.com/repo/"
)
maven(
"https://developer.hihonor.com/repo"
)
google() google()
mavenCentral() mavenCentral()
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -25,6 +25,41 @@
display: none !important; display: none !important;
} }
/* 去除高德默认的 label 边框和背景 */
.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 { #panel .amap-call {
display: none; display: none;
} }
@@ -32,7 +67,7 @@
/* --- 搜索栏样式 --- */ /* --- 搜索栏样式 --- */
#search-box { #search-box {
position: absolute; position: absolute;
top: 10px; top: 40px;
left: 10px; left: 10px;
right: 10px; right: 10px;
z-index: 100; z-index: 100;
@@ -63,7 +98,7 @@
button { button {
padding: 0 15px; padding: 0 15px;
height: 38px; height: 38px;
background: #3366FF; background: #017143FF;
color: #fff; color: #fff;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
@@ -74,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */ /* --- 导航结果面板 (底部弹出) --- */
#panel { #panel {
position: fixed; position: fixed;
bottom: 0; bottom: 75px;
left: 0; left: 0;
width: 100%; width: 100%;
height: 35%; height: 35%;
@@ -94,7 +129,7 @@
#location-btn { #location-btn {
position: fixed; position: fixed;
right: 10px; right: 10px;
bottom: 50px; bottom: 75px;
/* 默认位置 */ /* 默认位置 */
width: 44px; width: 44px;
height: 44px; height: 44px;
@@ -121,9 +156,24 @@
fill: #555; 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 类时,按钮上移 --- */
body.panel-active #location-btn { body.panel-active #location-btn {
bottom: 38%; bottom: 45%;
/* 对应 #panel 的 height + 一点间距 */ /* 对应 #panel 的 height + 一点间距 */
} }
</style> </style>
@@ -131,13 +181,13 @@
<!-- 1. 配置安全密钥 --> <!-- 1. 配置安全密钥 -->
<script type="text/javascript"> <script type="text/javascript">
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: '0529b72df6bf0c577ff2182cb8b1d970', securityJsCode: 'aa3a22c19ed76b27f8a587555d6981c8',
} }
</script> </script>
<!-- 2. 加载地图和插件 (去掉了 Geolocation 插件,避免弹窗) --> <!-- 2. 加载地图和插件 (去掉了 Geolocation 插件,避免弹窗) -->
<script <script
src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale"> src="https://webapi.amap.com/maps?v=2.0&key=ecd74ece8cb14c9dad67675f83c3274d&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale,AMap.Geocoder">
</script> </script>
</head> </head>
@@ -167,17 +217,24 @@
</div> </div>
<script> <script>
var map, marker, driving, truckDriving; var map, marker, destMarker, driving, truckDriving, geocoder;
var currentLat, currentLng; var currentLat, currentLng;
var isTruckMode = false; var isTruckMode = false;
var isInitialLocationSet = false;
function initMap() { function initMap() {
map = new AMap.Map('container', { map = new AMap.Map('container', {
resizeEnable: true, resizeEnable: true,
zoom: 15, zoom: 17,
viewMode: '3D' viewMode: '3D'
}); });
// --- 2. 初始化 geocoder ---
geocoder = new AMap.Geocoder({
city: "全国" // 设置地理编码范围
});
// 通知 Flutter 地图加载完毕 // 通知 Flutter 地图加载完毕
map.on('complete', function () { map.on('complete', function () {
console.log("JS->: Map is ready."); console.log("JS->: Map is ready.");
@@ -230,6 +287,7 @@
/** /**
* 核心功能 1: 接收 Flutter 传来的定位数据 * 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)") * Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
* 纬度 经度
*/ */
function updateMyLocation(lat, lng, angle) { function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat); var rawLat = parseFloat(lat);
@@ -244,18 +302,18 @@
currentLat = mPoint.lat; currentLat = mPoint.lat;
var position = [currentLng, currentLat]; var position = [currentLng, currentLat];
// 更新车辆标记位置 (保持不变)
if (!marker) { if (!marker) {
marker = new AMap.Marker({ marker = new AMap.Marker({
map: map, map: map,
position: position, position: position,
icon: "https://webapi.amap.com/images/car.png", icon: "car.png",
offset: new AMap.Pixel(-26, -13), offset: new AMap.Pixel(-23.5, -15),
autoRotation: true, autoRotation: true,
angle: isNaN(rawAngle) ? 0 : rawAngle, angle: isNaN(rawAngle) ? 0 : rawAngle,
}); });
map.setCenter(position); map.setCenter(position);
map.setZoom(13);
} else { } else {
marker.moveTo(position, { marker.moveTo(position, {
duration: 1000, duration: 1000,
@@ -263,12 +321,169 @@
}); });
if (!isNaN(rawAngle)) marker.setAngle(rawAngle); if (!isNaN(rawAngle)) marker.setAngle(rawAngle);
} }
// --- 4. 逆地理编码并设置默认起点 ---
// 只有在第一次获取到位置时,才设置默认起点,避免覆盖用户手动输入的起点
if (!isInitialLocationSet) {
geocoder.getAddress(position, function (status, result) {
if (status === 'complete' && result.regeocode) {
let shortAddress = '';
const regeo = result.regeocode;
const addressComponent = regeo.addressComponent;
const pois = regeo.pois;
console.log("地理:" + JSON.stringify(result));
fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI这类POI通常是具体的建筑或地点名
const significantPoi = pois.find(p => p.type.indexOf('商务住宅') === -
1 && p.type.indexOf('地名地址信息') === -1);
if (significantPoi) {
shortAddress = significantPoi.name;
} else {
// 如果找不到就用第一个POI的名字
shortAddress = pois[0].name;
}
}
// 策略2: 如果没有POI使用"道路+门牌号"
else if (addressComponent.street && addressComponent.streetNumber) {
shortAddress = addressComponent.district +
addressComponent.township +
addressComponent.street + addressComponent.streetNumber;
}
// 策略3: 如果还没有,使用"区+乡镇"
else if (addressComponent.district) {
shortAddress = addressComponent.district + (addressComponent
.township || '');
}
// 策略4: 降级到使用完整的、但可能很长的地址
else {
shortAddress = regeo.formattedAddress;
}
// 如果拼接出的地址过长,可以再做一次截断
if (shortAddress.length > 20) {
// 可以在这里添加更复杂的截断逻辑,比如按关键字
shortAddress = regeo.formattedAddress.substring(0, 20) + '...';
}
// 将获取到的地址填充到起点输入框
document.getElementById('startInput').value = shortAddress;
isInitialLocationSet = true; // 标记为已设置,不再更新
} else {
// 如果逆地理编码失败,依然使用“当前位置”作为提示
document.getElementById('startInput').placeholder = "当前位置";
console.error('逆地理编码失败:', result);
}
});
}
} }
}); });
} }
/** /**
* 核心功能 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',
// "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
},
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 markDestination(address, name, longitude, latitude) {
// 1. 清除旧的终点标记
if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标
// 假设图标大小为 32x32你可以根据实际图片尺寸调整 Size
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 设为宽的一半的负数,高度的负数
// 这样能确保图片的底部尖端指向地图上的精确位置
offset: new AMap.Pixel(-16, -32),
title: name,
label: {
content: '<div class="custom-bubble">' + name + '</div>',
direction: 'top'
}
});
// 4. 打印调试信息
console.log("JS->: 终点标记已添加", address, loc.toString());
// 5. 自动调整视野包含起点和终点
// if (marker) {
// // 如果起点标志已存在,缩放地图以展示两者
// map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
// } else {
// // 如果没有起点,直接跳到终点
// map.setCenter(loc);
// }
}
/**
* 点击按钮回到当前位置
*/ */
function backToLocation() { function backToLocation() {
if (currentLng && currentLat) { if (currentLng && currentLat) {
@@ -284,7 +499,7 @@
} }
/** /**
* 核心功能 3: 路径规划 * 路径规划
*/ */
function startRouteSearch() { function startRouteSearch() {
// 获取输入框的文字 // 获取输入框的文字

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: 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: 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: 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: 112 KiB

View File

@@ -1,6 +1,9 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '13.0' platform :ios, '13.0'
source 'https://cdn.cocoapods.org/'
source 'https://gitee.com/aliyun/aliyun-specs.git'
source 'https://github.com/aliyun/aliyun-specs.git'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -1,4 +1,12 @@
PODS: PODS:
- AlicloudELS (1.0.3)
- AlicloudPush (3.2.3):
- AlicloudELS (= 1.0.3)
- AlicloudUTDID (~> 1.0)
- AlicloudUTDID (1.6.1)
- aliyun_push_flutter (0.0.1):
- AlicloudPush (< 4.0, >= 3.2.3)
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
@@ -20,7 +28,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- MTBBarcodeScanner (5.0.11) - mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- OrderedSet (6.0.3) - OrderedSet (6.0.3)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
@@ -29,9 +39,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- qr_code_scanner_plus (0.2.6):
- Flutter
- MTBBarcodeScanner
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -39,6 +46,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- aliyun_push_flutter (from `.symlinks/plugins/aliyun_push_flutter/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@@ -47,19 +55,25 @@ DEPENDENCIES:
- flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`) - flutter_pdfview (from `.symlinks/plugins/flutter_pdfview/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- qr_code_scanner_plus (from `.symlinks/plugins/qr_code_scanner_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS: SPEC REPOS:
https://gitee.com/aliyun/aliyun-specs.git:
- AlicloudUTDID
https://github.com/aliyun/aliyun-specs.git:
- AlicloudELS
- AlicloudPush
trunk: trunk:
- MTBBarcodeScanner
- OrderedSet - OrderedSet
EXTERNAL SOURCES: EXTERNAL SOURCES:
aliyun_push_flutter:
:path: ".symlinks/plugins/aliyun_push_flutter/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
@@ -76,37 +90,40 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/geolocator_apple/darwin" :path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
qr_code_scanner_plus:
:path: ".symlinks/plugins/qr_code_scanner_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
AlicloudELS: fbf821383330465a5af84a033f36f263ae46ca41
AlicloudPush: 95150880af380f64cf1741f5586047c17d36c1d9
AlicloudUTDID: 5d2f22d50e11eecd38f30bc7a48c71925ea90976
aliyun_push_flutter: 0fc2f048a08687ef256c0cfdd72dd7a550ef3347
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_pdfview: 54e283d5851b0b247b3cc57877d35f1a05a204de flutter_pdfview: 32bf27bda6fd85b9dd2c09628a824df5081246cf
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
qr_code_scanner_plus: 7e087021bc69873140e0754750eb87d867bed755
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: 6416011b1bc721211379eaad259ff1cba3dbfad2 PODFILE CHECKSUM: 357c01ff4e7591871e8c4fd6462220a8c7447220
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@@ -50,6 +50,7 @@
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4B58A54CFC9A912F2BA04FF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 4B58A54CFC9A912F2BA04FF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6AF04C5CFFF0B4098EEDA799 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6D3F89E22F04C32900A154AD /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -143,6 +144,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6D3F89E22F04C32900A154AD /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -487,9 +489,10 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38; DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -497,7 +500,79 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_pdfview\"",
"-framework",
"\"geolocator_apple\"",
"-framework",
"\"image_picker_ios\"",
"-framework",
"\"mobile_scanner\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"url_launcher_ios\"",
"-framework",
"\"AlicloudELS\"",
"-framework",
"\"CloudPushSDK\"",
"-framework",
"\"aliyun_push_flutter\"",
);
"OTHER_LDFLAGS[arch=*]" = (
"$(inherited)",
"-ObjC",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_pdfview\"",
"-framework",
"\"geolocator_apple\"",
"-framework",
"\"image_picker_ios\"",
"-framework",
"\"mobile_scanner\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"url_launcher_ios\"",
);
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp; PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -682,9 +757,10 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38; DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -692,7 +768,79 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_pdfview\"",
"-framework",
"\"geolocator_apple\"",
"-framework",
"\"image_picker_ios\"",
"-framework",
"\"mobile_scanner\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"url_launcher_ios\"",
"-framework",
"\"AlicloudELS\"",
"-framework",
"\"CloudPushSDK\"",
"-framework",
"\"aliyun_push_flutter\"",
);
"OTHER_LDFLAGS[arch=*]" = (
"$(inherited)",
"-ObjC",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_pdfview\"",
"-framework",
"\"geolocator_apple\"",
"-framework",
"\"image_picker_ios\"",
"-framework",
"\"mobile_scanner\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"url_launcher_ios\"",
);
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp; PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -714,9 +862,10 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 2228B9MS38; DEVELOPMENT_TEAM = 2228B9MS38;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -724,7 +873,46 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-l\"swiftCoreGraphics\"",
"-framework",
"\"OrderedSet\"",
"-framework",
"\"connectivity_plus\"",
"-framework",
"\"device_info_plus\"",
"-framework",
"\"flutter_inappwebview_ios\"",
"-framework",
"\"flutter_native_splash\"",
"-framework",
"\"flutter_pdfview\"",
"-framework",
"\"geolocator_apple\"",
"-framework",
"\"image_picker_ios\"",
"-framework",
"\"mobile_scanner\"",
"-framework",
"\"package_info_plus\"",
"-framework",
"\"path_provider_foundation\"",
"-framework",
"\"permission_handler_apple\"",
"-framework",
"\"shared_preferences_foundation\"",
"-framework",
"\"url_launcher_ios\"",
"-framework",
"\"AlicloudELS\"",
"-framework",
"\"CloudPushSDK\"",
"-framework",
"\"aliyun_push_flutter\"",
);
PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp; PRODUCT_BUNDLE_IDENTIFIER = com.lnkj.lnJqApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -63,5 +63,18 @@
</array> </array>
<key>uses</key> <key>uses</key>
<string></string> <string></string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>zh-Hans</string>
<string>en</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

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

View File

@@ -6,7 +6,6 @@ class BaseModel<T> {
final String message; // 消息,例如 "success" final String message; // 消息,例如 "success"
final String msg; // 消息,例如 "success" final String msg; // 消息,例如 "success"
final T? data; // 核心数据,使用泛型 T可以是任何类型 final T? data; // 核心数据,使用泛型 T可以是任何类型
final int time; // 时间戳
final dynamic error; // 错误信息,可以是任何类型或 null final dynamic error; // 错误信息,可以是任何类型或 null
BaseModel({ BaseModel({
@@ -15,7 +14,6 @@ class BaseModel<T> {
required this.message, required this.message,
required this.msg, required this.msg,
this.data, // data 可以为 null this.data, // data 可以为 null
required this.time,
this.error, // error 可以为 null this.error, // error 可以为 null
}); });
@@ -60,7 +58,6 @@ class BaseModel<T> {
status: json['status'] as bool? ?? false, status: json['status'] as bool? ?? false,
message: json['message'] ?? '暂不可用,请稍后', message: json['message'] ?? '暂不可用,请稍后',
msg: json['msg'] ?? '暂不可用,请稍后', msg: json['msg'] ?? '暂不可用,请稍后',
time: _parseInt(json['time']),
data: finalData, data: finalData,
error: json['error'], error: json['error'],
); );
@@ -72,7 +69,6 @@ class BaseModel<T> {
'status': status, 'status': status,
'message': message, 'message': message,
'msg': msg, 'msg': msg,
'time': time,
'data': data, 'data': data,
'error': error, 'error': error,
}; };

View File

@@ -15,7 +15,7 @@ class VehicleInfo {
final String engineNum; final String engineNum;
final String truckNum; final String truckNum;
final num hydrogenCapacity; final num hydrogenCapacity;
final num maxHydrogen; final String maxHydrogen;
VehicleInfo({ VehicleInfo({
required this.plateNumber, required this.plateNumber,
@@ -36,7 +36,7 @@ class VehicleInfo {
engineNum: json["engineNum"] ?? '', engineNum: json["engineNum"] ?? '',
truckNum: json["truckNum"] ?? '', truckNum: json["truckNum"] ?? '',
hydrogenCapacity: json["hydrogenCapacity"] ?? 0, hydrogenCapacity: json["hydrogenCapacity"] ?? 0,
maxHydrogen: json["maxHydrogen"] ?? 0, maxHydrogen: json["maxHydrogen"] ?? '',
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {

View File

@@ -5,13 +5,20 @@ class AppTheme {
static const String Font_YuYang = 'YuYang'; 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;
static const String test_service_url = "http://47.100.49.118:8090/api/"; //http://192.168.110.222:8080/
//http://192.168.110.44:8080/
static String test_service_url = "https://beta-esg.api.lnh2e.com/";
static const String release_service_url = ""; static const String release_service_url = "";
//加氢站相关查询 //加氢站相关查询
static const String jiaqing_service_url = "https://lnh2e.com/api/lingniu-manager-v1/v1/"; static const String jiaqing_service_url =
"https://beta.lnh2e.com/api/lingniu-manager-v1/v1/";
//车辆信息 //车辆信息
static const String car_service_url = "http://47.99.166.38:20000/"; static const String car_service_url = "http://47.99.166.38:20000/";
@@ -19,6 +26,11 @@ class AppTheme {
static const Color darkThemeColor = Color(0xFF032896); static const Color darkThemeColor = Color(0xFF032896);
static const String android_key = "335642645";
static const String android_appsecret = "39628204345a4240b5b645b68a5896c7";
static const String ios_key = "335642649";
static const String ios_appsecret = "173bc08bd5df422da20c8e3ffbf0521b";
/// 亮色主题样式 /// 亮色主题样式
static ThemeData light = ThemeData( static ThemeData light = ThemeData(
useMaterial3: false, useMaterial3: false,
@@ -56,10 +68,7 @@ class AppTheme {
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color.fromARGB(255, 34, 34, 34), backgroundColor: Color.fromARGB(255, 34, 34, 34),
centerTitle: true, centerTitle: true,
titleTextStyle: TextStyle( titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
bottomAppBarTheme: BottomAppBarThemeData( bottomAppBarTheme: BottomAppBarThemeData(
color: Color.fromARGB(255, 34, 34, 34), color: Color.fromARGB(255, 34, 34, 34),

View File

@@ -1,16 +1,20 @@
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
/// 专门用于处理和添加 Token 的拦截器 /// 专门用于处理和添加 Token 的拦截器
class TokenInterceptor extends Interceptor { class TokenInterceptor extends Interceptor {
// 定义您想要使用的 Token Key // 定义您想要使用的 Token Key
final String tokenKey; final String tokenKey;
final String sourceKey;
// 构造函数,允许外部传入自定义的 Key默认为 'Authorization' // 构造函数,允许外部传入自定义的 Key默认为 'Authorization'
TokenInterceptor({this.tokenKey = 'Authorization'}); TokenInterceptor({this.tokenKey = 'Authorization', this.sourceKey = 'source'});
@override @override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { void onRequest(RequestOptions options, RequestInterceptorHandler handler) async{
// 从 StorageService 中获取已保存的 token // 从 StorageService 中获取已保存的 token
final String? token = StorageService.to.token; final String? token = StorageService.to.token;
@@ -23,6 +27,21 @@ class TokenInterceptor extends Interceptor {
} }
} }
String platformSource;
if (Platform.isAndroid) {
platformSource = 'Android';
} else if (Platform.isIOS) {
platformSource = 'iOS';
} else {
platformSource = '';
}
if (!options.headers.containsKey(sourceKey)) {
options.headers[sourceKey] = platformSource;
}
options.headers['appVersion'] = await getVersion();
options.headers['brand'] = await getDeviceModel();
// 调用 handler.next(options) 以继续执行请求 // 调用 handler.next(options) 以继续执行请求
// 这一步至关重要,否则请求会被中断 // 这一步至关重要,否则请求会被中断
super.onRequest(options, handler); super.onRequest(options, handler);

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
@@ -5,19 +6,22 @@ import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.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/common/token_interceptor.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'common/styles/theme.dart'; import 'common/styles/theme.dart';
import 'pages/home/view.dart';
import 'pages/login/view.dart'; import 'pages/login/view.dart';
import 'pages/welcome/view.dart'; // 引入启动页
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding widgetsBinding = await init( WidgetsBinding widgetsBinding = await init(
isDebug: false, isDebug: true,
logTag: '小羚羚', logTag: '小羚羚',
supportedLocales: [Locale('zh', 'CN')], supportedLocales: [const Locale('zh', 'CN')],
); );
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await GetStorage.init(); await GetStorage.init();
@@ -30,7 +34,7 @@ void main() async {
// 设计稿尺寸 单位dp // 设计稿尺寸 单位dp
designSize: const Size(390, 844), designSize: const Size(390, 844),
// Getx Log // Getx Log
enableLog: false, enableLog: true,
// 默认的跳转动画 // 默认的跳转动画
defaultTransition: Transition.rightToLeft, defaultTransition: Transition.rightToLeft,
// 主题模式 // 主题模式
@@ -41,20 +45,17 @@ void main() async {
darkTheme: AppTheme.light, darkTheme: AppTheme.light,
// AppTitle // AppTitle
title: '小羚羚', title: '小羚羚',
// 首页入口 // 将入口改为启动页
home: HomePage(), home: const WelcomePage(),
//组件国际化 fallbackLocale: const Locale('zh', 'CN'),
fallbackLocale: Locale('zh', 'CN'), supportedLocales: const [Locale('zh', 'CN')],
supportedLocales: [Locale('zh', 'CN')],
localizationsDelegates: const [ localizationsDelegates: const [
RefreshLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
// Builder
builder: (context, widget) { builder: (context, widget) {
// do something....
return widget!; return widget!;
}, },
), ),
@@ -62,20 +63,23 @@ void main() async {
} }
void initHttpSet() { void initHttpSet() {
// 设置基础 URL AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
HttpService.to.setBaseUrl(AppTheme.test_service_url); HttpService.to.setBaseUrl(AppTheme.test_service_url);
//指定请求头
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token')); HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
// 设置全局响应处理器
HttpService.to.setOnResponseHandler((response) async { HttpService.to.setOnResponseHandler((response) async {
try { try {
final baseModel = BaseModel<dynamic>.fromJson(response.data); final baseModel = BaseModel.fromJson(response.data);
if (baseModel.code == 0 || baseModel.code == 200) { if (baseModel.code == 0 || baseModel.code == 200) {
return null; return null;
} else if (baseModel.code == 401) { } else if (baseModel.code == 401) {
await StorageService.to.clearLoginInfo(); await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage()); Get.offAll(() => const LoginPage());
return baseModel.message; return baseModel.message;
} else {
return (baseModel.error.toString()).isEmpty
? "服务繁忙,稍后重试"
: baseModel.error.toString();
} }
} on Exception catch (e) { } on Exception catch (e) {
e.printInfo(); e.printInfo();

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:getx_scaffold/common/index.dart'; import 'package:getx_scaffold/common/index.dart';
import 'package:getx_scaffold/getx_scaffold.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/base_widgets/controller.dart';
import 'package:ln_jq_app/pages/b_page/reservation/view.dart'; import 'package:ln_jq_app/pages/b_page/reservation/view.dart';
import 'package:ln_jq_app/pages/b_page/site/view.dart'; import 'package:ln_jq_app/pages/b_page/site/view.dart';
@@ -9,14 +10,16 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
B_BaseWidgetsPage({super.key}); B_BaseWidgetsPage({super.key});
final PageController _pageController = PageController(); final PageController _pageController = PageController();
// 主视图 // 主视图
Widget _buildView() { Widget _buildView() {
return PageView( return PageView(
controller: _pageController, controller: _pageController,
physics: const NeverScrollableScrollPhysics(), // 禁止滑动
onPageChanged: (index) { onPageChanged: (index) {
jumpTabAndPage(index); jumpTabAndPage(index);
}, },
children: _buildPages(), // 页面的列表 children: _buildPages(),
); );
} }
@@ -25,33 +28,59 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
controller.updateUi(); // 更新 UI controller.updateUi(); // 更新 UI
_pageController.jumpToPage(controller.pageIndex); _pageController.jumpToPage(controller.pageIndex);
} }
// 对应的页面 // 对应的页面
List<Widget> _buildPages() { List<Widget> _buildPages() {
return [ return [SitePage(), ReservationPage()];
SitePage(),
ReservationPage(),
];
} }
//导航栏 // 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() { Widget _buildNavigationBar() {
return NavigationX( return SafeArea(
currentIndex: controller.pageIndex, // 当前选中的tab索引 child: Container(
onTap: (index) { height: 50.h,
jumpTabAndPage(index); margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
}, // 切换tab事件 decoration: BoxDecoration(
items: [ color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
NavigationItemModel( borderRadius: BorderRadius.circular(30),
label: '加氢预约', boxShadow: [
icon: AntdIcon.orderedlist, BoxShadow(
selectedIcon: AntdIcon.calendar_fill, color: Colors.black.withOpacity(0.05),
), blurRadius: 10,
NavigationItemModel( offset: const Offset(0, 5),
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', id: 'b_baseWidgets',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
extendBody: false, extendBody: true,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(), bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()), body: _buildView(),
); );
}, },
); );

View File

@@ -0,0 +1,216 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // Reuse ReservationModel
class HistoryController extends GetxController {
// --- 定义 API 需要的日期格式化器 ---
final DateFormat _apiDateFormat = DateFormat('yyyy-MM-dd');
// 默认查询最近7天
final Rx<DateTime> startDate = DateTime.now().subtract(const Duration(days: 7)).obs;
final Rx<DateTime> endDate = DateTime.now().obs;
final TextEditingController plateNumberController = TextEditingController();
final RxString totalHydrogen = '0 kg'.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 = "";
@override
void onInit() {
super.onInit();
final args = Get.arguments as Map<String, dynamic>;
stationName = args['stationName'] as String;
fetchHistoryData();
}
Future<void> getAllOrderCounts() async {
var response = await HttpService.to.post(
"appointment/orderAddHyd/getAllOrderCounts",
data: {
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
'plateNumber': plateNumberController.text,
'stationName': stationName, // 加氢站名称
},
);
if (response == null || response.data == null) {
totalHydrogen.value = '0 kg';
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}';
} catch (e) {
totalHydrogen.value = '0 kg';
totalCompletions.value = '0 次';
}
}
Future<void> fetchHistoryData() async {
isLoading.value = true;
//获取数据
getAllOrderCounts();
try {
var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage",
data: {
// --- 直接使用 DateFormat 来格式化日期 ---
'startTime': _apiDateFormat.format(startDate.value),
'endTime': _apiDateFormat.format(endDate.value),
'plateNumber': plateNumberController.text,
'pageNum': 1,
'pageSize': 50,
'stationName': stationName, // 加氢站名称
},
);
if (response == null || response.data == null) {
showToast('无法获取历史记录');
_resetData();
return;
}
final baseModel = BaseModel<dynamic>.fromJson(response.data);
if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? [];
historyList.assignAll(
listFromServer
.map((item) => ReservationModel.fromJson(item as Map<String, dynamic>))
.toList(),
);
hasData.value = historyList.isNotEmpty;
} else {
showToast(baseModel.message);
_resetData();
}
} catch (e) {
showToast('获取历史记录失败: $e');
_resetData();
} finally {
isLoading.value = false;
}
}
void _resetData() {
historyList.clear();
hasData.value = false;
}
void pickDate(BuildContext context, bool isStartDate) {
// 确定当前操作的日期和临时存储变量
final DateTime initialDate = isStartDate ? startDate.value : endDate.value;
DateTime tempDate = initialDate;
// 定义全局的最早可选日期
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, // 使底部工作表外的区域透明
);
}
@override
void onClose() {
plateNumberController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.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
class HistoryPage extends GetView<HistoryController> {
const HistoryPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(HistoryController());
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 _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),
),
),
),
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)),
),
),
],
),
),
);
}
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,
),
],
),
),
),
);
}
Widget _buildHistoryList() {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (!controller.hasData.value) {
return const Center(child: Text('没有找到相关记录'));
}
return ListView.builder(
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),
),
);
},
);
});
}
Widget _buildStatusChip(ReservationStatus status) {
String text;
Color color;
switch (status) {
case ReservationStatus.pending:
text = '待加氢';
color = Colors.orange;
break;
case ReservationStatus.completed:
text = '已加氢';
color = Colors.greenAccent;
break;
case ReservationStatus.rejected:
text = '拒绝加氢';
color = Colors.red;
break;
case ReservationStatus.unadded:
text = '未加氢';
color = Colors.red;
break;
case ReservationStatus.cancel:
text = '已取消';
color = Colors.red;
break;
default:
text = '未知状态';
color = Colors.grey;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: color, size: 8),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _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)),
],
),
);
}
}

View File

@@ -1,42 +1,194 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.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/common/model/base_model.dart';
import 'package:ln_jq_app/pages/login/view.dart'; import 'package:ln_jq_app/pages/login/view.dart';
import '../../../common/styles/theme.dart';
import '../../../storage_service.dart'; import '../../../storage_service.dart';
class ReservationController extends GetxController with BaseControllerMixin { class ReservationController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'b_reservation'; // 确保ID与View中一致 String get builderId => 'b_reservation';
// --- 运营状态下拉菜单所需的状态 ---
// 下拉菜单的选项列表
final List<String> operationStatusOptions = ["营运中", "维修中", "暂停营业", "站点关闭"]; final List<String> operationStatusOptions = ["营运中", "维修中", "暂停营业", "站点关闭"];
// 当前选中的值,默认为'运营中'
String selectedOperationStatus = "营运中"; String selectedOperationStatus = "营运中";
// --- 其它状态下的时间选择 ---
DateTime? customStartTime;
DateTime? customEndTime;
String get customStartTimeStr => customStartTime != null
? DateFormat('yyyy-MM-dd HH:mm').format(customStartTime!)
: '点击选择开始时间';
String get customEndTimeStr => customEndTime != null
? DateFormat('yyyy-MM-dd HH:mm').format(customEndTime!)
: '点击选择结束时间';
// --- 站点广播相关 ---
final TextEditingController broadcastTitleController = TextEditingController();
final TextEditingController broadcastContentController = TextEditingController();
final RxInt selectedTabIndex = 0.obs;
@override
bool get listenLifecycleEvent => true;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 1. 初始化默认时间
customStartTime = DateTime.now();
customEndTime = customStartTime!.add(const Duration(days: 1));
renderData(); renderData();
msgNotice(); // 红点消息
startAutoRefresh();
}
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
}
@override
void onClose() {
stopAutoRefresh();
broadcastTitleController.dispose();
broadcastContentController.dispose();
super.onClose();
}
void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
// 创建一个每5分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
renderData();
});
}
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
_refreshTimer = null; // 置为null方便判断
} }
String name = ""; String name = "";
String address = ""; String address = "";
String phone = ""; String phone = "";
String costPrice = ""; //氢气价格 String costPrice = "";
String customerPrice = ""; //官方价格 String customerPrice = "";
String startBusiness = ""; String startBusiness = "";
String endBusiness = ""; String endBusiness = "";
String timeStr = ""; String timeStr = "";
String operatingEnterprise = ""; String operatingEnterprise = "";
String hydrogenId = ""; String hydrogenId = "";
String jobTipStr = "";
String jobDetailsStr = "";
String jobId = "";
Timer? _refreshTimer;
bool isNotice = false;
Future<void> renderData() async { Future<void> renderData() async {
showLoading("加载中"); showLoading("加载中");
try { try {
//获取加氢站未执行的状态修改任务信息
var jobData = await HttpService.to.get('appointment/job/hyd/un-executed');
if (jobData != null) {
final jobDataResult = BaseModel.fromJson(jobData.data);
if (jobDataResult.code == 0) {
try {
final List<dynamic> dataList = jobDataResult.data is List
? jobDataResult.data
: [];
final firstJob = dataList[0];
jobId = firstJob["id"] ?? "";
String endTime = firstJob["endTime"] ?? "";
String beginTime = firstJob["beginTime"] ?? "";
String hydStatus = firstJob["hydStatus"].toString() ?? "";
String hydStatusStr = "";
if (hydStatus == "0") {
hydStatusStr = "营运中";
} else if (hydStatus == "1") {
hydStatusStr = "维修中";
} else if (hydStatus == "2") {
hydStatusStr = "站点关闭";
} else if (hydStatus == "3") {
hydStatusStr = "暂停营业";
}
//现在的时间晚于开始时间就不显示文案
bool isJobStarted = false;
try {
if (beginTime.isNotEmpty) {
DateTime beginDateTime = DateTime.parse(beginTime);
if (DateTime.now().isAfter(beginDateTime)) {
isJobStarted = true;
}
}
} catch (e) {
print("开始时间解析失败: $e");
}
if (isJobStarted) {
jobTipStr = "";
}
//结束时间
if (endTime.isNotEmpty) {
try {
// 解析时间字符串
DateTime endDateTime = DateTime.parse(endTime);
DateTime beginDateTime = DateTime.parse(beginTime);
DateTime now = DateTime.now(); //计算时间差 (endTime - now)
Duration diff = endDateTime.difference(now);
// 计算小时数 (允许小数,例如 0.5)
// inMinutes / 60 可以得到更精确的小数小时
double hoursLeft = diff.inMinutes / 60.0;
//计算当前时间-开始时间
Duration startDiff = beginDateTime.difference(now);
double hoursUntilStart = startDiff.inMinutes / 60.0;
// 只有在【当前时间早于开始时间】且【剩余时间大于0】时才显示文案
if (now.isBefore(beginDateTime) && hoursLeft > 0) {
// 如果是正数,表示还有多久结束
String timeTip = " ${hoursUntilStart.toStringAsFixed(2)}小时后";
jobTipStr = "$timeTip$hydStatusStr";
} else {
jobTipStr = "";
}
jobDetailsStr =
"当前站点已设置$beginTime至$endTime,共${hoursLeft.toStringAsFixed(2)}小时,为$hydStatusStr状态";
// 如果是处于非营运状态,自动回填开始和结束时间
// 假设 customStartTime 是现在customEndTime 是接口返回的结束时间
customStartTime = beginDateTime;
customEndTime = endDateTime;
} catch (e) {
print("时间解析失败: $e");
}
}
} catch (e) {
Logger.d("解析失败或者没返回信息: $e");
jobTipStr = "";
}
}
}
//获取站点信息
var responseData = await HttpService.to.get( var responseData = await HttpService.to.get(
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}', 'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
); );
@@ -49,32 +201,30 @@ class ReservationController extends GetxController with BaseControllerMixin {
try { try {
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
name = result.data["name"] ?? "";
name = result.data["name"];
hydrogenId = result.data["hydrogenId"].toString(); hydrogenId = result.data["hydrogenId"].toString();
address = result.data["address"]; address = result.data["address"] ?? "";
var rawCostPrice = result.data["costPrice"];
if (rawCostPrice != null && rawCostPrice.toString().isNotEmpty) {
costPrice = "¥$rawCostPrice";
} else {
costPrice = "暂无价格";
}
var customerPriceTemp = result.data["customerPrice"];
if (customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty) {
customerPrice = "$customerPriceTemp";
} else {
customerPrice = "暂无价格";
}
phone = result.data["phone"]; var rawCostPrice = result.data["costPrice"];
startBusiness = result.data["startBusiness"]; costPrice = (rawCostPrice != null && rawCostPrice.toString().isNotEmpty)
endBusiness = result.data["endBusiness"]; ? "¥$rawCostPrice"
: "暂无价格";
var customerPriceTemp = result.data["customerPrice"];
customerPrice =
(customerPriceTemp != null && customerPriceTemp.toString().isNotEmpty)
? "$customerPriceTemp"
: "暂无价格";
phone = result.data["phone"] ?? "";
startBusiness = result.data["startBusiness"] ?? "";
endBusiness = result.data["endBusiness"] ?? "";
operatingEnterprise = result.data["operatingEnterprise"].toString(); operatingEnterprise = result.data["operatingEnterprise"].toString();
String temp = result.data["siteStatusName"].toString(); String temp = result.data["siteStatusName"].toString();
selectedOperationStatus = (temp.isEmpty || temp == "null") ? "营运中" : temp; selectedOperationStatus = (temp.isEmpty || temp == "null") ? "营运中" : temp;
if (startBusiness.isNotEmpty && endBusiness.isNotEmpty) { if (startBusiness.isNotEmpty && endBusiness.isNotEmpty) {
// 营业时间为24小时的特殊处理
if (startBusiness == "00:00:00" && endBusiness == "23:59:59") { if (startBusiness == "00:00:00" && endBusiness == "23:59:59") {
timeStr = "24小时营业"; timeStr = "24小时营业";
} else { } else {
@@ -86,19 +236,39 @@ class ReservationController extends GetxController with BaseControllerMixin {
operatingEnterprise = operatingEnterprise.isEmpty ? "暂未设置" : operatingEnterprise; operatingEnterprise = operatingEnterprise.isEmpty ? "暂未设置" : operatingEnterprise;
updateUi();
dismissLoading(); dismissLoading();
} catch (e) { } catch (e) {
// 如果解析 JSON 失败
dismissLoading(); dismissLoading();
showToast('数据异常'); showToast('数据异常');
} }
} catch (e) { } catch (e) {
dismissLoading(); dismissLoading();
} finally {
updateUi();
}
}
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) { void onOperationStatusChanged(String? newValue) {
if (newValue != null) { if (newValue != null) {
selectedOperationStatus = newValue; selectedOperationStatus = newValue;
@@ -106,14 +276,105 @@ class ReservationController extends GetxController with BaseControllerMixin {
} }
} }
void saveInfo() async { /// 使用底部滚轮形式选择时间(下拉框效果)
showLoading("保存中"); void pickDateTime(BuildContext context, bool isStart) {
DateTime now = DateTime.now();
DateTime initialDate = isStart ? (customStartTime ?? now) : (customEndTime ?? now);
DateTime minLimit = isStart
? now.subtract(const Duration(minutes: 1))
: (customStartTime ?? now).subtract(const Duration(minutes: 1));
if (initialDate.isBefore(minLimit)) {
initialDate = isStart ? now : (customStartTime ?? now);
}
DateTime tempDate = initialDate;
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, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
onPressed: () => Get.back(),
child: const Text('取消', style: TextStyle(color: Colors.grey)),
),
CupertinoButton(
onPressed: () {
if (isStart) {
customStartTime = tempDate;
if (customEndTime != null &&
customEndTime!.isBefore(customStartTime!)) {
customEndTime = customStartTime!.add(const Duration(days: 1));
}
} else {
if (tempDate.isBefore(customStartTime ?? DateTime.now())) {
showToast('结束时间不能早于开始时间');
return;
}
customEndTime = tempDate;
}
updateUi();
Get.back();
},
child: const Text(
'确定',
style: TextStyle(
color: AppTheme.themeColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const Divider(height: 1),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
initialDateTime: initialDate,
minimumDate: minLimit,
use24hFormat: true,
onDateTimeChanged: (DateTime newDate) {
tempDate = newDate;
},
),
),
],
),
),
backgroundColor: Colors.transparent,
);
}
void saveInfo() async {
if (selectedOperationStatus != "营运中") {
if (customStartTime == null || customEndTime == null) {
showToast("请选择开始和结束时间");
return;
}
}
showLoading("保存中");
try { try {
var responseData = await HttpService.to.post( var responseData = await HttpService.to.post(
'appointment/station/updateStationStatus', 'appointment/station/updateStationStatus',
data: { data: {
'hydrogenId': hydrogenId, 'hydrogenId': hydrogenId,
'name': name,
'siteStatus': selectedOperationStatus == "营运中" 'siteStatus': selectedOperationStatus == "营运中"
? "0" ? "0"
: selectedOperationStatus == "维修中" : selectedOperationStatus == "维修中"
@@ -123,18 +384,27 @@ class ReservationController extends GetxController with BaseControllerMixin {
: selectedOperationStatus == "暂停营业" : selectedOperationStatus == "暂停营业"
? "3" ? "3"
: 0, : 0,
'beginTime': selectedOperationStatus == "营运中"
? null
: DateFormat('yyyy-MM-dd HH:mm:ss').format(customStartTime!),
'endTime': selectedOperationStatus == "营运中"
? null
: DateFormat('yyyy-MM-dd HH:mm:ss').format(customEndTime!),
'updateTime': getNowDateTimeString(), 'updateTime': getNowDateTimeString(),
}, },
); );
if (responseData == null && responseData!.data == null) { if (responseData == null || responseData.data == null) {
dismissLoading(); dismissLoading();
showToast('服务暂不可用,请稍后'); showToast('服务暂不可用,请稍后');
return; return;
} }
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) { if (result.code == 0) {
showSuccessToast("保存成功"); showSuccessToast("保存成功,已同步通知对应司机");
//重新刷新页面
renderData();
} }
dismissLoading(); dismissLoading();
} catch (e) { } catch (e) {
@@ -142,11 +412,87 @@ class ReservationController extends GetxController with BaseControllerMixin {
} }
} }
void logout() async { /// 显示当前未执行任务的详情弹窗
// TODO: 在这里执行退出登录的逻辑 void showJob() {
//清理本地缓存的用户信息 导航到登录页面 if (jobDetailsStr.isEmpty) {
await StorageService.to.clearLoginInfo(); showToast("当前没有正在生效的任务设置");
return;
}
DialogX.to.showConfirmDialog(
title: '当前设置详情',
content: Text(jobDetailsStr, style: const TextStyle(fontSize: 15, height: 1.5)),
confirmText: '好的',
cancelText: '取消设置',
onCancel: () {
// 点击“取消设置”调用删除接口
_cancelJob();
},
);
}
/// 内部私有方法:调用取消/删除任务接口
void _cancelJob() async {
showLoading("正在取消...");
try {
var response = await HttpService.to.delete('appointment/job/hyd/$jobId');
dismissLoading();
if (response != null) {
var result = BaseModel.fromJson(response.data);
if (result.code == 0) {
showSuccessToast("已成功取消该设置");
// 成功后重新刷新页面数据,重置状态
renderData();
} else {
showErrorToast(result.error);
}
}
} catch (e) {
dismissLoading();
showErrorToast("取消失败,请稍后重试");
Logger.d("取消任务失败: $e");
}
}
/// 发送站点广播
void sendBroadcast() async {
String title = broadcastTitleController.text.trim();
String content = broadcastContentController.text.trim();
if (title.isEmpty) {
showToast("请输入通知标题");
return;
}
if (content.isEmpty) {
showToast("请输入通知内容");
return;
}
showLoading("发送中...");
try {
var responseData = await HttpService.to.post(
'appointment/notice/push/station/broadcast',
data: {'title': title, 'content': content},
);
dismissLoading();
if (responseData != null) {
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("广播发送成功");
} else {
showErrorToast(result.error);
}
}
} catch (e) {
dismissLoading();
showToast("发送失败,请稍后重试");
}
}
void logout() async {
await StorageService.to.clearLoginInfo();
Get.offAll(() => LoginPage()); Get.offAll(() => LoginPage());
} }
} }

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.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/b_page/reservation/controller.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
class ReservationPage extends GetView<ReservationController> { class ReservationPage extends GetView<ReservationController> {
const ReservationPage({super.key}); const ReservationPage({super.key});
// 定义主题色
static const kPrimaryColor = Color(0xFF006D35); // 效果图深绿色
static const kBgColor = Color(0xFFF5F7F9); // 背景灰
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<ReservationController>( return GetBuilder<ReservationController>(
@@ -13,148 +19,178 @@ class ReservationPage extends GetView<ReservationController> {
id: 'b_reservation', id: 'b_reservation',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: kBgColor,
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildHeaderCard(), _buildTopSection(context),
const SizedBox(height: 12), Padding(
_buildInfoFormCard(context), padding: EdgeInsets.symmetric(horizontal: 20.w),
const SizedBox(height: 12), child: Column(
_buildTipsCard(), children: [
const SizedBox(height: 12), SizedBox(height: 16),
_buildBasicInfoCard(),
SizedBox(height: 16),
_buildOperationContentCard(context),
SizedBox(height: 16.h),
_buildSystemTips(),
SizedBox(height: 24),
_buildLogoutButton(), _buildLogoutButton(),
SizedBox(height: 75.h),
], ],
), ),
), ),
],
),
), ),
); );
}, },
); );
} }
/// 构建顶部的站点信息头卡片 /// 1. 顶部个人信息及统计栏
Widget _buildHeaderCard() { Widget _buildTopSection(BuildContext context) {
return Card( return Container(
elevation: 2, width: double.infinity,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), 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( child: Column(
children: [ children: [
ListTile( Row(
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40), children: [
title: Text( CircleAvatar(
controller.name, radius: 25,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
), ),
subtitle: Text(controller.address), const SizedBox(width: 12),
trailing: Container( Expanded(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), 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),
),
],
),
),
IconButton(
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
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( decoration: BoxDecoration(
color: Colors.blue[100], color: const Color(0xFFE1F5FE),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
), ),
child: Text( child: Text(
controller.selectedOperationStatus, controller.selectedOperationStatus,
style: TextStyle( style: TextStyle(
color: Colors.blue, color: Color(0xFF03A9F4),
fontWeight: FontWeight.bold, fontSize: 12.sp,
fontSize: 12, fontWeight: FontWeight.w600,
), ),
), ),
),
),
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%', '设备状态'),
],
),
),
],
),
); );
} }
/// 构建头部卡片中的单个统计项 Widget _buildStatBox(String title, String enTitle, String value, String unit) {
Widget _buildHeaderStat(String value, String label) { return Expanded(
return Column( child: Container(
children: [ padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
Text( decoration: BoxDecoration(
value, color: kBgColor,
style: const TextStyle( borderRadius: BorderRadius.circular(12),
color: Colors.blue,
fontSize: 20,
fontWeight: FontWeight.bold,
), ),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
],
);
}
/// 构建包含所有信息表单的卡片
Widget _buildInfoFormCard(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// --- 基本信息 --- Text(
_buildSectionTitle('基本信息'), title,
_buildDisplayField(label: '站点名称', value: controller.name), style: TextStyle(
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise), fontSize: 12.sp,
_buildDisplayField(label: '站点地址', value: controller.address), color: Color.fromRGBO(51, 51, 51, 0.8),
const SizedBox(height: 16), fontWeight: FontWeight.w400,
),
// --- 价格信息 --- ),
_buildSectionTitle('价格信息'), Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
// _buildDisplayField(label: '氢气价格 (元/kg)', value: controller.costPrice),
_buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice),
const SizedBox(height: 16),
// --- 运营信息 ---
_buildSectionTitle('运营信息'),
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
const SizedBox(height: 8), const SizedBox(height: 8),
//下拉选择框 Row(
DropdownButtonFormField<String>( crossAxisAlignment: CrossAxisAlignment.end,
value: controller.selectedOperationStatus, children: [
items: controller.operationStatusOptions.map((String value) { Text(
return DropdownMenuItem<String>( value,
value: value, style: TextStyle(
child: Text(value), fontSize: 12.sp,
); fontWeight: FontWeight.w500,
}).toList(), color: Color(0xFF333333),
onChanged: controller.onOperationStatusChanged,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
), ),
), ),
const SizedBox(height: 12), const SizedBox(width: 2),
_buildDisplayField(label: '营业时间', value: controller.timeStr), Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
_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)),
), ),
], ],
), ),
@@ -162,72 +198,264 @@ class ReservationPage extends GetView<ReservationController> {
); );
} }
/// 构建静态提示信息卡片 /// 2. 站点基本信息
Widget _buildTipsCard() { Widget _buildBasicInfoCard() {
return Card( return Container(
elevation: 2, padding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(
child: Padding( color: Colors.white,
padding: const EdgeInsets.all(16.0), 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( child: Column(
children: [ children: [
_buildInfoItem(Icons.info_outline, '请确保信息准确无误'), // 自定义 TabBar
const SizedBox(height: 10), Obx(
_buildInfoItem(Icons.help_outline, '价格信息将实时更新到用户端'), () => Padding(
const SizedBox(height: 10), padding: const EdgeInsets.only(left: 16, top: 16),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系技术支持: 400-021-1773'),
],
),
),
);
}
/// 构建退出登录按钮
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,
),
child: const Text(
'退出登录',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
/// 构建带标题的表单区域
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row( child: Row(
children: [ children: [
Container(width: 4, height: 16, color: Colors.blue), _buildTabTitle(0, "运营信息"),
const SizedBox(width: 8), const SizedBox(width: 30),
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), _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: [
Row(
children: [
Text(
'运营状态',
style: TextStyle(
color: Color.fromRGBO(51, 51, 51, 1),
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
//加氢站未执行的状态修改任务
if (controller.jobTipStr.isNotEmpty)
GestureDetector(
onTap: controller.showJob,
child: Row(
children: [
Text(
controller.jobTipStr,
style: TextStyle(color: Colors.yellow[800], fontSize: 14),
),
SizedBox(width: 2.w),
Icon(AntdIcon.info_circle, size: 14, color: Colors.yellow[800]),
],
),
),
],
),
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(),
),
SizedBox(height: 12.h),
if (controller.selectedOperationStatus == "营运中")
_buildDisplayField(label: '营业时间', value: controller.timeStr)
else
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInputLabel("开始时间"),
_buildDateTimePicker(
controller.customStartTimeStr,
() => controller.pickDateTime(context, true),
),
const SizedBox(height: 15),
_buildInputLabel("结束时间"),
_buildDateTimePicker(
controller.customEndTimeStr,
() => controller.pickDateTime(context, false),
),
const SizedBox(height: 15),
],
),
_buildDisplayField(label: '联系电话', value: controller.phone),
const SizedBox(height: 25),
Row(
children: [
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)),
),
),
],
),
], ],
), ),
); );
} }
/// 构建一个“标签+纯文本”的显示行
Widget _buildDisplayField({required String label, required String value}) { Widget _buildDisplayField({required String label, required String value}) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.only(bottom: 12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), const SizedBox(height: 8),
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], // 使用灰色背景以区分 color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.grey[300]!), border: Border.all(color: Colors.grey[300]!),
), ),
@@ -241,16 +469,177 @@ class ReservationPage extends GetView<ReservationController> {
); );
} }
/// 构建带图标的提示信息行 Widget _buildBroadcastForm() {
Widget _buildInfoItem(IconData icon, String text) { return Padding(
return Row( padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_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: [ children: [
Icon(icon, color: Colors.blue, size: 20),
const SizedBox(width: 10),
Expanded( Expanded(
child: Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)), 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 _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: [
Text(value, style: const TextStyle(color: Colors.black87)),
const Icon(Icons.calendar_today_outlined, size: 18, color: Color(0xFF00A870)),
],
),
),
);
}
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)),
),
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),
),
],
),
],
),
);
}
/// 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

@@ -1,21 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.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/base_model.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
enum ReservationStatus { enum ReservationStatus {
pending, // 待处理 ( addStatus: 0) pending, // 待处理 ( addStatus: 0)
completed, // 完成 ( addStatus: 1) completed, // 完成 ( addStatus: 1)
rejected, // 拒绝 ( -1) rejected, // 拒绝 ( -1)
unadded, // 未加 ( 2) unadded, // 未加 ( 2)
cancel, // 取消预约
unknown, // 未知状态 unknown, // 未知状态
} }
class ReservationModel { class ReservationModel {
final String id; final String id;
final String stationId;
final String plateNumber; final String plateNumber;
String amount; String amount;
final String time; final String time;
@@ -39,6 +43,7 @@ class ReservationModel {
ReservationModel({ ReservationModel({
required this.id, required this.id,
required this.stationId,
required this.plateNumber, required this.plateNumber,
required this.amount, required this.amount,
required this.time, required this.time,
@@ -79,6 +84,9 @@ class ReservationModel {
case 2: case 2:
currentStatus = ReservationStatus.unadded; currentStatus = ReservationStatus.unadded;
break; break;
case 6:
currentStatus = ReservationStatus.cancel;
break;
default: default:
currentStatus = ReservationStatus.unknown; currentStatus = ReservationStatus.unknown;
} }
@@ -96,6 +104,7 @@ class ReservationModel {
return ReservationModel( return ReservationModel(
// 原始字段用于UI兼容 // 原始字段用于UI兼容
id: json['id']?.toString() ?? '', id: json['id']?.toString() ?? '',
stationId: json['stationId']?.toString() ?? '',
plateNumber: json['plateNumber']?.toString() ?? '未知车牌', plateNumber: json['plateNumber']?.toString() ?? '未知车牌',
amount: '${json['hydAmount']?.toString() ?? '0'}kg', amount: '${json['hydAmount']?.toString() ?? '0'}kg',
time: timeRange, time: timeRange,
@@ -135,15 +144,26 @@ class SiteController extends GetxController with BaseControllerMixin {
Timer? _refreshTimer; Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
bool isNotice = false;
final RefreshController refreshController = RefreshController(initialRefresh: false);
@override
bool get listenLifecycleEvent => true;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
renderData(); renderData();
msgNotice();
startAutoRefresh(); startAutoRefresh();
} }
@override
void onPaused() {
stopAutoRefresh();
super.onPaused();
}
@override @override
void onClose() { void onClose() {
stopAutoRefresh(); stopAutoRefresh();
@@ -151,6 +171,27 @@ class SiteController extends GetxController with BaseControllerMixin {
super.onClose(); super.onClose();
} }
Future<void> msgNotice() async {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'isRead': 1,
'pageNum': 1,
'pageSize': 5,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
String total = result.data["total"].toString();
isNotice = int.parse(total) > 0;
updateUi();
}
}
}
void startAutoRefresh() { void startAutoRefresh() {
// 先停止已存在的定时器,防止重复启动 // 先停止已存在的定时器,防止重复启动
stopAutoRefresh(); stopAutoRefresh();
@@ -161,7 +202,9 @@ class SiteController extends GetxController with BaseControllerMixin {
}); });
} }
/// 【6. 新增】停止定时器的方法 void onRefresh() => renderData(isRefresh: true);
///停止定时器的方法
void stopAutoRefresh() { void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它 // 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel(); _refreshTimer?.cancel();
@@ -183,6 +226,7 @@ class SiteController extends GetxController with BaseControllerMixin {
'pageNum': 1, 'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条 'pageSize': 50, // 暂时不考虑分页一次获取30条
'keyword': searchText, // 加氢站名称、手机号 'keyword': searchText, // 加氢站名称、手机号
'stationId': StorageService.to.userId,
}, },
); );
@@ -191,6 +235,7 @@ class SiteController extends GetxController with BaseControllerMixin {
showToast('暂时无法获取预约数据'); showToast('暂时无法获取预约数据');
hasReservationData = false; hasReservationData = false;
reservationList = []; reservationList = [];
dismissLoading();
return; return;
} }
@@ -276,9 +321,10 @@ class SiteController extends GetxController with BaseControllerMixin {
child: TextField( child: TextField(
controller: amountController, controller: amountController,
textAlign: TextAlign.center, textAlign: TextAlign.center,
keyboardType: const TextInputType.numberWithOptions( keyboardType: TextInputType.number,
decimal: true, inputFormatters: [
), FilteringTextInputFormatter.digitsOnly, // 只允许数字输入
],
style: TextStyle( style: TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -546,7 +592,7 @@ class SiteController extends GetxController with BaseControllerMixin {
String orderTotalAmount = ""; String orderTotalAmount = "";
String orderUnfinishedAmount = ""; String orderUnfinishedAmount = "";
Future<void> renderData() async { Future<void> renderData({bool isRefresh = false}) async {
try { try {
var responseData = await HttpService.to.get( var responseData = await HttpService.to.get(
'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}', 'appointment/station/getStationInfoById?hydrogenId=${StorageService.to.userId}',
@@ -577,9 +623,12 @@ class SiteController extends GetxController with BaseControllerMixin {
} }
} catch (e) { } catch (e) {
} finally { } finally {
//加载列表数据 //加载列表数据
fetchReservationData(); fetchReservationData();
if (isRefresh) {
refreshController.refreshCompleted();
}
} }
} }
} }

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:ln_jq_app/common/styles/theme.dart';
import 'package:ln_jq_app/pages/b_page/history/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'controller.dart'; import 'controller.dart';
@@ -14,217 +18,339 @@ class SitePage extends GetView<SiteController> {
init: SiteController(), init: SiteController(),
id: 'site', id: 'site',
builder: (_) { builder: (_) {
return SingleChildScrollView(child: _buildView()); return Scaffold(
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
body: SmartRefresher(
controller: controller.refreshController,
enablePullUp: false,
onRefresh: controller.onRefresh,
child: SingleChildScrollView(child: _buildView(context)),
),
);
}, },
); );
} }
// 主视图 // 主视图
Widget _buildView() { Widget _buildView(BuildContext context) {
return Padding( return Column(
padding: const EdgeInsets.all(12.0), children: [
// 第一个卡片: 今日预约统计
_buildTopSection(context),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 第一个卡片: 今日预约统计
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: EdgeInsets.only(top: 17.h, bottom: 10.h),
child: Row( child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
const Icon(Icons.calendar_today, color: Colors.blue, size: 32),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'今日预约统计', "预约信息",
style: TextStyle( style: TextStyle(
fontSize: 16, color: Color.fromRGBO(51, 51, 51, 1),
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
fontSize: 14.sp,
), ),
), ),
Text( GestureDetector(
"Today's Reservation Statistics", onTap: () {
style: TextStyle(fontSize: 12, color: Colors.grey), Get.to(
), () => HistoryPage(),
], arguments: {'stationName': controller.name},
), );
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'实时',
style: TextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(height: 10),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.leftHydrogen, '剩余余量'),
_buildStatItem(controller.orderAmount, '预约车辆'),
_buildStatItem(controller.completedAmount, '已完成'),
],
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.orderTotalAmount, '加氢总量'),
_buildStatItem(controller.orderUnfinishedAmount, '未加氢总量'),
],
),
),
],
),
),
),
// 第二个卡片: 预约信息
Card(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
margin: const EdgeInsets.only(bottom: 12),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
Container(
color: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日预约信息',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 2),
Text(
'Reservation Information',
style: TextStyle(fontSize: 12, color: Colors.white70),
),
],
),
),
ElevatedButton.icon(
onPressed: () {
controller.renderData();
}, },
icon: const Icon(Icons.refresh, size: 16), child: Text(
label: const Text('刷新'), '历史记录',
style: ElevatedButton.styleFrom( style: TextStyle(
foregroundColor: Colors.blue, fontSize: 14.sp,
backgroundColor: Colors.white, fontWeight: FontWeight.bold,
padding: const EdgeInsets.symmetric(horizontal: 12), color: Color.fromRGBO(156, 163, 175, 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
), ),
), ),
), ),
], ],
), ),
), ),
// 第二个卡片: 预约信息
Column(
children: [
_buildSearchView(), _buildSearchView(),
controller.hasReservationData controller.hasReservationData
? _buildReservationListView() ? _buildReservationListView()
: _buildEmptyReservationView(), : _buildEmptyReservationView(),
], ],
), ),
),
SizedBox(height: 35.h),
//第三部分 //第三部分
Card( Container(
elevation: 3, padding: const EdgeInsets.all(15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), decoration: BoxDecoration(
margin: const EdgeInsets.only(bottom: 12), color: const Color(0xFFF1F9F6), // 极浅绿色背景
child: Padding( borderRadius: BorderRadius.circular(10),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), ),
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(
"数据每五分钟自动刷新,如需实时更新请下拉页面",
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,
),
),
],
),
],
),
),
],
),
),
SizedBox(height: 75.h),
],
);
}
Widget _buildTopSection(BuildContext context) {
return Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
),
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 10,
left: 20,
right: 20,
bottom: 25,
),
child: Column(
children: [
Row(
children: [
CircleAvatar(
radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
),
const SizedBox(width: 12),
Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildInfoItem(Icons.info_outline, '数据每5分钟自动刷新一次'), Row(
children: [
Text(
"今日预约统计",
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w400),
),
const SizedBox(width: 8),
],
),
const SizedBox(height: 4),
Text(
"Today's Reservation Statistics",
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
],
),
),
IconButton(
onPressed: () async {
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
const SizedBox(height: 25),
Row(
children: [
_buildStatBox("剩余氢量", "remaining quantity", controller.leftHydrogen, "kg"),
SizedBox(width: 4.w),
_buildStatBox(
"今日加氢量",
"Have been added",
controller.orderTotalAmount,
"kg",
),
SizedBox(width: 4.w),
_buildStatBox(
"未加氢总量",
"No quantity added",
controller.orderUnfinishedAmount,
"kg",
),
],
),
],
),
);
}
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: Color(0xFFF5F7F9),
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), const SizedBox(height: 8),
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'), 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)),
], ],
), ),
),
),
], ],
), ),
),
); );
} }
//搜索输入框,提示可以输入车牌或者手机 //搜索输入框,提示可以输入车牌或者手机
Widget _buildSearchView() { Widget _buildSearchView() {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: EdgeInsets.fromLTRB(0, 0, 0, 8.h),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 44, height: 42.h,
child: TextField( child: TextField(
textAlignVertical: TextAlignVertical.center,
controller: controller.searchController, // 绑定控制器 controller: controller.searchController, // 绑定控制器
decoration: InputDecoration( decoration: InputDecoration(
hintText: '输入车牌号或完整手机号查询', filled: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16), isDense: true,
fillColor: Colors.white,
hintText: '输入车牌号或手机号搜索...',
contentPadding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Get.theme.primaryColor, width: 1.5), borderSide: BorderSide(
color: Color.fromRGBO(229, 231, 235, 1),
width: 1.5,
),
), ),
// 清除按钮 // 清除按钮
suffixIcon: IconButton( suffix: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.clear, size: 20), icon: const Icon(Icons.clear, size: 20),
onPressed: () { onPressed: () {
controller.searchController.clear(); controller.searchController.clear();
controller.fetchReservationData(); // 清除后也刷新一次 controller.fetchReservationData(); // 清除后也刷新一次
}, },
), ),
GestureDetector(
onTap: () {
// 点击“搜索”按钮时触发
FocusScope.of(Get.context!).unfocus(); // 收起键盘
controller.fetchReservationData();
},
child: Container(
padding: EdgeInsets.only(
left: 16.w,
right: 16.h,
top: 4.5.h,
bottom: 4.5.h,
),
decoration: BoxDecoration(
color: Color.fromRGBO(1, 113, 55, 1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"搜索",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 12.sp,
),
),
),
),
],
),
), ),
onSubmitted: (value) { onSubmitted: (value) {
// 用户在键盘上点击“完成”或“搜索”时触发 // 用户在键盘上点击“完成”或“搜索”时触发
@@ -233,40 +359,6 @@ class SitePage extends GetView<SiteController> {
), ),
), ),
), ),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
// 点击“搜索”按钮时触发
FocusScope.of(Get.context!).unfocus(); // 收起键盘
controller.fetchReservationData();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(8),
),
child: const Icon(Icons.search_rounded),
),
],
),
);
}
/// 构建单个统计项
Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: valueColor,
),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
], ],
), ),
); );
@@ -281,14 +373,17 @@ class SitePage extends GetView<SiteController> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.inventory_2_outlined, size: 80, color: Colors.grey[300]), LoginUtil.getAssImg("ic_no_data@2x"),
const SizedBox(height: 16), SizedBox(height: 16.h),
const Text('暂无预约数据', style: TextStyle(fontSize: 16, color: Colors.black54)), Text(
const SizedBox(height: 8), '暂无订单',
const Text( style: TextStyle(
'点击右上角刷新按钮获取最新数据', fontSize: 16.sp,
style: TextStyle(fontSize: 14, color: Colors.grey), color: Colors.black54,
fontWeight: FontWeight.w600,
), ),
),
const SizedBox(height: 8),
], ],
), ),
); );
@@ -296,134 +391,295 @@ class SitePage extends GetView<SiteController> {
/// 构建“有预约数据”的列表视图 /// 构建“有预约数据”的列表视图
Widget _buildReservationListView() { Widget _buildReservationListView() {
return Container( return ListView.separated(
color: Colors.white,
child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
// 因为外层已有滚动,这里禁用内部滚动 // 因为外层已有滚动,这里禁用内部滚动
itemCount: controller.reservationList.length, itemCount: controller.reservationList.length,
padding: const EdgeInsets.all(12.0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = controller.reservationList[index]; final item = controller.reservationList[index];
// 调用新的方法来构建每一项 // 调用新的方法来构建每一项
return _buildReservationItem(index, item); return _buildReservationItem(index, item);
}, },
separatorBuilder: (context, index) => const SizedBox(height: 12), // 列表项之间的间距 separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
),
); );
} }
Widget _buildReservationItem(int index, ReservationModel item) { Widget _buildReservationItem(int index, ReservationModel item) {
const kPrimaryGreen = Color(0xFF006D35); // 深绿
const kLineColor = Color(0xFFE0E6ED); // 线条颜色
return IntrinsicHeight(
// 保证左侧竖线能跟右侧内容高度对齐
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. 左侧样式区域(竖线 + 圆点)
SizedBox(
width: 20,
child: Column(
children: [
// 顶部的装饰圆点
Container(
margin: EdgeInsets.only(top: 3.w),
width: 8,
height: 8,
decoration: BoxDecoration(
color: kPrimaryGreen.withOpacity(0.5),
shape: BoxShape.circle,
),
),
// 延伸的竖线
Expanded(child: Container(width: 1, color: kLineColor)),
],
),
),
// 2. 右侧内容区域(时间 + 卡片数据)
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 10.w, bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 时间显示
Text(
item.time, // 格式如 "09:00 - 10:00"
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 10),
// 数据卡片
_buildInfoCard(item),
],
),
),
),
],
),
);
}
/// 右侧具体数据卡片
Widget _buildInfoCard(ReservationModel item) {
return Container( return Container(
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[200]!, width: 1.0), border: Border.all(color: const Color(0xFFF0F0F0)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withOpacity(0.1), color: Colors.black.withOpacity(0.04),
spreadRadius: 1, blurRadius: 10,
blurRadius: 3, offset: const Offset(0, 4),
offset: const Offset(0, 2),
), ),
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 顶部:序号、车牌号、状态 // 车牌与状态标签
Padding( Row(
padding: const EdgeInsets.fromLTRB(8, 12, 16, 12),
child: Row(
children: [ children: [
// 序号 Text(
Container( item.plateNumber,
width: 24, style: TextStyle(
height: 24, fontSize: 14.sp,
alignment: Alignment.center, fontWeight: FontWeight.bold,
decoration: const BoxDecoration( color: Color(0xFF333333),
color: Colors.blue, ),
shape: BoxShape.circle, ),
const SizedBox(width: 8),
_buildStatusTag(item.status),
Spacer(),
Text(
"预约量:${item.amount}",
style: TextStyle(
color: Color(0xFF00A870),
fontSize: 13.sp,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
// 联系信息
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"${item.contactPerson} | ${item.contactPhone}",
style: TextStyle(
color: Color(0xFF999999),
fontSize: 13.sp,
fontWeight: FontWeight.w400,
),
),
],
),
//操作按钮(仅在待处理状态显示)
if (item.status == ReservationStatus.pending) ...[
const SizedBox(height: 15),
const Divider(height: 1, color: Color(0xFFF5F5F5)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildSmallButton(
"拒绝",
isOutline: true,
onTap: () {
controller.rejectReservation(item.id);
},
),
const SizedBox(width: 12),
_buildSmallButton(
"确认",
isOutline: false,
onTap: () {
controller.confirmReservation(item.id);
},
),
],
),
],
],
),
);
}
/// 通用小按钮
Widget _buildSmallButton(
String text, {
required bool isOutline,
required VoidCallback onTap,
}) {
const kPrimaryGreen = Color(0xFF006D35);
const kDangerRed = Color(0xFFFF7D7D);
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
color: isOutline ? Colors.white : kPrimaryGreen,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: isOutline ? kDangerRed : kPrimaryGreen),
), ),
child: Text( child: Text(
"${index + 1}", text,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: isOutline ? kDangerRed : Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
);
}
/// 状态标签构建(带圆角和浅色背景)
Widget _buildStatusTag(ReservationStatus status) {
Color textColor;
Color bgColor;
String text;
switch (status) {
case ReservationStatus.pending:
text = "待加氢";
textColor = const Color(0xFFFE9E62); // 橘色
bgColor = const Color(0xFFFFF2E9);
break;
case ReservationStatus.completed: // 假设已加氢状态
text = "已加氢";
textColor = const Color(0xFF00A870); // 绿色
bgColor = const Color(0xFFE6F7F1);
break;
case ReservationStatus.rejected:
text = "拒绝加氢";
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
case ReservationStatus.unadded:
text = '未加氢';
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
case ReservationStatus.cancel:
text = '已取消';
textColor = const Color(0xFFFF7D7D); // 红色
bgColor = const Color(0xFFFFEEEE);
break;
default:
text = "未知";
textColor = Colors.grey;
bgColor = Colors.grey[100]!;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.3)),
),
child: Text(
text,
style: TextStyle(color: textColor, fontSize: 11, fontWeight: FontWeight.bold),
),
);
}
/// 右侧操作按钮(拒绝/确认)
Widget _buildActionButtons(ReservationModel item) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 拒绝按钮(空心)
GestureDetector(
onTap: () => controller.rejectReservation(item.id),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFF7D7D)),
),
child: const Text(
"拒绝",
style: TextStyle(
color: Color(0xFFFF7D7D),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(Icons.directions_car, color: Colors.black54), // 确认按钮(实心深绿)
const SizedBox(width: 4), GestureDetector(
// 车牌号 onTap: () => controller.confirmReservation(item.id),
Expanded( child: Container(
child: Text( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
item.plateNumber, decoration: BoxDecoration(
style: const TextStyle( color: const Color(0xFF006D35),
fontSize: 16, borderRadius: BorderRadius.circular(10),
),
child: const Text(
"确认",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppTheme.themeColor,
), ),
), ),
), ),
// 状态标签
_buildStatusChip(item.status),
],
),
),
const Divider(height: 1),
// 中部:详细信息
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Column(
children: [
_buildDetailRow(
Icons.local_gas_station,
'加氢量',
item.amount,
valueColor: Colors.red,
),
const SizedBox(height: 8),
_buildDetailRow(Icons.access_time, '预约时间', item.time),
const SizedBox(height: 8),
_buildDetailRow(Icons.person, '联系人', item.contactPerson),
const SizedBox(height: 8),
_buildDetailRow(
Icons.phone,
'联系电话',
item.contactPhone,
valueColor: AppTheme.themeColor,
),
],
),
),
// 底部:操作按钮 (仅在待处理时显示)
if (item.status == ReservationStatus.pending)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => controller.confirmReservation(item.id),
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
child: const Text('确认'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => controller.rejectReservation(item.id),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('拒绝'),
),
), ),
], ],
),
),
],
),
); );
} }
@@ -448,6 +704,10 @@ class SitePage extends GetView<SiteController> {
text = '未加氢'; text = '未加氢';
color = Colors.red; color = Colors.red;
break; break;
case ReservationStatus.cancel:
text = '已取消';
color = Colors.red;
break;
default: default:
text = '未知状态'; text = '未知状态';
color = Colors.grey; color = Colors.grey;

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; 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: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/car_info/view.dart';
import 'package:ln_jq_app/pages/c_page/map/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/mine/view.dart';
@@ -12,6 +13,7 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
BaseWidgetsPage({super.key}); BaseWidgetsPage({super.key});
final PageController _pageController = PageController(); final PageController _pageController = PageController();
// 主视图 // 主视图
Widget _buildView() { Widget _buildView() {
return PageView( return PageView(
@@ -20,54 +22,68 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
onPageChanged: (index) { onPageChanged: (index) {
jumpTabAndPage(index); jumpTabAndPage(index);
}, },
children: _buildPages(), // 页面的列表 children: _buildPages(),
); );
} }
void jumpTabAndPage(int index) { void jumpTabAndPage(int index) {
controller.pageIndex = index; // 更新页面索引 controller.pageIndex = index;
controller.updateUi(); // 更新 UI controller.updateUi();
_pageController.jumpToPage(controller.pageIndex); _pageController.jumpToPage(controller.pageIndex);
} }
// 对应的页面
List<Widget> _buildPages() { List<Widget> _buildPages() {
return [ return [ReservationPage(), MapPage(), CarInfoPage(), MinePage()];
MapPage(),
ReservationPage(),
CarInfoPage(),
MinePage(),
];
} }
//导航栏 // 自定义导航栏 (悬浮胶囊样式)
Widget _buildNavigationBar() { Widget _buildNavigationBar() {
return NavigationX( return SafeArea(
currentIndex: controller.pageIndex, // 当前选中的tab索引 child: Container(
onTap: (index) { height: 50.h,
jumpTabAndPage(index); margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
}, // 切换tab事件 decoration: BoxDecoration(
items: [ color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
NavigationItemModel( borderRadius: BorderRadius.circular(30),
label: '地图', boxShadow: [
icon: AntdIcon.location, BoxShadow(
selectedIcon: AntdIcon.location_fill, color: Colors.black.withOpacity(0.05),
), blurRadius: 10,
NavigationItemModel( offset: const Offset(0, 5),
label: '加氢预约',
icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill,
),
NavigationItemModel(
label: '车辆信息',
icon: AntdIcon.car,
selectedIcon: AntdIcon.car_fill,
),
NavigationItemModel(
label: '我的',
icon: AntdIcon.user,
selectedIcon: AntdIcon.user,
), ),
], ],
),
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_car_select@2x", "ic_car@2x"),
_buildNavItem(3, "ic_user_select@2x", "ic_user@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: const EdgeInsets.symmetric(horizontal: 20, 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),),
),
); );
} }
@@ -78,10 +94,10 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
id: 'baseWidgets', id: 'baseWidgets',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
extendBody: false, extendBody: true, // 重要:让 body 延伸到导航栏后面
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
bottomNavigationBar: _buildNavigationBar(), bottomNavigationBar: _buildNavigationBar(),
body: SafeArea(child: _buildView()), body: _buildView(), // 移除 SafeArea 以获得更好的全屏沉浸感
); );
}, },
); );

View File

@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(AttachmentViewerController()); Get.put(AttachmentViewerController());
final fileName = controller.url.split('/').last; // final fileName = controller.url.split('/').last;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
fileName, "证件详情",
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -40,11 +40,10 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
if (controller.fileType == 'pdf') { if (controller.fileType == 'pdf') {
if (controller.localFilePath.isNotEmpty) { if (controller.localFilePath.isNotEmpty) {
return PDFView( return PDFView(
key: ValueKey(controller.localFilePath.value),
filePath: controller.localFilePath.value, filePath: controller.localFilePath.value,
enableSwipe: true, enableSwipe: true,
swipeHorizontal: false, swipeHorizontal: true,
autoSpacing: false, autoSpacing: true,
pageFling: true, pageFling: true,
onRender: (pages) { onRender: (pages) {
print("PDF rendered with $pages pages."); print("PDF rendered with $pages pages.");

View File

@@ -1,36 +1,87 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:path_provider/path_provider.dart';
import 'attachment_viewer_page.dart'; import 'attachment_viewer_page.dart';
class CertificateViewerController extends GetxController { class CertificateViewerController extends GetxController with BaseControllerMixin {
late final String title; late final String title;
late final List<String> attachments; late final List<String> attachments;
// --- 新增: 状态管理 ---
/// 用于存储网络PDF的本地路径key是网络urlvalue是本地路径
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
/// 用于跟踪每个附件的加载状态key是网络url
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
@override
String get builderId => "certificateviewer";
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 从 Get.to 的 arguments 中获取标题和附件列表
title = Get.arguments['title'] ?? '证件详情'; title = Get.arguments['title'] ?? '证件详情';
attachments = List<String>.from(Get.arguments['attachments'] ?? []); attachments = List<String>.from(Get.arguments['attachments'] ?? []);
// --- 新增: 初始化时开始加载所有PDF ---
_loadAllPdfs();
} }
/// 导航到通用的附件查看器页面 /// 遍历所有附件如果是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;
}
}
/// 导航到通用的附件查看器页面 (此方法保持不变)
void openAttachment(String url) { void openAttachment(String url) {
if (url.isEmpty) { if (url.isEmpty) {
showErrorToast('附件链接无效'); showErrorToast('附件链接无效');
return; return;
} }
Get.to( Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
() => const AttachmentViewerPage(),
arguments: {
'url': url,
},
);
} }
/// 检查 URL 是否为 PDF,以便在视图中显示不同的图标 /// 检查 URL 是否为 PDF (此方法保持不变)
bool isPdf(String url) { bool isPdf(String url) {
return url.toLowerCase().endsWith('.pdf'); return url.toLowerCase().endsWith('.pdf');
} }

View File

@@ -1,51 +1,114 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart'; // 引入PDFView
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/common/common.dart';
import 'certificate_viewer_controller.dart'; import 'certificate_viewer_controller.dart';
class CertificateViewerPage extends GetView<CertificateViewerController> { class CertificateViewerPage extends GetView<CertificateViewerController> {
const CertificateViewerPage({Key? key}) : super(key: key); const CertificateViewerPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.put(CertificateViewerController()); return GetBuilder<CertificateViewerController>(
init: CertificateViewerController(),
id: 'certificateviewer',
builder: (_) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(controller.title)),
title: Text(controller.title), body: Column(
centerTitle: true, children: [
SizedBox(height: 16,),
Text(
"点击可查看大图",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
body: ListView.builder( Expanded(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), child: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: controller.attachments.length, itemCount: controller.attachments.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final url = controller.attachments[index]; final url = controller.attachments[index];
// 从 URL 中提取文件名用于显示 return _buildAttachmentItem(url);
final fileName = url.split('/').last; },
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: Icon(
controller.isPdf(url)
? Icons.picture_as_pdf_rounded // PDF 图标
: Icons.image_rounded, // 图片图标
color: controller.isPdf(url) ? Colors.red.shade700 : Colors.blue.shade700,
size: 32,
), ),
title: Text(
fileName,
style: const TextStyle(fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: Colors.grey), ],
onTap: () => controller.openAttachment(url),
), ),
); );
}, },
);
}
/// 构建单个附件的显示项
Widget _buildAttachmentItem(String url) {
return GestureDetector(
onTap: () {
controller.openAttachment(url);
}, // 点击跳转到详情页
child: Card(
margin: const EdgeInsets.only(bottom: 16.0),
clipBehavior: Clip.antiAlias, // 确保内容不会溢出Card的圆角
elevation: 4,
// 等比缩放展示
child: 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(
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加载中指示器
Widget _buildLoadingIndicator() {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
// 辅助Widget错误指示器
Widget _buildErrorIndicator() {
return const SizedBox(
height: 200,
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
);
}
} }

View File

@@ -2,10 +2,12 @@ import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.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/base_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.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/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:path_provider/path_provider.dart';
import 'certificate_viewer_page.dart'; import 'certificate_viewer_page.dart';
import 'dart:io';
class CarInfoController extends GetxController with BaseControllerMixin { class CarInfoController extends GetxController with BaseControllerMixin {
@override @override
@@ -22,17 +24,48 @@ class CarInfoController extends GetxController with BaseControllerMixin {
final RxList<String> operationAttachments = <String>[].obs; final RxList<String> operationAttachments = <String>[].obs;
final RxList<String> hydrogenationAttachments = <String>[].obs; final RxList<String> hydrogenationAttachments = <String>[].obs;
final RxList<String> registerAttachments = <String>[].obs; final RxList<String> registerAttachments = <String>[].obs;
String color = "";
String hydrogenCapacity = "";
String rentFromCompany = "";
String address = "";
bool isNotice = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
getUserBindCarInfo(); 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 @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示 // 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) { if (!StorageService.to.hasShownBindVehicleDialog &&
StorageService.to.isLoggedIn &&
StorageService.to.loginChannel == LoginChannel.driver &&
!StorageService.to.hasVehicleInfo) {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog( DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆', title: '当前尚未绑定车辆',
@@ -88,8 +121,25 @@ class CarInfoController extends GetxController with BaseControllerMixin {
// 将解析出的 URL 列表赋值给对应的 RxList // 将解析出的 URL 列表赋值给对应的 RxList
drivingAttachments.assignAll(parseAttachments(data['drivingAttachment'])); drivingAttachments.assignAll(parseAttachments(data['drivingAttachment']));
operationAttachments.assignAll(parseAttachments(data['operationAttachment'])); operationAttachments.assignAll(parseAttachments(data['operationAttachment']));
hydrogenationAttachments.assignAll(parseAttachments(data['hydrogenationAttachment'])); hydrogenationAttachments.assignAll(
parseAttachments(data['hydrogenationAttachment']),
);
registerAttachments.assignAll(parseAttachments(data['registerAttachment'])); 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(); updateUi();
@@ -108,10 +158,72 @@ class CarInfoController extends GetxController with BaseControllerMixin {
} }
Get.to( Get.to(
() => const CertificateViewerPage(), () => const CertificateViewerPage(),
arguments: { arguments: {'title': title, 'attachments': attachments},
'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/material.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.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/qr_code/view.dart'; import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:photo_view/photo_view.dart';
import '../../../common/styles/theme.dart';
import 'controller.dart'; import 'controller.dart';
class CarInfoPage extends GetView<CarInfoController> { class CarInfoPage extends GetView<CarInfoController> {
@@ -16,78 +20,259 @@ class CarInfoPage extends GetView<CarInfoController> {
id: 'car_info', id: 'car_info',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100], backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Column(
padding: const EdgeInsets.all(12.0), children: [
_buildUserInfoCard(),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildDriverInfoCard(), const SizedBox(height: 16),
const SizedBox(height: 5), _buildCarInfoCard(),
_buildCarBindingCard(), _buildCertificatesCard(context),
const SizedBox(height: 5), const SizedBox(height: 12),
_buildCertificatesCard(), _buildSafetyReminderCard(),
const SizedBox(height: 5), SizedBox(height: 95.h),
_buildTipsCard(),
], ],
), ),
), ),
],
),
), ),
); );
}, },
); );
} }
/// 构建顶部的司机信息卡片 Widget _buildUserInfoCard() {
Widget _buildDriverInfoCard() {
return Card( return Card(
elevation: 2, elevation: 1,
color: Colors.white,
margin: EdgeInsets.zero, 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( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 50),
child: Row( child: Row(
children: [ children: [
const CircleAvatar( Stack(
radius: 20, children: [
backgroundColor: Colors.blue, CircleAvatar(
child: Icon(Icons.person, color: Colors.white, size: 34), radius: 25,
backgroundColor: Colors.white,
child: LoginUtil.getAssImg('ic_user_logo@2x'),
), ),
const SizedBox(width: 12), Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
SizedBox(width: 8.w),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Text( Text(
"${StorageService.to.name}", "${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
), ),
const SizedBox(height: 4), ),
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( Text(
"${StorageService.to.phone}", "绿色先锋",
style: TextStyle(color: Colors.grey, fontSize: 11), style: TextStyle(
color: Color(0xFF52C41A),
fontSize: 10,
),
), ),
], ],
), ),
), ),
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),
), ),
child: const Row( const SizedBox(height: 4),
children: [
Icon(Icons.shield_outlined, color: Colors.blue, size: 14),
SizedBox(width: 4),
Text( Text(
'已认证', "羚牛ID${StorageService.to.phone}",
style: TextStyle( style: const TextStyle(color: Colors.grey, fontSize: 11),
color: Colors.blue, ),
],
),
),
IconButton(
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
}
},
style: IconButton.styleFrom(
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.all(8),
),
icon: Badge(
smallSize: 8,
backgroundColor: controller.isNotice
? Colors.red
: Colors.transparent,
child: const Icon(
Icons.notifications_outlined,
color: Colors.black87,
size: 30,
),
),
),
],
),
),
Padding(
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
const SizedBox(width: 8),
_buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
const SizedBox(width: 8),
_buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
],
),
),
],
),
);
}
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 _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, color: Colors.grey)),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isPlate)
GestureDetector(
onTap: () => controller.doQrCode(),
child: Container(
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
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(
children: [
const Icon(
Icons.sync,
size: 12,
color: Color.fromRGBO(71, 174, 208, 1),
),
const SizedBox(width: 4),
Text(
StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定",
style: const TextStyle(
color: Color.fromRGBO(71, 174, 208, 1),
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -95,227 +280,275 @@ class CarInfoPage extends GetView<CarInfoController> {
], ],
), ),
), ),
],
), ),
), Text(
const Divider(height: 1, indent: 16, endIndent: 16), value,
Padding( style: const TextStyle(
padding: const EdgeInsets.symmetric(vertical: 16.0), fontSize: 14,
child: Row( fontWeight: FontWeight.w600,
mainAxisAlignment: MainAxisAlignment.spaceAround, color: Colors.black87,
children: [
_buildStatItem('156', '服务天数'),
_buildStatItem('4.9', '评分'),
_buildStatItem('98%', '准时率'),
],
), ),
), ),
], ],
), ),
],
); );
} }
// 司机信息卡片中的小统计项 Widget _buildH2LevelProgress() {
Widget _buildStatItem(String value, String label) {
return Column( return Column(
children: [ children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: const LinearProgressIndicator(
value: 0.75,
minHeight: 8,
backgroundColor: Color(0xFFF0F2F5),
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
),
),
const SizedBox(height: 8),
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
Text( Text(
value, "75%",
style: const TextStyle( style: TextStyle(
fontSize: 18, fontSize: 11,
color: Color.fromRGBO(16, 185, 129, 1),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
const SizedBox(height: 4), ],
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)), ),
], ],
); );
} }
/// 构建车辆绑定信息卡片 /// 3. 构建车辆证件卡片 (重构为 TabView 样式)
Widget _buildCarBindingCard() { Widget _buildCertificatesCard(BuildContext context) {
return Card( return DefaultTabController(
elevation: 2, length: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
child: Column( child: Column(
children: [ children: [
_buildInfoRow('车牌号: ${controller.plateNumber}', '扫码绑定'), TabBar(
const SizedBox(height: 12), isScrollable: false,
_buildInfoRow('车架号:', '${controller.vin}'), indicatorColor: Color.fromRGBO(16, 185, 129, 1),
const SizedBox(height: 12), labelColor: Color.fromRGBO(16, 185, 129, 1),
_buildInfoRow('车辆型号:', '${controller.modelName}'), unselectedLabelColor: Colors.grey,
const SizedBox(height: 12), labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
_buildInfoRow('车辆品牌:', '${controller.brandName}'), 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),
], ],
), ),
), ),
const SizedBox(width: 8),
Icon(Icons.propane_rounded, size: 50, color: Colors.blue.withOpacity(0.5)),
], ],
), ),
),
); );
} }
// 车辆绑定卡片中的信息行 /// 构建单个证件的展示内容
Widget _buildInfoRow(String label, String value) { Widget _buildCertificateContent(String title, RxList<String> attachments) {
bool isButton = value == '扫码绑定'; return Obx(() {
return Row( 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: [ children: [
Text(label, style: const TextStyle(fontSize: 13)), //证件文字
const SizedBox(width: 8), Row(
isButton mainAxisAlignment: MainAxisAlignment.spaceBetween,
? GestureDetector( children: [
onTap: () async { _buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
controller.doQrCode(); _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( child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w), height: 184.h,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.blue.shade300, width: 1), borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(5), border: Border.all(color: Color.fromRGBO(226, 232, 240, 1)),
color: Colors.blue.withOpacity(0.05), color: Color.fromRGBO(248, 250, 252, 1),
), ),
child: Row( child: Center(child: _buildAttachmentPreview(attachments[0])),
mainAxisSize: MainAxisSize.min, // Keep the row compact
children: [
Icon(
StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search,
size: 13,
color: Colors.blue,
), ),
const SizedBox(width: 3),
Text(
StorageService.to.hasVehicleInfo ? "换车牌" : value,
style: const TextStyle(
color: Colors.blue,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
),
),
)
: Text(
value,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
],
);
}
/// 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 _buildCertDetailItem(
Widget _buildCertificateRow({ String label,
required IconData icon, String value, {
required String title, Color? valueColor,
required List<String> attachments, bool isFull = false,
}) { }) {
return ListTile( return Container(
contentPadding: EdgeInsets.zero, width: isFull ? null : 140,
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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTipItem(Icons.info_outline, '请确保车辆证件齐全有效'), Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 10), const SizedBox(height: 4),
_buildTipItem(Icons.rule, '定期检查车辆状态和证件有效期'), Text(
const SizedBox(height: 10), value,
_buildTipItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'), style: TextStyle(
], fontSize: 13,
fontWeight: FontWeight.bold,
color: valueColor ?? Colors.black87,
), ),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
), ),
); );
} }
// 提示信息卡片中的列表项 /// 附件预览组件 (智能判断 PDF 或图片)
Widget _buildTipItem(IconData icon, String text) { Widget _buildAttachmentPreview(String url) {
return Row( 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: [ children: [
Icon(icon, color: Colors.blue, size: 20), Row(
const SizedBox(width: 10), children: [
Expanded( Icon(Icons.info_outline, color: AppTheme.themeColor, size: 24),
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)), 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,153 @@
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'model.dart';
class MessageController extends GetxController {
final RefreshController refreshController = RefreshController(initialRefresh: false);
final RxList<MessageModel> messageList = <MessageModel>[].obs;
int _pageNum = 1;
final int _pageSize = 20;
// 是否所有已读状态
final RxBool allRead = false.obs;
@override
void onInit() {
super.onInit();
_loadData(isRefresh: true);
}
void onRefresh() => _loadData(isRefresh: true);
void onLoading() => _loadData(isRefresh: false);
Future<void> _loadData({bool isRefresh = false}) async {
final int targetPage = isRefresh ? 1 : _pageNum + 1;
try {
final Map<String, dynamic> requestData = {
'appFlag': 1,
'pageNum': targetPage,
'pageSize': _pageSize,
};
final response = await HttpService.to.get(
'appointment/unread_notice/page',
params: requestData,
);
if (response == null) {
throw Exception("Response is null");
}
final result = BaseModel.fromJson(response.data);
if (result.code != 0) {
throw Exception(result.message ?? "Business error");
}
final recordsList = result.data?["records"] as List? ?? [];
final List<MessageModel> newData = recordsList.map((e) {
return MessageModel(
id: e['id']?.toString() ?? '',
title: e['noticeTitle'] ?? '',
content: e['noticeContent'] ?? '',
createTime: e['createTime'] ?? '',
isRead: int.tryParse(e['isRead']?.toString() ?? '0') ?? 0,
);
}).toList();
if (isRefresh) {
messageList.assignAll(newData);
_pageNum = 1; // 只有成功了才重置页码
} else {
messageList.addAll(newData);
if (newData.isNotEmpty) {
_pageNum++; // 只有成功且有数据才+1
}
}
_updateRefreshState(isRefresh, newData.length);
// 检查是否全部已读
_checkAllRead();
} catch (e) {
Logger.d("加载失败: $e"); // 建议加上日志
if (isRefresh) {
refreshController.refreshFailed();
} else {
refreshController.loadFailed();
}
}
}
/// 处理 RefreshController 状态
void _updateRefreshState(bool isRefresh, int count) {
if (isRefresh) {
refreshController.refreshCompleted();
// 如果刷新后的数据量小于 pageSize说明没有更多数据了
if (count < _pageSize) {
refreshController.loadNoData();
} else {
// 必须重置 footer 状态,否则之前如果是 NoData 状态无法再加载
refreshController.resetNoData();
}
} else {
if (count == 0) {
refreshController.loadNoData();
} else {
refreshController.loadComplete();
}
}
}
// 标记全部已读
void markAllRead() async {
showLoading('正在标记...');
try {
final response = await HttpService.to.post('appointment/unread_notice/read/all');
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0) {
for (var msg in messageList) {
msg.isRead = 0;
}
messageList.refresh();
allRead.value = true;
dismissLoading();
showSuccessToast('全部已读');
}
}
} catch (e) {
dismissLoading();
showErrorToast('操作失败');
}
}
// 标记单条已读(弹窗显示时调用)
void markRead(MessageModel message) async {
if (message.isRead == 0) return;
final response = await HttpService.to.post(
'/appointment/unread_notice/read/${message.id}',
);
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0) {
message.isRead = 0;
messageList.refresh();
_checkAllRead();
}
}
}
void _checkAllRead() {
allRead.value = messageList.every((msg) => msg.isRead == 0);
}
}

View File

@@ -0,0 +1,25 @@
class MessageModel {
final String id;
final String title;
final String content;
final String createTime;
int isRead; // 0: 已读, 1: 未读
MessageModel({
required this.id,
required this.title,
required this.content,
required this.createTime,
required this.isRead,
});
factory MessageModel.fromJson(Map<String, dynamic> json) {
return MessageModel(
id: json['id']?.toString() ?? '',
title: json['title']?.toString() ?? '消息通知',
content: json['content']?.toString() ?? '',
createTime: json['createTime']?.toString() ?? '',
isRead: json['isRead'] as int? ?? 0,
);
}
}

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'controller.dart';
import 'model.dart';
class MessagePage extends GetView<MessageController> {
const MessagePage({super.key});
@override
Widget build(BuildContext context) {
Get.put(MessageController());
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(title: const Text('消息通知'), centerTitle: true),
body: Column(
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(() => !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)),
),
)
: const SizedBox.shrink()),
],
),
);
}
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,
),
),
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]),
),
],
),
),
],
),
),
);
}
void _showMessageDialog(BuildContext context, MessageModel item) {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Text(
item.content,
style: const TextStyle(fontSize: 15, height: 1.5, color: Colors.black87),
),
const SizedBox(height: 24),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
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),
),
child: const Text('确认', style: TextStyle(color: Colors.blue)),
),
),
],
),
),
);
},
);
}
}

View File

@@ -16,7 +16,6 @@ class MineController extends GetxController with BaseControllerMixin {
void onInit() { void onInit() {
super.onInit(); super.onInit();
renderData(); renderData();
renderViolation();
} }
@override @override
@@ -31,13 +30,15 @@ class MineController extends GetxController with BaseControllerMixin {
} }
String rate = ""; String rate = "";
String rating = "";
String accident = ""; String accident = "";
String historyBreakRules = ""; String historyBreakRules = "";
String vin = ""; String vin = "";
String plateNumber = ""; String plateNumber = "";
String violationTotal = "0"; String violationTotal = "0";//违章总数
String violationScore = "0"; String violationScore = "0";//扣分总数
String violationDispose = "0"; String violationDispose = "0";//已处理
bool isNotice = false;
void renderData() async { void renderData() async {
if (StorageService.to.hasVehicleInfo) { if (StorageService.to.hasVehicleInfo) {
@@ -49,14 +50,17 @@ class MineController extends GetxController with BaseControllerMixin {
plateNumber = bean.plateNumber; plateNumber = bean.plateNumber;
} }
showLoading("加载中");
try { try {
showLoading("加载中");
await Future.wait([ await Future.wait([
_fetchCompletionRate(), // 请求1完成率 _fetchCompletionRate(), // 请求1完成率
_fetchAccidentCount(), // 请求2事故数 _fetchAccidentCount(), // 请求2事故数
_fetchBreakRulesCount(), // 请求3违章数 _fetchBreakRulesCount(), // 请求3违章数
_rating(), // 司机评分
msgNotice(), // 红点消息
]); ]);
await renderViolation();
} catch (e, stackTrace) { } catch (e, stackTrace) {
showErrorToast("加载数据失败,请稍后重试 $e"); showErrorToast("加载数据失败,请稍后重试 $e");
} finally { } finally {
@@ -65,6 +69,38 @@ class MineController 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();
}
}
}
Future<void> _rating() async {
final response = await HttpService.to.get('appointment/rating/driver');
if (response != null) {
final result = BaseModel.fromJson(response.data);
if (result.code == 0 && result.data != null) {
rating = result.data.toString();
}
}
}
Future<void> _fetchCompletionRate() async { Future<void> _fetchCompletionRate() async {
final response = await HttpService.to.get( final response = await HttpService.to.get(
'appointment/orderAddHyd/driverAppointmentCompletionRate?phone=${StorageService.to.phone}', 'appointment/orderAddHyd/driverAppointmentCompletionRate?phone=${StorageService.to.phone}',
@@ -104,15 +140,11 @@ class MineController extends GetxController with BaseControllerMixin {
} }
} }
void renderViolation() async { Future<void> renderViolation() async {
// 违章信息查询 // 违章信息查询
final originalHeaders = Map<String, dynamic>.from(HttpService.to.dio.options.headers);
try { try {
HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url);
HttpService.to.dio.options.headers['appId'] = '97ad10eeb6b346f79e0d6ffd81e4d3c3';
var responseData = await HttpService.to.get( var responseData = await HttpService.to.get(
"vehicleService/violation/queryViolationInfo_V2?plateNum=${plateNumber}", "appointment/truck/queryViolationInfo_V2?plateNum=${plateNumber}",
); );
if (responseData == null || responseData.data == null) { if (responseData == null || responseData.data == null) {
@@ -139,8 +171,6 @@ class MineController extends GetxController with BaseControllerMixin {
} }
} catch (e) { } catch (e) {
} finally { } finally {
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.options.headers = originalHeaders;
updateUi(); updateUi();
} }
} }

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:ln_jq_app/common/styles/theme.dart'; import 'package:getx_scaffold/common/index.dart';
import 'package:getx_scaffold/common/widgets/index.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:ln_jq_app/storage_service.dart';
import 'controller.dart'; import 'controller.dart';
@@ -14,87 +18,112 @@ class MinePage extends GetView<MineController> {
id: 'mine', id: 'mine',
builder: (_) { builder: (_) {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100], backgroundColor: const Color.fromRGBO(247, 249, 251, 1),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildUserInfoCard(), _buildUserInfoCard(),
const SizedBox(height: 5), const SizedBox(height: 8),
_buildDriverScoreCard(), // 新 UI 模块开始
const SizedBox(height: 5), Padding(
_buildMonthlyRecordCard(), padding: const EdgeInsets.symmetric(horizontal: 20.0),
const SizedBox(height: 5), child: Column(
_buildTipsCard(), children: [
const SizedBox(height: 20), _buildWalletCard(),
SizedBox(height: 16.h),
_buildGridMenu(),
SizedBox(height: 16.h),
_buildRecommendCard(context),
SizedBox(height: 8.h),
_buildSafetyReminderCard(),
SizedBox(height: 24.h),
_buildLogoutButton(), _buildLogoutButton(),
const SizedBox(height: 20), SizedBox(height: 95.h),
], ],
), ),
), ),
// 新 UI 模块结束
],
),
), ),
); );
}, },
); );
} }
/// 1. 构建顶部用户信息卡片 /// 构建顶部用户信息卡片
Widget _buildUserInfoCard() { Widget _buildUserInfoCard() {
return Card( return Card(
elevation: 2, elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white,
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40),
// 增加了顶部 padding 适配状态栏
child: Row( child: Row(
children: [ children: [
const CircleAvatar( Stack(
children: [
CircleAvatar(
radius: 25, radius: 25,
backgroundColor: Colors.blue, backgroundColor: Colors.white,
child: Icon(Icons.person, color: Colors.white, size: 40), child: LoginUtil.getAssImg('ic_user_logo@2x'),
), ),
const SizedBox(width: 16), Positioned(
right: 0,
bottom: 0,
child: SizedBox(
height: 16.h,
width: 16.w,
child: LoginUtil.getAssImg('ic_logo@2x'),
),
),
],
),
SizedBox(width: 8.w),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Text( Text(
"${StorageService.to.name}", "${StorageService.to.name}",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
), fontSize: 14,
const SizedBox(height: 4), fontWeight: FontWeight.bold,
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),
),
],
), ),
), ),
SizedBox(width: 8.w),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue[50], color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景
border: Border.all(color: const Color(0xFFB7E19F)), // 边框
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue, width: 0.5),
), ),
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.shield_outlined, color: Colors.blue, size: 14), Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
// 叶子图标
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
'已认证', "绿色先锋",
style: TextStyle( style: TextStyle(
color: Colors.blue, color: Color(0xFF52C41A),
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold,
), ),
), ),
], ],
@@ -102,231 +131,404 @@ class MinePage extends GetView<MineController> {
), ),
], ],
), ),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.violationTotal, '违章总数'),
_buildStatItem(controller.violationScore, '扣分总数'),
_buildStatItem(controller.violationDispose, '已处理'),
],
),
),
],
),
);
}
// 用户信息卡片中的小统计项
Widget _buildStatItem(String value, String label) {
return Column(
children: [
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)), Text(
"羚牛ID${StorageService.to.phone}",
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
], ],
); ),
),
IconButton(
onPressed: () async{
var scanResult = await Get.to(() => const MessagePage());
if (scanResult == null) {
controller.msgNotice();
} }
/// 2. 构建驾驶得分卡片 },
Widget _buildDriverScoreCard() { style: IconButton.styleFrom(
return Card( backgroundColor: Colors.grey[100],
elevation: 2, padding: const EdgeInsets.all(8),
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 Text('本月表现', style: TextStyle(fontSize: 12, color: Colors.grey)), icon: Badge(
const SizedBox(height: 20), smallSize: 8,
Center( backgroundColor: controller.isNotice
child: SizedBox( ? Colors.red
width: 100, : Colors.transparent,
height: 100, child: const Icon(
child: Stack( Icons.notifications_outlined,
fit: StackFit.expand, color: Colors.black87,
children: [ size: 30,
CircularProgressIndicator(
value: 10, // 假设得分80%
strokeWidth: 8,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
Center(
child: Text(
'10',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue,
), ),
), ),
), ),
], ],
), ),
), ),
),
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(
padding: const EdgeInsets.only(top: 12.0), padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( _buildModernStatItem('服务天数', 'service days', '156', ''),
'优秀驾驶员', const SizedBox(width: 8),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), _buildModernStatItem('准时率', 'Punctuality', controller.rate, ''),
), const SizedBox(width: 8),
const Spacer(), _buildModernStatItem('司机评分', 'Driver rating', controller.rating, ''),
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( Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
IconData icon, return Expanded(
String title, child: Container(
String subtitle, padding: const EdgeInsets.all(12.0),
bool isCompleted, decoration: BoxDecoration(
) { color: const Color(0xFFF8F9FA),
return Padding( borderRadius: BorderRadius.circular(12),
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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
title, title,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)), ),
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)),
], ],
), ),
),
if (isCompleted) const Icon(Icons.check_circle, color: Colors.blue),
], ],
), ),
),
); );
} }
/// 3. 构建本月记录卡片 /// 我的钱包卡片
Widget _buildMonthlyRecordCard() { Widget _buildWalletCard() {
return Card( return Card(
elevation: 2, elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'本月记录', "我的钱包",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 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,
), ),
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}"),
], ],
), ),
), ),
); );
} }
// 本月记录中的列表项 /// 2x2 功能网格菜单
Widget _buildRecordRow(IconData icon, String title, String value) { Widget _buildGridMenu() {
return ListTile( return Column(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
radius: 20,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Icon(icon, color: Colors.blue, size: 24),
),
title: Text(title, style: const TextStyle(fontSize: 14)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(value, style: const TextStyle(color: AppTheme.themeColor, fontSize: 14)), Row(
children: [
_buildGridItem(Icons.person_search_outlined, "客服评价", "3项可评"),
const SizedBox(width: 19),
_buildGridItem(
Icons.assignment_late_outlined,
"违章处理",
"${controller.historyBreakRules}项待办",
countColor: Colors.red,
),
], ],
), ),
onTap: () { const SizedBox(height: 16),
// TODO: 处理点击事件 Row(
}, children: [
_buildGridItem(Icons.book_outlined, "安全培训", "0个待看"),
const SizedBox(width: 19),
_buildGridItem(
Icons.verified_user_outlined,
"诚信加氢值",
"845",
isSpecial: true,
backgroundColor: const Color(0xFF006633),
),
],
),
],
); );
} }
/// 4. 构建提示信息卡片 Widget _buildGridItem(
Widget _buildTipsCard() { 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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 12,
color: isSpecial
? Colors.white.withOpacity(0.8)
: (countColor ?? Colors.grey),
),
),
],
),
),
);
}
/// 我要推荐卡片
Widget _buildRecommendCard(BuildContext context) {
return Card( return Card(
elevation: 2, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
children: [ children: [
_buildInfoItem(Icons.info_outline, '保持良好的驾驶习惯,提高安全评分'), const Row(
const SizedBox(height: 10), mainAxisAlignment: MainAxisAlignment.spaceBetween,
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'), children: [
const SizedBox(height: 10), Column(
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'), crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"我要推荐",
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
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: 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),
),
),
], ],
), ),
), ),
); );
} }
// 提示信息卡片中的列表项 /// Android 端下载二维码弹窗
Widget _buildInfoItem(IconData icon, String text) { void _showAndroidDownloadDialog(BuildContext context) {
return Row( Get.dialog(
Center(
child: Container(
width: 280.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(icon, color: Colors.blue, size: 20), Padding(
const SizedBox(width: 10), padding: const EdgeInsets.all(24.0),
Expanded( child: Column(
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)), 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),
),
),
],
),
),
),
);
}
/// 安全提醒卡片
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("");
}
// 如果加载完成且有数据
if (snapshot.hasData) {
return TextX.labelSmall(
"当前版本: ${snapshot.data}",
color: Colors.green[800],
);
}
// 错误处理
return const Text("");
},
),
),
],
),
],
),
); );
} }
@@ -336,7 +538,7 @@ class MinePage extends GetView<MineController> {
controller.logout(); controller.logout();
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[400], backgroundColor: Color.fromRGBO(204, 52, 46, 1),
foregroundColor: Colors.white, foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48), minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

View File

@@ -2,21 +2,16 @@ import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/common/common.dart';
import 'package:getx_scaffold/common/services/http.dart';
import 'package:getx_scaffold/getx_scaffold.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/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/station_model.dart'; import 'package:ln_jq_app/common/model/station_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart'; import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; import 'package:ln_jq_app/pages/b_page/site/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';
import 'package:ln_jq_app/pages/qr_code/view.dart'; import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import '../../../common/styles/theme.dart'; import '../../../common/styles/theme.dart';
import 'reservation_list_bottomsheet.dart';
/// Helper class for managing time slots /// Helper class for managing time slots
class TimeSlot { class TimeSlot {
@@ -38,12 +33,14 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'reservation'; String get builderId => 'reservation';
C_ReservationController();
final DateTime _now = DateTime.now(); final DateTime _now = DateTime.now();
// 计算当前时间属于哪个小时区间 // 计算当前时间属于哪个1小时区间
late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now); late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now);
late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime( late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime(
_getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 30)), _getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 60)),
); );
late final Rx<DateTime> selectedDate = DateTime(_now.year, _now.month, _now.day).obs; late final Rx<DateTime> selectedDate = DateTime(_now.year, _now.month, _now.day).obs;
late final Rx<TimeOfDay> startTime = _initialStartTime.obs; late final Rx<TimeOfDay> startTime = _initialStartTime.obs;
@@ -51,13 +48,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
/// 静态辅助方法,用于计算初始的开始时间 /// 静态辅助方法,用于计算初始的开始时间
static TimeOfDay _calculateInitialStartTime(DateTime now) { static TimeOfDay _calculateInitialStartTime(DateTime now) {
if (now.minute < 30) {
// 如果当前分钟小于30则开始时间为当前小时的0分
return TimeOfDay(hour: now.hour, minute: 0); return TimeOfDay(hour: now.hour, minute: 0);
} else {
// 如果当前分钟大于等于30则开始时间为当前小时的30分
return TimeOfDay(hour: now.hour, minute: 30);
}
} }
/// 静态辅助方法将TimeOfDay转换为DateTime /// 静态辅助方法将TimeOfDay转换为DateTime
@@ -185,7 +176,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
} }
} }
///30 分钟为间隔 时间选择器 ///60 分钟为间隔 时间选择器
void pickTime(BuildContext context) { void pickTime(BuildContext context) {
final now = DateTime.now(); final now = DateTime.now();
final isToday = final isToday =
@@ -194,21 +185,15 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
selectedDate.value.day == now.day; selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = []; final List<TimeSlot> availableSlots = [];
for (int i = 0; i < 48; i++) { for (int i = 0; i < 24; i++) {
final startMinutes = i * 30; // 每次增加 60 分钟
final endMinutes = startMinutes + 30; final startMinutes = i * 60;
final endMinutes = startMinutes + 60;
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60); final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
// 注意endMinutes % 60 始终为 0因为间隔是整小时
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60); final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
final slotStartDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
startTime.hour,
startTime.minute,
);
// 如果不是今天,所有时间段都有效 // 如果不是今天,所有时间段都有效
if (!isToday) { if (!isToday) {
availableSlots.add(TimeSlot(startTime, endTime)); availableSlots.add(TimeSlot(startTime, endTime));
@@ -223,8 +208,16 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
endTime.minute, 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 (slotEndDateTime.isAfter(now)) { if (realEndDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(startTime, endTime)); availableSlots.add(TimeSlot(startTime, endTime));
} }
} }
@@ -235,13 +228,11 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
return; return;
} }
// 查找当前选中的时间对应的新列表中的索引
int initialItem = availableSlots.indexWhere( int initialItem = availableSlots.indexWhere(
(slot) => (slot) => slot.start.hour == startTime.value.hour,
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
); );
if (initialItem == -1) { if (initialItem == -1) {
initialItem = 0; initialItem = 0;
} }
@@ -346,14 +337,14 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
final startTimeStr = final startTimeStr =
'$dateStr ${_formatTimeOfDay(startTime.value)}:00'; // Use helper directly '$dateStr ${_formatTimeOfDay(startTime.value)}:00'; // Use helper directly
if (lastSuccessfulReservation != null && /*if (lastSuccessfulReservation != null &&
lastSuccessfulReservation!.id == selectedStationId.value && lastSuccessfulReservation!.id == selectedStationId.value &&
lastSuccessfulReservation!.startTime == startTimeStr) { lastSuccessfulReservation!.startTime == startTimeStr) {
showToast("请勿重复提交相同时间段的预约,可在“查看预约”中修改"); showToast("请勿重复提交相同时间段的预约,可在“查看预约”中修改");
return; return;
} }*/
final reservationEndDateTime = DateTime( DateTime reservationEndDateTime = DateTime(
selectedDate.value.year, selectedDate.value.year,
selectedDate.value.month, selectedDate.value.month,
selectedDate.value.day, selectedDate.value.day,
@@ -361,7 +352,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
endTime.value.minute, 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( if (reservationEndDateTime.isBefore(
DateTime.now().subtract(const Duration(minutes: 1)), DateTime.now().subtract(const Duration(minutes: 1)),
)) { )) {
@@ -374,10 +371,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
(s) => s.hydrogenId == selectedStationId.value, (s) => s.hydrogenId == selectedStationId.value,
); );
if (selectedStation.siteStatusName != "营运中") { /*if (selectedStation.siteStatusName != "营运中") {
showToast("该站点${selectedStation.siteStatusName},暂无法预约"); showToast("该站点${selectedStation.siteStatusName},暂无法预约");
return; return;
} }*/
showLoading("提交中"); showLoading("提交中");
@@ -398,20 +395,21 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
'hydAmount': ampuntStr, 'hydAmount': ampuntStr,
}, },
); );
var result = BaseModel.fromJson(responseData?.data);
if (responseData == null) { if (responseData == null || result.code != 0) {
dismissLoading(); dismissLoading();
showToast('服务暂不可用,请稍后'); showToast(result.error);
return; return;
} }
dismissLoading(); dismissLoading();
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) { if (result.code == 0) {
showSuccessToast("预约成功"); showSuccessToast("预约成功");
lastSuccessfulReservation = ReservationModel( lastSuccessfulReservation = ReservationModel(
id: selectedStationId.value!, id: selectedStationId.value!,
stationId: '',
hydAmount: ampuntStr, hydAmount: ampuntStr,
startTime: startTimeStr, startTime: startTimeStr,
endTime: endTimeStr, endTime: endTimeStr,
@@ -434,28 +432,29 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
//打开预约列表 //打开预约列表
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
getReservationList(); getReservationList(showPopup: true, addStatus: '');
}); });
} else { } else {
showToast(result.error); showToast(result.error);
} }
} catch (e) { } catch (e) {
dismissLoading(); dismissLoading();
showToast('服务暂不可用,请稍后');
} }
} }
/// 状态变量:是否有预约数据 /// 状态变量:是否有预约数据
bool hasReservationData = false; final RxBool hasReservationData = false.obs;
// 新增预约数据列表 // 新增预约数据列表
List<ReservationModel> reservationList = []; final RxList<ReservationModel> reservationList = <ReservationModel>[].obs;
final RxBool shouldShowReservationList = false.obs;
// --- 用于防抖的 Timer --- // --- 用于防抖的 Timer ---
Timer? _debounce; Timer? _debounce;
//查看预约列表 //查看预约列表
void getReservationList() async { void getReservationList({bool showPopup = false, String? addStatus}) async {
// 增加 addStatus 参数
if (_debounce?.isActive ?? false) { if (_debounce?.isActive ?? false) {
return; return;
} }
@@ -464,19 +463,25 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
showLoading("加载中"); showLoading("加载中");
try { try {
final Map<String, dynamic> requestData = {
'phone': StorageService.to.phone,
'pageNum': 1,
'pageSize': 50,
};
// 将 addStatus 参数添加到请求中
if (addStatus != null && addStatus.isNotEmpty) {
requestData['addStatus'] = addStatus;
}
var response = await HttpService.to.post( var response = await HttpService.to.post(
"appointment/orderAddHyd/driverOrderPage", "appointment/orderAddHyd/driverOrderPage",
data: { data: requestData,
'phone': StorageService.to.phone, // 使用从 renderData 中获取到的 name
'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条
},
); );
if (response == null || response.data == null) { if (response == null || response.data == null) {
showToast('暂时无法获取预约数据'); showToast('暂时无法获取预约数据');
hasReservationData = false; hasReservationData.value = false;
reservationList = []; reservationList.clear();
return; return;
} }
@@ -485,13 +490,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (baseModel.code == 0 && baseModel.data != null) { if (baseModel.code == 0 && baseModel.data != null) {
final dataMap = baseModel.data as Map<String, dynamic>; final dataMap = baseModel.data as Map<String, dynamic>;
final List<dynamic> listFromServer = dataMap['records'] ?? []; final List<dynamic> listFromServer = dataMap['records'] ?? [];
reservationList = listFromServer.map((item) {
// 使用 .value 来更新响应式列表
reservationList.value = listFromServer.map((item) {
return ReservationModel.fromJson(item as Map<String, dynamic>); return ReservationModel.fromJson(item as Map<String, dynamic>);
}).toList(); }).toList();
// 根据列表是否为空来更新 hasReservationData 状态 // 更新 hasEdit 状态
hasReservationData = reservationList.isNotEmpty;
for (var reservation in reservationList) { for (var reservation in reservationList) {
try { try {
// 获取当前时间和预约的结束时间 // 获取当前时间和预约的结束时间
@@ -510,205 +515,25 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
reservation.hasEdit = false; reservation.hasEdit = false;
} }
} }
hasReservationData.value = reservationList.isNotEmpty;
if (showPopup) {
shouldShowReservationList.value = true;
}
} else { } else {
showToast(baseModel.message); showToast(baseModel.message);
hasReservationData = false; hasReservationData.value = false;
reservationList = []; // 清空列表 reservationList.clear();
} }
} catch (e) { } catch (e) {
Logger.d("${e.toString()}");
showToast('获取预约数据失败'); showToast('获取预约数据失败');
hasReservationData = false; hasReservationData.value = false;
reservationList = []; // 清空列表 reservationList.clear();
} finally { } finally {
dismissLoading(); dismissLoading();
} }
Get.bottomSheet(
Container(
height: Get.height * 0.55,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
//标题
Container(
padding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'我的预约',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
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)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: !hasReservationData
? Container(
margin: EdgeInsets.only(top: 40),
child: TextX.bodyLarge('暂无预约', weight: FontWeight.w500),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: reservationList.length,
itemBuilder: (context, index) {
final ReservationModel reservation = reservationList[index];
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: const Color(
0xFFE6F7FF,
), // Light blue background
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: const Color(0xFF91D5FF),
), // Blue border
),
child: Text(
reservation.stateName +
"-" +
reservation.addStatusName,
style: const TextStyle(
color: Color(0xFF1890FF),
fontWeight: FontWeight.bold,
),
),
),
!reservation.hasEdit
? SizedBox()
: GestureDetector(
onTap: () async {
var result = await Get.to(
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': difference,
},
binding: BindingsBuilder(() {
Get.put(ReservationEditController());
}),
preventDuplicates: false,
);
if (result == true) {
Get.back();
getReservationList();
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(
0xFFFFF7E6,
), // Light orange background
borderRadius: BorderRadius.circular(12),
),
child: Text(
"修改",
style: const TextStyle(
color: Color(0xFFFA8C16),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
_buildDetailRow('联系人:', reservation.contacts),
_buildDetailRow('联系电话:', reservation.phone),
reservation.addStatus == "5"
? _buildDetailRow('拒绝原因:', reservation.rejectReason)
: SizedBox(),
],
),
),
);
},
),
),
],
),
),
isScrollControlled: true,
backgroundColor:
Colors.transparent, // Make background transparent to see the rounded corners
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 85,
child: Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
);
} }
String workEfficiency = "0"; String workEfficiency = "0";
@@ -719,6 +544,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
String leftHydrogen = "0"; String leftHydrogen = "0";
num maxHydrogen = 0; num maxHydrogen = 0;
String difference = ""; String difference = "";
var progressValue = 0.0;
//用来管理查看预约的弹窗
Worker? _sheetWorker;
bool init = false;
Timer? _refreshTimer;
@override @override
bool get listenLifecycleEvent => true; bool get listenLifecycleEvent => true;
@@ -728,6 +559,68 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
super.onInit(); super.onInit();
getUserBindCarInfo(); getUserBindCarInfo();
getSiteList(); getSiteList();
startAutoRefresh();
msgNotice();
if (!init) {
_setupListener();
init = true;
}
}
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() {
// 先停止已存在的定时器,防止重复启动
stopAutoRefresh();
// 创建一个每1分钟执行一次的周期性定时器
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
getSiteList();
});
}
///停止定时器的方法
void stopAutoRefresh() {
// 如果定时器存在并且是激活状态,就取消它
_refreshTimer?.cancel();
_refreshTimer = null; // 置为null方便判断
}
void _setupListener() {
_sheetWorker = ever(shouldShowReservationList, (bool shouldShow) {
if (shouldShow) {
Get.bottomSheet(
const ReservationListBottomSheet(),
isScrollControlled: true, // 允许弹窗使用更多屏幕高度
backgroundColor: Colors.transparent,
);
// 重要:显示后立即将信号重置为 false防止不必要的重复弹出
shouldShowReservationList.value = false;
}
});
} }
void getUserBindCarInfo() { void getUserBindCarInfo() {
@@ -739,7 +632,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
plateNumber = bean.plateNumber; plateNumber = bean.plateNumber;
vin = bean.vin; vin = bean.vin;
plateNumberController = TextEditingController(text: plateNumber); plateNumberController = TextEditingController(text: plateNumber);
maxHydrogen = bean.maxHydrogen; maxHydrogen = num.tryParse(bean.maxHydrogen) ?? 0;
getCatinfo(); getCatinfo();
getJqinfo(); getJqinfo();
} }
@@ -766,8 +659,10 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
fillingWeight = final value = double.tryParse(result.data["fillingWeight"]?.toString() ?? '0') ?? 0;
"${result.data["fillingWeight"]}${result.data["fillingWeightUnit"]}"; final String formatted = value.toStringAsFixed(2);
fillingWeight = "$formatted${result.data["fillingWeightUnit"]}";
fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}"; fillingTimes = "${result.data["fillingTimes"]}${result.data["fillingTimesUnit"]}";
updateUi(); updateUi();
@@ -779,9 +674,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
void getCatinfo() async { void getCatinfo() async {
try { try {
HttpService.to.setBaseUrl(AppTheme.car_service_url);
var responseData = await HttpService.to.post( var responseData = await HttpService.to.post(
'VehicleData/getHydrogenInfoByPlateNumber', 'appointment/vehicle/getHydrogenInfoByPlateNumber',
data: { data: {
'userName': "xll@lingniu", 'userName': "xll@lingniu",
'password': "4q%3!l6s0p", 'password': "4q%3!l6s0p",
@@ -801,29 +695,69 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0; final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2); difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
updateUi(); int flooredDifference = (maxHydrogen - leftHydrogenNum).floor();
} catch (e) { if (flooredDifference > 0) {
} finally { amountController.text = flooredDifference.toString();
HttpService.to.setBaseUrl(AppTheme.test_service_url);
} }
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) {}
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 { void getSiteList() async {
showLoading("加载中"); if (StorageService.to.phone == "13344444444") {
final originalHeaders = Map<String, dynamic>.from(HttpService.to.dio.options.headers); //该账号给stationOptions手动添加一个数据
try { final testStation = StationModel(
HttpService.to.setBaseUrl(AppTheme.jiaqing_service_url); hydrogenId: '1142167389150920704',
HttpService.to.dio.options.headers['appId'] = '97ad10eeb6b346f79e0d6ffd81e4d3c3'; name: '演示加氢站',
address: '上海市嘉定区于田南路111号于田大厦',
price: '35.00',
// 价格
siteStatusName: '营运中',
// 状态
isSelect: 1, // 默认可选
);
// 使用 assignAll 可以确保列表只包含这个测试数据
stationOptions.assignAll([testStation]);
var responseData = await HttpService.to.get("hydrogen/queryHydrogenSiteInfo"); if (stationOptions.isNotEmpty) {
selectedStationId.value = stationOptions.first.hydrogenId;
}
return;
}
try {
showLoading("加氢站数据加载中");
var responseData = await HttpService.to.get(
"appointment/station/queryHydrogenSiteInfo",
);
if (responseData == null || responseData.data == null) { if (responseData == null || responseData.data == null) {
showToast('暂时无法获取站点信息'); showToast('暂时无法获取站点信息');
dismissLoading(); dismissLoading();
return; return;
} }
try {
dismissLoading(); dismissLoading();
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
var stationDataList = result.data['data'] as List; var stationDataList = result.data['data'] as List;
@@ -847,19 +781,30 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
if (stationOptions.isEmpty) { if (stationOptions.isEmpty) {
showToast('附近暂无可用加氢站'); showToast('附近暂无可用加氢站');
} else { } else {
showToast('站点列表已刷新'); // showToast('站点列表已刷新');
}
// 找到第一个可选的站点作为默认值
if (stationOptions.isNotEmpty) {
final firstSelectable = stationOptions.firstWhere(
(station) => station.isSelect == 1,
orElse: () => stationOptions.first, // 降级:如果没有可选的,就用第一个
);
selectedStationId.value = firstSelectable.hydrogenId;
} else {
// 如果列表为空,确保 selectedStationId 也为空
selectedStationId.value = null;
} }
} catch (e) { } catch (e) {
dismissLoading();
showToast('数据异常'); showToast('数据异常');
}
} catch (e) {
} finally { } finally {
dismissLoading(); dismissLoading();
HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.options.headers = originalHeaders;
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示 // 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog && StorageService.to.isLoggedIn) { if (!StorageService.to.hasShownBindVehicleDialog &&
StorageService.to.isLoggedIn &&
StorageService.to.loginChannel == LoginChannel.driver &&
!StorageService.to.hasVehicleInfo) {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog( DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆', title: '当前尚未绑定车辆',
@@ -884,11 +829,13 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
@override @override
void onClose() { void onClose() {
super.onClose();
amountController.dispose(); amountController.dispose();
plateNumberController.dispose(); plateNumberController.dispose();
if (_debounce != null) { if (_debounce != null) {
_debounce?.cancel(); _debounce?.cancel();
} }
super.onClose(); _sheetWorker?.dispose();
stopAutoRefresh();
} }
} }

View File

@@ -0,0 +1,284 @@
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/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';
//加氢预约列表
class ReservationListBottomSheet extends StatefulWidget {
const ReservationListBottomSheet({super.key});
@override
State<ReservationListBottomSheet> createState() => _ReservationListBottomSheetState();
}
class _ReservationListBottomSheetState extends State<ReservationListBottomSheet> {
final C_ReservationController _controller = Get.find<C_ReservationController>();
final Map<String, String> _statusOptions = {
'': '所有状态', // 增加一个“所有状态”的选项
'0': '待加氢',
'1': '加氢完成',
'2': '未加氢',
'5': '拒绝加氢',
};
String _selectedStatus = ''; // 默认选中 '0' (待加氢)
@override
void initState() {
super.initState();
// Widget 初始化时,立即调用接口加载默认状态(待加氢)的数据
_controller.getReservationList(addStatus: _selectedStatus);
}
@override
Widget build(BuildContext context) {
return Container(
height: Get.height * 0.55,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
// 构建标题和下拉框
_buildHeader(),
const Divider(height: 1),
// 下拉筛选框
_buildChoice(),
// 构建列表(使用 Obx 监听数据变化)
_buildList(),
],
),
);
}
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),
),
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);
}
},
),
),
);
}
/// 构建标题、关闭按钮和下拉筛选框
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)),
),
),
],
),
);
}
/// 构建预约列表
Widget _buildList() {
return Expanded(
child: Obx(() {
if (!_controller.hasReservationData.value) {
return Container(
margin: const EdgeInsets.only(top: 40),
child: TextX.bodyLarge('暂无该状态下的预约', weight: FontWeight.w500),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _controller.reservationList.length,
itemBuilder: (context, index) {
final reservation = _controller.reservationList[index];
return Card(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 12.0),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 状态标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.blue.shade50, // 淡蓝色背景
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,
),
),
),
Spacer(),
//取消预约
if (reservation.hasEdit)
SizedBox(
height: 28, // 限制按钮高度,显得精致
child: OutlinedButton(
onPressed: () async{
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/vehicle-cancel',
data: {
'id': reservation.id,
},
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
dismissLoading();
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("已取消");
_controller.getReservationList(
addStatus: _selectedStatus,
);
} else {
showToast(result.error);
}
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
side: BorderSide(color: Colors.grey.shade400), // 灰色边框
shape: const StadiumBorder(), // 胶囊形状
),
child: Text(
'取消预约',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
),
),
SizedBox(width: 10.w,),
// 修改按钮 (仅在 hasEdit 为 true 时显示)
if (reservation.hasEdit)
SizedBox(
height: 28,
child: OutlinedButton(
onPressed: () async{
var result = await Get.to(
() => ReservationEditPage(),
arguments: {
'reservation': reservation,
'difference': _controller.difference,
},
binding: BindingsBuilder(() {
Get.put(ReservationEditController());
}),
preventDuplicates: false,
);
if (result == true) {
_controller.getReservationList(
addStatus: _selectedStatus,
);
}
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12),
side: const BorderSide(color: Colors.blue), // 蓝色边框
shape: const StadiumBorder(),
backgroundColor: Colors.white,
),
child: const Text(
'修改',
style: TextStyle(color: Colors.blue, fontSize: 12),
),
),),
],
),
const SizedBox(height: 12),
_buildDetailRow('车牌号:', reservation.plateNumber),
_buildDetailRow('预约日期:', reservation.date),
_buildDetailRow('预约氢量:', reservation.hydAmount),
_buildDetailRow('加氢站:', reservation.stationName),
_buildDetailRow('开始时间:', reservation.startTime),
_buildDetailRow('结束时间:', reservation.endTime),
_buildDetailRow('联系人:', reservation.contacts),
_buildDetailRow('联系电话:', reservation.phone),
reservation.addStatus == "5"
? _buildDetailRow('拒绝原因:', reservation.rejectReason)
: SizedBox(),
],
),
),
);
},
);
}),
);
}
/// 构建详情行 (这是一个辅助Widget)
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Text(label, style: const TextStyle(color: Colors.grey)),
const SizedBox(width: 8),
Expanded(
child: Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -78,28 +78,41 @@ class ReservationEditController extends GetxController with BaseControllerMixin
selectedDate.value.day == now.day; selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = []; final List<TimeSlot> availableSlots = [];
for (int i = 0; i < 48; i++) { for (int i = 0; i < 24; i++) {
final startMinutes = i * 30; // 每次增加 60 分钟
final endMinutes = startMinutes + 30; final startMinutes = i * 60;
final slotStartTime = TimeOfDay( final endMinutes = startMinutes + 60;
hour: startMinutes ~/ 60,
minute: startMinutes % 60,
);
final slotEndTime = TimeOfDay(
hour: (endMinutes ~/ 60) % 24,
minute: endMinutes % 60,
);
final slotStartDateTime = DateTime( 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.year,
selectedDate.value.month, selectedDate.value.month,
selectedDate.value.day, selectedDate.value.day,
slotStartTime.hour, endTime.hour,
slotStartTime.minute, endTime.minute,
); );
if (!isToday || slotStartDateTime.isAfter(now)) { // 注意:如果是跨天的 00:00 (例如 23:00 - 00:00),需要将日期加一天,否则 isAfter 判断会出错
availableSlots.add(TimeSlot(slotStartTime, slotEndTime)); // 但由于我们用的是 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));
}
} }
} }
@@ -108,14 +121,14 @@ class ReservationEditController extends GetxController with BaseControllerMixin
return; return;
} }
// 查找当前选中的时间对应的新列表中的索引
int initialItem = availableSlots.indexWhere( int initialItem = availableSlots.indexWhere(
(slot) => (slot) => slot.start.hour == startTime.value.hour,
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
); );
if (initialItem == -1) initialItem = 0;
if (initialItem == -1) {
initialItem = 0;
}
TimeSlot tempSlot = availableSlots[initialItem]; TimeSlot tempSlot = availableSlots[initialItem];
@@ -214,6 +227,7 @@ class ReservationEditController extends GetxController with BaseControllerMixin
'appointment/orderAddHyd/saveOrUpdate', 'appointment/orderAddHyd/saveOrUpdate',
data: { data: {
'id': reservation.id, 'id': reservation.id,
'stationId': reservation.stationId,
'startTime': startTimeStr, 'startTime': startTimeStr,
'endTime': endTimeStr, 'endTime': endTimeStr,
'hydAmount': amountStr, 'hydAmount': amountStr,

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

@@ -1,7 +1,13 @@
import 'dart:io';
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.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/b_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/message/view.dart';
import 'package:ln_jq_app/pages/login/view.dart'; import 'package:ln_jq_app/pages/login/view.dart';
import '../../storage_service.dart'; import '../../storage_service.dart';
@@ -12,14 +18,19 @@ class HomeController extends GetxController with BaseControllerMixin {
HomeController(); HomeController();
final _aliyunPush = AliyunPushFlutter();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
initAliyunPush();
addPushCallback();
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
} }
// 根据登录状态和登录渠道返回不同的首页 // 根据登录状态和登录渠道返回不同的首页
Widget getHomePage() { Widget getHomePage() {
requestPermission();
//登录状态跳转 //登录状态跳转
if (StorageService.to.isLoggedIn) { if (StorageService.to.isLoggedIn) {
// 如果已登录,再判断是哪个渠道 // 如果已登录,再判断是哪个渠道
@@ -35,4 +46,133 @@ class HomeController extends GetxController with BaseControllerMixin {
return LoginPage(); return LoginPage();
} }
} }
void requestPermission() async {
PermissionStatus status = await Permission.notification.status;
if (status.isGranted) {
Logger.d("通知权限已开启");
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 (Platform.isIOS) {
await _setupIOSConfig();
} else if (Platform.isAndroid) {
await _setupAndroidConfig();
}
} catch (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']}');
}
}
/// Android 专属配置
Future<void> _setupAndroidConfig() async {
await _aliyunPush.setNotificationInGroup(true);
final res = await _aliyunPush.createAndroidChannel(
"xll_push_android",
'新消息通知',
4,
'用于接收加氢站实时状态提醒',
);
if (res['code'] == kAliyunPushSuccessCode) {
Logger.d('Android 通知通道创建成功');
} else {
Logger.d('Android 通道创建失败: ${res['code']} - ${res['errorMsg']}');
}
}
void addPushCallback() {
_aliyunPush.addMessageReceiver(
onNotification: _onNotification,
onNotificationOpened: _onNotificationOpened,
onNotificationRemoved: _onNotificationRemoved,
onMessage: _onMessage,
onAndroidNotificationReceivedInApp: _onAndroidNotificationReceivedInApp,
onAndroidNotificationClickedWithNoAction: _onAndroidNotificationClickedWithNoAction,
onIOSChannelOpened: _onIOSChannelOpened,
onIOSRegisterDeviceTokenSuccess: _onIOSRegisterDeviceTokenSuccess,
onIOSRegisterDeviceTokenFailed: _onIOSRegisterDeviceTokenFailed,
);
}
Future<void> _onAndroidNotificationClickedWithNoAction(
Map<dynamic, dynamic> message,
) async {
Logger.d('onAndroidNotificationClickedWithNoAction ====> $message');
}
Future<void> _onAndroidNotificationReceivedInApp(Map<dynamic, dynamic> message) async {
Logger.d('onAndroidNotificationReceivedInApp ====> $message');
}
Future<void> _onMessage(Map<dynamic, dynamic> message) async {
Logger.d('onMessage ====> $message');
}
Future<void> _onNotification(Map<dynamic, dynamic> message) async {
Logger.d('onNotification ====> $message');
}
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> _onIOSChannelOpened(Map<dynamic, dynamic> message) async {
Logger.d('onIOSChannelOpened ====> $message');
}
Future<void> _onIOSRegisterDeviceTokenSuccess(Map<dynamic, dynamic> message) async {
Logger.d('onIOSRegisterDeviceTokenSuccess ====> $message');
}
Future<void> _onIOSRegisterDeviceTokenFailed(Map<dynamic, dynamic> message) async {
Logger.d('onIOSRegisterDeviceTokenFailed====> $message');
}
} }

View File

@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
class LoginController extends GetxController with BaseControllerMixin { class LoginController extends GetxController with BaseControllerMixin {
@override @override
@@ -7,19 +10,73 @@ class LoginController extends GetxController with BaseControllerMixin {
LoginController(); LoginController();
// 控制输入框的 TextEditingController // 控制输入框的 TextEditingController
final TextEditingController driverIdentityController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final TextEditingController codeController = TextEditingController();
final TextEditingController stationIdController = TextEditingController(); final TextEditingController stationIdController = TextEditingController();
final TextEditingController passwordController = 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 @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
} }
@override @override
void onClose() { void onClose() {
_timer?.cancel();
phoneController.dispose();
codeController.dispose();
stationIdController.dispose();
passwordController.dispose();
super.onClose(); super.onClose();
} }
} }

View File

@@ -1,11 +1,20 @@
import 'dart:io';
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart'; import 'package:ln_jq_app/common/login_util.dart';
import 'package:ln_jq_app/common/model/base_model.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/common/styles/theme.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/b_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart'; import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
import 'package:ln_jq_app/pages/common/webview/view.dart';
import 'package:ln_jq_app/pages/login/controller.dart'; import 'package:ln_jq_app/pages/login/controller.dart';
import 'package:ln_jq_app/pages/url_host/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
@@ -16,350 +25,636 @@ class LoginPage extends StatefulWidget {
} }
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin { class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
late TabController tabController; late TabController _tabController;
bool _isAgreed = false;
bool cLogin = true;
bool _obscureText = true; bool _obscureText = true;
// 用于管理“记住密码”的复选框状态
bool _rememberPassword = true; bool _rememberPassword = true;
// 用于确保凭证只在首次加载时回填一次
bool _credentialsLoaded = false; bool _credentialsLoaded = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 2, vsync: this);
tabController.addListener(_tabChangeListener); _tabController.addListener(() {
if (!_tabController.indexIsChanging) {
setState(() {});
} }
void _tabChangeListener() {
if (!tabController.indexIsChanging) {
switchTab(tabController.index);
}
}
void switchTab(int index) {
setState(() {
cLogin = (index == 0);
}); });
} }
@override @override
void dispose() { void dispose() {
tabController.dispose(); _tabController.dispose();
super.dispose(); super.dispose();
} }
Widget _buildView(LoginController controller) {
// 在视图构建时,检查并回填已保存的凭证
if (!_credentialsLoaded) {
final savedAccount = StorageService.to.stationAccount;
final savedPassword = StorageService.to.stationPassword;
if (savedAccount != null && savedPassword != null) {
controller.stationIdController.text = savedAccount;
controller.passwordController.text = savedPassword;
_rememberPassword = true; // 如果有保存的密码,则默认勾选
}
_credentialsLoaded = true; // 标记为已加载,防止重复执行
}
return Container(
color: Color(0xFFEFF4F7),
child: <Widget>[
Icon(cLogin ? AntdIcon.car : AntdIcon.USB),
SizedBox(height: 5.h),
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700),
SizedBox(height: 5.h),
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"),
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), // 设置圆角弧度
),
margin: EdgeInsets.all(15),
elevation: 4,
child: Container(
height: cLogin ? 285.h : 360.h,
padding: EdgeInsets.all(15),
child: Column(
children: [
Card(
elevation: 2,
child: Container(
height: 55.h,
padding: EdgeInsets.all(3),
child: TabBar(
controller: tabController,
onTap: (index) {
delayed(300, () {
switchTab(index);
});
},
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
indicator: BoxDecoration(
color: AppTheme.themeColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 6,
),
],
),
tabs: [
Tab(text: '司机端登录'),
Tab(text: '加氢站登录'),
],
isScrollable: false,
),
),
),
Flexible(
child: TabBarView(
controller: tabController,
children: [
_driverLoginView(controller),
_stationLoginView(controller),
],
),
),
],
),
),
),
].toColumn(mainAxisSize: MainAxisSize.min).center(),
);
}
Widget _driverLoginView(LoginController controller) {
return !cLogin
? SizedBox()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20.h),
TextFormField(
controller: controller.driverIdentityController,
cursorColor: AppTheme.themeColor,
maxLength: 8,
style: TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: '请输入身份后8位',
border: OutlineInputBorder(),
hintStyle: TextStyle(fontSize: 14),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 20.h),
ElevatedButton(
onPressed: () async {
String password = controller.driverIdentityController.text;
if (password.isEmpty) {
showToast("请输入密码");
return;
}
showLoading('登录中...');
try {
var responseData = await HttpService.to.post(
'appointment/login/loginForDriver',
data: {'idNo': password},
);
if (responseData == null && responseData!.data == null) {
dismissLoading();
showToast('登录失败:无法获取凭证');
return;
}
try {
var result = BaseModel.fromJson(responseData.data);
String token = result.data['token'] ?? '';
String idCard = result.data['idCard'] ?? '';
String name = result.data['name'] ?? '';
String phone = result.data['phone'] ?? '';
await StorageService.to.saveLoginInfo(
token: token,
userId: "",
channel: "driver",
idCard: idCard,
name: name,
phone: phone,
);
dismissLoading();
showToast('登录成功,欢迎您');
Get.offAll(() => BaseWidgetsPage());
} catch (e) {
dismissLoading();
showToast('登录失败:数据异常');
}
} catch (e) {
dismissLoading();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.themeColor,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text('登录'),
),
],
);
}
Widget _stationLoginView(LoginController controller) {
return cLogin
? SizedBox()
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20),
TextFormField(
controller: controller.stationIdController,
cursorColor: AppTheme.themeColor,
style: TextStyle(fontSize: 14),
decoration: InputDecoration(
hintText: '请输入加氢站编号',
border: OutlineInputBorder(),
hintStyle: TextStyle(fontSize: 14),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 10),
TextFormField(
controller: controller.passwordController,
obscureText: _obscureText,
style: TextStyle(fontSize: 14),
cursorColor: AppTheme.themeColor,
decoration: InputDecoration(
hintStyle: TextStyle(fontSize: 14),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
hintText: '请输入密码',
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
prefixIcon: Icon(Icons.lock_outline, color: Colors.grey),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.themeColor),
),
),
),
SizedBox(height: 10),
//记住密码复选框 ---
Row(
children: <Widget>[
SizedBox(
height: 24,
width: 24,
child: Checkbox(
value: _rememberPassword,
activeColor: AppTheme.themeColor,
onChanged: (bool? value) {
setState(() {
_rememberPassword = value ?? true;
});
},
),
),
GestureDetector(
onTap: () => setState(() => _rememberPassword = !_rememberPassword),
child: const Text('记住密码', style: TextStyle(color: Colors.grey)),
)
],
),
SizedBox(height: 20), // 调整间距
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.themeColor,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
String account = controller.stationIdController.text;
String password = controller.passwordController.text;
if (account.isEmpty || password.isEmpty) {
showToast("请输入账号和密码");
return;
}
showLoading('登录中...');
try {
String encryptedPassword = LoginUtil.encrypt(password);
var responseData = await HttpService.to.post(
'/login/password',
data: {
'account': account,
'password': encryptedPassword,
'loginType': "station",
},
);
if (responseData == null && responseData!.data == null) {
dismissLoading();
showToast('登录失败:无法获取凭证');
return;
}
try {
var result = BaseModel.fromJson(responseData.data);
String token = result.data['token'] ?? '';
String userId = result.data['userId'] ?? '';
await StorageService.to.saveLoginInfo(
token: token,
userId: userId,
channel: "station",
);
// 根据复选框状态保存或清除密码 ---
if (_rememberPassword) {
await StorageService.to.saveStationCredentials(account, password);
} else {
await StorageService.to.clearStationCredentials();
}
dismissLoading();
showToast('登录成功,欢迎您');
Get.offAll(() => B_BaseWidgetsPage());
} catch (e) {
dismissLoading();
showToast('登录失败:数据异常');
}
} catch (e) {
dismissLoading();
}
},
child: Text('登录'),
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<LoginController>( return GetBuilder<LoginController>(
init: LoginController(), init: LoginController(),
id: 'login', id: 'login',
builder: (controller) { builder: (controller) {
return Scaffold(body: _buildView(controller)); // 站点登录凭证回填逻辑
if (!_credentialsLoaded) {
final savedAccount = StorageService.to.stationAccount;
final savedPassword = StorageService.to.stationPassword;
if (savedAccount != null && savedPassword != null) {
controller.stationIdController.text = savedAccount;
controller.passwordController.text = savedPassword;
_rememberPassword = true;
}
_credentialsLoaded = true;
}
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: GestureDetector(
onTap: () {
hideKeyboard();
},
child: Stack(
children: [
//顶部背景与装饰
Positioned(
top: 0,
left: 0,
right: 0,
child: LoginUtil.getAssImg("bg_login"),
),
Positioned(
top: 0,
left: 0,
child: SizedBox(
width: 180.w,
height: 218.h,
child: LoginUtil.getAssImg("ic_login_bg@2x"),
),
),
_buildBrandingHeader(),
// 登录表单主体
Positioned(
top: 300.h,
left: 0,
right: 0,
bottom: 0,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40),
topRight: Radius.circular(40),
),
),
child: Column(
children: [
const SizedBox(height: 20),
// TabBar 切换
Container(
margin: EdgeInsets.symmetric(horizontal: 60.w),
child: TabBar(
controller: _tabController,
indicatorColor: const Color(0xFF006633),
indicatorWeight: 3,
labelColor: const Color(0xFF006633),
unselectedLabelColor: Colors.grey,
labelStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
tabs: const [
Tab(text: "司机登录"),
Tab(text: "站点登录"),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 30.w),
child: Column(
children: [
const SizedBox(height: 30),
// 根据 Tab 显示不同的输入框
_tabController.index == 0
? _buildDriverInputFields(controller)
: _buildStationInputFields(controller),
const SizedBox(height: 30),
// 统一登录按钮
_buildLoginButton(controller),
const SizedBox(height: 10),
buildAgreement(),
const SizedBox(height: 40),
],
),
),
),
],
),
),
),
Positioned(left: 0, right: 0, bottom: 33.h, child: _buildFooterSlogan()),
if (AppTheme.is_show_host)
Positioned(
top: 40.h,
right: 20.w,
child: TextButton(
onPressed: () => Get.to(() => const UrlHostPage()),
child: const Text(
"域名配置",
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}, },
); );
} }
/// 品牌头部
Widget _buildBrandingHeader() {
return Positioned(
top: 0,
left: 32.w,
right: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 100.h),
SizedBox(height: 60.h, child: LoginUtil.getAssImg('ic_logo_unbg@2x')),
SizedBox(height: 30.h),
const Text(
"HELLO,",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(51, 51, 51, 1),
),
),
const SizedBox(height: 8),
Row(
children: [
const Text(
"欢迎使用 ",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(51, 51, 51, 1),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color.fromRGBO(56, 198, 151, 1),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
"“羚牛氢能智慧服务平台”",
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
);
}
/// 司机登录输入框 (手机号+验证码)
Widget _buildDriverInputFields(LoginController controller) {
return Column(
children: [
_buildInputWrapper(
child: TextField(
controller: controller.phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
hintText: '请输入手机号',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
const SizedBox(height: 20),
_buildInputWrapper(
child: Row(
children: [
Expanded(
child: TextField(
controller: controller.codeController,
keyboardType: TextInputType.number,
inputFormatters: [LengthLimitingTextInputFormatter(6)],
decoration: const InputDecoration(
hintText: '请输入验证码',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
Obx(
() => GestureDetector(
onTap: controller.countdown.value == 0
? controller.startCountdown
: null,
child: Padding(
padding: const EdgeInsets.only(right: 24.0),
child: Text(
controller.countdown.value == 0
? "获取验证码"
: "${controller.countdown.value}s后重发",
style: TextStyle(
color: controller.countdown.value == 0
? const Color(0xFF006633)
: Colors.grey,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
],
);
}
/// 站点登录输入框 (账号+密码)
Widget _buildStationInputFields(LoginController controller) {
return Column(
children: [
_buildInputWrapper(
child: TextField(
controller: controller.stationIdController,
decoration: const InputDecoration(
hintText: '请输入加氢站编号',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
const SizedBox(height: 20),
_buildInputWrapper(
child: Row(
children: [
Expanded(
child: TextField(
controller: controller.passwordController,
obscureText: _obscureText,
decoration: const InputDecoration(
hintText: '请输入密码',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
),
),
),
IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () => setState(() => _obscureText = !_obscureText),
),
],
),
),
const SizedBox(height: 10),
Row(
children: [
SizedBox(
width: 40,
child: Checkbox(
value: _rememberPassword,
activeColor: const Color(0xFF006633),
onChanged: (val) => setState(() => _rememberPassword = val ?? false),
),
),
const Text("记住密码", style: TextStyle(color: Colors.grey, fontSize: 14)),
],
),
],
);
}
/// 通用输入框包装
Widget _buildInputWrapper({required Widget child}) {
return Container(
height: 55.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xFFF7F9FB),
borderRadius: BorderRadius.circular(28),
),
child: child,
);
}
/// 统一登录按钮逻辑
Widget _buildLoginButton(LoginController controller) {
return ElevatedButton(
onPressed: () => _handleLogin(controller),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF006633),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 55),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
elevation: 0,
),
child: Text(
_tabController.index == 0 ? "司机登录" : "站点登录",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
void _handleLogin(LoginController controller) async {
if (!_isAgreed) {
DialogX.to.showConfirmDialog(
icon: DialogIcon.warn,
content: _buildDialogContent(),
confirmText: '同意',
cancelText: '拒绝',
onConfirm: () {
_isAgreed = true;
controller.updateUi();
},
);
return;
}
_tabController.index == 0
? _handleDriverLogin(controller)
: _handleStationLogin(controller);
}
/// 司机登录逻辑 (短信登录)
void _handleDriverLogin(LoginController controller) async {
if (!_validateAgreed()) return;
String phone = controller.phoneController.text;
String code = controller.codeController.text;
if (phone.isEmpty || !phone.isPhoneNumber) {
showToast("请输入正确的手机号");
return;
}
if (code.isEmpty) {
showToast("请输入验证码");
return;
}
showLoading('登录中...');
try {
var responseData = await HttpService.to.post(
'appointment/login/login',
data: {'mobile': phone, 'code': code},
);
_processLoginResponse(responseData, "driver", phone);
} catch (e) {
dismissLoading();
}
}
/// 站点登录逻辑 (账号密码)
void _handleStationLogin(LoginController controller) async {
if (!_validateAgreed()) return;
String account = controller.stationIdController.text;
String password = controller.passwordController.text;
if (account.isEmpty || password.isEmpty) {
showToast("请输入账号和密码");
return;
}
showLoading('登录中...');
try {
String encryptedPassword = LoginUtil.encrypt(password);
var responseData = await HttpService.to.post(
'appointment/login/password',
data: {'account': account, 'password': encryptedPassword, 'loginType': "station"},
);
if (responseData != null && responseData.data != null) {
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
// 登录成功,处理记住密码逻辑
if (_rememberPassword) {
await StorageService.to.saveStationCredentials(account, password);
} else {
await StorageService.to.clearStationCredentials();
}
}
}
_processLoginResponse(responseData, "station", account);
} catch (e) {
dismissLoading();
}
}
bool _validateAgreed() {
if (!_isAgreed) {
DialogX.to.showConfirmDialog(
icon: DialogIcon.warn,
message: '请阅读并同意用户协议和隐私政策',
confirmText: '确定',
onConfirm: () {},
);
return false;
}
return true;
}
void _processLoginResponse(
dynamic responseData,
String channel,
String identifier,
) async {
if (responseData == null || responseData.data == null) {
dismissLoading();
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code != 0) {
showToast(result.error);
dismissLoading();
return;
}
String token = result.data['token'] ?? '';
if (channel == "driver") {
await StorageService.to.saveLoginInfo(
token: token,
userId: "",
channel: "driver",
name: result.data['name'],
phone: result.data['phone'],
);
// 成功后自动获取车辆信息
try {
var carInfo = await HttpService.to.get(
"appointment/driver/getTruckInfoByDriver?phone=${result.data['phone']}",
);
if (carInfo != null) {
var carInforesult = BaseModel.fromJson(carInfo.data);
if (carInforesult.data != null) {
final vehicle = VehicleInfo.fromJson(
carInforesult.data as Map<String, dynamic>,
);
//保存使用
await StorageService.to.saveVehicleInfo(vehicle);
}
}
} catch (e) {
Logger.d("暂时不处理 查询车辆信息失败的情况");
}
dismissLoading();
Get.offAll(() => BaseWidgetsPage());
} else {
await StorageService.to.saveLoginInfo(
token: token,
userId: result.data['userId'],
phone: result.data['mobile'],
channel: "station",
);
dismissLoading();
Get.offAll(() => B_BaseWidgetsPage());
}
addAlias(identifier);
}
final _aliyunPush = AliyunPushFlutter();
void addAlias(String alias) async {
var result = await _aliyunPush.bindAccount(alias);
var code = result['code'];
if (code == kAliyunPushSuccessCode) {
Logger.d('添加别名$alias成功');
} else {
var errorCode = result['code'];
var errorMsg = result['errorMsg'];
Logger.d('添加别名$alias失败: $errorCode - $errorMsg');
}
}
Widget buildAgreement() {
return Padding(
padding: EdgeInsets.only(top: 13.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 勾选框
SizedBox(
width: 22.w,
height: 22.h,
child: Checkbox(
value: _isAgreed,
activeColor: AppTheme.themeColor,
// 简单的圆角样式
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
onChanged: (bool? value) {
setState(() {
_isAgreed = value ?? false;
});
},
),
),
const SizedBox(width: 4),
// 富文本协议部分
Text.rich(
TextSpan(
text: '我已阅读并同意',
style: const TextStyle(color: Colors.grey, fontSize: 13),
children: [
TextSpan(
text: '《用户协议》',
style: TextStyle(color: AppTheme.themeColor, fontSize: 13),
recognizer: TapGestureRecognizer()
..onTap = () {
openPage("用户协议", "https://lnh2e.com/user_agreement.html");
},
),
const TextSpan(text: ''),
TextSpan(
text: '《隐私政策》',
style: TextStyle(color: AppTheme.themeColor, fontSize: 13),
recognizer: TapGestureRecognizer()
..onTap = () {
openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html");
},
),
],
),
),
],
),
);
}
Widget _buildDialogContent() {
return RichTextX(
children: [
TextSpanItem('请阅读并同意'),
TextSpanItem(
'《隐私协议》',
onTap: () => openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"),
),
TextSpanItem(''),
TextSpanItem(
'《用户政策》',
onTap: () => openPage("用户协议", "https://lnh2e.com/user_agreement.html"),
),
TextSpanItem(',我们将在协议框架内为您提供更优质的服务。'),
],
);
}
void openPage(String title, String url) {
if (Platform.isIOS) {
openWebPage(url);
return;
}
Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url});
}
Widget _buildFooterSlogan() {
return Center(
child: Column(
children: [
Text(
"H Y P A I",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: const Color.fromRGBO(51, 51, 51, 1),
letterSpacing: 8,
),
),
Text(
"HYDROGEN MOBILITY",
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 9,
color: Colors.grey.shade400,
letterSpacing: 1,
),
),
],
),
);
}
} }

View File

@@ -1,208 +1,162 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'package:dio/dio.dart' as dio;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:image/image.dart' as img;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:ln_jq_app/common/model/base_model.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/common/model/vehicle_info.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:zxing_lib/common.dart';
import 'package:zxing_lib/qrcode.dart';
import 'package:zxing_lib/zxing.dart';
class QrCodeController extends GetxController class QrCodeController extends GetxController with BaseControllerMixin {
with BaseControllerMixin, GetSingleTickerProviderStateMixin {
@override @override
String get builderId => 'qrcode'; String get builderId => 'qrcode';
// --- Animation --- final RxBool hasCameraPermission = false.obs;
late final AnimationController animationController;
late final Animation<double> scanAnimation;
// --- QR Scanning ---
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
QRViewController? qrViewController;
final Rx<Barcode?> result = Rx<Barcode?>(null);
final RxBool isFlashOn = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
requestPermission(); _checkPermission();
animationController = AnimationController(
duration: const Duration(milliseconds: 2500),
vsync: this,
);
scanAnimation = Tween<double>(begin: 0, end: 1).animate(animationController);
animationController.repeat(reverse: false);
} }
/// 当 QRView 创建时调用 /// 检查权限
void onQRViewCreated(QRViewController controller) { void _checkPermission() async {
this.qrViewController = controller; var status = await Permission.camera.status;
// 监听扫描到的数据 hasCameraPermission.value = status.isGranted;
controller.scannedDataStream.listen((scanData) {
if (scanData.code != null && result.value?.code != scanData.code) {
result.value = scanData;
qrViewController?.pauseCamera();
animationController.stop();
renderResult(scanData.code!);
}
});
} }
void resumeScanner() { /// 1. 拍照并识别车牌流程
result.value = null; void takePhotoAndRecognize() async {
qrViewController?.resumeCamera();
animationController.repeat(reverse: false);
}
/// 从相册选择图片并扫描二维码
void scanFromGallery() async {
try {
final XFile? imageFile = await ImagePicker().pickImage(source: ImageSource.gallery);
if (imageFile == null) return; // 用户取消了选择
qrViewController?.pauseCamera();
animationController.stop();
String? scanResult;
try {
final image = img.decodeImage(await File(imageFile.path).readAsBytes());
if (image != null) {
//扫描图片
final pixels = Int32List.fromList(
image.map((pixel) {
return (pixel.a.toInt() << 24) |
(pixel.r.toInt() << 16) |
(pixel.g.toInt() << 8) |
pixel.b.toInt();
}).toList(),
);
final source = RGBLuminanceSource(image.width, image.height, pixels);
final bitmap = BinaryBitmap(HybridBinarizer(source));
final reader = QRCodeReader();
final result = reader.decode(bitmap);
scanResult = result.text;
}
} on NotFoundException {
scanResult = null;
} catch (e) {
//异常
scanResult = null;
}
if (scanResult != null) {
renderResult(scanResult);
} else {
showErrorToast('未识别到二维码');
resumeScanner();
}
} catch (e) {
showErrorToast('从相册选择失败');
resumeScanner();
}
}
/// 切换闪光灯
void toggleFlash() async {
await qrViewController?.toggleFlash();
isFlashOn.value = (await qrViewController?.getFlashStatus()) ?? false;
}
/// 翻转相机
void flipCamera() async {
await qrViewController?.flipCamera();
}
void requestPermission() async {
if(Platform.isIOS){
var status = await Permission.camera.request(); var status = await Permission.camera.request();
if (status.isGranted) { if (!status.isGranted) {
} if (status.isPermanentlyDenied) openAppSettings();
else if (status.isPermanentlyDenied) { showErrorToast("需要相机权限才能拍照识别");
openAppSettings();
}
else {
showErrorToast('需要相机权限才能扫描二维码');
}
return; return;
} }
final bool results = await requestCameraPermission(); final XFile? photo = await ImagePicker().pickImage(
if (!results) { source: ImageSource.camera,
showErrorToast('相机权限未被授予,请到权限管理中打开'); imageQuality: 80, // 压缩图片质量以加快上传
} );
} if (photo == null) return;
void requestPhotoPermission() async { // 1.1 上传文件
if (Platform.isAndroid) { String? imageUrl = await uploadFile(photo.path);
final bool results = await requestPhotosPermission(); if (imageUrl != null) {
if (!results) { // 1.2 获取车牌号
showErrorToast('相册权限未被授予,请到权限管理中打开'); String? plateNumber = await getPlateNumber(imageUrl);
} else { if (plateNumber != null) {
scanFromGallery(); // 1.3 弹窗确认
} manualInputBind(plateNumber, 0);
}
if(Platform.isIOS){
var status = await Permission.photos.request();
print("权限状态: $status"); // 在控制台看这个输出
if (status.isGranted) {
scanFromGallery();
}
else if (status.isPermanentlyDenied) {
openAppSettings();
}
else {
showErrorToast('需要相册权限才能从相册中选择图片');
} }
} }
} }
//扫码结果处理 /// 手动输入车牌绑定
void renderResult(String resultStr) async { void manualInputBind(String plateNumber, int source) {
showLoading("正在获取车辆信息..."); final TextEditingController controller = TextEditingController(
try { text: plateNumber.toUpperCase() ?? '',
var responseData = await HttpService.to.get(
"appointment/truck/base-info?vin=$resultStr",
); );
if (responseData == null || responseData.data == null) { DialogX.to.showConfirmDialog(
showToast('无法获取车辆信息,请检查网络或稍后重试'); title: source == 0 ? "识别结果" : '手动输入车牌',
resumeScanner(); content: SizedBox(
return; 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: source == 0 ? "重新拍摄" : '取消',
onCancel: () {
if (source == 0) {
takePhotoAndRecognize();
}
},
onConfirm: () {
final plate = controller.text.trim().toUpperCase();
if (plate.isNotEmpty) {
bindVehicleByPlate(plate);
}
},
);
} }
var result = BaseModel.fromJson(responseData.data); /// 上传图片
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>); Future<String?> uploadFile(String filePath) async {
//保存使用 showLoading("正在上传图片...");
await StorageService.to.saveVehicleInfo(vehicle); 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); Get.back(result: true);
} on DioException catch (e) { } else {
showErrorToast("网络请求失败,请稍后重试"); showErrorToast(result.error);
resumeScanner(); }
} catch (e, stackTrace) { } catch (e) {
showErrorToast("处理失败,请稍后重试");
resumeScanner(); // 未知异常,恢复扫描
} finally { } finally {
dismissLoading(); dismissLoading();
} }
} }
@override
void onClose() {
qrViewController?.dispose();
animationController.dispose();
super.onClose();
}
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
import 'controller.dart'; import 'controller.dart';
@@ -13,148 +12,71 @@ class QrCodePage extends GetView<QrCodeController> {
return GetBuilder<QrCodeController>( return GetBuilder<QrCodeController>(
init: QrCodeController(), init: QrCodeController(),
id: 'qrcode', id: 'qrcode',
builder: (_) { builder: (controller) {
return Scaffold( return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar( appBar: AppBar(
title: const Text('扫码', style: TextStyle(color: Colors.white)), title: const Text('绑定车辆'),
centerTitle: true, centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Get.back(),
), ),
actions: [ body: Container(
TextButton( width: double.infinity,
onPressed: controller.requestPhotoPermission, padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: const Text( child: SingleChildScrollView(child: Column(
'相册', mainAxisAlignment: MainAxisAlignment.center,
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
],
),
body: Stack(
children: <Widget>[
_buildQrView(context),
Positioned(
bottom: 80.h,
left: 0,
right: 0,
child: _buildControlButtons(),
),
],
),
);
},
);
}
/// 构建二维码扫描视图(带动画)
Widget _buildQrView(BuildContext context) {
// 定义扫描区域的大小
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 250.0
: 300.0;
return Stack(
alignment: Alignment.center,
children: <Widget>[
// 底层是相机视图和半透明遮罩
QRView(
key: controller.qrKey,
onQRViewCreated: controller.onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.blueAccent,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea,
),
),
// 上层是扫描动画
AnimatedBuilder(
animation: controller.scanAnimation,
builder: (context, child) {
return Positioned(
// 计算扫描框的顶部位置,以便动画从顶部开始
top: (MediaQuery.of(context).size.height - scanArea) / 2,
child: Transform.translate(
offset: Offset(0, controller.scanAnimation.value * scanArea),
child: Container(
width: scanArea,
height: 2, // 扫描线的高度
decoration: BoxDecoration(
color: Colors.blueAccent,
boxShadow: [
BoxShadow(
color: Colors.blueAccent.withOpacity(0.7),
blurRadius: 8,
spreadRadius: 2,
),
],
),
),
),
);
},
),
],
);
}
/// 构建底部的控制按钮
Widget _buildControlButtons() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [ 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( const Text(
'将二维码/条形码放入框内,即可自动扫描', "请选择绑定方式",
style: TextStyle(color: Colors.white, fontSize: 14), 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),
// 拍照识别按钮
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),
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// 闪光灯按钮
_buildIconButton(
onPressed: controller.toggleFlash,
//闪光灯状态的变化
child: Obx(() => Icon(
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
color: Colors.white,
size: 28,
)),
),
// 翻转相机按钮
_buildIconButton(
onPressed: controller.flipCamera,
child: const Icon(
Icons.flip_camera_ios,
color: Colors.white,
size: 28,
),
),
],
),
],
);
}
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) { // 3. 手动输入按钮
return Container( OutlinedButton.icon(
decoration: BoxDecoration( onPressed: (){
color: Colors.black.withOpacity(0.3), controller.manualInputBind("",1);
shape: BoxShape.circle, },
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),
), ),
child: IconButton(
onPressed: onPressed,
icon: child,
iconSize: 32, // 增大点击区域
), ),
const SizedBox(height: 100), // 底部留白
],
),),
),
);
},
); );
} }
} }

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.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/storage_service.dart';
class UrlHostController extends GetxController {
final TextEditingController urlController = TextEditingController();
// 预设的域名列表
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 = [
'测试环境',
'线上环境',
'沈辰本地环境',
'何斐本地环境',
];
@override
void onInit() {
super.onInit();
// 初始化时,尝试从 StorageService 获取已保存的域名
// 如果没有保存过,则使用当前的全局配置
String? savedUrl = StorageService.to.hostUrl;
if (savedUrl != null && savedUrl.isNotEmpty) {
urlController.text = savedUrl;
} else {
urlController.text = AppTheme.test_service_url;
}
}
/// 当用户点击列表中的某一项时,将其填入编辑框
void selectUrl(String url) {
urlController.text = url;
}
/// 保存配置并关闭页面
void saveAndExit() async {
String newUrl = urlController.text.trim();
if (newUrl.isEmpty) {
showToast('请输入有效的域名');
return;
}
// 保存到本地存储,以便下次启动时加载
await StorageService.to.saveHostUrl(newUrl);
AppTheme.test_service_url = newUrl;
//设置框架
HttpService.to.setBaseUrl(AppTheme.test_service_url);
showSuccessToast('域名已更新:${AppTheme.test_service_url}');
Get.back();
}
@override
void onClose() {
urlController.dispose();
super.onClose();
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'controller.dart';
class UrlHostPage extends GetView<UrlHostController> {
const UrlHostPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(UrlHostController());
return Scaffold(
appBar: AppBar(
title: const Text('域名配置'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'当前环境配置',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
TextField(
controller: controller.urlController,
decoration: InputDecoration(
hintText: '请输入或选择API域名',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
),
const SizedBox(height: 24),
const Text(
'预设环境',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemCount: controller.presetUrls.length,
itemBuilder: (context, index) {
final url = controller.presetUrls[index];
return Card(
elevation: 1,
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(controller.urlNames[index], style: const TextStyle(fontSize: 14)),
subtitle: Text(url, style: const TextStyle(fontSize: 14)),
trailing: const Icon(Icons.touch_app, size: 18, color: Colors.grey),
onTap: () {
// 点击列表项:先填入编辑框,然后直接保存退出
controller.selectUrl(url);
controller.saveAndExit();
},
),
);
},
),
),
ElevatedButton(
onPressed: controller.saveAndExit,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('保存配置', style: TextStyle(fontSize: 16)),
),
],
),
),
);
}
}

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.fill
),
),
],
),
),
);
}
}

View File

@@ -1,5 +1,7 @@
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:getx_scaffold/common/utils/log_util.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart'; import 'package:ln_jq_app/common/model/vehicle_info.dart';
/// 定义登录渠道的枚举类型 /// 定义登录渠道的枚举类型
@@ -22,9 +24,11 @@ class StorageService extends GetxService {
static const String _vehicleInfoKey = 'vehicle_info'; static const String _vehicleInfoKey = 'vehicle_info';
static const String _stationAccountKey = 'station_account'; static const String _stationAccountKey = 'station_account';
static const String _stationPasswordKey = 'station_password'; static const String _stationPasswordKey = 'station_password';
// 新增:用于标记“绑定车辆”弹窗是否已在本会话中显示过 // 新增:用于标记“绑定车辆”弹窗是否已在本会话中显示过
static const String _bindDialogShownKey = 'bind_vehicle_dialog_shown'; static const String _bindDialogShownKey = 'bind_vehicle_dialog_shown';
static const String _hostUrlKey = 'host_url';
static StorageService get to => Get.find(); static StorageService get to => Get.find();
@@ -34,21 +38,34 @@ class StorageService extends GetxService {
} }
// --- Getters --- // --- Getters ---
String? get hostUrl => _box.read<String?>(_hostUrlKey);
///:保存自定义域名
Future<void> saveHostUrl(String url) async {
await _box.write(_hostUrlKey, url);
}
bool get isLoggedIn => _box.read<String?>(_tokenKey)?.isNotEmpty ?? false; bool get isLoggedIn => _box.read<String?>(_tokenKey)?.isNotEmpty ?? false;
String? get token => _box.read<String?>(_tokenKey); String? get token => _box.read<String?>(_tokenKey);
String? get userId => _box.read<String?>(_userIdKey); String? get userId => _box.read<String?>(_userIdKey);
String? get name => _box.read<String?>(_nameKey); String? get name => _box.read<String?>(_nameKey);
String? get phone => _box.read<String?>(_phoneKey); String? get phone => _box.read<String?>(_phoneKey);
String? get idCard => _box.read<String?>(_idCardKey); String? get idCard => _box.read<String?>(_idCardKey);
bool get hasVehicleInfo => _box.hasData(_vehicleInfoKey); bool get hasVehicleInfo => _box.hasData(_vehicleInfoKey);
String? get stationAccount => _box.read<String?>(_stationAccountKey); String? get stationAccount => _box.read<String?>(_stationAccountKey);
String? get stationPassword => _box.read<String?>(_stationPasswordKey); String? get stationPassword => _box.read<String?>(_stationPasswordKey);
// 新增:获取“绑定车辆”弹窗是否已显示的标志 // 新增:获取“绑定车辆”弹窗是否已显示的标志
bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false; bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false;
VehicleInfo? get vehicleInfo { VehicleInfo? get vehicleInfo {
final vehicleJson = _box.read<String?>(_vehicleInfoKey); final vehicleJson = _box.read<String?>(_vehicleInfoKey);
if (vehicleJson != null) { if (vehicleJson != null) {
@@ -98,7 +115,6 @@ class StorageService extends GetxService {
await _box.write(_bindDialogShownKey, true); await _box.write(_bindDialogShownKey, true);
} }
Future<void> clearVehicleInfo() async { Future<void> clearVehicleInfo() async {
await _box.remove(_vehicleInfoKey); await _box.remove(_vehicleInfoKey);
} }
@@ -118,5 +134,22 @@ class StorageService extends GetxService {
await clearVehicleInfo(); await clearVehicleInfo();
// 登出时,清除“绑定车辆”弹窗的显示记录,以便下次登录时可以再次弹出 // 登出时,清除“绑定车辆”弹窗的显示记录,以便下次登录时可以再次弹出
await _box.remove(_bindDialogShownKey); await _box.remove(_bindDialogShownKey);
delAlias();
}
final _aliyunPush = AliyunPushFlutter();
void delAlias() async {
String phoen = StorageService.to.phone ?? "";
var result = await await _aliyunPush.unbindAccount();
var code = result['code'];
if (code == kAliyunPushSuccessCode) {
Logger.d('删除别名$phoen成功');
} else {
var errorCode = result['code'];
var errorMsg = result['errorMsg'];
Logger.d('删除别名$phoen失败: $errorCode - $errorMsg');
}
} }
} }

View File

@@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
aliyun_push_flutter:
dependency: "direct main"
description:
name: aliyun_push_flutter
sha256: fd1a13ab37f30274e1eaa9c0f68001bf268f8f2e1c83730b4520aa4dff46ce70
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.6"
ansicolor: ansicolor:
dependency: transitive dependency: transitive
description: description:
@@ -65,14 +73,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
charset:
dependency: transitive
description:
name: charset
sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -117,18 +117,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239" sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.3.5" version: "0.3.5+1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
name: crypto name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.6" version: "3.0.7"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@@ -261,18 +261,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_linux name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.3+2" version: "0.9.4"
file_selector_macos: file_selector_macos:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2" sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.4+5" version: "0.9.5"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -285,10 +285,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_windows name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.3+4" version: "0.9.3+5"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -399,18 +399,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_pdfview name: flutter_pdfview
sha256: a9055bf920c7095bf08c2781db431ba23577aa5da5a056a7152dc89a18fbec6f sha256: c0b2cc4ebf461a5a4bb9312a165222475a7d93845c7a0703f4abb7f442eb6d54
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.2" version: "1.4.3"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.32" version: "2.0.33"
flutter_screenutil: flutter_screenutil:
dependency: transitive dependency: transitive
description: description:
@@ -431,10 +431,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_svg name: flutter_svg
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.1" version: "2.2.3"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -513,10 +513,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: get name: get
sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.7.2" version: "4.7.3"
get_storage: get_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -553,10 +553,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.0" version: "1.6.0"
http_client_helper: http_client_helper:
dependency: transitive dependency: transitive
description: description:
@@ -577,10 +577,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.5.4" version: "4.7.2"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -593,26 +593,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6 sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.8.13+7" version: "0.8.13+10"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
name: image_picker_for_web name: image_picker_for_web
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
image_picker_ios: image_picker_ios:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58 sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.8.13+1" version: "0.8.13+3"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
@@ -733,6 +733,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.1.4"
modal_bottom_sheet: modal_bottom_sheet:
dependency: transitive dependency: transitive
description: description:
@@ -793,18 +801,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16 sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.20" version: "2.2.22"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.3" version: "2.5.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -925,14 +933,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
qr_code_scanner_plus: pull_to_refresh:
dependency: "direct main" dependency: "direct main"
description: description:
name: qr_code_scanner_plus name: pull_to_refresh
sha256: b764e5004251c58d9dee0c295e6006e05bd8d249e78ac3383abdb5afe0a996cd sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.14" version: "2.0.0"
rational: rational:
dependency: transitive dependency: transitive
description: description:
@@ -945,26 +953,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.5.3" version: "2.5.4"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.15" version: "2.4.18"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.5.5" version: "2.5.6"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1070,10 +1078,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: universal_io name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.2" version: "2.3.1"
url_launcher: url_launcher:
dependency: transitive dependency: transitive
description: description:
@@ -1086,34 +1094,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.3.24" version: "6.3.28"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.3.5" version: "6.3.6"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.2.1" version: "3.2.2"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.2.4" version: "3.2.5"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1134,10 +1142,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.4" version: "3.1.5"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
@@ -1234,14 +1242,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
zxing_lib:
dependency: "direct main"
description:
name: zxing_lib
sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
sdks: sdks:
dart: ">=3.9.0 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0" flutter: ">=3.35.0"

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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.2.0+3 version: 1.2.3+6
environment: environment:
sdk: ^3.9.0 sdk: ^3.9.0
@@ -43,14 +43,17 @@ dependencies:
flutter_native_splash: ^2.4.7 flutter_native_splash: ^2.4.7
dropdown_button2: ^2.3.8 dropdown_button2: ^2.3.8
qr_code_scanner_plus: ^2.0.14
image_picker: ^1.2.1 # 用于从相册选择图片 image_picker: ^1.2.1 # 用于从相册选择图片
image: ^4.5.4 image: ^4.5.4
zxing_lib: ^1.1.4 mobile_scanner: ^7.1.4
flutter_pdfview: 1.3.2 #显示pdf flutter_pdfview: 1.4.3 #显示pdf
photo_view: ^0.15.0 #操作图片 photo_view: ^0.15.0 #操作图片
flutter_inappwebview: ^6.1.5 # WebView插件 flutter_inappwebview: ^6.1.5 # WebView插件
geolocator: ^14.0.2 # 获取精确定位 geolocator: ^14.0.2 # 获取精确定位
aliyun_push_flutter: ^1.3.6
pull_to_refresh: ^2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: