Compare commits
24 Commits
dev_featur
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 35bd3a78a5 | |||
| 1278c38b7e | |||
| 032e60d362 | |||
| 171f556b40 | |||
| 55eade54b6 | |||
| bc99ffd691 | |||
| aa52a56bcf | |||
| 73343ca297 | |||
| d09faac1d2 | |||
| 1177be821a | |||
| e59b89c225 | |||
| 45f5035d1b | |||
| f792915429 | |||
| edbacc502b | |||
| 5722e3ace0 | |||
| d41b21654a | |||
| 2eb059defd | |||
| fbcc85af2a | |||
| 9a97b56505 | |||
| 8302d7c179 | |||
| e7a9e4483a | |||
| 9b64fdfa52 | |||
| d8f335eb4e | |||
| d1b7a9eb76 |
@@ -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 = 4
|
versionCode = 6
|
||||||
versionName = "1.2.1"
|
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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
ln_jq_app/android/app/proguard-rules.pro
vendored
Normal 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.**
|
||||||
|
Before Width: | Height: | Size: 6.0 KiB |
BIN
ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
ln_jq_app/assets/html/ic_tag.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
@@ -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>
|
||||||
@@ -167,7 +217,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var map, marker, driving, truckDriving, geocoder;
|
var map, marker, destMarker, driving, truckDriving, geocoder;
|
||||||
var currentLat, currentLng;
|
var currentLat, currentLng;
|
||||||
var isTruckMode = false;
|
var isTruckMode = false;
|
||||||
var isInitialLocationSet = false;
|
var isInitialLocationSet = false;
|
||||||
@@ -176,7 +226,7 @@
|
|||||||
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'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +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);
|
||||||
@@ -263,6 +313,7 @@
|
|||||||
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,
|
||||||
@@ -281,6 +332,10 @@
|
|||||||
const addressComponent = regeo.addressComponent;
|
const addressComponent = regeo.addressComponent;
|
||||||
const pois = regeo.pois;
|
const pois = regeo.pois;
|
||||||
|
|
||||||
|
console.log("地理:" + JSON.stringify(result));
|
||||||
|
fetchStationInfo(addressComponent.province, addressComponent.city,
|
||||||
|
addressComponent.district, lat, lng);
|
||||||
|
|
||||||
// 策略1: 优先使用最近的、类型合适的POI的名称
|
// 策略1: 优先使用最近的、类型合适的POI的名称
|
||||||
if (pois && pois.length > 0) {
|
if (pois && pois.length > 0) {
|
||||||
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
|
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
|
||||||
@@ -295,8 +350,9 @@
|
|||||||
}
|
}
|
||||||
// 策略2: 如果没有POI,使用"道路+门牌号"
|
// 策略2: 如果没有POI,使用"道路+门牌号"
|
||||||
else if (addressComponent.street && addressComponent.streetNumber) {
|
else if (addressComponent.street && addressComponent.streetNumber) {
|
||||||
shortAddress = addressComponent.street + addressComponent
|
shortAddress = addressComponent.district +
|
||||||
.streetNumber;
|
addressComponent.township +
|
||||||
|
addressComponent.street + addressComponent.streetNumber;
|
||||||
}
|
}
|
||||||
// 策略3: 如果还没有,使用"区+乡镇"
|
// 策略3: 如果还没有,使用"区+乡镇"
|
||||||
else if (addressComponent.district) {
|
else if (addressComponent.district) {
|
||||||
@@ -330,7 +386,104 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核心功能 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) {
|
||||||
@@ -346,7 +499,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核心功能 3: 路径规划
|
* 路径规划
|
||||||
*/
|
*/
|
||||||
function startRouteSearch() {
|
function startRouteSearch() {
|
||||||
// 获取输入框的文字
|
// 获取输入框的文字
|
||||||
|
|||||||
BIN
ln_jq_app/assets/images/ic_h2_my@2x.png
Normal file
|
After Width: | Height: | Size: 573 B |
BIN
ln_jq_app/assets/images/ic_h2_my_select@2x.png
Normal file
|
After Width: | Height: | Size: 508 B |
BIN
ln_jq_app/assets/images/ic_no_data@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
@@ -13,7 +13,7 @@
|
|||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
"filename" : "Icon-App-29x29 1.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
@@ -31,25 +31,25 @@
|
|||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-80x80.jpg",
|
"filename" : "Icon-App-80x80.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-120x120.jpg",
|
"filename" : "Icon-App-120x120.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-120x120 1.jpg",
|
"filename" : "Icon-App-120x120 1.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "60x60"
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-180.jpg",
|
"filename" : "Icon-App-180.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"scale" : "3x",
|
"scale" : "3x",
|
||||||
"size" : "60x60"
|
"size" : "60x60"
|
||||||
@@ -61,19 +61,19 @@
|
|||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
"filename" : "Icon-App-20x20@2x 1.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "20x20"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
"filename" : "Icon-App-29x29.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
"filename" : "Icon-App-29x29@2x 1.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "29x29"
|
"size" : "29x29"
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"size" : "40x40"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
"filename" : "Icon-App-76x76.png",
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "76x76"
|
"size" : "76x76"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 509 B After Width: | Height: | Size: 465 B |
|
After Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 613 B |
|
After Width: | Height: | Size: 613 B |
|
Before Width: | Height: | Size: 707 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -70,5 +70,11 @@
|
|||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>zh-Hans</string>
|
||||||
|
<string>en</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class LoginUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ui.Image getAssImg(String imgName){
|
static ui.Image getAssImg(String imgName){
|
||||||
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'));
|
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'),fit: ui.BoxFit.cover,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ class AppTheme {
|
|||||||
static const Color themeColor = Color(0xFF017137);
|
static const Color themeColor = Color(0xFF017137);
|
||||||
|
|
||||||
//是否开放域名切换
|
//是否开放域名切换
|
||||||
static const bool is_show_host = true;
|
static const bool is_show_host = false;
|
||||||
|
|
||||||
//http://192.168.110.222:8080/
|
//http://192.168.110.222:8080/
|
||||||
//http://192.168.110.44:8080/
|
//http://192.168.110.44:8080/
|
||||||
static String test_service_url = "https://beta-esg.api.lnh2e.com/";
|
static String test_service_url = "http://47.101.201.13:8443/api/";
|
||||||
static const String release_service_url = "";
|
static const String release_service_url = "";
|
||||||
|
|
||||||
//加氢站相关查询
|
//加氢站相关查询
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ void main() async {
|
|||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
WidgetsBinding widgetsBinding = await init(
|
WidgetsBinding widgetsBinding = await init(
|
||||||
isDebug: true,
|
isDebug: false,
|
||||||
logTag: '小羚羚',
|
logTag: '小羚羚',
|
||||||
supportedLocales: [const Locale('zh', 'CN')],
|
supportedLocales: [const Locale('zh', 'CN')],
|
||||||
);
|
);
|
||||||
@@ -31,12 +31,19 @@ void main() async {
|
|||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
GetxApp(
|
GetxApp(
|
||||||
|
// 设计稿尺寸 单位:dp
|
||||||
designSize: const Size(390, 844),
|
designSize: const Size(390, 844),
|
||||||
enableLog: true,
|
// Getx Log
|
||||||
|
enableLog: false,
|
||||||
|
// 默认的跳转动画
|
||||||
defaultTransition: Transition.rightToLeft,
|
defaultTransition: Transition.rightToLeft,
|
||||||
|
// 主题模式
|
||||||
themeMode: GlobalService.to.themeMode,
|
themeMode: GlobalService.to.themeMode,
|
||||||
|
// 主题
|
||||||
theme: AppTheme.light,
|
theme: AppTheme.light,
|
||||||
|
// Dark主题
|
||||||
darkTheme: AppTheme.light,
|
darkTheme: AppTheme.light,
|
||||||
|
// AppTitle
|
||||||
title: '小羚羚',
|
title: '小羚羚',
|
||||||
// 将入口改为启动页
|
// 将入口改为启动页
|
||||||
home: const WelcomePage(),
|
home: const WelcomePage(),
|
||||||
@@ -62,6 +69,9 @@ void initHttpSet() {
|
|||||||
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 {
|
||||||
|
if (response.data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final baseModel = BaseModel.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;
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ class HistoryPage extends GetView<HistoryController> {
|
|||||||
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;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
customStartTime = DateTime.now();
|
customStartTime = DateTime.now();
|
||||||
customEndTime = customStartTime!.add(const Duration(days: 1));
|
customEndTime = customStartTime!.add(const Duration(days: 1));
|
||||||
renderData();
|
renderData();
|
||||||
_msgNotice(); // 红点消息
|
msgNotice(); // 红点消息
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _msgNotice() async {
|
Future<void> msgNotice() async {
|
||||||
final Map<String, dynamic> requestData = {
|
final Map<String, dynamic> requestData = {
|
||||||
'appFlag': 1,
|
'appFlag': 1,
|
||||||
'isRead': 1,
|
'isRead': 1,
|
||||||
@@ -264,6 +264,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
if (result.code == 0 && result.data != null) {
|
if (result.code == 0 && result.data != null) {
|
||||||
String total = result.data["total"].toString();
|
String total = result.data["total"].toString();
|
||||||
isNotice = int.parse(total) > 0;
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
_buildSystemTips(),
|
_buildSystemTips(),
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
_buildLogoutButton(),
|
_buildLogoutButton(),
|
||||||
SizedBox(height: 40),
|
SizedBox(height: 75.h),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -98,8 +98,12 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async{
|
||||||
Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
@@ -332,7 +336,8 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
controller.jobTipStr,
|
controller.jobTipStr,
|
||||||
style: TextStyle(color: Colors.yellow[800], fontSize: 14),
|
style: TextStyle(color: Colors.yellow[800], fontSize: 14),
|
||||||
),
|
),
|
||||||
Icon(AntdIcon.question_circle, size: 14, color: Colors.yellow[800]),
|
SizedBox(width: 2.w),
|
||||||
|
Icon(AntdIcon.info_circle, size: 14, color: Colors.yellow[800]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -358,9 +363,9 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
status,
|
status,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11.sp,
|
fontSize: 13.sp,
|
||||||
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
|
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ 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;
|
||||||
@@ -40,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,
|
||||||
@@ -80,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;
|
||||||
}
|
}
|
||||||
@@ -97,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,
|
||||||
@@ -137,15 +145,25 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
bool isNotice = false;
|
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();
|
msgNotice();
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPaused() {
|
||||||
|
stopAutoRefresh();
|
||||||
|
super.onPaused();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
@@ -153,7 +171,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _msgNotice() async {
|
Future<void> msgNotice() async {
|
||||||
final Map<String, dynamic> requestData = {
|
final Map<String, dynamic> requestData = {
|
||||||
'appFlag': 1,
|
'appFlag': 1,
|
||||||
'isRead': 1,
|
'isRead': 1,
|
||||||
@@ -169,6 +187,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
if (result.code == 0 && result.data != null) {
|
if (result.code == 0 && result.data != null) {
|
||||||
String total = result.data["total"].toString();
|
String total = result.data["total"].toString();
|
||||||
isNotice = int.parse(total) > 0;
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +202,9 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 【6. 新增】停止定时器的方法
|
void onRefresh() => renderData(isRefresh: true);
|
||||||
|
|
||||||
|
///停止定时器的方法
|
||||||
void stopAutoRefresh() {
|
void stopAutoRefresh() {
|
||||||
// 如果定时器存在并且是激活状态,就取消它
|
// 如果定时器存在并且是激活状态,就取消它
|
||||||
_refreshTimer?.cancel();
|
_refreshTimer?.cancel();
|
||||||
@@ -205,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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -570,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}',
|
||||||
@@ -603,6 +625,10 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
} finally {
|
} finally {
|
||||||
//加载列表数据
|
//加载列表数据
|
||||||
fetchReservationData();
|
fetchReservationData();
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
refreshController.refreshCompleted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/b_page/history/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/message/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';
|
||||||
|
|
||||||
@@ -19,7 +20,12 @@ class SitePage extends GetView<SiteController> {
|
|||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
|
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
|
||||||
body: SingleChildScrollView(child: _buildView(context)),
|
body: SmartRefresher(
|
||||||
|
controller: controller.refreshController,
|
||||||
|
enablePullUp: false,
|
||||||
|
onRefresh: controller.onRefresh,
|
||||||
|
child: SingleChildScrollView(child: _buildView(context)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -78,7 +84,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 35.h,),
|
SizedBox(height: 35.h),
|
||||||
//第三部分
|
//第三部分
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
@@ -130,6 +136,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 75.h),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -142,10 +149,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: MediaQuery
|
top: MediaQuery.of(context).padding.top + 10,
|
||||||
.of(context)
|
|
||||||
.padding
|
|
||||||
.top + 10,
|
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
bottom: 25,
|
bottom: 25,
|
||||||
@@ -182,8 +186,11 @@ class SitePage extends GetView<SiteController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
@@ -204,8 +211,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildStatBox(
|
_buildStatBox("剩余氢量", "remaining quantity", controller.leftHydrogen, "kg"),
|
||||||
"剩余氢量", "remaining quantity", controller.leftHydrogen, "kg"),
|
|
||||||
SizedBox(width: 4.w),
|
SizedBox(width: 4.w),
|
||||||
_buildStatBox(
|
_buildStatBox(
|
||||||
"今日加氢量",
|
"今日加氢量",
|
||||||
@@ -367,15 +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(
|
Text(
|
||||||
'暂无预约数据', style: TextStyle(fontSize: 16, color: Colors.black54)),
|
'暂无订单',
|
||||||
const SizedBox(height: 8),
|
style: TextStyle(
|
||||||
const Text(
|
fontSize: 16.sp,
|
||||||
'点击右上角刷新按钮获取最新数据',
|
color: Colors.black54,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -492,7 +500,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
"预约量:${item.amount}",
|
"预约量:${item.amount}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF00A870),
|
color: Color(0xFF00A870),
|
||||||
fontSize: 12.sp,
|
fontSize: 13.sp,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -507,7 +515,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
"${item.contactPerson} | ${item.contactPhone}",
|
"${item.contactPerson} | ${item.contactPhone}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF999999),
|
color: Color(0xFF999999),
|
||||||
fontSize: 12.sp,
|
fontSize: 13.sp,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -546,7 +554,8 @@ class SitePage extends GetView<SiteController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 通用小按钮
|
/// 通用小按钮
|
||||||
Widget _buildSmallButton(String text, {
|
Widget _buildSmallButton(
|
||||||
|
String text, {
|
||||||
required bool isOutline,
|
required bool isOutline,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
@@ -601,6 +610,11 @@ class SitePage extends GetView<SiteController> {
|
|||||||
textColor = const Color(0xFFFF7D7D); // 红色
|
textColor = const Color(0xFFFF7D7D); // 红色
|
||||||
bgColor = const Color(0xFFFFEEEE);
|
bgColor = const Color(0xFFFFEEEE);
|
||||||
break;
|
break;
|
||||||
|
case ReservationStatus.cancel:
|
||||||
|
text = '已取消';
|
||||||
|
textColor = const Color(0xFFFF7D7D); // 红色
|
||||||
|
bgColor = const Color(0xFFFFEEEE);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
text = "未知";
|
text = "未知";
|
||||||
textColor = Colors.grey;
|
textColor = Colors.grey;
|
||||||
@@ -690,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;
|
||||||
@@ -716,7 +734,8 @@ class SitePage extends GetView<SiteController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 构建信息详情行
|
/// 构建信息详情行
|
||||||
Widget _buildDetailRow(IconData icon,
|
Widget _buildDetailRow(
|
||||||
|
IconData icon,
|
||||||
String label,
|
String label,
|
||||||
String value, {
|
String value, {
|
||||||
Color valueColor = Colors.black87,
|
Color valueColor = Colors.black87,
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
getUserBindCarInfo();
|
getUserBindCarInfo();
|
||||||
_msgNotice();
|
msgNotice();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _msgNotice() async {
|
Future<void> msgNotice() async {
|
||||||
final Map<String, dynamic> requestData = {
|
final Map<String, dynamic> requestData = {
|
||||||
'appFlag': 1,
|
'appFlag': 1,
|
||||||
'isRead': 1,
|
'isRead': 1,
|
||||||
@@ -53,6 +53,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
if (result.code == 0 && result.data != null) {
|
if (result.code == 0 && result.data != null) {
|
||||||
String total = result.data["total"].toString();
|
String total = result.data["total"].toString();
|
||||||
isNotice = int.parse(total) > 0;
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,7 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
|
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
|
||||||
if (!StorageService.to.hasShownBindVehicleDialog &&
|
if (!StorageService.to.hasShownBindVehicleDialog &&
|
||||||
StorageService.to.isLoggedIn &&
|
StorageService.to.isLoggedIn &&
|
||||||
|
StorageService.to.loginChannel == LoginChannel.driver &&
|
||||||
!StorageService.to.hasVehicleInfo) {
|
!StorageService.to.hasVehicleInfo) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
DialogX.to.showConfirmDialog(
|
DialogX.to.showConfirmDialog(
|
||||||
|
|||||||
@@ -133,8 +133,11 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async{
|
||||||
Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
@@ -345,7 +348,7 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 9),
|
const SizedBox(height: 9),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 336.h, // 给定一个高度,或者使用别的方式布局
|
height: 356.h, // 给定一个高度,或者使用别的方式布局
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_buildCertificateContent('行驶证', controller.drivingAttachments),
|
_buildCertificateContent('行驶证', controller.drivingAttachments),
|
||||||
@@ -379,7 +382,7 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: true),
|
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
|
||||||
_buildCertDetailItem('运营城市', controller.address),
|
_buildCertDetailItem('运营城市', controller.address),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ class MessageController extends GetxController {
|
|||||||
_loadData(isRefresh: true);
|
_loadData(isRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onRefresh() => _loadData(isRefresh: true);
|
||||||
|
|
||||||
|
void onLoading() => _loadData(isRefresh: false);
|
||||||
|
|
||||||
Future<void> _loadData({bool isRefresh = false}) async {
|
Future<void> _loadData({bool isRefresh = false}) async {
|
||||||
final int targetPage = isRefresh ? 1 : _pageNum + 1;
|
final int targetPage = isRefresh ? 1 : _pageNum + 1;
|
||||||
|
|
||||||
@@ -100,9 +104,7 @@ class MessageController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRefresh() => _loadData(isRefresh: true);
|
|
||||||
|
|
||||||
void onLoading() => _loadData(isRefresh: false);
|
|
||||||
|
|
||||||
// 标记全部已读
|
// 标记全部已读
|
||||||
void markAllRead() async {
|
void markAllRead() async {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
_fetchAccidentCount(), // 请求2:事故数
|
_fetchAccidentCount(), // 请求2:事故数
|
||||||
_fetchBreakRulesCount(), // 请求3:违章数
|
_fetchBreakRulesCount(), // 请求3:违章数
|
||||||
_rating(), // 司机评分
|
_rating(), // 司机评分
|
||||||
_msgNotice(), // 红点消息
|
msgNotice(), // 红点消息
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await renderViolation();
|
await renderViolation();
|
||||||
@@ -69,7 +69,7 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _msgNotice() async {
|
Future<void> msgNotice() async {
|
||||||
final Map<String, dynamic> requestData = {
|
final Map<String, dynamic> requestData = {
|
||||||
'appFlag': 1,
|
'appFlag': 1,
|
||||||
'isRead': 1,
|
'isRead': 1,
|
||||||
@@ -85,6 +85,7 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
if (result.code == 0 && result.data != null) {
|
if (result.code == 0 && result.data != null) {
|
||||||
String total = result.data["total"].toString();
|
String total = result.data["total"].toString();
|
||||||
isNotice = int.parse(total) > 0;
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,8 +140,12 @@ class MinePage extends GetView<MineController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async{
|
||||||
Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ 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小时区间
|
// 计算当前时间属于哪个1小时区间
|
||||||
@@ -335,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,
|
||||||
@@ -350,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)),
|
||||||
)) {
|
)) {
|
||||||
@@ -363,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("提交中");
|
||||||
|
|
||||||
@@ -401,6 +409,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
lastSuccessfulReservation = ReservationModel(
|
lastSuccessfulReservation = ReservationModel(
|
||||||
id: selectedStationId.value!,
|
id: selectedStationId.value!,
|
||||||
|
stationId: '',
|
||||||
hydAmount: ampuntStr,
|
hydAmount: ampuntStr,
|
||||||
startTime: startTimeStr,
|
startTime: startTimeStr,
|
||||||
endTime: endTimeStr,
|
endTime: endTimeStr,
|
||||||
@@ -540,6 +549,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
//用来管理查看预约的弹窗
|
//用来管理查看预约的弹窗
|
||||||
Worker? _sheetWorker;
|
Worker? _sheetWorker;
|
||||||
bool init = false;
|
bool init = false;
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get listenLifecycleEvent => true;
|
bool get listenLifecycleEvent => true;
|
||||||
@@ -549,7 +559,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
getUserBindCarInfo();
|
getUserBindCarInfo();
|
||||||
getSiteList();
|
getSiteList();
|
||||||
_msgNotice();
|
startAutoRefresh();
|
||||||
|
msgNotice();
|
||||||
|
|
||||||
if (!init) {
|
if (!init) {
|
||||||
_setupListener();
|
_setupListener();
|
||||||
@@ -559,7 +570,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
bool isNotice = false;
|
bool isNotice = false;
|
||||||
|
|
||||||
Future<void> _msgNotice() async {
|
Future<void> msgNotice() async {
|
||||||
final Map<String, dynamic> requestData = {
|
final Map<String, dynamic> requestData = {
|
||||||
'appFlag': 1,
|
'appFlag': 1,
|
||||||
'isRead': 1,
|
'isRead': 1,
|
||||||
@@ -575,14 +586,26 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
if (result.code == 0 && result.data != null) {
|
if (result.code == 0 && result.data != null) {
|
||||||
String total = result.data["total"].toString();
|
String total = result.data["total"].toString();
|
||||||
isNotice = int.parse(total) > 0;
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void startAutoRefresh() {
|
||||||
void dispose() {
|
// 先停止已存在的定时器,防止重复启动
|
||||||
_sheetWorker?.dispose();
|
stopAutoRefresh();
|
||||||
super.dispose();
|
|
||||||
|
// 创建一个每1分钟执行一次的周期性定时器
|
||||||
|
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
|
||||||
|
getSiteList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///停止定时器的方法
|
||||||
|
void stopAutoRefresh() {
|
||||||
|
// 如果定时器存在并且是激活状态,就取消它
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_refreshTimer = null; // 置为null,方便判断
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupListener() {
|
void _setupListener() {
|
||||||
@@ -636,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();
|
||||||
@@ -684,8 +709,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUi();
|
updateUi();
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
}
|
|
||||||
renderSliderTheme();
|
renderSliderTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,7 +781,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
if (stationOptions.isEmpty) {
|
if (stationOptions.isEmpty) {
|
||||||
showToast('附近暂无可用加氢站');
|
showToast('附近暂无可用加氢站');
|
||||||
} else {
|
} else {
|
||||||
showToast('站点列表已刷新');
|
// showToast('站点列表已刷新');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到第一个可选的站点作为默认值
|
// 找到第一个可选的站点作为默认值
|
||||||
@@ -779,6 +803,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
|
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
|
||||||
if (!StorageService.to.hasShownBindVehicleDialog &&
|
if (!StorageService.to.hasShownBindVehicleDialog &&
|
||||||
StorageService.to.isLoggedIn &&
|
StorageService.to.isLoggedIn &&
|
||||||
|
StorageService.to.loginChannel == LoginChannel.driver &&
|
||||||
!StorageService.to.hasVehicleInfo) {
|
!StorageService.to.hasVehicleInfo) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
DialogX.to.showConfirmDialog(
|
DialogX.to.showConfirmDialog(
|
||||||
@@ -804,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.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/station_model.dart';
|
import 'package:ln_jq_app/common/model/station_model.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
|
||||||
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/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 'controller.dart';
|
import 'controller.dart';
|
||||||
import 'reservation_list_bottomsheet.dart';
|
|
||||||
|
|
||||||
///加氢预约
|
///加氢预约
|
||||||
class ReservationPage extends GetView<C_ReservationController> {
|
class ReservationPage extends GetView<C_ReservationController> {
|
||||||
@@ -152,7 +147,12 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Get.to(() => const MessagePage()),
|
onPressed: () async {
|
||||||
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
|
},
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
smallSize: 8,
|
smallSize: 8,
|
||||||
backgroundColor: controller.isNotice
|
backgroundColor: controller.isNotice
|
||||||
@@ -519,7 +519,7 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(13.0),
|
padding: EdgeInsets.all(13.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -566,19 +566,27 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _updateAmount(-1),
|
onPressed: () => _updateAmount(-1),
|
||||||
icon: const Icon(Icons.remove, size: 18, color: Colors.grey),
|
icon: Icon(Icons.remove, size: 14.sp, color: Colors.grey),
|
||||||
),
|
),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
"${controller.amountController.text}Kg",
|
"${controller.amountController.text}Kg",
|
||||||
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
textAlign: TextAlign.center,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.sp,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _updateAmount(1),
|
onPressed: () => _updateAmount(1),
|
||||||
icon: const Icon(Icons.add, size: 18, color: Colors.grey),
|
icon: Icon(Icons.add, size: 14.sp, color: Colors.grey),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -758,6 +766,9 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
controller.amountController.text = newAmount.toStringAsFixed(2);
|
controller.amountController.text = newAmount.toStringAsFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//更新进度条
|
||||||
|
controller.current = newAmount;
|
||||||
|
|
||||||
// 移动光标到末尾,防止光标跳到前面
|
// 移动光标到末尾,防止光标跳到前面
|
||||||
controller.amountController.selection = TextSelection.fromPosition(
|
controller.amountController.selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: controller.amountController.text.length),
|
TextPosition(offset: controller.amountController.text.length),
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ class ReservationEditController extends GetxController with BaseControllerMixin
|
|||||||
|
|
||||||
TimeSlot tempSlot = availableSlots[initialItem];
|
TimeSlot tempSlot = availableSlots[initialItem];
|
||||||
|
|
||||||
|
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
Container(
|
Container(
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -228,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,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:getx_scaffold/getx_scaffold.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/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';
|
||||||
@@ -156,6 +157,7 @@ class HomeController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
|
Future<void> _onNotificationOpened(Map<dynamic, dynamic> message) async {
|
||||||
Logger.d('onNotificationOpened ====> $message');
|
Logger.d('onNotificationOpened ====> $message');
|
||||||
|
await Get.to(() => const MessagePage());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {
|
Future<void> _onNotificationRemoved(Map<dynamic, dynamic> message) async {
|
||||||
|
|||||||
@@ -68,13 +68,14 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
hideKeyboard();
|
hideKeyboard();
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// 1. 顶部背景与装饰
|
//顶部背景与装饰
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -92,9 +93,9 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
),
|
),
|
||||||
_buildBrandingHeader(),
|
_buildBrandingHeader(),
|
||||||
|
|
||||||
// 2. 登录表单主体
|
// 登录表单主体
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 280.h,
|
top: 300.h,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
@@ -443,11 +444,18 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
data: {'account': account, 'password': encryptedPassword, 'loginType': "station"},
|
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) {
|
if (_rememberPassword) {
|
||||||
await StorageService.to.saveStationCredentials(account, password);
|
await StorageService.to.saveStationCredentials(account, password);
|
||||||
} else {
|
} else {
|
||||||
await StorageService.to.clearStationCredentials();
|
await StorageService.to.clearStationCredentials();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_processLoginResponse(responseData, "station", account);
|
_processLoginResponse(responseData, "station", account);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -475,7 +483,6 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
) async {
|
) async {
|
||||||
if (responseData == null || responseData.data == null) {
|
if (responseData == null || responseData.data == null) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('登录失败');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
@@ -495,14 +502,25 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
phone: result.data['phone'],
|
phone: result.data['phone'],
|
||||||
);
|
);
|
||||||
// 成功后自动获取车辆信息
|
// 成功后自动获取车辆信息
|
||||||
|
try {
|
||||||
var carInfo = await HttpService.to.get(
|
var carInfo = await HttpService.to.get(
|
||||||
"appointment/driver/getTruckInfoByDriver?phone=${result.data['phone']}",
|
"appointment/driver/getTruckInfoByDriver?phone=${result.data['phone']}",
|
||||||
);
|
);
|
||||||
if (carInfo != null) {
|
if (carInfo != null) {
|
||||||
var carResult = BaseModel.fromJson(carInfo.data);
|
var carInforesult = BaseModel.fromJson(carInfo.data);
|
||||||
if (carResult.data != null)
|
if (carInforesult.data != null) {
|
||||||
await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(carResult.data));
|
final vehicle = VehicleInfo.fromJson(
|
||||||
|
carInforesult.data as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
//保存使用
|
||||||
|
await StorageService.to.saveVehicleInfo(vehicle);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.d("暂时不处理 查询车辆信息失败的情况");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
Get.offAll(() => BaseWidgetsPage());
|
Get.offAll(() => BaseWidgetsPage());
|
||||||
} else {
|
} else {
|
||||||
@@ -521,7 +539,16 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
final _aliyunPush = AliyunPushFlutter();
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
|
|
||||||
void addAlias(String alias) async {
|
void addAlias(String alias) async {
|
||||||
await _aliyunPush.addAlias(alias);
|
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() {
|
Widget buildAgreement() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
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';
|
||||||
@@ -6,243 +7,156 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/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:mobile_scanner/mobile_scanner.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.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;
|
|
||||||
|
|
||||||
// --- 使用 MobileScanner 的控制器 ---
|
|
||||||
final MobileScannerController scannerController = MobileScannerController();
|
|
||||||
|
|
||||||
final RxBool isFlashOn = false.obs;
|
|
||||||
final RxBool isProcessingResult = false.obs;
|
|
||||||
|
|
||||||
final RxBool hasPermission = 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// MobileScanner 的 onDetect 回调方法
|
/// 检查权限
|
||||||
void onDetect(BarcodeCapture capture) {
|
void _checkPermission() async {
|
||||||
if (isProcessingResult.value) return;
|
var status = await Permission.camera.status;
|
||||||
|
hasCameraPermission.value = status.isGranted;
|
||||||
final Barcode? barcode = capture.barcodes.firstOrNull;
|
|
||||||
if (barcode?.rawValue != null && barcode!.rawValue!.isNotEmpty) {
|
|
||||||
isProcessingResult.value = true;
|
|
||||||
scannerController.stop();
|
|
||||||
animationController.stop();
|
|
||||||
renderResult(barcode.rawValue!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 恢复扫描状态
|
/// 1. 拍照并识别车牌流程
|
||||||
void resumeScanner() {
|
void takePhotoAndRecognize() async {
|
||||||
isProcessingResult.value = false;
|
|
||||||
try {
|
|
||||||
scannerController.start();
|
|
||||||
animationController.repeat(reverse: false);
|
|
||||||
} catch (e) {
|
|
||||||
print("无法重启相机: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从相册选择图片并扫描二维码
|
|
||||||
void scanFromGallery() async {
|
|
||||||
try {
|
|
||||||
final XFile? imageFile =
|
|
||||||
await ImagePicker().pickImage(source: ImageSource.gallery);
|
|
||||||
if (imageFile == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scannerController.stop();
|
|
||||||
animationController.stop();
|
|
||||||
showLoading("正在识别...");
|
|
||||||
|
|
||||||
final BarcodeCapture? capture =
|
|
||||||
await scannerController.analyzeImage(imageFile.path);
|
|
||||||
|
|
||||||
dismissLoading();
|
|
||||||
|
|
||||||
final Barcode? firstBarcode = capture?.barcodes.firstOrNull;
|
|
||||||
|
|
||||||
if (firstBarcode?.rawValue != null &&
|
|
||||||
firstBarcode!.rawValue!.isNotEmpty) {
|
|
||||||
final String scanResult = firstBarcode.rawValue!;
|
|
||||||
print("相册识别到的内容: $scanResult");
|
|
||||||
renderResult(scanResult);
|
|
||||||
} else {
|
|
||||||
showErrorToast('未识别到二维码');
|
|
||||||
resumeScanner();
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
dismissLoading();
|
|
||||||
showErrorToast('从相册选择失败,请稍后重试');
|
|
||||||
print("scanFromGallery Error: $e\n$stackTrace");
|
|
||||||
resumeScanner();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 切换闪光灯
|
|
||||||
void toggleFlash() async {
|
|
||||||
try {
|
|
||||||
await scannerController.toggleTorch();
|
|
||||||
final currentTorchState = scannerController.value.torchState;
|
|
||||||
isFlashOn.value = currentTorchState == TorchState.on;
|
|
||||||
} catch (e) {
|
|
||||||
print("切换闪光灯失败: $e");
|
|
||||||
showErrorToast("无法打开闪光灯");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 翻转相机
|
|
||||||
void flipCamera() async {
|
|
||||||
await scannerController.switchCamera();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 请求相机权限
|
|
||||||
void requestPermission() async {
|
|
||||||
var status = await Permission.camera.request();
|
var status = await Permission.camera.request();
|
||||||
|
|
||||||
hasPermission.value = status.isGranted;
|
|
||||||
|
|
||||||
if (!status.isGranted) {
|
if (!status.isGranted) {
|
||||||
if (status.isPermanentlyDenied) {
|
if (status.isPermanentlyDenied) openAppSettings();
|
||||||
showErrorToast('相机权限已被永久拒绝,请到系统设置中开启');
|
showErrorToast("需要相机权限才能拍照识别");
|
||||||
// 延迟一会再引导用户去设置
|
return;
|
||||||
Future.delayed(const Duration(seconds: 2), () => openAppSettings());
|
}
|
||||||
} else {
|
|
||||||
showErrorToast('请授予相机权限以使用扫描功能');
|
final XFile? photo = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80, // 压缩图片质量以加快上传
|
||||||
|
);
|
||||||
|
if (photo == null) return;
|
||||||
|
|
||||||
|
// 1.1 上传文件
|
||||||
|
String? imageUrl = await uploadFile(photo.path);
|
||||||
|
if (imageUrl != null) {
|
||||||
|
// 1.2 获取车牌号
|
||||||
|
String? plateNumber = await getPlateNumber(imageUrl);
|
||||||
|
if (plateNumber != null) {
|
||||||
|
// 1.3 弹窗确认
|
||||||
|
manualInputBind(plateNumber, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/// 手动输入车牌绑定
|
||||||
|
void manualInputBind(String plateNumber, int source) {
|
||||||
void requestPhotoPermission() async {
|
final TextEditingController controller = TextEditingController(
|
||||||
var status = await Permission.photos.request();
|
text: plateNumber.toUpperCase() ?? '',
|
||||||
if (status.isGranted) {
|
|
||||||
scanFromGallery();
|
|
||||||
} else if (status.isPermanentlyDenied) {
|
|
||||||
openAppSettings();
|
|
||||||
} else {
|
|
||||||
showErrorToast('需要相册权限才能从相册中选择图片');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理扫描结果
|
|
||||||
void renderResult(String resultStr, {plateNumber}) async {
|
|
||||||
showLoading("正在获取车辆信息...");
|
|
||||||
try {
|
|
||||||
final Map<String, dynamic> requestData = {
|
|
||||||
"code": resultStr,
|
|
||||||
"phone": StorageService.to.phone,
|
|
||||||
};
|
|
||||||
if (plateNumber != null && plateNumber.isNotEmpty) {
|
|
||||||
requestData['plateNumber'] = plateNumber;
|
|
||||||
}
|
|
||||||
var responseData = await HttpService.to.post(
|
|
||||||
"appointment/truck/bindTruck",
|
|
||||||
data: requestData,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (responseData == null) {
|
|
||||||
dismissLoading();
|
|
||||||
showToast('无法获取车辆信息,请检查网络或稍后重试');
|
|
||||||
resumeScanner();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
showToast(result.error);
|
|
||||||
dismissLoading();
|
|
||||||
resumeScanner(); // 绑定失败也要恢复扫描
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.data == null) {
|
|
||||||
dismissLoading();
|
|
||||||
showBindDialog(resultStr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final vehicle = VehicleInfo.fromJson(result.data as Map<String, dynamic>);
|
|
||||||
await StorageService.to.saveVehicleInfo(vehicle);
|
|
||||||
dismissLoading();
|
|
||||||
Get.back(result: true);
|
|
||||||
|
|
||||||
} on DioException catch (_) {
|
|
||||||
showErrorToast("网络请求失败,请稍后重试");
|
|
||||||
resumeScanner();
|
|
||||||
} catch (e, _) {
|
|
||||||
showErrorToast("处理失败,请稍后重舍");
|
|
||||||
resumeScanner();
|
|
||||||
} finally {
|
|
||||||
if (Get.isDialogOpen ?? false) {
|
|
||||||
dismissLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 显示绑定确认对话框
|
|
||||||
void showBindDialog(String resultStr) {
|
|
||||||
final TextEditingController plateNumberController = TextEditingController();
|
|
||||||
// 使用 showConfirmDialog,它有 onCancel 回调
|
|
||||||
DialogX.to.showConfirmDialog(
|
DialogX.to.showConfirmDialog(
|
||||||
title: '请输入车牌号',
|
title: source == 0 ? "识别结果" : '手动输入车牌',
|
||||||
barrierDismissible: false,
|
content: SizedBox(
|
||||||
content: TextField(
|
height: 40.h,
|
||||||
controller: plateNumberController,
|
child: TextField(
|
||||||
autofocus: false,
|
textAlign: TextAlign.start,
|
||||||
|
controller: controller,
|
||||||
|
autofocus: plateNumber.isEmpty,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.only(left: 5),
|
||||||
hintText: '请输入完整的车牌号',
|
hintText: '请输入完整的车牌号',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12.0),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
confirmText: '确认绑定',
|
confirmText: '确认绑定',
|
||||||
cancelText: '取消', // showConfirmDialog 有 cancelText
|
cancelText: source == 0 ? "重新拍摄" : '取消',
|
||||||
onConfirm: () {
|
|
||||||
final String plateNumber = plateNumberController.text.trim();
|
|
||||||
if (plateNumber.isEmpty) {
|
|
||||||
showToast("请输入车牌号");
|
|
||||||
// 返回 false 可以阻止弹窗关闭,让用户继续输入
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
renderResult(resultStr, plateNumber: plateNumber);
|
|
||||||
//关闭弹窗
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
// 如果用户点击取消,恢复扫描
|
if (source == 0) {
|
||||||
resumeScanner();
|
takePhotoAndRecognize();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onConfirm: () {
|
||||||
|
final plate = controller.text.trim().toUpperCase();
|
||||||
|
if (plate.isNotEmpty) {
|
||||||
|
bindVehicleByPlate(plate);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
/// 上传图片
|
||||||
void onClose() {
|
Future<String?> uploadFile(String filePath) async {
|
||||||
scannerController.dispose();
|
showLoading("正在上传图片...");
|
||||||
animationController.dispose();
|
try {
|
||||||
super.onClose();
|
dio.FormData formData = dio.FormData.fromMap({
|
||||||
|
'file': await dio.MultipartFile.fromFile(filePath, filename: 'ocr_plate.jpg'),
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await HttpService.to.post("appointment/ocr/upload", data: formData);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) return result.data.toString();
|
||||||
|
showErrorToast(result.error);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorToast("图片上传失败");
|
||||||
|
} finally {
|
||||||
|
dismissLoading();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// OCR 识别
|
||||||
|
Future<String?> getPlateNumber(String imageUrl) async {
|
||||||
|
showLoading("正在识别车牌...");
|
||||||
|
try {
|
||||||
|
var response = await HttpService.to.get(
|
||||||
|
"appointment/ocr/getPlateNumber",
|
||||||
|
params: {'imageUrl': imageUrl},
|
||||||
|
);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0) return result.data.toString();
|
||||||
|
showErrorToast(result.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorToast("车牌识别失败");
|
||||||
|
} finally {
|
||||||
|
dismissLoading();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 核心绑定方法
|
||||||
|
void bindVehicleByPlate(String plateNumber) async {
|
||||||
|
showLoading("正在绑定车辆...");
|
||||||
|
try {
|
||||||
|
var responseData = await HttpService.to.post(
|
||||||
|
"appointment/truck/bindOcrTruck",
|
||||||
|
data: {"plateNumber": plateNumber, "phone": StorageService.to.phone},
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = BaseModel.fromJson(responseData?.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
await StorageService.to.saveVehicleInfo(VehicleInfo.fromJson(result.data));
|
||||||
|
dismissLoading();
|
||||||
|
showSuccessToast("绑定成功");
|
||||||
|
Get.back(result: true);
|
||||||
|
} else {
|
||||||
|
showErrorToast(result.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
dismissLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,285 +1,82 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
class QrCodePage extends GetView<QrCodeController> {
|
class QrCodePage extends GetView<QrCodeController> {
|
||||||
const QrCodePage({super.key});
|
const QrCodePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(QrCodeController());
|
return GetBuilder<QrCodeController>(
|
||||||
|
init: QrCodeController(),
|
||||||
|
id: 'qrcode',
|
||||||
|
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('相册', style: TextStyle(color: Colors.white, fontSize: 16)),
|
child: SingleChildScrollView(child: Column(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Obx(() { // 1. 使用 Obx 包裹整个 body
|
|
||||||
// 根据权限状态来决定显示什么
|
|
||||||
if (controller.hasPermission.value) {
|
|
||||||
// 如果有权限,显示扫描器
|
|
||||||
return _buildScannerView(context);
|
|
||||||
} else {
|
|
||||||
// 如果没有权限,显示引导界面
|
|
||||||
return _buildPermissionDeniedView();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Widget _buildScannerView(BuildContext context){
|
|
||||||
if (!controller.animationController.isAnimating) {
|
|
||||||
controller.animationController.repeat(reverse: false);
|
|
||||||
}
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
// 使用 MobileScanner 作为扫描视图
|
const SizedBox(height: 24),
|
||||||
MobileScanner(
|
Icon(
|
||||||
controller: controller.scannerController,
|
Icons.directions_car_rounded,
|
||||||
onDetect: controller.onDetect,
|
size: 100,
|
||||||
// 您可以自定义扫描框的样式
|
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||||
scanWindow: Rect.fromCenter(
|
|
||||||
center: Offset(
|
|
||||||
MediaQuery.of(context).size.width / 2,
|
|
||||||
MediaQuery.of(context).size.height / 2 - 50,
|
|
||||||
),
|
),
|
||||||
width: 250,
|
const SizedBox(height: 24),
|
||||||
height: 250,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 扫描动画和覆盖层
|
|
||||||
_buildScannerOverlay(context),
|
|
||||||
// 底部的功能按钮
|
|
||||||
Positioned(bottom: 80, left: 0, right: 0, child: _buildActionButtons()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPermissionDeniedView() {
|
|
||||||
// 确保动画是停止的
|
|
||||||
if (controller.animationController.isAnimating) {
|
|
||||||
controller.animationController.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
color: Colors.black,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.no_photography, color: Colors.white70, size: 64),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const Text(
|
const Text(
|
||||||
'需要相机权限',
|
"请选择绑定方式",
|
||||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'请授予相机权限以使用扫码功能。',
|
"您可以拍摄照片自动识别,\n或手动输入车牌号进行绑定。",
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 60),
|
||||||
ElevatedButton(
|
|
||||||
onPressed: controller.requestPermission, // 点击按钮重新请求权限
|
|
||||||
child: const Text('授予权限'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建扫描区域的覆盖层和动画
|
// 拍照识别按钮
|
||||||
Widget _buildScannerOverlay(BuildContext context) {
|
ElevatedButton.icon(
|
||||||
// 模拟扫描框的位置和大小
|
onPressed: controller.takePhotoAndRecognize,
|
||||||
const double scanAreaSize = 250.0;
|
icon: const Icon(Icons.camera_alt_rounded),
|
||||||
return Stack(
|
label: const Text("拍照识别车牌"),
|
||||||
children: [
|
style: ElevatedButton.styleFrom(
|
||||||
// 半透明的覆盖层
|
minimumSize: const Size(double.infinity, 56),
|
||||||
ColorFiltered(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||||
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.5), BlendMode.srcOut),
|
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Container(decoration: const BoxDecoration(color: Colors.transparent)),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 100), // 微调位置
|
|
||||||
width: scanAreaSize,
|
|
||||||
height: scanAreaSize,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// 3. 手动输入按钮
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: (){
|
||||||
|
controller.manualInputBind("",1);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.edit_note_rounded),
|
||||||
|
label: const Text("手动输入车牌"),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||||
|
side: BorderSide(color: Theme.of(context).primaryColor, width: 1.5),
|
||||||
|
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 100), // 底部留白
|
||||||
],
|
],
|
||||||
),
|
),),
|
||||||
),
|
|
||||||
// 扫描动画
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 100),
|
|
||||||
width: scanAreaSize,
|
|
||||||
height: scanAreaSize,
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: controller.scanAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return CustomPaint(
|
|
||||||
painter: ScannerAnimationPainter(
|
|
||||||
controller.scanAnimation.value,
|
|
||||||
AppTheme.themeColor,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建底部的功能按钮(闪光灯、相册)
|
|
||||||
Widget _buildActionButtons() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'将二维码/条形码放入框内,即可自动扫描',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
// 闪光灯按钮
|
|
||||||
_buildIconButton(
|
|
||||||
onPressed: controller.toggleFlash,
|
|
||||||
//闪光灯状态的变化
|
|
||||||
child: Obx(
|
|
||||||
() => IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
controller.isFlashOn.value ? Icons.flash_on : Icons.flash_off,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
onPressed: controller.toggleFlash,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 翻转相机按钮
|
|
||||||
_buildIconButton(
|
|
||||||
onPressed: controller.flipCamera,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.flip_camera_ios,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Widget _buildIconButton({required VoidCallback onPressed, required Widget child}) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.3),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: child,
|
|
||||||
iconSize: 32, // 增大点击区域
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 扫描动画的绘制器
|
|
||||||
class ScannerAnimationPainter extends CustomPainter {
|
|
||||||
final double value;
|
|
||||||
final Color borderColor;
|
|
||||||
|
|
||||||
ScannerAnimationPainter(this.value, this.borderColor);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = borderColor
|
|
||||||
..strokeWidth = 3
|
|
||||||
..style = PaintingStyle.stroke;
|
|
||||||
|
|
||||||
final cornerLength = 20.0;
|
|
||||||
// 绘制四个角的边框
|
|
||||||
// Top-left
|
|
||||||
canvas.drawPath(
|
|
||||||
Path()
|
|
||||||
..moveTo(0, cornerLength)
|
|
||||||
..lineTo(0, 0)
|
|
||||||
..lineTo(cornerLength, 0),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
// Top-right
|
|
||||||
canvas.drawPath(
|
|
||||||
Path()
|
|
||||||
..moveTo(size.width - cornerLength, 0)
|
|
||||||
..lineTo(size.width, 0)
|
|
||||||
..lineTo(size.width, cornerLength),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
// Bottom-left
|
|
||||||
canvas.drawPath(
|
|
||||||
Path()
|
|
||||||
..moveTo(0, size.height - cornerLength)
|
|
||||||
..lineTo(0, size.height)
|
|
||||||
..lineTo(cornerLength, size.height),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
// Bottom-right
|
|
||||||
canvas.drawPath(
|
|
||||||
Path()
|
|
||||||
..moveTo(size.width - cornerLength, size.height)
|
|
||||||
..lineTo(size.width, size.height)
|
|
||||||
..lineTo(size.width, size.height - cornerLength),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 绘制扫描线
|
|
||||||
final linePaint = Paint()
|
|
||||||
..color = borderColor.withOpacity(0.8)
|
|
||||||
..strokeWidth = 2
|
|
||||||
..shader = LinearGradient(
|
|
||||||
colors: [borderColor.withOpacity(0), borderColor, borderColor.withOpacity(0)],
|
|
||||||
stops: const [0.0, 0.5, 1.0],
|
|
||||||
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
|
||||||
|
|
||||||
final y = size.height * value;
|
|
||||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class StorageService extends GetxService {
|
|||||||
|
|
||||||
void delAlias() async {
|
void delAlias() async {
|
||||||
String phoen = StorageService.to.phone ?? "";
|
String phoen = StorageService.to.phone ?? "";
|
||||||
var result = await _aliyunPush.removeAlias(phoen);
|
var result = await await _aliyunPush.unbindAccount();
|
||||||
var code = result['code'];
|
var code = result['code'];
|
||||||
if (code == kAliyunPushSuccessCode) {
|
if (code == kAliyunPushSuccessCode) {
|
||||||
Logger.d('删除别名$phoen成功');
|
Logger.d('删除别名$phoen成功');
|
||||||
|
|||||||
@@ -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.1+4
|
version: 1.2.3+6
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0
|
sdk: ^3.9.0
|
||||||
|
|||||||