Compare commits
25 Commits
45f5035d1b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 35bd3a78a5 | |||
| 1278c38b7e | |||
| 032e60d362 | |||
| 171f556b40 | |||
| 55eade54b6 | |||
| bc99ffd691 | |||
| aa52a56bcf | |||
| 73343ca297 | |||
| d09faac1d2 | |||
| 1177be821a | |||
| e59b89c225 | |||
| 79fe3257b5 | |||
| 55569839a7 | |||
| 7112d70aba | |||
| f8a8ecb0ed | |||
| 18c04272e2 | |||
| 14e7fb3d78 | |||
| 5ffaf81223 | |||
| 907983a1d1 | |||
| 9fdca9136d | |||
| 16bae6a1e9 | |||
| aabfbfae0c | |||
| 5236670e7c | |||
| cf3ad579d3 | |||
| 70a752b6e5 |
@@ -37,8 +37,8 @@ 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 = 5
|
versionCode = 6
|
||||||
versionName = "1.2.2"
|
versionName = "1.2.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
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/android_apk_img.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
ln_jq_app/assets/images/bg_login.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
BIN
ln_jq_app/assets/images/bg_map@2x.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
ln_jq_app/assets/images/ic_car@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ln_jq_app/assets/images/ic_car_bg@2x.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
ln_jq_app/assets/images/ic_car_select@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ln_jq_app/assets/images/ic_h2@2x.png
Normal file
|
After Width: | Height: | Size: 914 B |
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_h2_select@2x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
ln_jq_app/assets/images/ic_jqz@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
ln_jq_app/assets/images/ic_label@2x.png
Normal file
|
After Width: | Height: | Size: 638 B |
BIN
ln_jq_app/assets/images/ic_login_bg@2x.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
ln_jq_app/assets/images/ic_logo@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
ln_jq_app/assets/images/ic_logo_unbg@2x.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
ln_jq_app/assets/images/ic_mall@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
ln_jq_app/assets/images/ic_mall_select@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
ln_jq_app/assets/images/ic_map@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ln_jq_app/assets/images/ic_map_select@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
ln_jq_app/assets/images/ic_no_data@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
ln_jq_app/assets/images/ic_pj@2x.png
Normal file
|
After Width: | Height: | Size: 779 B |
BIN
ln_jq_app/assets/images/ic_px@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
ln_jq_app/assets/images/ic_user@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
ln_jq_app/assets/images/ic_user_logo@2x.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
ln_jq_app/assets/images/ic_user_select@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
ln_jq_app/assets/images/ic_wz@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
ln_jq_app/assets/images/welcome.png
Normal file
|
After Width: | Height: | Size: 112 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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
import 'package:flutter/material.dart' as ui;
|
||||||
|
|
||||||
class LoginUtil {
|
class LoginUtil {
|
||||||
static final _keyString = '915eae87951a448c86c47796e44c1fcf';
|
static final _keyString = '915eae87951a448c86c47796e44c1fcf';
|
||||||
@@ -26,5 +27,9 @@ class LoginUtil {
|
|||||||
final decrypted = _encrypter.decrypt(encrypted);
|
final decrypted = _encrypter.decrypt(encrypted);
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ui.Image getAssImg(String imgName){
|
||||||
|
return ui.Image(image: ui.AssetImage('assets/images/$imgName.png'),fit: ui.BoxFit.cover,);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class AppTheme {
|
|||||||
|
|
||||||
static const String Font_YuYang = 'YuYang';
|
static const String Font_YuYang = 'YuYang';
|
||||||
|
|
||||||
static const Color themeColor = Color(0xFF0c83c3);
|
static const Color themeColor = Color(0xFF017137);
|
||||||
|
|
||||||
//是否开放域名切换
|
//是否开放域名切换
|
||||||
static const bool is_show_host = false;
|
static const bool is_show_host = false;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
@@ -8,8 +9,8 @@ import 'package:ln_jq_app/storage_service.dart';
|
|||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
import 'common/styles/theme.dart';
|
import 'common/styles/theme.dart';
|
||||||
import 'pages/home/view.dart';
|
|
||||||
import 'pages/login/view.dart';
|
import 'pages/login/view.dart';
|
||||||
|
import 'pages/welcome/view.dart'; // 引入启动页
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -17,8 +18,10 @@ void main() async {
|
|||||||
WidgetsBinding widgetsBinding = await init(
|
WidgetsBinding widgetsBinding = await init(
|
||||||
isDebug: false,
|
isDebug: false,
|
||||||
logTag: '小羚羚',
|
logTag: '小羚羚',
|
||||||
supportedLocales: [Locale('zh', 'CN')],
|
supportedLocales: [const Locale('zh', 'CN')],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 保持原生闪屏页,直到 WelcomeController 调用 remove()
|
||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
|
|
||||||
await GetStorage.init();
|
await GetStorage.init();
|
||||||
@@ -42,22 +45,17 @@ void main() async {
|
|||||||
darkTheme: AppTheme.light,
|
darkTheme: AppTheme.light,
|
||||||
// AppTitle
|
// AppTitle
|
||||||
title: '小羚羚',
|
title: '小羚羚',
|
||||||
// 首页入口
|
// 将入口改为启动页
|
||||||
home: HomePage(),
|
home: const WelcomePage(),
|
||||||
//组件国际化
|
fallbackLocale: const Locale('zh', 'CN'),
|
||||||
fallbackLocale: Locale('zh', 'CN'),
|
supportedLocales: const [Locale('zh', 'CN')],
|
||||||
supportedLocales: [Locale('zh', 'CN')],
|
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
//pull_to_refresh
|
|
||||||
RefreshLocalizations.delegate,
|
RefreshLocalizations.delegate,
|
||||||
GlobalMaterialLocalizations.delegate,
|
GlobalMaterialLocalizations.delegate,
|
||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
|
|
||||||
// Builder
|
|
||||||
builder: (context, widget) {
|
builder: (context, widget) {
|
||||||
// do something....
|
|
||||||
return widget!;
|
return widget!;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -67,11 +65,8 @@ void main() async {
|
|||||||
void initHttpSet() {
|
void initHttpSet() {
|
||||||
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
|
AppTheme.test_service_url = StorageService.to.hostUrl ?? AppTheme.test_service_url;
|
||||||
|
|
||||||
// 设置基础 URL
|
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
||||||
//指定请求头
|
|
||||||
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
|
HttpService.to.dio.interceptors.add(TokenInterceptor(tokenKey: 'asoco-token'));
|
||||||
// 设置全局响应处理器
|
|
||||||
HttpService.to.setOnResponseHandler((response) async {
|
HttpService.to.setOnResponseHandler((response) async {
|
||||||
try {
|
try {
|
||||||
if (response.data == null) {
|
if (response.data == null) {
|
||||||
@@ -79,11 +74,10 @@ void initHttpSet() {
|
|||||||
}
|
}
|
||||||
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;
|
||||||
} else if (baseModel.code == 401) {
|
} else if (baseModel.code == 401) {
|
||||||
await StorageService.to.clearLoginInfo();
|
await StorageService.to.clearLoginInfo();
|
||||||
Get.offAll(() => LoginPage());
|
Get.offAll(() => const LoginPage());
|
||||||
return baseModel.message;
|
return baseModel.message;
|
||||||
} else {
|
} else {
|
||||||
return (baseModel.error.toString()).isEmpty
|
return (baseModel.error.toString()).isEmpty
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,6 +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(); // 红点消息
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
String jobDetailsStr = "";
|
String jobDetailsStr = "";
|
||||||
String jobId = "";
|
String jobId = "";
|
||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
|
bool isNotice = false;
|
||||||
|
|
||||||
Future<void> renderData() async {
|
Future<void> renderData() async {
|
||||||
showLoading("加载中");
|
showLoading("加载中");
|
||||||
@@ -246,6 +248,27 @@ class ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> msgNotice() async {
|
||||||
|
final Map<String, dynamic> requestData = {
|
||||||
|
'appFlag': 1,
|
||||||
|
'isRead': 1,
|
||||||
|
'pageNum': 1,
|
||||||
|
'pageSize': 5,
|
||||||
|
};
|
||||||
|
final response = await HttpService.to.get(
|
||||||
|
'appointment/unread_notice/page',
|
||||||
|
params: requestData,
|
||||||
|
);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
String total = result.data["total"].toString();
|
||||||
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onOperationStatusChanged(String? newValue) {
|
void onOperationStatusChanged(String? newValue) {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
selectedOperationStatus = newValue;
|
selectedOperationStatus = newValue;
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
|
import 'package:ln_jq_app/pages/b_page/reservation/controller.dart';
|
||||||
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
|
|
||||||
class ReservationPage extends GetView<ReservationController> {
|
class ReservationPage extends GetView<ReservationController> {
|
||||||
const ReservationPage({super.key});
|
const ReservationPage({super.key});
|
||||||
|
|
||||||
|
// 定义主题色
|
||||||
|
static const kPrimaryColor = Color(0xFF006D35); // 效果图深绿色
|
||||||
|
static const kBgColor = Color(0xFFF5F7F9); // 背景灰
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<ReservationController>(
|
return GetBuilder<ReservationController>(
|
||||||
@@ -13,180 +19,313 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
id: 'b_reservation',
|
id: 'b_reservation',
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: kBgColor,
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
_buildHeaderCard(),
|
_buildTopSection(context),
|
||||||
const SizedBox(height: 12),
|
Padding(
|
||||||
_buildInfoFormCard(context),
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||||
const SizedBox(height: 12),
|
child: Column(
|
||||||
_buildTipsCard(),
|
children: [
|
||||||
const SizedBox(height: 12),
|
SizedBox(height: 16),
|
||||||
|
_buildBasicInfoCard(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_buildOperationContentCard(context),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
_buildSystemTips(),
|
||||||
|
SizedBox(height: 24),
|
||||||
_buildLogoutButton(),
|
_buildLogoutButton(),
|
||||||
|
SizedBox(height: 75.h),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建顶部的站点信息头卡片
|
/// 1. 顶部个人信息及统计栏
|
||||||
Widget _buildHeaderCard() {
|
Widget _buildTopSection(BuildContext context) {
|
||||||
return Card(
|
return Container(
|
||||||
elevation: 2,
|
width: double.infinity,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(30)),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top + 10,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 25,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
Row(
|
||||||
leading: const Icon(Icons.local_gas_station, color: Colors.blue, size: 40),
|
|
||||||
title: Text(
|
|
||||||
controller.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
|
||||||
),
|
|
||||||
subtitle: Text(controller.address),
|
|
||||||
trailing: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue[100],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
controller.selectedOperationStatus,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
children: [
|
||||||
_buildHeaderStat(controller.customerPrice, '氢气价格'),
|
CircleAvatar(
|
||||||
_buildHeaderStat(controller.timeStr, '营业时间'),
|
radius: 25,
|
||||||
_buildHeaderStat('98%', '设备状态'),
|
backgroundColor: Colors.white,
|
||||||
],
|
child: LoginUtil.getAssImg('ic_user_logo@2x'),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
],
|
Expanded(
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建头部卡片中的单个统计项
|
|
||||||
Widget _buildHeaderStat(String value, String label) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建包含所有信息表单的卡片(增加 Tab 切换功能)
|
|
||||||
Widget _buildInfoFormCard(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
clipBehavior: Clip.antiAlias, // 确保 Tab 背景圆角生效
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Tab 切换栏
|
|
||||||
Obx(
|
|
||||||
() => Container(
|
|
||||||
color: Colors.grey[50],
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
_buildTabItem(0, Icons.business_outlined, '站点信息'),
|
|
||||||
_buildTabItem(1, Icons.campaign_outlined, '站点广播'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
// 内容区域
|
|
||||||
Obx(
|
|
||||||
() => controller.selectedTabIndex.value == 0
|
|
||||||
? _buildStationInfo(context)
|
|
||||||
: _buildStationBroadcast(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建单个 Tab 项
|
|
||||||
Widget _buildTabItem(int index, IconData icon, String label) {
|
|
||||||
bool isSelected = controller.selectedTabIndex.value == index;
|
|
||||||
return Expanded(
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => controller.selectedTabIndex.value = index,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: isSelected ? Colors.blue : Colors.transparent,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 20, color: isSelected ? Colors.blue : Colors.grey[600]),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
||||||
color: isSelected ? Colors.blue : Colors.grey[600],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 站点信息子视图
|
|
||||||
Widget _buildStationInfo(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSectionTitle('基本信息'),
|
|
||||||
_buildDisplayField(label: '站点名称', value: controller.name),
|
|
||||||
_buildDisplayField(label: '运营企业', value: controller.operatingEnterprise),
|
|
||||||
_buildDisplayField(label: '站点地址', value: controller.address),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildSectionTitle('价格信息'),
|
|
||||||
_buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildSectionTitle('运营信息'),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('运营状态', style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
Text(
|
||||||
|
controller.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildStatusTag(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"站点:${controller.address}",
|
||||||
|
style: TextStyle(color: Colors.grey[500], fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async{
|
||||||
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[100],
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
),
|
||||||
|
icon: Badge(
|
||||||
|
smallSize: 8,
|
||||||
|
backgroundColor: controller.isNotice
|
||||||
|
? Colors.red
|
||||||
|
: Colors.transparent,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.notifications_outlined,
|
||||||
|
color: Colors.black87,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildStatBox("氢气价格", "Hydrogen price", controller.customerPrice, "/kg"),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
_buildStatBox("营业时间", "Opening time", controller.timeStr, ""),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
_buildStatBox("设备状态", "Anlagenzustand", "98", "%"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusTag() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE1F5FE),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
controller.selectedOperationStatus,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF03A9F4),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kBgColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 0.8),
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2. 站点基本信息
|
||||||
|
Widget _buildBasicInfoCard() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"站点基本信息",
|
||||||
|
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SizedBox(height: 15),
|
||||||
|
_buildInfoRow("站点名称", controller.name),
|
||||||
|
_buildInfoRow("运营企业", controller.operatingEnterprise),
|
||||||
|
_buildInfoRow("站点地址", controller.address),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 11.sp),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3. 运营信息/站点广播 Tab 及内容
|
||||||
|
Widget _buildOperationContentCard(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: hideKeyboard,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 自定义 TabBar
|
||||||
|
Obx(
|
||||||
|
() => Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildTabTitle(0, "运营信息"),
|
||||||
|
const SizedBox(width: 30),
|
||||||
|
_buildTabTitle(1, "站点广播"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => controller.selectedTabIndex.value == 0
|
||||||
|
? _buildOperatingForm(context)
|
||||||
|
: _buildBroadcastForm(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTabTitle(int index, String title) {
|
||||||
|
bool isSelected = controller.selectedTabIndex.value == index;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => controller.selectedTabIndex.value = index,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isSelected ? Colors.black87 : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
width: 25,
|
||||||
|
height: 3,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF00A870), // 效果图中的亮绿色横线
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOperatingForm(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'运营状态',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
//加氢站未执行的状态修改任务
|
//加氢站未执行的状态修改任务
|
||||||
if (controller.jobTipStr.isNotEmpty)
|
if (controller.jobTipStr.isNotEmpty)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@@ -204,157 +343,113 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
DropdownButtonFormField<String>(
|
// 状态网格选择
|
||||||
value: controller.selectedOperationStatus,
|
Wrap(
|
||||||
items: controller.operationStatusOptions.map((String value) {
|
spacing: 4,
|
||||||
return DropdownMenuItem<String>(value: value, child: Text(value));
|
runSpacing: 4,
|
||||||
|
children: controller.operationStatusOptions.map((status) {
|
||||||
|
bool isSelected = controller.selectedOperationStatus == status;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => controller.onOperationStatusChanged(status),
|
||||||
|
child: Container(
|
||||||
|
width: (Get.width - 80) / 2,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? kPrimaryColor : const Color(0xFFEBEBEB),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
status,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13.sp,
|
||||||
|
color: isSelected ? Colors.white : Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: controller.onOperationStatusChanged,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 12.h),
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (controller.selectedOperationStatus == "营运中")
|
if (controller.selectedOperationStatus == "营运中")
|
||||||
_buildDisplayField(label: '营业时间', value: controller.timeStr)
|
_buildDisplayField(label: '营业时间', value: controller.timeStr)
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildClickField(
|
_buildInputLabel("开始时间"),
|
||||||
label: '开始时间',
|
_buildDateTimePicker(
|
||||||
value: controller.customStartTimeStr,
|
controller.customStartTimeStr,
|
||||||
onTap: () => controller.pickDateTime(context, true),
|
() => controller.pickDateTime(context, true),
|
||||||
),
|
),
|
||||||
_buildClickField(
|
const SizedBox(height: 15),
|
||||||
label: '结束时间',
|
_buildInputLabel("结束时间"),
|
||||||
value: controller.customEndTimeStr,
|
_buildDateTimePicker(
|
||||||
onTap: () => controller.pickDateTime(context, false),
|
controller.customEndTimeStr,
|
||||||
|
() => controller.pickDateTime(context, false),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_buildDisplayField(label: '联系电话', value: controller.phone),
|
_buildDisplayField(label: '联系电话', value: controller.phone),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 25),
|
||||||
ElevatedButton(
|
|
||||||
onPressed: controller.saveInfo,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(double.infinity, 48),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
),
|
|
||||||
child: const Text('保存信息', style: TextStyle(fontSize: 16)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 站点广播子视图
|
|
||||||
Widget _buildStationBroadcast(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.campaign, color: Colors.blue, size: 28),
|
Expanded(
|
||||||
const SizedBox(width: 10),
|
flex: 1,
|
||||||
const Text(
|
child: OutlinedButton(
|
||||||
'站点广播通知',
|
onPressed: () {
|
||||||
style: TextStyle(
|
controller.renderData();
|
||||||
fontSize: 18,
|
}, // 重置逻辑
|
||||||
fontWeight: FontWeight.bold,
|
style: OutlinedButton.styleFrom(
|
||||||
color: Colors.black87,
|
side: const BorderSide(color: kPrimaryColor),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
|
||||||
),
|
|
||||||
const SizedBox(height: 13),
|
|
||||||
_buildTextFieldLabel('通知标题'),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
|
||||||
height: 45.h,
|
|
||||||
child: TextField(
|
|
||||||
controller: controller.broadcastTitleController,
|
|
||||||
maxLength: 30,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '例如:临时闭站通知',
|
|
||||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
counterText: '', // 隐藏原生计数器,我们可以按需自定义
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 15),
|
||||||
const SizedBox(height: 20),
|
Expanded(
|
||||||
_buildTextFieldLabel('通知内容'),
|
flex: 2,
|
||||||
const SizedBox(height: 8),
|
child: ElevatedButton(
|
||||||
TextField(
|
onPressed: controller.saveInfo,
|
||||||
controller: controller.broadcastContentController,
|
|
||||||
maxLength: 150,
|
|
||||||
maxLines: 5,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '请输入通知内容...',
|
|
||||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: controller.sendBroadcast,
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: kPrimaryColor,
|
||||||
foregroundColor: Colors.white,
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
minimumSize: const Size(double.infinity, 50),
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(10),
|
||||||
elevation: 0,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'发送',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
child: const Text("保存设置", style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextFieldLabel(String label) {
|
|
||||||
return Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建带标题的表单区域
|
|
||||||
Widget _buildSectionTitle(String title) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(width: 4, height: 16, color: Colors.blue),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建一个“标签+纯文本”的显示行
|
|
||||||
Widget _buildDisplayField({required String label, required String value}) {
|
Widget _buildDisplayField({required String label, required String value}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12.0),
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -374,118 +469,177 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建一个“可点击”的选择行
|
Widget _buildBroadcastForm() {
|
||||||
Widget _buildClickField({
|
|
||||||
required String label,
|
|
||||||
required String value,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
}) {
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12.0),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
_buildInputLabel("通知标题"),
|
||||||
const SizedBox(height: 8),
|
TextField(
|
||||||
InkWell(
|
controller: controller.broadcastTitleController,
|
||||||
|
decoration: _inputDecoration("例如:临时闭站通知"),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
_buildInputLabel("通知内容"),
|
||||||
|
TextField(
|
||||||
|
controller: controller.broadcastContentController,
|
||||||
|
maxLines: 4,
|
||||||
|
decoration: _inputDecoration("请输入通知内容..."),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.broadcastTitleController.clear();
|
||||||
|
controller.broadcastContentController.clear();
|
||||||
|
}, // 重置逻辑
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: kPrimaryColor),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text("重置", style: TextStyle(color: kPrimaryColor)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.sendBroadcast,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: kPrimaryColor,
|
||||||
|
minimumSize: const Size(double.infinity, 50),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: const Text("发送广播", style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInputLabel(String label) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 0, bottom: 8),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateTimePicker(String value, VoidCallback onTap) {
|
||||||
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
border: Border.all(color: const Color(0xFF00A870).withOpacity(0.5)),
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.blue.withOpacity(0.5)),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(value, style: const TextStyle(color: Colors.black87)),
|
||||||
|
const Icon(Icons.calendar_today_outlined, size: 18, color: Color(0xFF00A870)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDecoration _inputDecoration(String hint) {
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE0E0E0)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 4. 系统提醒
|
||||||
|
Widget _buildSystemTips() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF1F9F6), // 极浅绿色背景
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, color: Color.fromRGBO(1, 113, 55, 1), size: 20),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value,
|
"系统提醒",
|
||||||
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 1),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
Text(
|
||||||
|
"请您确保所提供的信息准确无误,价格信息也将实时\n更新至用户端",
|
||||||
|
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
Text(
|
||||||
|
"如有疑问请联系客服:400-021-1773",
|
||||||
|
style: TextStyle(color: Color.fromRGBO(1, 113, 55, 0.8), fontSize: 12.sp),
|
||||||
),
|
),
|
||||||
const Icon(Icons.calendar_month, size: 18, color: Colors.blue),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建静态提示信息卡片
|
/// 5. 退出登录按钮
|
||||||
Widget _buildTipsCard() {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildInfoItem(Icons.info_outline, '请确保信息准确无误'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoItem(Icons.help_outline, '价格信息将实时更新到用户端'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系技术支持: 400-021-1773'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: FutureBuilder<String>(
|
|
||||||
future: getVersion(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Text("");
|
|
||||||
}
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return TextX.labelSmall(
|
|
||||||
"当前版本: ${snapshot.data}",
|
|
||||||
color: Colors.black54,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const Text("");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建退出登录按钮
|
|
||||||
Widget _buildLogoutButton() {
|
Widget _buildLogoutButton() {
|
||||||
return ElevatedButton(
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: controller.logout,
|
onPressed: controller.logout,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Color.fromRGBO(204, 52, 46, 1),
|
||||||
foregroundColor: Colors.white,
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
||||||
minimumSize: const Size(double.infinity, 48),
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
||||||
elevation: 2,
|
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'退出登录',
|
"退出登录",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建带图标的提示信息行
|
|
||||||
Widget _buildInfoItem(IconData icon, String text) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.blue, size: 20),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(text, style: const TextStyle(fontSize: 14, color: Colors.black54)),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:getx_scaffold/getx_scaffold.dart';
|
|||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/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)
|
||||||
@@ -143,6 +144,8 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
Timer? _refreshTimer;
|
Timer? _refreshTimer;
|
||||||
|
|
||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
bool isNotice = false;
|
||||||
|
final RefreshController refreshController = RefreshController(initialRefresh: false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get listenLifecycleEvent => true;
|
bool get listenLifecycleEvent => true;
|
||||||
@@ -167,7 +170,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
searchController.dispose();
|
searchController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
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,
|
||||||
@@ -199,6 +202,8 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onRefresh() => renderData(isRefresh: true);
|
||||||
|
|
||||||
///停止定时器的方法
|
///停止定时器的方法
|
||||||
void stopAutoRefresh() {
|
void stopAutoRefresh() {
|
||||||
// 如果定时器存在并且是激活状态,就取消它
|
// 如果定时器存在并且是激活状态,就取消它
|
||||||
@@ -221,7 +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
|
'stationId': StorageService.to.userId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -587,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}',
|
||||||
@@ -620,6 +625,10 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
} finally {
|
} finally {
|
||||||
//加载列表数据
|
//加载列表数据
|
||||||
fetchReservationData();
|
fetchReservationData();
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
refreshController.refreshCompleted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/history/view.dart';
|
import 'package:ln_jq_app/pages/b_page/history/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/message/view.dart' show MessagePage;
|
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';
|
||||||
|
|
||||||
@@ -16,32 +18,151 @@ class SitePage extends GetView<SiteController> {
|
|||||||
init: SiteController(),
|
init: SiteController(),
|
||||||
id: 'site',
|
id: 'site',
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return SingleChildScrollView(child: _buildView());
|
return Scaffold(
|
||||||
|
backgroundColor: Color.fromRGBO(247, 249, 251, 1),
|
||||||
|
body: SmartRefresher(
|
||||||
|
controller: controller.refreshController,
|
||||||
|
enablePullUp: false,
|
||||||
|
onRefresh: controller.onRefresh,
|
||||||
|
child: SingleChildScrollView(child: _buildView(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主视图
|
// 主视图
|
||||||
Widget _buildView() {
|
Widget _buildView(BuildContext context) {
|
||||||
return Padding(
|
return Column(
|
||||||
padding: const EdgeInsets.all(12.0),
|
children: [
|
||||||
|
// 第一个卡片: 今日预约统计
|
||||||
|
_buildTopSection(context),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 20.w, right: 20.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// 第一个卡片: 今日预约统计
|
Padding(
|
||||||
Card(
|
padding: EdgeInsets.only(top: 17.h, bottom: 10.h),
|
||||||
elevation: 3,
|
child: Row(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
children: [
|
||||||
child: Padding(
|
Text(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
"预约信息",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Get.to(
|
||||||
|
() => HistoryPage(),
|
||||||
|
arguments: {'stationName': controller.name},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'历史记录',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color.fromRGBO(156, 163, 175, 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 第二个卡片: 预约信息
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
_buildSearchView(),
|
||||||
|
controller.hasReservationData
|
||||||
|
? _buildReservationListView()
|
||||||
|
: _buildEmptyReservationView(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 35.h),
|
||||||
|
//第三部分
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF1F9F6), // 极浅绿色背景
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 1),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"系统提醒",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 1),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
Text(
|
||||||
|
"数据每五分钟自动刷新,如需实时更新请下拉页面",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 0.8),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.h),
|
||||||
|
Text(
|
||||||
|
"如有疑问请联系客服:400-021-1773",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 0.8),
|
||||||
|
fontSize: 12.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 75.h),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTopSection(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: MediaQuery.of(context).padding.top + 10,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 25,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.calendar_today, color: Colors.blue, size: 32),
|
CircleAvatar(
|
||||||
|
radius: 25,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
child: LoginUtil.getAssImg('ic_user_logo@2x'),
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -50,230 +171,186 @@ class SitePage extends GetView<SiteController> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'今日预约统计',
|
"今日预约统计",
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w400),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 5.w,),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'实时',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
"Today's Reservation Statistics",
|
"Today's Reservation Statistics",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
style: TextStyle(color: Colors.grey[500], fontSize: 13),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// 跳转消息中心
|
|
||||||
var scanResult = await Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
if (scanResult == null) {
|
if (scanResult == null) {
|
||||||
controller.msgNotice();
|
controller.msgNotice();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 这里的 style 是为了模拟你图片里的灰色圆形背景
|
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
),
|
),
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
// label: Text('3'), // 如果你想显示数字,就加 label
|
|
||||||
smallSize: 8,
|
smallSize: 8,
|
||||||
// 红点的大小
|
backgroundColor: controller.isNotice ? Colors.red : Colors.transparent,
|
||||||
backgroundColor: controller.isNotice
|
child: const Icon(
|
||||||
? Colors.red
|
|
||||||
: Colors.white,
|
|
||||||
// 红点颜色
|
|
||||||
child: Icon(
|
|
||||||
Icons.notifications_outlined,
|
Icons.notifications_outlined,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
size: 25,
|
size: 30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 25),
|
||||||
const SizedBox(height: 10),
|
Row(
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
children: [
|
||||||
_buildStatItem(controller.leftHydrogen, '剩余余量'),
|
_buildStatBox("剩余氢量", "remaining quantity", controller.leftHydrogen, "kg"),
|
||||||
_buildStatItem(controller.orderAmount, '预约车辆'),
|
SizedBox(width: 4.w),
|
||||||
_buildStatItem(controller.completedAmount, '已完成'),
|
_buildStatBox(
|
||||||
],
|
"今日加氢量",
|
||||||
),
|
"Have been added",
|
||||||
),
|
controller.orderTotalAmount,
|
||||||
const SizedBox(height: 12),
|
"kg",
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
_buildStatItem(controller.orderTotalAmount, '加氢总量'),
|
|
||||||
_buildStatItem(controller.orderUnfinishedAmount, '未加氢总量'),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
_buildStatBox(
|
||||||
|
"未加氢总量",
|
||||||
|
"No quantity added",
|
||||||
|
controller.orderUnfinishedAmount,
|
||||||
|
"kg",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 第二个卡片: 预约信息
|
|
||||||
Card(
|
|
||||||
elevation: 3,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
|
||||||
margin: EdgeInsets.only(bottom: 12),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color: Colors.blue,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
controller.renderData();
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'预约信息',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.refresh,
|
|
||||||
size: 18,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Get.to(
|
|
||||||
() => HistoryPage(),
|
|
||||||
arguments: {'stationName': controller.name},
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue.shade700,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
elevation: 2,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'历史记录',
|
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildSearchView(),
|
|
||||||
controller.hasReservationData
|
|
||||||
? _buildReservationListView()
|
|
||||||
: _buildEmptyReservationView(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
//第三部分
|
Widget _buildStatBox(String title, String enTitle, String value, String unit) {
|
||||||
Card(
|
return Expanded(
|
||||||
elevation: 3,
|
child: Container(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
|
padding: EdgeInsets.only(left: 12.w, top: 4.h, bottom: 4.h),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
decoration: BoxDecoration(
|
||||||
child: Padding(
|
color: Color(0xFFF5F7F9),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoItem(Icons.info_outline, '数据每5分钟自动刷新一次'),
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 0.8),
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(enTitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(unit, style: const TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//搜索输入框,提示可以输入车牌或者手机
|
//搜索输入框,提示可以输入车牌或者手机
|
||||||
Widget _buildSearchView() {
|
Widget _buildSearchView() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
padding: EdgeInsets.fromLTRB(0, 0, 0, 8.h),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 44,
|
height: 42.h,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
controller: controller.searchController, // 绑定控制器
|
controller: controller.searchController, // 绑定控制器
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '输入车牌号或完整手机号查询',
|
filled: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
isDense: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
hintText: '输入车牌号或手机号搜索...',
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
borderSide: BorderSide(color: Color.fromRGBO(229, 231, 235, 1)),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(color: Get.theme.primaryColor, width: 1.5),
|
borderSide: BorderSide(
|
||||||
|
color: Color.fromRGBO(229, 231, 235, 1),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// 清除按钮
|
// 清除按钮
|
||||||
suffixIcon: IconButton(
|
suffix: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
icon: const Icon(Icons.clear, size: 20),
|
icon: const Icon(Icons.clear, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.searchController.clear();
|
controller.searchController.clear();
|
||||||
controller.fetchReservationData(); // 清除后也刷新一次
|
controller.fetchReservationData(); // 清除后也刷新一次
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// 点击“搜索”按钮时触发
|
||||||
|
FocusScope.of(Get.context!).unfocus(); // 收起键盘
|
||||||
|
controller.fetchReservationData();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 16.w,
|
||||||
|
right: 16.h,
|
||||||
|
top: 4.5.h,
|
||||||
|
bottom: 4.5.h,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"搜索",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 12.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
// 用户在键盘上点击“完成”或“搜索”时触发
|
// 用户在键盘上点击“完成”或“搜索”时触发
|
||||||
@@ -282,40 +359,6 @@ class SitePage extends GetView<SiteController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
// 点击“搜索”按钮时触发
|
|
||||||
FocusScope.of(Get.context!).unfocus(); // 收起键盘
|
|
||||||
controller.fetchReservationData();
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.search_rounded),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建单个统计项
|
|
||||||
Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) {
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: valueColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -330,14 +373,17 @@ class SitePage extends GetView<SiteController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.inventory_2_outlined, size: 80, color: Colors.grey[300]),
|
LoginUtil.getAssImg("ic_no_data@2x"),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: 16.h),
|
||||||
const Text('暂无预约数据', style: TextStyle(fontSize: 16, color: Colors.black54)),
|
Text(
|
||||||
const SizedBox(height: 8),
|
'暂无订单',
|
||||||
const Text(
|
style: TextStyle(
|
||||||
'点击右上角刷新按钮获取最新数据',
|
fontSize: 16.sp,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
color: Colors.black54,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -345,134 +391,295 @@ class SitePage extends GetView<SiteController> {
|
|||||||
|
|
||||||
/// 构建“有预约数据”的列表视图
|
/// 构建“有预约数据”的列表视图
|
||||||
Widget _buildReservationListView() {
|
Widget _buildReservationListView() {
|
||||||
return Container(
|
return ListView.separated(
|
||||||
color: Colors.white,
|
|
||||||
child: ListView.separated(
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
// 因为外层已有滚动,这里禁用内部滚动
|
// 因为外层已有滚动,这里禁用内部滚动
|
||||||
itemCount: controller.reservationList.length,
|
itemCount: controller.reservationList.length,
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = controller.reservationList[index];
|
final item = controller.reservationList[index];
|
||||||
// 调用新的方法来构建每一项
|
// 调用新的方法来构建每一项
|
||||||
return _buildReservationItem(index, item);
|
return _buildReservationItem(index, item);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 12), // 列表项之间的间距
|
separatorBuilder: (context, index) => const SizedBox(height: 0), // 列表项之间的间距
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReservationItem(int index, ReservationModel item) {
|
Widget _buildReservationItem(int index, ReservationModel item) {
|
||||||
|
const kPrimaryGreen = Color(0xFF006D35); // 深绿
|
||||||
|
const kLineColor = Color(0xFFE0E6ED); // 线条颜色
|
||||||
|
|
||||||
|
return IntrinsicHeight(
|
||||||
|
// 保证左侧竖线能跟右侧内容高度对齐
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 1. 左侧样式区域(竖线 + 圆点)
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 顶部的装饰圆点
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 3.w),
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kPrimaryGreen.withOpacity(0.5),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 延伸的竖线
|
||||||
|
Expanded(child: Container(width: 1, color: kLineColor)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 2. 右侧内容区域(时间 + 卡片数据)
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 10.w, bottom: 8.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 时间显示
|
||||||
|
Text(
|
||||||
|
item.time, // 格式如 "09:00 - 10:00"
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF333333),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// 数据卡片
|
||||||
|
_buildInfoCard(item),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 右侧具体数据卡片
|
||||||
|
Widget _buildInfoCard(ReservationModel item) {
|
||||||
return Container(
|
return Container(
|
||||||
|
padding: EdgeInsets.only(left: 16.w, top: 8.5, bottom: 8.5, right: 16.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: Colors.grey[200]!, width: 1.0),
|
border: Border.all(color: const Color(0xFFF0F0F0)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withOpacity(0.1),
|
color: Colors.black.withOpacity(0.04),
|
||||||
spreadRadius: 1,
|
blurRadius: 10,
|
||||||
blurRadius: 3,
|
offset: const Offset(0, 4),
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 顶部:序号、车牌号、状态
|
// 车牌与状态标签
|
||||||
Padding(
|
Row(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 12, 16, 12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
// 序号
|
Text(
|
||||||
Container(
|
item.plateNumber,
|
||||||
width: 24,
|
style: TextStyle(
|
||||||
height: 24,
|
fontSize: 14.sp,
|
||||||
alignment: Alignment.center,
|
fontWeight: FontWeight.bold,
|
||||||
decoration: const BoxDecoration(
|
color: Color(0xFF333333),
|
||||||
color: Colors.blue,
|
),
|
||||||
shape: BoxShape.circle,
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildStatusTag(item.status),
|
||||||
|
Spacer(),
|
||||||
|
Text(
|
||||||
|
"预约量:${item.amount}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF00A870),
|
||||||
|
fontSize: 13.sp,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 联系信息
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${item.contactPerson} | ${item.contactPhone}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF999999),
|
||||||
|
fontSize: 13.sp,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
//操作按钮(仅在待处理状态显示)
|
||||||
|
if (item.status == ReservationStatus.pending) ...[
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
const Divider(height: 1, color: Color(0xFFF5F5F5)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
_buildSmallButton(
|
||||||
|
"拒绝",
|
||||||
|
isOutline: true,
|
||||||
|
onTap: () {
|
||||||
|
controller.rejectReservation(item.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildSmallButton(
|
||||||
|
"确认",
|
||||||
|
isOutline: false,
|
||||||
|
onTap: () {
|
||||||
|
controller.confirmReservation(item.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通用小按钮
|
||||||
|
Widget _buildSmallButton(
|
||||||
|
String text, {
|
||||||
|
required bool isOutline,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
const kPrimaryGreen = Color(0xFF006D35);
|
||||||
|
const kDangerRed = Color(0xFFFF7D7D);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isOutline ? Colors.white : kPrimaryGreen,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: isOutline ? kDangerRed : kPrimaryGreen),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${index + 1}",
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: isOutline ? kDangerRed : Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 状态标签构建(带圆角和浅色背景)
|
||||||
|
Widget _buildStatusTag(ReservationStatus status) {
|
||||||
|
Color textColor;
|
||||||
|
Color bgColor;
|
||||||
|
String text;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case ReservationStatus.pending:
|
||||||
|
text = "待加氢";
|
||||||
|
textColor = const Color(0xFFFE9E62); // 橘色
|
||||||
|
bgColor = const Color(0xFFFFF2E9);
|
||||||
|
break;
|
||||||
|
case ReservationStatus.completed: // 假设已加氢状态
|
||||||
|
text = "已加氢";
|
||||||
|
textColor = const Color(0xFF00A870); // 绿色
|
||||||
|
bgColor = const Color(0xFFE6F7F1);
|
||||||
|
break;
|
||||||
|
case ReservationStatus.rejected:
|
||||||
|
text = "拒绝加氢";
|
||||||
|
textColor = const Color(0xFFFF7D7D); // 红色
|
||||||
|
bgColor = const Color(0xFFFFEEEE);
|
||||||
|
break;
|
||||||
|
case ReservationStatus.unadded:
|
||||||
|
text = '未加氢';
|
||||||
|
textColor = const Color(0xFFFF7D7D); // 红色
|
||||||
|
bgColor = const Color(0xFFFFEEEE);
|
||||||
|
break;
|
||||||
|
case ReservationStatus.cancel:
|
||||||
|
text = '已取消';
|
||||||
|
textColor = const Color(0xFFFF7D7D); // 红色
|
||||||
|
bgColor = const Color(0xFFFFEEEE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text = "未知";
|
||||||
|
textColor = Colors.grey;
|
||||||
|
bgColor = Colors.grey[100]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: textColor.withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(color: textColor, fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 右侧操作按钮(拒绝/确认)
|
||||||
|
Widget _buildActionButtons(ReservationModel item) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 拒绝按钮(空心)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => controller.rejectReservation(item.id),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: const Color(0xFFFF7D7D)),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"拒绝",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFFFF7D7D),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Icon(Icons.directions_car, color: Colors.black54),
|
// 确认按钮(实心深绿)
|
||||||
const SizedBox(width: 4),
|
GestureDetector(
|
||||||
// 车牌号
|
onTap: () => controller.confirmReservation(item.id),
|
||||||
Expanded(
|
child: Container(
|
||||||
child: Text(
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
item.plateNumber,
|
decoration: BoxDecoration(
|
||||||
style: const TextStyle(
|
color: const Color(0xFF006D35),
|
||||||
fontSize: 16,
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"确认",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppTheme.themeColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 状态标签
|
|
||||||
_buildStatusChip(item.status),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
|
||||||
// 中部:详细信息
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildDetailRow(
|
|
||||||
Icons.local_gas_station,
|
|
||||||
'加氢量',
|
|
||||||
item.amount,
|
|
||||||
valueColor: Colors.red,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildDetailRow(Icons.access_time, '预约时间', item.time),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildDetailRow(Icons.person, '联系人', item.contactPerson),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildDetailRow(
|
|
||||||
Icons.phone,
|
|
||||||
'联系电话',
|
|
||||||
item.contactPhone,
|
|
||||||
valueColor: AppTheme.themeColor,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 底部:操作按钮 (仅在待处理时显示)
|
|
||||||
if (item.status == ReservationStatus.pending)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => controller.confirmReservation(item.id),
|
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
|
|
||||||
child: const Text('确认'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () => controller.rejectReservation(item.id),
|
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
|
||||||
child: const Text('拒绝'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getx_scaffold/common/index.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
|
import 'package:ln_jq_app/pages/c_page/car_info/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/map/view.dart';
|
import 'package:ln_jq_app/pages/c_page/map/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
|
import 'package:ln_jq_app/pages/c_page/mine/view.dart';
|
||||||
@@ -12,6 +13,7 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
BaseWidgetsPage({super.key});
|
BaseWidgetsPage({super.key});
|
||||||
|
|
||||||
final PageController _pageController = PageController();
|
final PageController _pageController = PageController();
|
||||||
|
|
||||||
// 主视图
|
// 主视图
|
||||||
Widget _buildView() {
|
Widget _buildView() {
|
||||||
return PageView(
|
return PageView(
|
||||||
@@ -20,54 +22,68 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
onPageChanged: (index) {
|
onPageChanged: (index) {
|
||||||
jumpTabAndPage(index);
|
jumpTabAndPage(index);
|
||||||
},
|
},
|
||||||
children: _buildPages(), // 页面的列表
|
children: _buildPages(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void jumpTabAndPage(int index) {
|
void jumpTabAndPage(int index) {
|
||||||
controller.pageIndex = index; // 更新页面索引
|
controller.pageIndex = index;
|
||||||
controller.updateUi(); // 更新 UI
|
controller.updateUi();
|
||||||
_pageController.jumpToPage(controller.pageIndex);
|
_pageController.jumpToPage(controller.pageIndex);
|
||||||
}
|
}
|
||||||
// 对应的页面
|
|
||||||
List<Widget> _buildPages() {
|
List<Widget> _buildPages() {
|
||||||
return [
|
return [ReservationPage(), MapPage(), CarInfoPage(), MinePage()];
|
||||||
ReservationPage(),
|
|
||||||
MapPage(),
|
|
||||||
CarInfoPage(),
|
|
||||||
MinePage(),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//导航栏
|
// 自定义导航栏 (悬浮胶囊样式)
|
||||||
Widget _buildNavigationBar() {
|
Widget _buildNavigationBar() {
|
||||||
return NavigationX(
|
return SafeArea(
|
||||||
currentIndex: controller.pageIndex, // 当前选中的tab索引
|
child: Container(
|
||||||
onTap: (index) {
|
height: 50.h,
|
||||||
jumpTabAndPage(index);
|
margin: const EdgeInsets.fromLTRB(24, 0, 24, 10), // 悬浮边距
|
||||||
}, // 切换tab事件
|
decoration: BoxDecoration(
|
||||||
items: [
|
color: Color.fromRGBO(240, 244, 247, 1), // 浅灰色背景
|
||||||
NavigationItemModel(
|
borderRadius: BorderRadius.circular(30),
|
||||||
label: '加氢预约',
|
boxShadow: [
|
||||||
icon: AntdIcon.orderedlist,
|
BoxShadow(
|
||||||
selectedIcon: AntdIcon.calendar_fill,
|
color: Colors.black.withOpacity(0.05),
|
||||||
),
|
blurRadius: 10,
|
||||||
NavigationItemModel(
|
offset: const Offset(0, 5),
|
||||||
label: '地图',
|
|
||||||
icon: AntdIcon.location,
|
|
||||||
selectedIcon: AntdIcon.location_fill,
|
|
||||||
),
|
|
||||||
NavigationItemModel(
|
|
||||||
label: '车辆信息',
|
|
||||||
icon: AntdIcon.car,
|
|
||||||
selectedIcon: AntdIcon.car_fill,
|
|
||||||
),
|
|
||||||
NavigationItemModel(
|
|
||||||
label: '我的',
|
|
||||||
icon: AntdIcon.user,
|
|
||||||
selectedIcon: AntdIcon.user,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildNavItem(0, "ic_h2_select@2x", "ic_h2@2x"),
|
||||||
|
_buildNavItem(1, "ic_map_select@2x", "ic_map@2x"),
|
||||||
|
_buildNavItem(2, "ic_car_select@2x", "ic_car@2x"),
|
||||||
|
_buildNavItem(3, "ic_user_select@2x", "ic_user@2x"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建单个导航项
|
||||||
|
Widget _buildNavItem(int index, String icon, String selectedIcon) {
|
||||||
|
bool isSelected = controller.pageIndex == index;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => jumpTabAndPage(index),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? const Color(0xFF006633) : Colors.transparent, // 选中时的深绿色背景
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: LoginUtil.getAssImg(isSelected ? selectedIcon : icon),),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +94,10 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
|
|||||||
id: 'baseWidgets',
|
id: 'baseWidgets',
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBody: false,
|
extendBody: true, // 重要:让 body 延伸到导航栏后面
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
bottomNavigationBar: _buildNavigationBar(),
|
bottomNavigationBar: _buildNavigationBar(),
|
||||||
body: SafeArea(child: _buildView()),
|
body: _buildView(), // 移除 SafeArea 以获得更好的全屏沉浸感
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ class AttachmentViewerPage extends GetView<AttachmentViewerController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(AttachmentViewerController());
|
Get.put(AttachmentViewerController());
|
||||||
final fileName = controller.url.split('/').last;
|
// final fileName = controller.url.split('/').last;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
fileName,
|
"证件详情",
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
|
|
||||||
import 'attachment_viewer_page.dart';
|
import 'attachment_viewer_page.dart';
|
||||||
|
|
||||||
class CertificateViewerController extends GetxController with BaseControllerMixin{
|
class CertificateViewerController extends GetxController with BaseControllerMixin {
|
||||||
late final String title;
|
late final String title;
|
||||||
late final List<String> attachments;
|
late final List<String> attachments;
|
||||||
|
|
||||||
@@ -78,18 +78,11 @@ class CertificateViewerController extends GetxController with BaseControllerMixi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.to(
|
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
|
||||||
() => const AttachmentViewerPage(),
|
|
||||||
arguments: {
|
|
||||||
'url': url,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查 URL 是否为 PDF (此方法保持不变)
|
/// 检查 URL 是否为 PDF (此方法保持不变)
|
||||||
bool isPdf(String url) {
|
bool isPdf(String url) {
|
||||||
return url.toLowerCase().endsWith('.pdf');
|
return url.toLowerCase().endsWith('.pdf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:get/get.dart';
|
|||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/model/base_model.dart';
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
||||||
|
import 'package:ln_jq_app/pages/c_page/car_info/attachment_viewer_page.dart';
|
||||||
import 'package:ln_jq_app/pages/qr_code/view.dart';
|
import 'package:ln_jq_app/pages/qr_code/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'certificate_viewer_page.dart';
|
import 'certificate_viewer_page.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
class CarInfoController extends GetxController with BaseControllerMixin {
|
class CarInfoController extends GetxController with BaseControllerMixin {
|
||||||
@override
|
@override
|
||||||
@@ -22,11 +24,38 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
final RxList<String> operationAttachments = <String>[].obs;
|
final RxList<String> operationAttachments = <String>[].obs;
|
||||||
final RxList<String> hydrogenationAttachments = <String>[].obs;
|
final RxList<String> hydrogenationAttachments = <String>[].obs;
|
||||||
final RxList<String> registerAttachments = <String>[].obs;
|
final RxList<String> registerAttachments = <String>[].obs;
|
||||||
|
String color = "";
|
||||||
|
String hydrogenCapacity = "";
|
||||||
|
String rentFromCompany = "";
|
||||||
|
String address = "";
|
||||||
|
bool isNotice = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
getUserBindCarInfo();
|
getUserBindCarInfo();
|
||||||
|
msgNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> msgNotice() async {
|
||||||
|
final Map<String, dynamic> requestData = {
|
||||||
|
'appFlag': 1,
|
||||||
|
'isRead': 1,
|
||||||
|
'pageNum': 1,
|
||||||
|
'pageSize': 5,
|
||||||
|
};
|
||||||
|
final response = await HttpService.to.get(
|
||||||
|
'appointment/unread_notice/page',
|
||||||
|
params: requestData,
|
||||||
|
);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
String total = result.data["total"].toString();
|
||||||
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -96,6 +125,21 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
parseAttachments(data['hydrogenationAttachment']),
|
parseAttachments(data['hydrogenationAttachment']),
|
||||||
);
|
);
|
||||||
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
|
registerAttachments.assignAll(parseAttachments(data['registerAttachment']));
|
||||||
|
|
||||||
|
// 初始化时开始加载所有PDF
|
||||||
|
attachments = [
|
||||||
|
...drivingAttachments,
|
||||||
|
...operationAttachments,
|
||||||
|
...hydrogenationAttachments,
|
||||||
|
...registerAttachments,
|
||||||
|
];
|
||||||
|
|
||||||
|
color = data['color'].toString();
|
||||||
|
hydrogenCapacity = data['hydrogenCapacity'].toString();
|
||||||
|
rentFromCompany = data['rentFromCompany'].toString();
|
||||||
|
address = data['address'].toString();
|
||||||
|
|
||||||
|
loadAllPdfs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateUi();
|
updateUi();
|
||||||
@@ -117,4 +161,69 @@ class CarInfoController extends GetxController with BaseControllerMixin {
|
|||||||
arguments: {'title': title, 'attachments': attachments},
|
arguments: {'title': title, 'attachments': attachments},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 导航到通用的附件查看器页面
|
||||||
|
void openAttachment(String url) {
|
||||||
|
if (url.isEmpty) {
|
||||||
|
showErrorToast('附件链接无效');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.to(() => const AttachmentViewerPage(), arguments: {'url': url});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 URL 是否为 PDF
|
||||||
|
bool isPdf(String url) {
|
||||||
|
return url.toLowerCase().endsWith('.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> attachments = [];
|
||||||
|
|
||||||
|
// --- 新增: 状态管理 ---
|
||||||
|
/// 用于存储网络PDF的本地路径,key是网络url,value是本地路径
|
||||||
|
final RxMap<String, String> localPdfPaths = <String, String>{}.obs;
|
||||||
|
|
||||||
|
/// 用于跟踪每个附件的加载状态,key是网络url
|
||||||
|
final RxMap<String, bool> isLoading = <String, bool>{}.obs;
|
||||||
|
|
||||||
|
/// 遍历所有附件,如果是PDF则进行下载
|
||||||
|
void loadAllPdfs() {
|
||||||
|
for (var url in attachments) {
|
||||||
|
if (isPdf(url)) {
|
||||||
|
_downloadPdf(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载单个PDF文件
|
||||||
|
Future<void> _downloadPdf(String url) async {
|
||||||
|
if (url.isEmpty) return;
|
||||||
|
|
||||||
|
// 开始加载
|
||||||
|
isLoading[url] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final dio = Dio();
|
||||||
|
final Directory tempDir = await getTemporaryDirectory();
|
||||||
|
final String savePath = '${tempDir.path}/${url.split('/').last}';
|
||||||
|
|
||||||
|
// 检查文件是否已存在,避免重复下载
|
||||||
|
if (await File(savePath).exists()) {
|
||||||
|
localPdfPaths[url] = savePath;
|
||||||
|
isLoading[url] = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dio.download(url, savePath);
|
||||||
|
|
||||||
|
// 下载成功后,更新本地路径
|
||||||
|
localPdfPaths[url] = savePath;
|
||||||
|
} catch (e) {
|
||||||
|
print('PDF download error for $url: $e');
|
||||||
|
// 出错时也可以更新状态,以便UI显示错误提示
|
||||||
|
} finally {
|
||||||
|
// 结束加载
|
||||||
|
isLoading[url] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/qr_code/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
import '../../../common/styles/theme.dart';
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
|
|
||||||
class CarInfoPage extends GetView<CarInfoController> {
|
class CarInfoPage extends GetView<CarInfoController> {
|
||||||
@@ -16,78 +20,259 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
id: 'car_info',
|
id: 'car_info',
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: const Color.fromRGBO(240, 244, 247, 0.4),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(12.0),
|
children: [
|
||||||
|
_buildUserInfoCard(),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 20.w, right: 20.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildDriverInfoCard(),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 5),
|
_buildCarInfoCard(),
|
||||||
_buildCarBindingCard(),
|
_buildCertificatesCard(context),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 12),
|
||||||
_buildCertificatesCard(),
|
_buildSafetyReminderCard(),
|
||||||
const SizedBox(height: 5),
|
SizedBox(height: 95.h),
|
||||||
_buildTipsCard(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建顶部的司机信息卡片
|
Widget _buildUserInfoCard() {
|
||||||
Widget _buildDriverInfoCard() {
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
|
color: Colors.white,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(20),
|
||||||
|
bottomRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 50),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const CircleAvatar(
|
Stack(
|
||||||
radius: 20,
|
children: [
|
||||||
backgroundColor: Colors.blue,
|
CircleAvatar(
|
||||||
child: Icon(Icons.person, color: Colors.white, size: 34),
|
radius: 25,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
child: LoginUtil.getAssImg('ic_user_logo@2x'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 16.h,
|
||||||
|
width: 16.w,
|
||||||
|
child: LoginUtil.getAssImg('ic_logo@2x'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${StorageService.to.name}",
|
"${StorageService.to.name}",
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color.fromRGBO(236, 255, 234, 1),
|
||||||
|
border: Border.all(color: const Color(0xFFB7E19F)),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
|
||||||
|
SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
"${StorageService.to.phone}",
|
"绿色先锋",
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 11),
|
style: TextStyle(
|
||||||
|
color: Color(0xFF52C41A),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
],
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue[50],
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.blue, width: 0.5),
|
|
||||||
),
|
),
|
||||||
child: const Row(
|
const SizedBox(height: 4),
|
||||||
children: [
|
|
||||||
Icon(Icons.shield_outlined, color: Colors.blue, size: 14),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
Text(
|
||||||
'已认证',
|
"羚牛ID:${StorageService.to.phone}",
|
||||||
style: TextStyle(
|
style: const TextStyle(color: Colors.grey, fontSize: 11),
|
||||||
color: Colors.blue,
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async{
|
||||||
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
|
if (scanResult == null) {
|
||||||
|
controller.msgNotice();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[100],
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
),
|
||||||
|
icon: Badge(
|
||||||
|
smallSize: 8,
|
||||||
|
backgroundColor: controller.isNotice
|
||||||
|
? Colors.red
|
||||||
|
: Colors.transparent,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.notifications_outlined,
|
||||||
|
color: Colors.black87,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildModernStatItem('本月里程数', 'Accumulated', '2,852km', ''),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildModernStatItem('总里程', 'Refuel Count', "2.5W km", ''),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildModernStatItem('服务评分', 'Driver rating', "4.9分", ''),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8F9FA),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCarInfoCard() {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildDetailRow('车牌号', controller.plateNumber, isPlate: true),
|
||||||
|
const SizedBox(height: 11),
|
||||||
|
_buildDetailRow('车架号', controller.vin),
|
||||||
|
const SizedBox(height: 11),
|
||||||
|
_buildDetailRow('车辆型号', controller.modelName),
|
||||||
|
const SizedBox(height: 11),
|
||||||
|
_buildDetailRow('车辆品牌', controller.brandName),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildH2LevelProgress(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailRow(String label, String value, {bool isPlate = false}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isPlate)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => controller.doQrCode(),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(right: 10),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: const Color.fromRGBO(71, 174, 208, 1)),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: const Color.fromRGBO(235, 250, 255, 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.sync,
|
||||||
|
size: 12,
|
||||||
|
color: Color.fromRGBO(71, 174, 208, 1),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
StorageService.to.hasVehicleInfo ? "换车牌" : "扫码绑定",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color.fromRGBO(71, 174, 208, 1),
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -95,227 +280,275 @@ class CarInfoPage extends GetView<CarInfoController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
value,
|
||||||
Padding(
|
style: const TextStyle(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
fontSize: 14,
|
||||||
child: Row(
|
fontWeight: FontWeight.w600,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
color: Colors.black87,
|
||||||
children: [
|
|
||||||
_buildStatItem('156', '服务天数'),
|
|
||||||
_buildStatItem('4.9', '评分'),
|
|
||||||
_buildStatItem('98%', '准时率'),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 司机信息卡片中的小统计项
|
Widget _buildH2LevelProgress() {
|
||||||
Widget _buildStatItem(String value, String label) {
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: const LinearProgressIndicator(
|
||||||
|
value: 0.75,
|
||||||
|
minHeight: 8,
|
||||||
|
backgroundColor: Color(0xFFF0F2F5),
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color.fromRGBO(16, 185, 129, 1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("H2 Level", style: TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
Text(
|
Text(
|
||||||
value,
|
"75%",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 11,
|
||||||
|
color: Color.fromRGBO(16, 185, 129, 1),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.blue,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
],
|
||||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建车辆绑定信息卡片
|
/// 3. 构建车辆证件卡片 (重构为 TabView 样式)
|
||||||
Widget _buildCarBindingCard() {
|
Widget _buildCertificatesCard(BuildContext context) {
|
||||||
return Card(
|
return DefaultTabController(
|
||||||
elevation: 2,
|
length: 4,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildInfoRow('车牌号: ${controller.plateNumber}', '扫码绑定'),
|
TabBar(
|
||||||
const SizedBox(height: 12),
|
isScrollable: false,
|
||||||
_buildInfoRow('车架号:', '${controller.vin}'),
|
indicatorColor: Color.fromRGBO(16, 185, 129, 1),
|
||||||
const SizedBox(height: 12),
|
labelColor: Color.fromRGBO(16, 185, 129, 1),
|
||||||
_buildInfoRow('车辆型号:', '${controller.modelName}'),
|
unselectedLabelColor: Colors.grey,
|
||||||
const SizedBox(height: 12),
|
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
_buildInfoRow('车辆品牌:', '${controller.brandName}'),
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: '行驶证'),
|
||||||
|
Tab(text: '营运证'),
|
||||||
|
Tab(text: '加氢证'),
|
||||||
|
Tab(text: '登记证'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 9),
|
||||||
|
SizedBox(
|
||||||
|
height: 356.h, // 给定一个高度,或者使用别的方式布局
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
_buildCertificateContent('行驶证', controller.drivingAttachments),
|
||||||
|
_buildCertificateContent('营运证', controller.operationAttachments),
|
||||||
|
_buildCertificateContent('加氢资格证', controller.hydrogenationAttachments),
|
||||||
|
_buildCertificateContent('登记证', controller.registerAttachments),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(Icons.propane_rounded, size: 50, color: Colors.blue.withOpacity(0.5)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 车辆绑定卡片中的信息行
|
/// 构建单个证件的展示内容
|
||||||
Widget _buildInfoRow(String label, String value) {
|
Widget _buildCertificateContent(String title, RxList<String> attachments) {
|
||||||
bool isButton = value == '扫码绑定';
|
return Obx(() {
|
||||||
return Row(
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: attachments.isEmpty
|
||||||
|
? const Center(child: Text('暂无相关证件信息'))
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(fontSize: 13)),
|
//证件文字
|
||||||
const SizedBox(width: 8),
|
Row(
|
||||||
isButton
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
? GestureDetector(
|
children: [
|
||||||
onTap: () async {
|
_buildCertDetailItem('所属公司', controller.rentFromCompany, isFull: false),
|
||||||
controller.doQrCode();
|
_buildCertDetailItem('运营城市', controller.address),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildCertDetailItem(
|
||||||
|
'车辆颜色',
|
||||||
|
controller.color,
|
||||||
|
valueColor: const Color(0xFF52C41A),
|
||||||
|
),
|
||||||
|
_buildCertDetailItem('氢瓶容量', controller.hydrogenCapacity),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 附件预览部分
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.navigateToCertificateViewer(title, attachments);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsetsGeometry.only(left: 10.w),
|
height: 184.h,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.blue.shade300, width: 1),
|
borderRadius: BorderRadius.circular(16),
|
||||||
borderRadius: BorderRadius.circular(5),
|
border: Border.all(color: Color.fromRGBO(226, 232, 240, 1)),
|
||||||
color: Colors.blue.withOpacity(0.05),
|
color: Color.fromRGBO(248, 250, 252, 1),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Center(child: _buildAttachmentPreview(attachments[0])),
|
||||||
mainAxisSize: MainAxisSize.min, // Keep the row compact
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
StorageService.to.hasVehicleInfo ? Icons.repeat : Icons.search,
|
|
||||||
size: 13,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 3),
|
|
||||||
Text(
|
|
||||||
StorageService.to.hasVehicleInfo ? "换车牌" : value,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
value,
|
|
||||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 3. 构建车辆证件卡片
|
|
||||||
Widget _buildCertificatesCard() {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildCertificateRow(
|
|
||||||
icon: Icons.credit_card_rounded,
|
|
||||||
title: '行驶证',
|
|
||||||
attachments: controller.drivingAttachments,
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
_buildCertificateRow(
|
|
||||||
icon: Icons.article_rounded,
|
|
||||||
title: '营运证',
|
|
||||||
attachments: controller.operationAttachments,
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
_buildCertificateRow(
|
|
||||||
icon: Icons.propane_tank_rounded,
|
|
||||||
title: '加氢证',
|
|
||||||
attachments: controller.hydrogenationAttachments,
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
_buildCertificateRow(
|
|
||||||
icon: Icons.app_registration_rounded,
|
|
||||||
title: '登记证',
|
|
||||||
attachments: controller.registerAttachments,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 证件展示
|
Widget _buildCertDetailItem(
|
||||||
Widget _buildCertificateRow({
|
String label,
|
||||||
required IconData icon,
|
String value, {
|
||||||
required String title,
|
Color? valueColor,
|
||||||
required List<String> attachments,
|
bool isFull = false,
|
||||||
}) {
|
}) {
|
||||||
return ListTile(
|
return Container(
|
||||||
contentPadding: EdgeInsets.zero,
|
width: isFull ? null : 140,
|
||||||
leading: CircleAvatar(
|
|
||||||
radius: 24,
|
|
||||||
backgroundColor: Colors.blue.withOpacity(0.1),
|
|
||||||
child: Icon(icon, color: Colors.blue, size: 28),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
|
||||||
),
|
|
||||||
// 使用 Obx 响应式地显示附件数量
|
|
||||||
subtitle: Obx(
|
|
||||||
() => Text(
|
|
||||||
'共 ${attachments.length} 个附件',
|
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[200],
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.find_in_page_outlined, color: AppTheme.themeColor),
|
|
||||||
),
|
|
||||||
// 更新 onTap 逻辑
|
|
||||||
onTap: () => controller.navigateToCertificateViewer(title, attachments),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTipsCard() {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildTipItem(Icons.info_outline, '请确保车辆证件齐全有效'),
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 4),
|
||||||
_buildTipItem(Icons.rule, '定期检查车辆状态和证件有效期'),
|
Text(
|
||||||
const SizedBox(height: 10),
|
value,
|
||||||
_buildTipItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
|
style: TextStyle(
|
||||||
],
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: valueColor ?? Colors.black87,
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提示信息卡片中的列表项
|
/// 附件预览组件 (智能判断 PDF 或图片)
|
||||||
Widget _buildTipItem(IconData icon, String text) {
|
Widget _buildAttachmentPreview(String url) {
|
||||||
return Row(
|
return AspectRatio(
|
||||||
|
aspectRatio: 4 / 3,
|
||||||
|
child: controller.isPdf(url)
|
||||||
|
? Obx(() {
|
||||||
|
final bool loading = controller.isLoading[url] ?? true;
|
||||||
|
final String? localPath = controller.localPdfPaths[url];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return _buildLoadingIndicator();
|
||||||
|
} else if (localPath != null && localPath.isNotEmpty) {
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: true, // 设置为 true 来忽略所有指针事件
|
||||||
|
child: PDFView(
|
||||||
|
backgroundColor: Color.fromRGBO(248, 250, 252, 1),
|
||||||
|
fitEachPage: true,
|
||||||
|
filePath: localPath,
|
||||||
|
fitPolicy: FitPolicy.WIDTH,
|
||||||
|
// 适配宽度
|
||||||
|
enableSwipe: false,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: false,
|
||||||
|
pageFling: false,
|
||||||
|
preventLinkNavigation: true, // 顺便禁用PDF内部链接的跳转
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// PDF加载失败
|
||||||
|
return _buildErrorIndicator();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: Image.network(
|
||||||
|
url,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
// 图片加载时显示loading
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return _buildLoadingIndicator();
|
||||||
|
},
|
||||||
|
// 图片加载失败时显示错误
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return _buildErrorIndicator();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingIndicator() {
|
||||||
|
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorIndicator() {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Center(child: Icon(Icons.error_outline, color: Colors.red, size: 48)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 安全提醒卡片
|
||||||
|
Widget _buildSafetyReminderCard() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color.fromRGBO(242, 249, 248, 1),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: Colors.blue, size: 20),
|
Row(
|
||||||
const SizedBox(width: 10),
|
children: [
|
||||||
Expanded(
|
Icon(Icons.info_outline, color: AppTheme.themeColor, size: 24),
|
||||||
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"安全提醒",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
"请确保车辆证件齐全有效,定期检查车辆状态和证件有效期,以确保运输作业合规安全。",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Color.fromRGBO(1, 113, 55, 0.8),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
"如有疑问请联系客服:400-021-1773",
|
||||||
|
style: TextStyle(fontSize: 13, color: Color.fromRGBO(1, 113, 55, 0.8)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ class MineController extends GetxController with BaseControllerMixin {
|
|||||||
String historyBreakRules = "";
|
String historyBreakRules = "";
|
||||||
String vin = "";
|
String vin = "";
|
||||||
String plateNumber = "";
|
String plateNumber = "";
|
||||||
String violationTotal = "0";
|
String violationTotal = "0";//违章总数
|
||||||
String violationScore = "0";
|
String violationScore = "0";//扣分总数
|
||||||
String violationDispose = "0";
|
String violationDispose = "0";//已处理
|
||||||
bool isNotice = false;
|
bool isNotice = false;
|
||||||
|
|
||||||
void renderData() async {
|
void renderData() async {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/common/index.dart';
|
import 'package:getx_scaffold/common/index.dart';
|
||||||
import 'package:getx_scaffold/common/widgets/index.dart';
|
import 'package:getx_scaffold/common/widgets/index.dart';
|
||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
import 'package:ln_jq_app/pages/c_page/message/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
@@ -17,108 +18,164 @@ class MinePage extends GetView<MineController> {
|
|||||||
id: 'mine',
|
id: 'mine',
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: const Color.fromRGBO(247, 249, 251, 1),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
_buildUserInfoCard(),
|
_buildUserInfoCard(),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 8),
|
||||||
_buildDriverScoreCard(),
|
// 新 UI 模块开始
|
||||||
const SizedBox(height: 5),
|
Padding(
|
||||||
_buildMonthlyRecordCard(),
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
const SizedBox(height: 5),
|
child: Column(
|
||||||
_buildTipsCard(),
|
children: [
|
||||||
const SizedBox(height: 20),
|
_buildWalletCard(),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
_buildGridMenu(),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
_buildRecommendCard(context),
|
||||||
|
SizedBox(height: 8.h),
|
||||||
|
_buildSafetyReminderCard(),
|
||||||
|
SizedBox(height: 24.h),
|
||||||
_buildLogoutButton(),
|
_buildLogoutButton(),
|
||||||
const SizedBox(height: 20),
|
SizedBox(height: 95.h),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 新 UI 模块结束
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 1. 构建顶部用户信息卡片
|
/// 构建顶部用户信息卡片
|
||||||
Widget _buildUserInfoCard() {
|
Widget _buildUserInfoCard() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
color: Colors.white,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(20),
|
||||||
|
bottomRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 16, top: 40),
|
||||||
|
// 增加了顶部 padding 适配状态栏
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const CircleAvatar(
|
Stack(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
radius: 25,
|
radius: 25,
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.white,
|
||||||
child: Icon(Icons.person, color: Colors.white, size: 40),
|
child: LoginUtil.getAssImg('ic_user_logo@2x'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 16.h,
|
||||||
|
width: 16.w,
|
||||||
|
child: LoginUtil.getAssImg('ic_logo@2x'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${StorageService.to.name}",
|
"${StorageService.to.name}",
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color.fromRGBO(236, 255, 234, 1), // 极浅绿色背景
|
||||||
|
border: Border.all(color: const Color(0xFFB7E19F)), // 边框
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.eco, size: 12, color: Color(0xFF52C41A)),
|
||||||
|
// 叶子图标
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
"绿色先锋",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF52C41A),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
"${StorageService.to.phone}",
|
"羚牛ID:${StorageService.to.phone}",
|
||||||
style: TextStyle(color: Colors.grey, fontSize: 11),
|
style: const TextStyle(color: Colors.grey, fontSize: 11),
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
StorageService.to.hasVehicleInfo ? "已绑定车辆" : '未绑定车辆',
|
|
||||||
style: TextStyle(color: Colors.orange, fontSize: 12),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async{
|
||||||
// 跳转消息中心
|
|
||||||
var scanResult = await Get.to(() => const MessagePage());
|
var scanResult = await Get.to(() => const MessagePage());
|
||||||
if (scanResult == null) {
|
if (scanResult == null) {
|
||||||
controller.msgNotice();
|
controller.msgNotice();
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
// 这里的 style 是为了模拟你图片里的灰色圆形背景
|
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[100],
|
backgroundColor: Colors.grey[100],
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
),
|
),
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
// label: Text('3'), // 如果你想显示数字,就加 label
|
|
||||||
smallSize: 8,
|
smallSize: 8,
|
||||||
// 红点的大小
|
backgroundColor: controller.isNotice
|
||||||
backgroundColor: controller.isNotice ? Colors.red : Colors.white,
|
? Colors.red
|
||||||
// 红点颜色
|
: Colors.transparent,
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
Icons.notifications_outlined,
|
Icons.notifications_outlined,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
size: 25,
|
size: 30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildStatItem(controller.violationTotal, '违章总数'),
|
_buildModernStatItem('服务天数', 'service days', '156', ''),
|
||||||
_buildStatItem(controller.violationScore, '扣分总数'),
|
const SizedBox(width: 8),
|
||||||
_buildStatItem(controller.violationDispose, '已处理'),
|
_buildModernStatItem('准时率', 'Punctuality', controller.rate, ''),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildModernStatItem('司机评分', 'Driver rating', controller.rating, ''),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -127,202 +184,325 @@ class MinePage extends GetView<MineController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户信息卡片中的小统计项
|
// 统计项
|
||||||
Widget _buildStatItem(String value, String label) {
|
Widget _buildModernStatItem(String title, String subtitle, String value, String unit) {
|
||||||
return Column(
|
return Expanded(
|
||||||
children: [
|
child: Container(
|
||||||
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
padding: const EdgeInsets.all(12.0),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 2. 构建驾驶得分卡片
|
|
||||||
Widget _buildDriverScoreCard() {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'驾驶得分',
|
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const Text('本月表现', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(
|
|
||||||
value: (double.tryParse(controller.rating) ?? 0) / 10,
|
|
||||||
strokeWidth: 8,
|
|
||||||
backgroundColor: Colors.grey[200],
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Text(
|
|
||||||
controller.rating,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildScoreDetailRow(Icons.directions_car, '安全驾驶', '无违章记录', true),
|
|
||||||
const Divider(),
|
|
||||||
_buildScoreDetailRow(Icons.timer, '准时率', '100%准时到达', true),
|
|
||||||
const Divider(),
|
|
||||||
_buildScoreDetailRow(Icons.thumb_up, '服务质量', '用户满意度高', true),
|
|
||||||
const Divider(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'优秀驾驶员',
|
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue,
|
color: const Color(0xFFF8F9FA),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Text(
|
|
||||||
'A+',
|
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 驾驶得分卡片中的评分项
|
|
||||||
Widget _buildScoreDetailRow(
|
|
||||||
IconData icon,
|
|
||||||
String title,
|
|
||||||
String subtitle,
|
|
||||||
bool isCompleted,
|
|
||||||
) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 20,
|
|
||||||
backgroundColor: Colors.blue.withOpacity(0.1),
|
|
||||||
child: Icon(icon, color: Colors.blue, size: 24),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
Text(subtitle, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
),
|
||||||
|
Text(subtitle, style: const TextStyle(fontSize: 9, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.black54)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (isCompleted) const Icon(Icons.check_circle, color: Colors.blue),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 3. 构建本月记录卡片
|
/// 我的钱包卡片
|
||||||
Widget _buildMonthlyRecordCard() {
|
Widget _buildWalletCard() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'本月记录',
|
"我的钱包",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text("User wallet", style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"¥ 0,00元",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.green[700],
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildRecordRow(Icons.rate_review, '加氢预约践行率', controller.rate),
|
|
||||||
const Divider(),
|
|
||||||
_buildRecordRow(
|
|
||||||
Icons.report_problem_outlined,
|
|
||||||
'违章',
|
|
||||||
"${controller.historyBreakRules}起",
|
|
||||||
),
|
),
|
||||||
const Divider(),
|
|
||||||
_buildRecordRow(Icons.car_crash_outlined, '交通事故', "${controller.accident}起"),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本月记录中的列表项
|
/// 2x2 功能网格菜单
|
||||||
Widget _buildRecordRow(IconData icon, String title, String value) {
|
Widget _buildGridMenu() {
|
||||||
return ListTile(
|
return Column(
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
leading: CircleAvatar(
|
|
||||||
radius: 20,
|
|
||||||
backgroundColor: Colors.blue.withOpacity(0.1),
|
|
||||||
child: Icon(icon, color: Colors.blue, size: 24),
|
|
||||||
),
|
|
||||||
title: Text(title, style: const TextStyle(fontSize: 14)),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Text(value, style: const TextStyle(color: AppTheme.themeColor, fontSize: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
// TODO: 处理点击事件
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 4. 构建提示信息卡片
|
|
||||||
Widget _buildTipsCard() {
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildInfoItem(Icons.info_outline, '保持良好的驾驶习惯,提高安全评分'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoItem(Icons.rule, '遵守交通规则,避免违章扣分'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildInfoItem(Icons.headset_mic_outlined, '如有疑问请联系客服: 400-021-1773'),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.verified_outlined, color: Colors.blue, size: 20),
|
_buildGridItem(Icons.person_search_outlined, "客服评价", "3项可评"),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 19),
|
||||||
|
_buildGridItem(
|
||||||
|
Icons.assignment_late_outlined,
|
||||||
|
"违章处理",
|
||||||
|
"${controller.historyBreakRules}项待办",
|
||||||
|
countColor: Colors.red,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildGridItem(Icons.book_outlined, "安全培训", "0个待看"),
|
||||||
|
const SizedBox(width: 19),
|
||||||
|
_buildGridItem(
|
||||||
|
Icons.verified_user_outlined,
|
||||||
|
"诚信加氢值",
|
||||||
|
"845",
|
||||||
|
isSpecial: true,
|
||||||
|
backgroundColor: const Color(0xFF006633),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridItem(
|
||||||
|
IconData icon,
|
||||||
|
String title,
|
||||||
|
String subtitle, {
|
||||||
|
Color? countColor,
|
||||||
|
bool isSpecial = false,
|
||||||
|
Color? backgroundColor,
|
||||||
|
}) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 100,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSpecial ? backgroundColor : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: isSpecial ? Colors.white : Colors.black87, size: 28),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isSpecial ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isSpecial
|
||||||
|
? Colors.white.withOpacity(0.8)
|
||||||
|
: (countColor ?? Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 我要推荐卡片
|
||||||
|
Widget _buildRecommendCard(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"我要推荐",
|
||||||
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text("Recommend", style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text("累计奖励(积分)", style: TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
|
Text(
|
||||||
|
"0,00",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (GetPlatform.isIOS) {
|
||||||
|
// 跳转到 iOS 应用商店 (这里使用一个通用的应用商店链接模板,请确保替换为正式的 AppID)
|
||||||
|
openWebPage("https://apps.apple.com/cn/app/羚牛氢能/6756245815");
|
||||||
|
} else if (GetPlatform.isAndroid) {
|
||||||
|
// Android 弹出二维码图片
|
||||||
|
_showAndroidDownloadDialog(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF006633),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"下载推荐",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Android 端下载二维码弹窗
|
||||||
|
void _showAndroidDownloadDialog(BuildContext context) {
|
||||||
|
Get.dialog(
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 280.w,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"扫描二维码下载",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// 使用 LoginUtil.getAssImg 加载你的图片 android_apk_img.png
|
||||||
|
SizedBox(
|
||||||
|
width: 180.w,
|
||||||
|
height: 180.w,
|
||||||
|
child: LoginUtil.getAssImg('android_apk_img'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
"请让被推荐人扫描上方二维码进行下载安装",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: TextButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
|
||||||
|
child: const Text(
|
||||||
|
"确 定",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 安全提醒卡片
|
||||||
|
Widget _buildSafetyReminderCard() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color.fromRGBO(242, 249, 248, 1), // 极浅绿色背景
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, color: Colors.green[700], size: 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"安全提醒",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"请保持良好驾驶习惯,提高安全评分,遵守交通规则,避免违章扣分。",
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.green[800], height: 1.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"如有疑问请联系客服:400-021-1773",
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.green[800]),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FutureBuilder<String>(
|
child: FutureBuilder<String>(
|
||||||
future: getVersion(),
|
future: getVersion(),
|
||||||
@@ -336,7 +516,7 @@ class MinePage extends GetView<MineController> {
|
|||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return TextX.labelSmall(
|
return TextX.labelSmall(
|
||||||
"当前版本: ${snapshot.data}",
|
"当前版本: ${snapshot.data}",
|
||||||
color: Colors.black54,
|
color: Colors.green[800],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,20 +529,6 @@ class MinePage extends GetView<MineController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提示信息卡片中的列表项
|
|
||||||
Widget _buildInfoItem(IconData icon, String text) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.blue, size: 20),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(text, style: const TextStyle(fontSize: 12, color: Colors.black54)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +538,7 @@ class MinePage extends GetView<MineController> {
|
|||||||
controller.logout();
|
controller.logout();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red[400],
|
backgroundColor: Color.fromRGBO(204, 52, 46, 1),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
minimumSize: const Size(double.infinity, 48),
|
minimumSize: const Size(double.infinity, 48),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
|
|||||||
@@ -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小时区间
|
||||||
@@ -342,7 +344,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
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("提交中");
|
||||||
|
|
||||||
@@ -536,6 +544,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
String leftHydrogen = "0";
|
String leftHydrogen = "0";
|
||||||
num maxHydrogen = 0;
|
num maxHydrogen = 0;
|
||||||
String difference = "";
|
String difference = "";
|
||||||
|
var progressValue = 0.0;
|
||||||
|
|
||||||
//用来管理查看预约的弹窗
|
//用来管理查看预约的弹窗
|
||||||
Worker? _sheetWorker;
|
Worker? _sheetWorker;
|
||||||
@@ -551,16 +560,35 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
getUserBindCarInfo();
|
getUserBindCarInfo();
|
||||||
getSiteList();
|
getSiteList();
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
|
msgNotice();
|
||||||
|
|
||||||
if (!init) {
|
if (!init) {
|
||||||
_setupListener();
|
_setupListener();
|
||||||
init = true;
|
init = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
bool isNotice = false;
|
||||||
void onPaused() {
|
|
||||||
stopAutoRefresh();
|
Future<void> msgNotice() async {
|
||||||
super.onPaused();
|
final Map<String, dynamic> requestData = {
|
||||||
|
'appFlag': 1,
|
||||||
|
'isRead': 1,
|
||||||
|
'pageNum': 1,
|
||||||
|
'pageSize': 5,
|
||||||
|
};
|
||||||
|
final response = await HttpService.to.get(
|
||||||
|
'appointment/unread_notice/page',
|
||||||
|
params: requestData,
|
||||||
|
);
|
||||||
|
if (response != null) {
|
||||||
|
final result = BaseModel.fromJson(response.data);
|
||||||
|
if (result.code == 0 && result.data != null) {
|
||||||
|
String total = result.data["total"].toString();
|
||||||
|
isNotice = int.parse(total) > 0;
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void startAutoRefresh() {
|
void startAutoRefresh() {
|
||||||
@@ -631,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();
|
||||||
@@ -644,9 +674,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
|
|
||||||
void getCatinfo() async {
|
void getCatinfo() async {
|
||||||
try {
|
try {
|
||||||
HttpService.to.setBaseUrl(AppTheme.car_service_url);
|
|
||||||
var responseData = await HttpService.to.post(
|
var responseData = await HttpService.to.post(
|
||||||
'VehicleData/getHydrogenInfoByPlateNumber',
|
'appointment/vehicle/getHydrogenInfoByPlateNumber',
|
||||||
data: {
|
data: {
|
||||||
'userName': "xll@lingniu",
|
'userName': "xll@lingniu",
|
||||||
'password': "4q%3!l6s0p",
|
'password': "4q%3!l6s0p",
|
||||||
@@ -671,11 +700,28 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
amountController.text = flooredDifference.toString();
|
amountController.text = flooredDifference.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUi();
|
if (maxHydrogen > 0) {
|
||||||
} catch (e) {
|
progressValue = leftHydrogenNum / maxHydrogen;
|
||||||
} finally {
|
|
||||||
HttpService.to.setBaseUrl(AppTheme.test_service_url);
|
// 边界处理:确保值在 0 到 1 之间
|
||||||
|
if (progressValue > 1.0) progressValue = 1.0;
|
||||||
|
if (progressValue < 0.0) progressValue = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUi();
|
||||||
|
} catch (e) {}
|
||||||
|
renderSliderTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
double current = 0.0;
|
||||||
|
double maxVal = 0.0;
|
||||||
|
|
||||||
|
void renderSliderTheme() {
|
||||||
|
current = double.tryParse(amountController.text) ?? 0.0;
|
||||||
|
maxVal = double.tryParse(difference) ?? 100.0;
|
||||||
|
if (maxVal <= 0) maxVal = 100.0;
|
||||||
|
|
||||||
|
updateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
void getSiteList() async {
|
void getSiteList() async {
|
||||||
@@ -735,7 +781,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
if (stationOptions.isEmpty) {
|
if (stationOptions.isEmpty) {
|
||||||
showToast('附近暂无可用加氢站');
|
showToast('附近暂无可用加氢站');
|
||||||
} else {
|
} else {
|
||||||
showToast('站点列表已刷新');
|
// showToast('站点列表已刷新');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到第一个可选的站点作为默认值
|
// 找到第一个可选的站点作为默认值
|
||||||
@@ -783,6 +829,7 @@ 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) {
|
||||||
@@ -790,6 +837,5 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
}
|
}
|
||||||
_sheetWorker?.dispose();
|
_sheetWorker?.dispose();
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
super.onClose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
ln_jq_app/lib/pages/common/webview/controller.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class WebController extends GetxController {
|
||||||
|
late String title;
|
||||||
|
late String url;
|
||||||
|
|
||||||
|
final RxDouble progress = 0.0.obs;
|
||||||
|
InAppWebViewController? webViewController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
// 从参数中获取标题和URL
|
||||||
|
title = Get.arguments['title'] ?? '详情';
|
||||||
|
url = Get.arguments['url'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
void onWebViewCreated(InAppWebViewController controller) {
|
||||||
|
webViewController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onProgressChanged(InAppWebViewController controller, int progressValue) {
|
||||||
|
progress.value = progressValue / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
ln_jq_app/lib/pages/common/webview/view.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class WebViewPage extends GetView<WebController> {
|
||||||
|
const WebViewPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Get.put(WebController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(controller.title),
|
||||||
|
centerTitle: true,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(2.0),
|
||||||
|
child: Obx(
|
||||||
|
() => controller.progress.value < 1.0
|
||||||
|
? LinearProgressIndicator(
|
||||||
|
value: controller.progress.value,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
minHeight: 2.0,
|
||||||
|
)
|
||||||
|
: const SizedBox(height: 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: InAppWebView(
|
||||||
|
initialUrlRequest: URLRequest(url: WebUri(controller.url)),
|
||||||
|
initialSettings: InAppWebViewSettings(
|
||||||
|
isInspectable: true,
|
||||||
|
javaScriptEnabled: true,
|
||||||
|
javaScriptCanOpenWindowsAutomatically: true,
|
||||||
|
useShouldOverrideUrlLoading: true,
|
||||||
|
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
|
||||||
|
mediaPlaybackRequiresUserGesture: false,
|
||||||
|
allowsInlineMediaPlayback: true,
|
||||||
|
),
|
||||||
|
onWebViewCreated: controller.onWebViewCreated,
|
||||||
|
onProgressChanged: controller.onProgressChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
|
import 'package:ln_jq_app/common/model/base_model.dart';
|
||||||
|
|
||||||
class LoginController extends GetxController with BaseControllerMixin {
|
class LoginController extends GetxController with BaseControllerMixin {
|
||||||
@override
|
@override
|
||||||
@@ -7,19 +10,73 @@ class LoginController extends GetxController with BaseControllerMixin {
|
|||||||
LoginController();
|
LoginController();
|
||||||
|
|
||||||
// 控制输入框的 TextEditingController
|
// 控制输入框的 TextEditingController
|
||||||
final TextEditingController driverIdentityController = TextEditingController();
|
final TextEditingController phoneController = TextEditingController();
|
||||||
|
final TextEditingController codeController = TextEditingController();
|
||||||
|
|
||||||
final TextEditingController stationIdController = TextEditingController();
|
final TextEditingController stationIdController = TextEditingController();
|
||||||
final TextEditingController passwordController = TextEditingController();
|
final TextEditingController passwordController = TextEditingController();
|
||||||
|
|
||||||
|
|
||||||
|
// --- 倒计时逻辑 ---
|
||||||
|
final RxInt countdown = 0.obs;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
void startCountdown() async {
|
||||||
|
if (phoneController.text.isEmpty || !phoneController.text.isPhoneNumber) {
|
||||||
|
showToast("请输入正确的手机号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countdown.value > 0) return;
|
||||||
|
|
||||||
|
// 调用发送验证码接口
|
||||||
|
var responseData = await HttpService.to.post(
|
||||||
|
'appointment/login/sendCode',
|
||||||
|
data: {"mobile": phoneController.text},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseData == null) {
|
||||||
|
showToast('验证码发送失败,请稍后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
|
||||||
|
if (result.code != 0) {
|
||||||
|
showToast(result.error);
|
||||||
|
dismissLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("验证码已发送");
|
||||||
|
|
||||||
|
countdown.value = 60;
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (countdown.value > 0) {
|
||||||
|
countdown.value--;
|
||||||
|
} else {
|
||||||
|
_timer?.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
showToast('验证码服务异常,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
|
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
phoneController.dispose();
|
||||||
|
codeController.dispose();
|
||||||
|
|
||||||
|
stationIdController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
import 'package:aliyun_push_flutter/aliyun_push_flutter.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:getx_scaffold/getx_scaffold.dart';
|
import 'package:getx_scaffold/getx_scaffold.dart';
|
||||||
import 'package:ln_jq_app/common/login_util.dart';
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
@@ -9,6 +12,7 @@ import 'package:ln_jq_app/common/model/vehicle_info.dart';
|
|||||||
import 'package:ln_jq_app/common/styles/theme.dart';
|
import 'package:ln_jq_app/common/styles/theme.dart';
|
||||||
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/b_page/base_widgets/view.dart';
|
||||||
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
import 'package:ln_jq_app/pages/c_page/base_widgets/view.dart';
|
||||||
|
import 'package:ln_jq_app/pages/common/webview/view.dart';
|
||||||
import 'package:ln_jq_app/pages/login/controller.dart';
|
import 'package:ln_jq_app/pages/login/controller.dart';
|
||||||
import 'package:ln_jq_app/pages/url_host/view.dart';
|
import 'package:ln_jq_app/pages/url_host/view.dart';
|
||||||
import 'package:ln_jq_app/storage_service.dart';
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
@@ -21,170 +25,467 @@ class LoginPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
|
class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
|
||||||
late TabController tabController;
|
late TabController _tabController;
|
||||||
|
bool _isAgreed = false;
|
||||||
bool cLogin = true;
|
|
||||||
bool _obscureText = true;
|
bool _obscureText = true;
|
||||||
|
|
||||||
// 用于管理“记住密码”的复选框状态
|
|
||||||
bool _rememberPassword = true;
|
bool _rememberPassword = true;
|
||||||
|
|
||||||
// 用于确保凭证只在首次加载时回填一次
|
|
||||||
bool _credentialsLoaded = false;
|
bool _credentialsLoaded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
tabController.addListener(_tabChangeListener);
|
_tabController.addListener(() {
|
||||||
|
if (!_tabController.indexIsChanging) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _tabChangeListener() {
|
|
||||||
if (!tabController.indexIsChanging) {
|
|
||||||
switchTab(tabController.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void switchTab(int index) {
|
|
||||||
setState(() {
|
|
||||||
cLogin = (index == 0);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
tabController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildView(LoginController controller) {
|
@override
|
||||||
// 在视图构建时,检查并回填已保存的凭证
|
Widget build(BuildContext context) {
|
||||||
|
return GetBuilder<LoginController>(
|
||||||
|
init: LoginController(),
|
||||||
|
id: 'login',
|
||||||
|
builder: (controller) {
|
||||||
|
// 站点登录凭证回填逻辑
|
||||||
if (!_credentialsLoaded) {
|
if (!_credentialsLoaded) {
|
||||||
final savedAccount = StorageService.to.stationAccount;
|
final savedAccount = StorageService.to.stationAccount;
|
||||||
final savedPassword = StorageService.to.stationPassword;
|
final savedPassword = StorageService.to.stationPassword;
|
||||||
if (savedAccount != null && savedPassword != null) {
|
if (savedAccount != null && savedPassword != null) {
|
||||||
controller.stationIdController.text = savedAccount;
|
controller.stationIdController.text = savedAccount;
|
||||||
controller.passwordController.text = savedPassword;
|
controller.passwordController.text = savedPassword;
|
||||||
_rememberPassword = true; // 如果有保存的密码,则默认勾选
|
_rememberPassword = true;
|
||||||
}
|
}
|
||||||
_credentialsLoaded = true; // 标记为已加载,防止重复执行
|
_credentialsLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Scaffold(
|
||||||
color: Color(0xFFEFF4F7),
|
backgroundColor: Colors.white,
|
||||||
child: <Widget>[
|
resizeToAvoidBottomInset: false,
|
||||||
Icon(cLogin ? AntdIcon.car : AntdIcon.USB),
|
body: GestureDetector(
|
||||||
SizedBox(height: 5.h),
|
onTap: () {
|
||||||
TextX.bodyLarge(cLogin ? '司机端' : "加氢站", weight: FontWeight.w700),
|
hideKeyboard();
|
||||||
SizedBox(height: 5.h),
|
},
|
||||||
TextX.bodyLarge(cLogin ? '安全驾驶·智能服务' : "氢能服务·专业运营"),
|
child: Stack(
|
||||||
Card(
|
children: [
|
||||||
shape: RoundedRectangleBorder(
|
//顶部背景与装饰
|
||||||
borderRadius: BorderRadius.circular(15), // 设置圆角弧度
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: LoginUtil.getAssImg("bg_login"),
|
||||||
),
|
),
|
||||||
margin: EdgeInsets.all(15),
|
Positioned(
|
||||||
elevation: 4,
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 180.w,
|
||||||
|
height: 218.h,
|
||||||
|
child: LoginUtil.getAssImg("ic_login_bg@2x"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildBrandingHeader(),
|
||||||
|
|
||||||
|
// 登录表单主体
|
||||||
|
Positioned(
|
||||||
|
top: 300.h,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: cLogin ? 285.h : 360.h,
|
decoration: const BoxDecoration(
|
||||||
padding: EdgeInsets.all(15),
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(40),
|
||||||
|
topRight: Radius.circular(40),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Card(
|
const SizedBox(height: 20),
|
||||||
elevation: 2,
|
// TabBar 切换
|
||||||
child: Container(
|
Container(
|
||||||
height: 55.h,
|
margin: EdgeInsets.symmetric(horizontal: 60.w),
|
||||||
padding: EdgeInsets.all(3),
|
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
controller: tabController,
|
controller: _tabController,
|
||||||
onTap: (index) {
|
indicatorColor: const Color(0xFF006633),
|
||||||
delayed(300, () {
|
indicatorWeight: 3,
|
||||||
switchTab(index);
|
labelColor: const Color(0xFF006633),
|
||||||
});
|
unselectedLabelColor: Colors.grey,
|
||||||
},
|
labelStyle: const TextStyle(
|
||||||
labelColor: Colors.white,
|
fontSize: 16,
|
||||||
unselectedLabelColor: Colors.black,
|
fontWeight: FontWeight.bold,
|
||||||
indicator: BoxDecoration(
|
|
||||||
color: AppTheme.themeColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue.withOpacity(0.2),
|
|
||||||
spreadRadius: 1,
|
|
||||||
blurRadius: 6,
|
|
||||||
),
|
),
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: "司机登录"),
|
||||||
|
Tab(text: "站点登录"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
tabs: [
|
|
||||||
Tab(text: '司机端登录'),
|
|
||||||
Tab(text: '加氢站登录'),
|
|
||||||
],
|
|
||||||
isScrollable: false,
|
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
),
|
child: SingleChildScrollView(
|
||||||
Flexible(
|
padding: EdgeInsets.symmetric(horizontal: 30.w),
|
||||||
child: TabBarView(
|
child: Column(
|
||||||
controller: tabController,
|
|
||||||
children: [
|
children: [
|
||||||
_driverLoginView(controller),
|
const SizedBox(height: 30),
|
||||||
_stationLoginView(controller),
|
// 根据 Tab 显示不同的输入框
|
||||||
|
_tabController.index == 0
|
||||||
|
? _buildDriverInputFields(controller)
|
||||||
|
: _buildStationInputFields(controller),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
// 统一登录按钮
|
||||||
|
_buildLoginButton(controller),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
buildAgreement(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
].toColumn(mainAxisSize: MainAxisSize.min).center(),
|
Positioned(left: 0, right: 0, bottom: 33.h, child: _buildFooterSlogan()),
|
||||||
|
if (AppTheme.is_show_host)
|
||||||
|
Positioned(
|
||||||
|
top: 40.h,
|
||||||
|
right: 20.w,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => Get.to(() => const UrlHostPage()),
|
||||||
|
child: const Text(
|
||||||
|
"域名配置",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _driverLoginView(LoginController controller) {
|
/// 品牌头部
|
||||||
return !cLogin
|
Widget _buildBrandingHeader() {
|
||||||
? SizedBox()
|
return Positioned(
|
||||||
: Column(
|
top: 0,
|
||||||
|
left: 32.w,
|
||||||
|
right: 0,
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 100.h),
|
||||||
TextFormField(
|
SizedBox(height: 60.h, child: LoginUtil.getAssImg('ic_logo_unbg@2x')),
|
||||||
controller: controller.driverIdentityController,
|
SizedBox(height: 30.h),
|
||||||
cursorColor: AppTheme.themeColor,
|
const Text(
|
||||||
maxLength: 8,
|
"HELLO,",
|
||||||
style: TextStyle(fontSize: 14),
|
style: TextStyle(
|
||||||
decoration: InputDecoration(
|
fontSize: 32,
|
||||||
hintText: '请输入身份后8位',
|
fontWeight: FontWeight.w500,
|
||||||
border: OutlineInputBorder(),
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
hintStyle: TextStyle(fontSize: 14),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
),
|
||||||
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey),
|
const SizedBox(height: 8),
|
||||||
focusedBorder: OutlineInputBorder(
|
Row(
|
||||||
borderSide: BorderSide(color: AppTheme.themeColor),
|
children: [
|
||||||
|
const Text(
|
||||||
|
"欢迎使用 ",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color.fromRGBO(56, 198, 151, 1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"“羚牛氢能智慧服务平台”",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.h),
|
],
|
||||||
ElevatedButton(
|
),
|
||||||
onPressed: () async {
|
],
|
||||||
String password = controller.driverIdentityController.text;
|
),
|
||||||
if (password.isEmpty) {
|
);
|
||||||
showToast("请输入密码");
|
}
|
||||||
|
|
||||||
|
/// 司机登录输入框 (手机号+验证码)
|
||||||
|
Widget _buildDriverInputFields(LoginController controller) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildInputWrapper(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '请输入手机号',
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildInputWrapper(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.codeController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [LengthLimitingTextInputFormatter(6)],
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '请输入验证码',
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => GestureDetector(
|
||||||
|
onTap: controller.countdown.value == 0
|
||||||
|
? controller.startCountdown
|
||||||
|
: null,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 24.0),
|
||||||
|
child: Text(
|
||||||
|
controller.countdown.value == 0
|
||||||
|
? "获取验证码"
|
||||||
|
: "${controller.countdown.value}s后重发",
|
||||||
|
style: TextStyle(
|
||||||
|
color: controller.countdown.value == 0
|
||||||
|
? const Color(0xFF006633)
|
||||||
|
: Colors.grey,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 站点登录输入框 (账号+密码)
|
||||||
|
Widget _buildStationInputFields(LoginController controller) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildInputWrapper(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.stationIdController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '请输入加氢站编号',
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildInputWrapper(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.passwordController,
|
||||||
|
obscureText: _obscureText,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '请输入密码',
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureText ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _obscureText = !_obscureText),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 40,
|
||||||
|
child: Checkbox(
|
||||||
|
value: _rememberPassword,
|
||||||
|
activeColor: const Color(0xFF006633),
|
||||||
|
onChanged: (val) => setState(() => _rememberPassword = val ?? false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text("记住密码", style: TextStyle(color: Colors.grey, fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 通用输入框包装
|
||||||
|
Widget _buildInputWrapper({required Widget child}) {
|
||||||
|
return Container(
|
||||||
|
height: 55.h,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF7F9FB),
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 统一登录按钮逻辑
|
||||||
|
Widget _buildLoginButton(LoginController controller) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () => _handleLogin(controller),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF006633),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 55),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_tabController.index == 0 ? "司机登录" : "站点登录",
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLogin(LoginController controller) async {
|
||||||
|
if (!_isAgreed) {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
icon: DialogIcon.warn,
|
||||||
|
content: _buildDialogContent(),
|
||||||
|
confirmText: '同意',
|
||||||
|
cancelText: '拒绝',
|
||||||
|
onConfirm: () {
|
||||||
|
_isAgreed = true;
|
||||||
|
controller.updateUi();
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_tabController.index == 0
|
||||||
|
? _handleDriverLogin(controller)
|
||||||
|
: _handleStationLogin(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 司机登录逻辑 (短信登录)
|
||||||
|
void _handleDriverLogin(LoginController controller) async {
|
||||||
|
if (!_validateAgreed()) return;
|
||||||
|
String phone = controller.phoneController.text;
|
||||||
|
String code = controller.codeController.text;
|
||||||
|
if (phone.isEmpty || !phone.isPhoneNumber) {
|
||||||
|
showToast("请输入正确的手机号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code.isEmpty) {
|
||||||
|
showToast("请输入验证码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLoading('登录中...');
|
showLoading('登录中...');
|
||||||
try {
|
try {
|
||||||
var responseData = await HttpService.to.post(
|
var responseData = await HttpService.to.post(
|
||||||
'appointment/login/loginForDriver',
|
'appointment/login/login',
|
||||||
data: {'idNo': password},
|
data: {'mobile': phone, 'code': code},
|
||||||
);
|
);
|
||||||
if (responseData == null && responseData!.data == null) {
|
_processLoginResponse(responseData, "driver", phone);
|
||||||
|
} catch (e) {
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('登录失败:无法获取凭证');
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 站点登录逻辑 (账号密码)
|
||||||
|
void _handleStationLogin(LoginController controller) async {
|
||||||
|
if (!_validateAgreed()) return;
|
||||||
|
String account = controller.stationIdController.text;
|
||||||
|
String password = controller.passwordController.text;
|
||||||
|
if (account.isEmpty || password.isEmpty) {
|
||||||
|
showToast("请输入账号和密码");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//登录信息处理
|
|
||||||
try {
|
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
|
||||||
|
|
||||||
|
showLoading('登录中...');
|
||||||
|
try {
|
||||||
|
String encryptedPassword = LoginUtil.encrypt(password);
|
||||||
|
var responseData = await HttpService.to.post(
|
||||||
|
'appointment/login/password',
|
||||||
|
data: {'account': account, 'password': encryptedPassword, 'loginType': "station"},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (responseData != null && responseData.data != null) {
|
||||||
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
|
if (result.code == 0) {
|
||||||
|
// 登录成功,处理记住密码逻辑
|
||||||
|
if (_rememberPassword) {
|
||||||
|
await StorageService.to.saveStationCredentials(account, password);
|
||||||
|
} else {
|
||||||
|
await StorageService.to.clearStationCredentials();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_processLoginResponse(responseData, "station", account);
|
||||||
|
} catch (e) {
|
||||||
|
dismissLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _validateAgreed() {
|
||||||
|
if (!_isAgreed) {
|
||||||
|
DialogX.to.showConfirmDialog(
|
||||||
|
icon: DialogIcon.warn,
|
||||||
|
message: '请阅读并同意用户协议和隐私政策',
|
||||||
|
confirmText: '确定',
|
||||||
|
onConfirm: () {},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processLoginResponse(
|
||||||
|
dynamic responseData,
|
||||||
|
String channel,
|
||||||
|
String identifier,
|
||||||
|
) async {
|
||||||
|
if (responseData == null || responseData.data == null) {
|
||||||
|
dismissLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var result = BaseModel.fromJson(responseData.data);
|
||||||
if (result.code != 0) {
|
if (result.code != 0) {
|
||||||
showToast(result.error);
|
showToast(result.error);
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
@@ -192,25 +493,18 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
}
|
}
|
||||||
|
|
||||||
String token = result.data['token'] ?? '';
|
String token = result.data['token'] ?? '';
|
||||||
String idCard = result.data['idCard'] ?? '';
|
if (channel == "driver") {
|
||||||
String name = result.data['name'] ?? '';
|
|
||||||
String phone = result.data['phone'] ?? '';
|
|
||||||
await StorageService.to.saveLoginInfo(
|
await StorageService.to.saveLoginInfo(
|
||||||
token: token,
|
token: token,
|
||||||
userId: "",
|
userId: "",
|
||||||
channel: "driver",
|
channel: "driver",
|
||||||
idCard: idCard,
|
name: result.data['name'],
|
||||||
name: name,
|
phone: result.data['phone'],
|
||||||
phone: phone,
|
|
||||||
);
|
);
|
||||||
|
// 成功后自动获取车辆信息
|
||||||
//注册推送别名
|
|
||||||
addAlias(phone);
|
|
||||||
|
|
||||||
//登录后查询已绑定车辆信息
|
|
||||||
try {
|
try {
|
||||||
var carInfo = await HttpService.to.get(
|
var carInfo = await HttpService.to.get(
|
||||||
"appointment/driver/getTruckInfoByDriver?phone=$phone",
|
"appointment/driver/getTruckInfoByDriver?phone=${result.data['phone']}",
|
||||||
);
|
);
|
||||||
if (carInfo != null) {
|
if (carInfo != null) {
|
||||||
var carInforesult = BaseModel.fromJson(carInfo.data);
|
var carInforesult = BaseModel.fromJson(carInfo.data);
|
||||||
@@ -226,179 +520,20 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
Logger.d("暂时不处理 查询车辆信息失败的情况");
|
Logger.d("暂时不处理 查询车辆信息失败的情况");
|
||||||
}
|
}
|
||||||
|
|
||||||
//页面操作
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('登录成功,欢迎您');
|
|
||||||
Get.offAll(() => BaseWidgetsPage());
|
Get.offAll(() => BaseWidgetsPage());
|
||||||
} catch (e) {
|
} else {
|
||||||
dismissLoading();
|
|
||||||
showToast('登录失败:数据异常');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
dismissLoading();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.themeColor,
|
|
||||||
minimumSize: Size(double.infinity, 50),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
),
|
|
||||||
child: Text('登录'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _stationLoginView(LoginController controller) {
|
|
||||||
return cLogin
|
|
||||||
? SizedBox()
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 20),
|
|
||||||
TextFormField(
|
|
||||||
controller: controller.stationIdController,
|
|
||||||
cursorColor: AppTheme.themeColor,
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '请输入加氢站编号',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
hintStyle: TextStyle(fontSize: 14),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
prefixIcon: Icon(Icons.person_2_outlined, color: Colors.grey),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: AppTheme.themeColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
TextFormField(
|
|
||||||
controller: controller.passwordController,
|
|
||||||
obscureText: _obscureText,
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
cursorColor: AppTheme.themeColor,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintStyle: TextStyle(fontSize: 14),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
hintText: '请输入密码',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_obscureText = !_obscureText;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
prefixIcon: Icon(Icons.lock_outline, color: Colors.grey),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(color: AppTheme.themeColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 10),
|
|
||||||
//记住密码复选框 ---
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child: Checkbox(
|
|
||||||
value: _rememberPassword,
|
|
||||||
activeColor: AppTheme.themeColor,
|
|
||||||
onChanged: (bool? value) {
|
|
||||||
setState(() {
|
|
||||||
_rememberPassword = value ?? true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => setState(() => _rememberPassword = !_rememberPassword),
|
|
||||||
child: const Text('记住密码', style: TextStyle(color: Colors.grey)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 20), // 调整间距
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.themeColor,
|
|
||||||
minimumSize: Size(double.infinity, 50),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
String account = controller.stationIdController.text;
|
|
||||||
String password = controller.passwordController.text;
|
|
||||||
|
|
||||||
if (account.isEmpty || password.isEmpty) {
|
|
||||||
showToast("请输入账号和密码");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('登录中...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
String encryptedPassword = LoginUtil.encrypt(password);
|
|
||||||
var responseData = await HttpService.to.post(
|
|
||||||
'appointment/login/password',
|
|
||||||
data: {
|
|
||||||
'account': account,
|
|
||||||
'password': encryptedPassword,
|
|
||||||
'loginType': "station",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (responseData == null && responseData!.data == null) {
|
|
||||||
dismissLoading();
|
|
||||||
showToast('登录失败:无法获取凭证');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var result = BaseModel.fromJson(responseData.data);
|
|
||||||
|
|
||||||
if (result.code != 0) {
|
|
||||||
showToast(result.error);
|
|
||||||
dismissLoading();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String token = result.data['token'] ?? '';
|
|
||||||
String userId = result.data['userId'] ?? '';
|
|
||||||
String mobile = result.data['mobile'] ?? '';
|
|
||||||
|
|
||||||
await StorageService.to.saveLoginInfo(
|
await StorageService.to.saveLoginInfo(
|
||||||
token: token,
|
token: token,
|
||||||
userId: userId,
|
userId: result.data['userId'],
|
||||||
phone: mobile,
|
phone: result.data['mobile'],
|
||||||
channel: "station",
|
channel: "station",
|
||||||
);
|
);
|
||||||
|
|
||||||
//注册推送别名
|
|
||||||
addAlias(mobile);
|
|
||||||
|
|
||||||
// 根据复选框状态保存或清除密码 ---
|
|
||||||
if (_rememberPassword) {
|
|
||||||
await StorageService.to.saveStationCredentials(account, password);
|
|
||||||
} else {
|
|
||||||
await StorageService.to.clearStationCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissLoading();
|
dismissLoading();
|
||||||
showToast('登录成功,欢迎您');
|
|
||||||
Get.offAll(() => B_BaseWidgetsPage());
|
Get.offAll(() => B_BaseWidgetsPage());
|
||||||
} catch (e) {
|
|
||||||
dismissLoading();
|
|
||||||
showToast('登录失败:数据异常');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
addAlias(identifier);
|
||||||
dismissLoading();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('登录'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final _aliyunPush = AliyunPushFlutter();
|
final _aliyunPush = AliyunPushFlutter();
|
||||||
@@ -416,38 +551,110 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget buildAgreement() {
|
||||||
Widget build(BuildContext context) {
|
return Padding(
|
||||||
return GetBuilder<LoginController>(
|
padding: EdgeInsets.only(top: 13.h),
|
||||||
init: LoginController(),
|
child: Row(
|
||||||
id: 'login',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
builder: (controller) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(child: _buildView(controller)),
|
// 勾选框
|
||||||
if (AppTheme.is_show_host)
|
SizedBox(
|
||||||
Positioned(
|
width: 22.w,
|
||||||
top: 40.h,
|
height: 22.h,
|
||||||
right: 20.w,
|
child: Checkbox(
|
||||||
child: TextButton(
|
value: _isAgreed,
|
||||||
onPressed: () {
|
activeColor: AppTheme.themeColor,
|
||||||
Get.to(() => const UrlHostPage());
|
// 简单的圆角样式
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
_isAgreed = value ?? false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: const Text(
|
|
||||||
"域名配置",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// 富文本协议部分
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
text: '我已阅读并同意',
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 13),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '《用户协议》',
|
||||||
|
style: TextStyle(color: AppTheme.themeColor, fontSize: 13),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
openPage("用户协议", "https://lnh2e.com/user_agreement.html");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const TextSpan(text: ' 和 '),
|
||||||
|
TextSpan(
|
||||||
|
text: '《隐私政策》',
|
||||||
|
style: TextStyle(color: AppTheme.themeColor, fontSize: 13),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
Widget _buildDialogContent() {
|
||||||
|
return RichTextX(
|
||||||
|
children: [
|
||||||
|
TextSpanItem('请阅读并同意'),
|
||||||
|
TextSpanItem(
|
||||||
|
'《隐私协议》',
|
||||||
|
onTap: () => openPage("隐私政策", "https://lnh2e.com/privacy_agreement.html"),
|
||||||
|
),
|
||||||
|
TextSpanItem('和'),
|
||||||
|
TextSpanItem(
|
||||||
|
'《用户政策》',
|
||||||
|
onTap: () => openPage("用户协议", "https://lnh2e.com/user_agreement.html"),
|
||||||
|
),
|
||||||
|
TextSpanItem(',我们将在协议框架内为您提供更优质的服务。'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openPage(String title, String url) {
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
openWebPage(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Get.to(() => const WebViewPage(), arguments: {'title': title, 'url': url});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFooterSlogan() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"H Y P A I",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: const Color.fromRGBO(51, 51, 51, 1),
|
||||||
|
letterSpacing: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"HYDROGEN MOBILITY",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 9,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ class UrlHostController extends GetxController {
|
|||||||
// 预设的域名列表
|
// 预设的域名列表
|
||||||
final List<String> presetUrls = [
|
final List<String> presetUrls = [
|
||||||
'https://beta-esg.api.lnh2e.com/', // 测试环境
|
'https://beta-esg.api.lnh2e.com/', // 测试环境
|
||||||
|
'http://47.101.201.13:8443/api/', // 测试环境
|
||||||
'http://192.168.110.44:8080/', // 沈辰本地
|
'http://192.168.110.44:8080/', // 沈辰本地
|
||||||
'http://192.168.110.222:8080/', // 何斐本地
|
'http://192.168.110.222:8080/', // 何斐本地
|
||||||
];
|
];
|
||||||
|
|
||||||
final List<String> urlNames = [
|
final List<String> urlNames = [
|
||||||
'测试环境',
|
'测试环境',
|
||||||
|
'线上环境',
|
||||||
'沈辰本地环境',
|
'沈辰本地环境',
|
||||||
'何斐本地环境',
|
'何斐本地环境',
|
||||||
];
|
];
|
||||||
|
|||||||
32
ln_jq_app/lib/pages/welcome/controller.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:ln_jq_app/pages/home/view.dart';
|
||||||
|
import 'package:ln_jq_app/pages/login/view.dart';
|
||||||
|
import 'package:ln_jq_app/storage_service.dart';
|
||||||
|
|
||||||
|
class WelcomeController extends GetxController {
|
||||||
|
@override
|
||||||
|
void onReady() {
|
||||||
|
super.onReady();
|
||||||
|
// 移除原生闪屏页(如果有的话)
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
_startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startTimer() {
|
||||||
|
// 1.5秒后执行跳转逻辑
|
||||||
|
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||||
|
Get.offAll(() => const HomePage());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _jumpToNextPage() {
|
||||||
|
if (StorageService.to.isLoggedIn) {
|
||||||
|
// 已登录,跳转到首页
|
||||||
|
Get.offAll(() => const HomePage());
|
||||||
|
} else {
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
Get.offAll(() => const LoginPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ln_jq_app/lib/pages/welcome/view.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:ln_jq_app/common/login_util.dart';
|
||||||
|
import 'controller.dart';
|
||||||
|
|
||||||
|
class WelcomePage extends GetView<WelcomeController> {
|
||||||
|
const WelcomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 初始化控制器
|
||||||
|
Get.put(WelcomeController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
body: SizedBox.expand(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/welcome.png',
|
||||||
|
fit: BoxFit.fill
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.2+5
|
version: 1.2.3+6
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0
|
sdk: ^3.9.0
|
||||||
|
|||||||