Compare commits

..

1 Commits

Author SHA1 Message Date
684f0445ba v1.0 线上地址修改 2025-11-25 15:13:10 +08:00
25 changed files with 518 additions and 1273 deletions

View File

@@ -19,7 +19,7 @@
<application <application
android:label="小羚羚" android:label="小羚羚"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/logo"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1,35 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width"> <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>路径规划</title> <title>导航规划</title>
<style> <style>
/* --- 全局样式 --- */ html, body, #container { width: 100%; height: 100%; margin: 0; }
html,
body,
#container {
width: 100%;
height: 100%;
margin: 0;
font-family: sans-serif;
}
/* --- 暴力隐藏高德自带的导流链接和Logo (关键) --- */ /* 搜索栏样式 */
.amap-callamap,
.amap-lib-driving-callBtn,
.amap-copyright,
.amap-logo {
display: none !important;
}
#panel .amap-call {
display: none;
}
/* --- 搜索栏样式 --- */
#search-box { #search-box {
position: absolute; position: absolute;
top: 10px; top: 10px;
@@ -38,342 +16,209 @@
z-index: 100; z-index: 100;
background: #fff; background: #fff;
padding: 10px; padding: 10px;
border-radius: 8px; border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 5px rgba(0,0,0,0.3);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.input-row { display: flex; gap: 5px; }
input { flex: 1; padding: 8px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 0 15px; background: #3366FF; color: #fff; border: none; border-radius: 4px; font-weight: bold; }
.input-row { /* 导航结果面板 (仿高德原生) */
display: flex;
gap: 8px;
align-items: center;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f9f9f9;
font-size: 14px;
}
button {
padding: 0 15px;
height: 38px;
background: #3366FF;
color: #fff;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
}
/* --- 导航结果面板 (底部弹出) --- */
#panel { #panel {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 35%; height: 30%; /* 占据底部30% */
/* 面板高度 */
background-color: white; background-color: white;
overflow-y: auto; overflow-y: auto;
border-top: 1px solid #eee; border-top: 1px solid #ccc;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 99; z-index: 99;
display: none; display: none; /* 默认隐藏,规划成功后显示 */
/* 默认隐藏 */
}
/* --- 自定义定位按钮样式 --- */
#location-btn {
position: fixed;
right: 10px;
bottom: 50px;
/* 默认位置 */
width: 44px;
height: 44px;
background-color: #fff;
border-radius: 50%;
/* 圆形更符合现代审美 */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
z-index: 150;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: bottom 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
/* 平滑动画 */
}
#location-btn:active {
background-color: #f2f2f2;
}
#location-btn svg {
width: 24px;
height: 24px;
fill: #555;
}
/* --- 关键:当 body 有 panel-active 类时,按钮上移 --- */
body.panel-active #location-btn {
bottom: 38%;
/* 对应 #panel 的 height + 一点间距 */
} }
</style> </style>
<!-- 1. 配置安全密钥 --> <!-- 1. 必须最先配置安全密钥 -->
<script type="text/javascript"> <script type="text/javascript">
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: '0529b72df6bf0c577ff2182cb8b1d970', securityJsCode: '0529b72df6bf0c577ff2182cb8b1d970',
} }
</script> </script>
<!-- 2. 加载地图和插件 (去掉了 Geolocation 插件,避免弹窗) --> <!-- 2. 加载地图和插件 -->
<script <!-- 注意如果你要货车规划需要在plugin里加上 AMap.TruckDriving -->
src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale"> <script src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete"></script>
</script>
</head> </head>
<body> <body>
<div id="search-box"> <div id="search-box">
<div class="input-row"> <div class="input-row">
<input id="startInput" placeholder="起点: 请输入当前地点" /> <input id="startInput" placeholder="起点: 默认使用当前位置" />
</div>
<div class="input-row">
<input id="endInput" placeholder="终点: 请输入目的地" />
<button onclick="startRouteSearch()">路径规划</button>
</div>
</div> </div>
<div class="input-row">
<div id="container"></div> <input id="endInput" placeholder="终点: 请输入目的地" />
<button onclick="startRouteSearch()">路径规划</button>
<div id="panel"></div>
<!-- 自定义定位按钮 -->
<div id="location-btn" onclick="backToLocation()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path
d="M512 85.333333a426.666667 426.666667 0 1 0 0 853.333334 426.666667 426.666667 0 0 0 0-853.333334z m0 149.333334a277.333333 277.333333 0 1 1 0 554.666666 277.333333 277.333333 0 0 1 0-554.666666z m0 149.333333a128 128 0 1 0 0 256 128 128 0 0 0 0-256z"
fill="#666666"></path>
</svg>
</div> </div>
</div>
<script> <div id="container"></div>
var map, marker, driving, truckDriving; <div id="panel"></div>
var currentLat, currentLng;
var isTruckMode = false;
function initMap() { <script>
map = new AMap.Map('container', { var map, marker, driving, truckDriving;
resizeEnable: true, var currentLat, currentLng;
zoom: 15,
viewMode: '3D'
});
// 通知 Flutter 地图加载完毕 // 标记是否使用货车模式 (true: 货车, false: 轿车)
map.on('complete', function () { var isTruckMode = false;
console.log("JS->: Map is ready.");
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('mapReady');
}
});
// 添加基础控件 function initMap() {
map.addControl(new AMap.Scale()); map = new AMap.Map('container', {
map.addControl(new AMap.ToolBar({ resizeEnable: true,
visible: true, zoom: 15,
position: { viewMode: '3D'
top: '200px', });
right: '10px'
} // 稍微避开定位按钮
}));
// --- 初始化轿车规划 --- // --- 初始化轿车规划 ---
driving = new AMap.Driving({ driving = new AMap.Driving({
map: map,
panel: "panel", // 结果显示在 panel div 中
policy: AMap.DrivingPolicy.LEAST_TIME
});
// --- 初始化货车规划 (按需开启) ---
// 货车需要设置 size (1:微型, 2:轻型, 3:中型, 4:重型)
if(AMap.TruckDriving) {
truckDriving = new AMap.TruckDriving({
map: map, map: map,
panel: "panel", panel: "panel",
policy: AMap.DrivingPolicy.LEAST_TIME size: 2,
}); policy: 0, // 0: 躲避拥堵
width: 2.5,
// --- 初始化货车规划 --- height: 2,
if (AMap.TruckDriving) { load: 1,
truckDriving = new AMap.TruckDriving({ weight: 10,
map: map, axlesNum: 2,
panel: "panel",
size: 2,
policy: 0,
width: 2.5,
height: 2,
load: 1,
weight: 10,
axlesNum: 2,
});
}
// --- 输入提示 ---
new AMap.AutoComplete({
input: "startInput"
});
new AMap.AutoComplete({
input: "endInput"
}); });
} }
/** // --- 输入提示 (自动补全) ---
* 核心功能 1: 接收 Flutter 传来的定位数据 new AMap.AutoComplete({ input: "startInput" });
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)") new AMap.AutoComplete({ input: "endInput" });
*/ }
function updateMyLocation(lat, lng, angle) {
var rawLat = parseFloat(lat);
var rawLng = parseFloat(lng);
var rawAngle = parseFloat(angle);
var gps = [rawLng, rawLat];
AMap.convertFrom(gps, 'gps', function (status, result) { // --- 被动接收 Flutter 传来的定位 ---
if (result.info === 'ok') { function updateMyLocation(lat, lng, angle) {
var mPoint = result.locations[0]; currentLat = lat;
currentLng = mPoint.lng; currentLng = lng;
currentLat = mPoint.lat; var position = [lng, lat];
var position = [currentLng, currentLat];
if (!marker) { if (!marker) {
marker = new AMap.Marker({ marker = new AMap.Marker({
map: map, map: map,
position: position, position: position,
icon: "https://webapi.amap.com/images/car.png", icon: "https://webapi.amap.com/images/car.png",
offset: new AMap.Pixel(-26, -13), offset: new AMap.Pixel(-26, -13),
autoRotation: true, autoRotation: true,
angle: isNaN(rawAngle) ? 0 : rawAngle, angle: angle || 0,
});
map.setCenter(position);
} else {
marker.moveTo(position, {
duration: 1000,
autoRotation: true
});
if (!isNaN(rawAngle)) marker.setAngle(rawAngle);
}
}
}); });
// 第一次定位移动中心
map.setCenter(position);
} else {
marker.setPosition(position);
if (angle) marker.setAngle(angle);
}
}
// --- 核心:规划路线 ---
function startRouteSearch() {
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
if(!endKw) {
console.log("FlutterLog: 终点不能为空");
return;
} }
/** console.log("FlutterLog: 开始规划..." + (isTruckMode ? "货车" : "轿车"));
* 核心功能 2: 点击按钮回到当前位置
*/ // 清除之前的路线
function backToLocation() { if(driving) driving.clear();
if(truckDriving) truckDriving.clear();
// 构造起点和终点参数
// 高德API Search方法支持点对象数组[{keyword: '名字', city: '城市'}, {keyword: '...'}]
var points = [];
// 1. 处理起点
if (startKw) {
// 如果用户输入了文字
points.push({ keyword: startKw });
} else {
// 如果用户没输入,使用当前定位
if (currentLng && currentLat) { if (currentLng && currentLat) {
map.panTo([currentLng, currentLat]); // 关键点:当混合使用坐标和关键字时,必须构建 LngLat 对象
map.setZoom(17);
console.log("JS: 已回到当前位置");
} else {
console.log("JS: 暂无定位数据,向 Flutter 请求...");
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
}
}
/**
* 核心功能 3: 路径规划
*/
function startRouteSearch() {
// 获取输入框的文字
var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value;
if (!startKw) {
alert("请输入起点");
return;
}
if (!endKw) {
alert("请输入终点");
return;
}
// 清除旧路线
if (driving) driving.clear();
// 收起键盘
document.getElementById('startInput').blur();
document.getElementById('endInput').blur();
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
var points = [];
// 1. 处理起点逻辑
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
if (!startKw || startKw === '我的位置') {
if (!currentLng || !currentLat) {
// 如果还没获取到定位
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
alert("正在获取定位,请稍后...");
return;
}
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
points.push({ points.push({
keyword: '我的位置', // 用于显示的名字 keyword: '我的位置',
location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标 location: new AMap.LngLat(currentLng, currentLat)
}); });
} else { } else {
// 如果用户手动输入了地点 (例如 "北京南站") console.log("FlutterLog: 无定位数据且无输入");
// 直接存入关键词,让高德自己去搜 alert("未获取到定位,请输入起点");
points.push({ return;
keyword: startKw
});
} }
// 2. 处理终点逻辑 (通常是关键词)
points.push({
keyword: endKw
});
// 3. 发起搜索
// points 数组里现在是一个起点对象和一个终点对象
driving.search(points, function (status, result) {
if (status === 'complete') {
console.log('JS: 规划成功');
var panel = document.getElementById('panel');
panel.style.display = 'block';
document.body.classList.add('panel-active');
} else {
console.log('JS: 规划失败', result);
alert("规划失败,请检查起终点名称");
}
});
} }
// 辅助功能:手动绘制路线 (Flutter 可能用到) // 2. 处理终点
function drawCustomRoute(pointsJsonString) { points.push({ keyword: endKw });
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
if (driving) driving.clear(); // 显示底部面板
var polyline = new AMap.Polyline({ document.getElementById('panel').style.display = 'block';
path: path,
isOutline: true, // 3. 发起请求
outlineColor: '#ffeeee', if (isTruckMode && truckDriving) {
borderWeight: 3, // 货车接口略有不同,需要传入 path 数组
strokeColor: "#3366FF", // truckDriving.search(path, callback)
strokeWeight: 6, // 这里为了简化,我们先演示轿车。货车通常需要具体的经纬度,建议先通过 Geocoder 把 endKw 转成经纬度再传给货车接口
lineJoin: 'round' console.log("FlutterLog: 货车API需要更严格的经纬度参数建议先使用轿车演示");
});
map.add(polyline);
map.setFitView([polyline]);
} }
window.onload = initMap; // 默认使用轿车规划 (支持 keyword + location 混合)
</script> driving.search(points, function(status, result) {
if (status === 'complete') {
console.log('FlutterLog: 规划成功');
} else {
console.log('FlutterLog: 规划失败: ' + result);
// 常见错误: "USER_DAILY_QUERY_OVER_LIMIT" (Key额度超限)
// "INVALID_USER_KEY" (Key错误)
// "no_data" (地点没找到)
}
});
}
// --- 绘制自定义路径 (保持原有功能) ---
function drawCustomRoute(pointsJsonString) {
// 如果是从Flutter传来的JSON字符串需要Parse如果是对象则直接用
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
if (driving) driving.clear();
var polyline = new AMap.Polyline({
path: path,
isOutline: true,
outlineColor: '#ffeeee',
borderWeight: 3,
strokeColor: "#3366FF",
strokeWeight: 6,
lineJoin: 'round'
});
map.add(polyline);
map.setFitView([polyline]);
}
window.onload = initMap;
</script>
</body> </body>
</html> </html>

View File

@@ -1,122 +1,122 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "Icon-App-20x20@2x.png", "size" : "20x20",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "filename" : "Icon-App-20x20@2x.png",
"size" : "20x20" "scale" : "2x"
}, },
{ {
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png", "filename" : "Icon-App-20x20@3x.png",
"idiom" : "iphone", "scale" : "3x"
"scale" : "3x",
"size" : "20x20"
}, },
{ {
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29@1x.png",
"idiom" : "iphone", "scale" : "1x"
"scale" : "1x",
"size" : "29x29"
}, },
{ {
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x.png",
"idiom" : "iphone", "scale" : "2x"
"scale" : "2x",
"size" : "29x29"
}, },
{ {
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png", "filename" : "Icon-App-29x29@3x.png",
"idiom" : "iphone", "scale" : "3x"
"scale" : "3x",
"size" : "29x29"
}, },
{ {
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png", "filename" : "Icon-App-40x40@2x.png",
"idiom" : "iphone", "scale" : "2x"
"scale" : "2x",
"size" : "40x40"
}, },
{ {
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png", "filename" : "Icon-App-40x40@3x.png",
"idiom" : "iphone", "scale" : "3x"
"scale" : "3x",
"size" : "40x40"
}, },
{ {
"filename" : "icon 1.png", "size" : "60x60",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "filename" : "Icon-App-60x60@2x.png",
"size" : "60x60" "scale" : "2x"
}, },
{ {
"filename" : "icon.png", "size" : "60x60",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "filename" : "Icon-App-60x60@3x.png",
"size" : "60x60" "scale" : "3x"
}, },
{ {
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png", "filename" : "Icon-App-20x20@1x.png",
"idiom" : "ipad", "scale" : "1x"
"scale" : "1x",
"size" : "20x20"
}, },
{ {
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png", "filename" : "Icon-App-20x20@2x.png",
"idiom" : "ipad", "scale" : "2x"
"scale" : "2x",
"size" : "20x20"
}, },
{ {
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png", "filename" : "Icon-App-29x29@1x.png",
"idiom" : "ipad", "scale" : "1x"
"scale" : "1x",
"size" : "29x29"
}, },
{ {
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png", "filename" : "Icon-App-29x29@2x.png",
"idiom" : "ipad", "scale" : "2x"
"scale" : "2x",
"size" : "29x29"
}, },
{ {
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png", "filename" : "Icon-App-40x40@1x.png",
"idiom" : "ipad", "scale" : "1x"
"scale" : "1x",
"size" : "40x40"
}, },
{ {
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png", "filename" : "Icon-App-40x40@2x.png",
"idiom" : "ipad", "scale" : "2x"
"scale" : "2x",
"size" : "40x40"
}, },
{ {
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png", "filename" : "Icon-App-76x76@1x.png",
"idiom" : "ipad", "scale" : "1x"
"scale" : "1x",
"size" : "76x76"
}, },
{ {
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png", "filename" : "Icon-App-76x76@2x.png",
"idiom" : "ipad", "scale" : "2x"
"scale" : "2x",
"size" : "76x76"
}, },
{ {
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png", "filename" : "Icon-App-83.5x83.5@2x.png",
"idiom" : "ipad", "scale" : "2x"
"scale" : "2x",
"size" : "83.5x83.5"
}, },
{ {
"filename" : "Icon-App-1024x1024@1x.png", "size" : "1024x1024",
"idiom" : "ios-marketing", "idiom" : "ios-marketing",
"scale" : "1x", "filename" : "Icon-App-1024x1024@1x.png",
"size" : "1024x1024" "scale" : "1x"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "version" : 1,
"version" : 1 "author" : "xcode"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -4,7 +4,6 @@ class StationModel {
final String address; final String address;
final String price; final String price;
final String siteStatusName; // 例如 "维修中" final String siteStatusName; // 例如 "维修中"
final int isSelect; // 新增字段 1是可用 0是不可用
StationModel({ StationModel({
required this.hydrogenId, required this.hydrogenId,
@@ -12,7 +11,6 @@ class StationModel {
required this.address, required this.address,
required this.price, required this.price,
required this.siteStatusName, required this.siteStatusName,
required this.isSelect,
}); });
// 从 JSON map 创建对象的工厂构造函数 // 从 JSON map 创建对象的工厂构造函数
@@ -23,7 +21,6 @@ class StationModel {
address: json['address'] ?? '地址未知', address: json['address'] ?? '地址未知',
price: json['price']?.toString() ?? '0.00', price: json['price']?.toString() ?? '0.00',
siteStatusName: json['siteStatusName'] ?? '', siteStatusName: json['siteStatusName'] ?? '',
isSelect: json['isSelect'] as int? ?? 0, // 新增字段的解析,默认为 0
); );
} }
} }

View File

@@ -7,14 +7,11 @@ class AppTheme {
static const Color themeColor = Color(0xFF0c83c3); static const Color themeColor = Color(0xFF0c83c3);
//http://192.168.110.222:8080/
static const String test_service_url = "https://beta-esg.api.lnh2e.com/"; static const String test_service_url = "http://47.100.49.118:8090/api/";
static const String release_service_url = ""; static const String release_service_url = "";
//加氢站相关查询 //加氢站相关查询
static const String jiaqing_service_url = static const String jiaqing_service_url = "https://lnh2e.com/api/lingniu-manager-v1/v1/";
"https://beta.lnh2e.com/api/lingniu-manager-v1/v1/";
//车辆信息 //车辆信息
static const String car_service_url = "http://47.99.166.38:20000/"; static const String car_service_url = "http://47.99.166.38:20000/";
@@ -59,9 +56,12 @@ class AppTheme {
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color.fromARGB(255, 34, 34, 34), backgroundColor: Color.fromARGB(255, 34, 34, 34),
centerTitle: true, centerTitle: true,
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), titleTextStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
bottomAppBarTheme: BottomAppBarThemeData( bottomAppBarTheme: BottomAppBarThemeData(
color: Color.fromARGB(255, 34, 34, 34), color: Color.fromARGB(255, 34, 34, 34),
elevation: 4.0, elevation: 4.0,
), ),

View File

@@ -30,7 +30,7 @@ void main() async {
// 设计稿尺寸 单位dp // 设计稿尺寸 单位dp
designSize: const Size(390, 844), designSize: const Size(390, 844),
// Getx Log // Getx Log
enableLog: true, enableLog: false,
// 默认的跳转动画 // 默认的跳转动画
defaultTransition: Transition.rightToLeft, defaultTransition: Transition.rightToLeft,
// 主题模式 // 主题模式

View File

@@ -45,6 +45,8 @@ class B_BaseWidgetsPage extends GetView<B_BaseWidgetsController> {
label: '加氢预约', label: '加氢预约',
icon: AntdIcon.orderedlist, icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill, selectedIcon: AntdIcon.calendar_fill,
badge: '99+',
dot: true,
), ),
NavigationItemModel( NavigationItemModel(
label: '站点信息', label: '站点信息',

View File

@@ -71,7 +71,7 @@ class ReservationPage extends GetView<ReservationController> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
_buildHeaderStat(controller.customerPrice, '氢气价格'), _buildHeaderStat(controller.costPrice, '氢气价格'),
_buildHeaderStat(controller.timeStr, '营业时间'), _buildHeaderStat(controller.timeStr, '营业时间'),
_buildHeaderStat('98%', '设备状态'), _buildHeaderStat('98%', '设备状态'),
], ],
@@ -119,7 +119,7 @@ class ReservationPage extends GetView<ReservationController> {
// --- 价格信息 --- // --- 价格信息 ---
_buildSectionTitle('价格信息'), _buildSectionTitle('价格信息'),
// _buildDisplayField(label: '氢气价格 (元/kg)', value: controller.costPrice), _buildDisplayField(label: '氢气价格 (元/kg)', value: controller.costPrice),
_buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice), _buildDisplayField(label: '官方价格 (元/kg)', value: controller.customerPrice),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -76,8 +76,7 @@ class ReservationModel {
String startTimeStr = json['startTime']?.toString() ?? ''; String startTimeStr = json['startTime']?.toString() ?? '';
String endTimeStr = json['endTime']?.toString() ?? ''; String endTimeStr = json['endTime']?.toString() ?? '';
String dateStr = json['date']?.toString() ?? ''; String dateStr = json['date']?.toString() ?? '';
String timeRange = String timeRange = (startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty)
(startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty)
? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm ? '$dateStr ${startTimeStr.substring(11, 16)}-${endTimeStr.substring(11, 16)}' // 截取 HH:mm
: '时间未定'; : '时间未定';
@@ -120,8 +119,6 @@ class SiteController extends GetxController with BaseControllerMixin {
List<ReservationModel> reservationList = []; List<ReservationModel> reservationList = [];
Timer? _refreshTimer; Timer? _refreshTimer;
final TextEditingController searchController = TextEditingController();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -133,7 +130,6 @@ class SiteController extends GetxController with BaseControllerMixin {
@override @override
void onClose() { void onClose() {
stopAutoRefresh(); stopAutoRefresh();
searchController.dispose();
super.onClose(); super.onClose();
} }
@@ -159,17 +155,13 @@ class SiteController extends GetxController with BaseControllerMixin {
Future<void> fetchReservationData() async { Future<void> fetchReservationData() async {
showLoading("加载中"); showLoading("加载中");
final String searchText = searchController.text.trim();
try { try {
var response = await HttpService.to.post( var response = await HttpService.to.post(
"appointment/orderAddHyd/sitOrderPage", "appointment/orderAddHyd/sitOrderPage",
data: { data: {
'stationName': name, // 使用从 renderData 中获取到的 name 'stationName': name, // 使用从 renderData 中获取到的 name
'pageNum': 1, 'pageNum': 1,
'pageSize': 50, // 暂时不考虑分页一次获取30条 'pageSize': 30, // 暂时不考虑分页一次获取30条
'plateNumber': searchText, // 加氢站名称
'phone': searchText, //手机号
}, },
); );
@@ -275,8 +267,6 @@ class SiteController extends GetxController with BaseControllerMixin {
String orderAmount = ""; String orderAmount = "";
String completedAmount = ""; String completedAmount = "";
String name = ""; String name = "";
String orderTotalAmount = "";
String orderUnfinishedAmount = "";
Future<void> renderData() async { Future<void> renderData() async {
try { try {
@@ -296,14 +286,8 @@ class SiteController extends GetxController with BaseControllerMixin {
orderAmount = result.data["orderAmount"].toString(); orderAmount = result.data["orderAmount"].toString();
completedAmount = result.data["completedAmount"].toString(); completedAmount = result.data["completedAmount"].toString();
name = result.data["name"].toString(); name = result.data["name"].toString();
orderTotalAmount = result.data["orderTotalAmount"] ?? "";
orderUnfinishedAmount = result.data["orderUnfinishedAmount"] ?? "";
leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString(); leftHydrogen = leftHydrogen.isEmpty ? "统计中" : leftHydrogen.toString();
orderTotalAmount = orderTotalAmount.isEmpty ? "统计中" : orderTotalAmount.toString();
orderUnfinishedAmount = orderUnfinishedAmount.isEmpty
? "统计中"
: orderUnfinishedAmount.toString();
//加载列表数据 //加载列表数据
fetchReservationData(); fetchReservationData();

View File

@@ -94,17 +94,6 @@ class SitePage extends GetView<SiteController> {
], ],
), ),
), ),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(controller.orderTotalAmount, '加氢总量'),
_buildStatItem(controller.orderUnfinishedAmount, '未加氢总量'),
],
),
),
], ],
), ),
), ),
@@ -161,7 +150,6 @@ class SitePage extends GetView<SiteController> {
], ],
), ),
), ),
_buildSearchView(),
controller.hasReservationData controller.hasReservationData
? _buildReservationListView() ? _buildReservationListView()
: _buildEmptyReservationView(), : _buildEmptyReservationView(),
@@ -191,66 +179,6 @@ class SitePage extends GetView<SiteController> {
); );
} }
//搜索输入框,提示可以输入车牌或者手机
Widget _buildSearchView() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 44,
child: TextField(
controller: controller.searchController, // 绑定控制器
decoration: InputDecoration(
hintText: '输入车牌号或完整手机号查询',
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(22),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(22),
borderSide: BorderSide(color: Get.theme.primaryColor, width: 1.5),
),
// 清除按钮
suffixIcon: IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
controller.searchController.clear();
controller.fetchReservationData(); // 清除后也刷新一次
},
),
),
onSubmitted: (value) {
// 用户在键盘上点击“完成”或“搜索”时触发
controller.fetchReservationData();
},
),
),
),
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}) { Widget _buildStatItem(String value, String label, {Color valueColor = Colors.blue}) {
return Expanded( return Expanded(

View File

@@ -51,11 +51,14 @@ class BaseWidgetsPage extends GetView<BaseWidgetsController> {
label: '地图', label: '地图',
icon: AntdIcon.location, icon: AntdIcon.location,
selectedIcon: AntdIcon.location_fill, selectedIcon: AntdIcon.location_fill,
dot: false,
), ),
NavigationItemModel( NavigationItemModel(
label: '加氢预约', label: '加氢预约',
icon: AntdIcon.orderedlist, icon: AntdIcon.orderedlist,
selectedIcon: AntdIcon.calendar_fill, selectedIcon: AntdIcon.calendar_fill,
badge: '99+',
dot: true,
), ),
NavigationItemModel( NavigationItemModel(
label: '车辆信息', label: '车辆信息',

View File

@@ -2,7 +2,6 @@ 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/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import 'certificate_viewer_page.dart'; import 'certificate_viewer_page.dart';
@@ -28,33 +27,6 @@ class CarInfoController extends GetxController with BaseControllerMixin {
super.onInit(); super.onInit();
getUserBindCarInfo(); getUserBindCarInfo();
} }
@override
void onReady() {
super.onReady();
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog) {
Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆',
confirmText: "去绑定",
cancelText: "稍后",
onConfirm: () {
doQrCode();
},
);
// 标记为已显示,本次会话不再提示
StorageService.to.markBindVehicleDialogAsShown();
});
}
}
void doQrCode() async {
var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) {
getUserBindCarInfo();
refreshAppui();
}
}
void getUserBindCarInfo() async { void getUserBindCarInfo() async {
if (StorageService.to.hasVehicleInfo) { if (StorageService.to.hasVehicleInfo) {

View File

@@ -173,7 +173,12 @@ class CarInfoPage extends GetView<CarInfoController> {
isButton isButton
? GestureDetector( ? GestureDetector(
onTap: () async { onTap: () async {
controller.doQrCode(); //判断是否绑定成功
var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) {
controller.getUserBindCarInfo();
refreshAppui();
}
}, },
child: Container( child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w), margin: EdgeInsetsGeometry.only(left: 10.w),

View File

@@ -4,133 +4,124 @@ import 'dart:convert';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart'; // 确保引用这个做权限请求
class MapController extends GetxController with BaseControllerMixin ,WidgetsBindingObserver{ class MapController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'map'; String get builderId => 'map';
InAppWebViewController? webViewController; InAppWebViewController? webViewController;
StreamSubscription<Position>? positionStream; StreamSubscription<Position>? positionStream;
// --- 状态标志 ---
final RxBool isLocationPermissionGranted = false.obs; final RxBool isLocationPermissionGranted = false.obs;
final RxBool isMapReady = false.obs; // 跟踪地图是否加载完成
// 缓存最后一次的位置,防止 WebView 加载慢于定位
Position? _lastKnownPosition; Position? _lastKnownPosition;
// 是否已经初始化过地图中心
bool _hasInitializedMapCenter = false;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
WidgetsBinding.instance.addObserver(this); // 注册监听
requestLocationPermission(); requestLocationPermission();
} }
@override @override
void onClose() { void onClose() {
WidgetsBinding.instance.removeObserver(this); // 移除监听
positionStream?.cancel(); positionStream?.cancel();
super.onClose(); super.onClose();
} }
@override // 请求权限
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// 回到前台,如果之前有权限且开启过,恢复定位
if(isLocationPermissionGranted.value) _startLocationTracking();
} else if (state == AppLifecycleState.paused) {
// 切到后台,取消定位流
positionStream?.cancel();
}
}
/// WebView 创建完成时调用
void onWebViewCreated(InAppWebViewController controller) {
webViewController = controller;
// 添加 JS Handler 来监听来自网页的 'mapReady' 事件
controller.addJavaScriptHandler(
handlerName: 'mapReady',
callback: (args) {
print("Flutter<-JS: Map is ready, starting location tracking.");
isMapReady.value = true;
// 地图就绪后,如果已有权限和缓存的定位,则开始定位
if (isLocationPermissionGranted.value) {
_startLocationTracking();
}
},
);
}
/// 优化后的权限请求函数
void requestLocationPermission() async { void requestLocationPermission() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
showErrorToast('请开启手机的定位服务');
return;
}
var status = await Permission.locationWhenInUse.request(); var status = await Permission.locationWhenInUse.request();
switch (status) { print("权限状态: $status"); // 在控制台看这个输出
case PermissionStatus.granted:
isLocationPermissionGranted.value = true; if (status.isGranted) {
// 权限已获取,如果地图也已就绪,则开始定位 isLocationPermissionGranted.value = true;
if (isMapReady.value) { showToast('定位权限已获取');
_startLocationTracking(); _startLocationTracking();
}
break;
case PermissionStatus.denied:
showErrorToast('需要定位权限才能显示您的位置');
break;
case PermissionStatus.permanentlyDenied:
showErrorToast('定位权限已被永久拒绝,请到设置中手动开启');
openAppSettings();
break;
default:
showErrorToast('获取定位权限时发生未知错误');
break;
} }
else if (status.isPermanentlyDenied) {
// iOS 特性:如果你之前点过“不允许”,之后再请求都会直接进这里
isLocationPermissionGranted.value = false;
// 引导用户去设置页面开启
openAppSettings();
}
else {
// 第一次被拒绝,或者其他情况
isLocationPermissionGranted.value = false;
showErrorToast('需要定位权限才能显示您的位置');
}
} }
///定位监听 (不再需要 delay) // 开定位监听
void _startLocationTracking() async { void _startLocationTracking() async {
if (!isLocationPermissionGranted.value || // 再次检查服务是否开启
!await Geolocator.isLocationServiceEnabled()) { bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return; return;
} }
const LocationSettings locationSettings = LocationSettings( const LocationSettings locationSettings = LocationSettings(
accuracy: LocationAccuracy.high, accuracy: LocationAccuracy.high,
distanceFilter: 10, distanceFilter: 50, // 移动更新范围
); );
// 立即获取一次当前位置,用于快速初始化 // 获取一次当前位置,用于快速初始化中心
try { Geolocator.getCurrentPosition().then((position) {
Position position = await Geolocator.getCurrentPosition();
_lastKnownPosition = position; _lastKnownPosition = position;
_syncLocationToMap(position); _syncLocationToMap(position, isInit: true);
} catch (e) { });
print("Error getting initial position: $e");
}
// 持续监听位置变化 // 持续监听
positionStream?.cancel(); // 先取消旧的监听
positionStream = Geolocator.getPositionStream(locationSettings: locationSettings) positionStream = Geolocator.getPositionStream(locationSettings: locationSettings)
.listen((Position position) { .listen((Position position) {
_lastKnownPosition = position; _lastKnownPosition = position;
_syncLocationToMap(position); _syncLocationToMap(position, isInit: false);
}); });
} }
// 将 Flutter 的定位数据同步给 JS // 将位置同步给 JS
void _syncLocationToMap(Position position) { void _syncLocationToMap(Position position, {required bool isInit}) {
if (webViewController == null || !isMapReady.value) return; if (webViewController == null) return;
// 只有当 heading 有效且大于 0 时才传给 JS否则传 null让 JS 保持上一次的角度
String headingVal = (position.heading > 0) ? "${position.heading}" : "null";
webViewController!.evaluateJavascript( // 如果是第一次定位,或者 WebView 刚加载完且从未设置过中心
source: if (isInit || !_hasInitializedMapCenter) {
"updateMyLocation(${position.latitude}, ${position.longitude}, ${position.heading})", webViewController!.evaluateJavascript(
); source: "initLocation(${position.latitude}, ${position.longitude})"
);
_hasInitializedMapCenter = true;
} else {
// 后续只更新图标位置,不强行移动地图中心(防止用户拖动地图看别处时被拉回)
webViewController!.evaluateJavascript(
source: "updateMyLocation(${position.latitude}, ${position.longitude}, ${position.heading})"
);
}
} }
}
// WebView 加载完成回调
void onWebViewLoadStop() {
// 页面加载完了,如果我们已经拿到了定位数据,立刻设置地图中心
if (_lastKnownPosition != null) {
_syncLocationToMap(_lastKnownPosition!, isInit: true);
}
// 如果业务需要,可以在这里加载默认的推荐路径
// loadRecommendedRoute();
}
// 模拟:获取你的后台推荐路径
void loadRecommendedRoute() {
// 示例数据
List<List<double>> routePoints = [
[116.397428, 39.90923],
[116.398000, 39.90950],
[116.400000, 39.91000],
];
String jsonPoints = jsonEncode(routePoints);
webViewController?.evaluateJavascript(source: "drawCustomRoute($jsonPoints)");
}
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
@@ -11,21 +10,23 @@ class MapPage extends GetView<MapController> {
return Stack( return Stack(
children: [ children: [
InAppWebView( InAppWebView(
// 确保 pubspec.yaml 中声明了 assets/html/map.html
initialFile: 'assets/html/map.html', initialFile: 'assets/html/map.html',
initialSettings: InAppWebViewSettings( initialSettings: InAppWebViewSettings(
isInspectable: true, isInspectable: true,
// 既然完全依赖 Flutter 定位,建议把 WebView 的定位彻底关掉,防止 JS 意外触发 geolocationEnabled: true,
geolocationEnabled: false, // 允许 JS 弹窗 (Alert) 用于调试
javaScriptEnabled: true, javaScriptCanOpenWindowsAutomatically: true,
// Android 混合开发推荐配置
useHybridComposition: true, // 提升地图渲染性能(重要)
allowFileAccessFromFileURLs: true, // 允许本地 html 访问本地资源
allowUniversalAccessFromFileURLs: true,
), ),
// 当 WebView 创建完成后,将 controller 实例传递给我们的 MapController onWebViewCreated: (c) {
onWebViewCreated: controller.onWebViewCreated, controller.webViewController = c;
},
onLoadStop: (c, url) {
// 通知 Controller 页面加载完毕
controller.onWebViewLoadStop();
},
onConsoleMessage: (controller, consoleMessage) { onConsoleMessage: (controller, consoleMessage) {
// 方便在 Flutter 控制台查看来自 JS 日志 // 方便在 Flutter 控制台 JS 日志
print("JS Log: ${consoleMessage.message}"); print("JS Log: ${consoleMessage.message}");
}, },
), ),

View File

@@ -9,72 +9,31 @@ import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/common/model/station_model.dart'; import 'package:ln_jq_app/common/model/station_model.dart';
import 'package:ln_jq_app/common/model/vehicle_info.dart'; import 'package:ln_jq_app/common/model/vehicle_info.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; import 'package:ln_jq_app/pages/b_page/site/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/controller.dart';
import 'package:ln_jq_app/pages/c_page/reservation_edit/view.dart';
import 'package:ln_jq_app/pages/qr_code/view.dart';
import 'package:ln_jq_app/storage_service.dart'; import 'package:ln_jq_app/storage_service.dart';
import '../../../common/styles/theme.dart'; import '../../../common/styles/theme.dart';
/// Helper class for managing time slots
class TimeSlot {
final TimeOfDay start;
final TimeOfDay end;
TimeSlot(this.start, this.end);
String get display {
final startStr =
'${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}';
final endStr =
'${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}';
return '$startStr - $endStr';
}
}
class C_ReservationController extends GetxController with BaseControllerMixin { class C_ReservationController extends GetxController with BaseControllerMixin {
@override @override
String get builderId => 'reservation'; String get builderId => 'reservation';
final DateTime _now = DateTime.now(); final Rx<DateTime> selectedDate = DateTime.now().obs;
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
// 计算当前时间属于哪个半小时区间 final Rx<TimeOfDay> endTime = TimeOfDay.fromDateTime(
late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now); DateTime.now().add(const Duration(minutes: 30)),
late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime( ).obs;
_getDateTimeFromTimeOfDay(_initialStartTime).add(const Duration(minutes: 30)),
);
late final Rx<DateTime> selectedDate = DateTime(_now.year, _now.month, _now.day).obs;
late final Rx<TimeOfDay> startTime = _initialStartTime.obs;
late final Rx<TimeOfDay> endTime = _initialEndTime.obs;
/// 静态辅助方法,用于计算初始的开始时间
static TimeOfDay _calculateInitialStartTime(DateTime now) {
if (now.minute < 30) {
// 如果当前分钟小于30则开始时间为当前小时的0分
return TimeOfDay(hour: now.hour, minute: 0);
} else {
// 如果当前分钟大于等于30则开始时间为当前小时的30分
return TimeOfDay(hour: now.hour, minute: 30);
}
}
/// 静态辅助方法将TimeOfDay转换为DateTime
static DateTime _getDateTimeFromTimeOfDay(TimeOfDay time) {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day, time.hour, time.minute);
}
final TextEditingController amountController = TextEditingController(); final TextEditingController amountController = TextEditingController();
TextEditingController plateNumberController = TextEditingController(); TextEditingController plateNumberController = TextEditingController();
final RxList<StationModel> stationOptions = <StationModel>[].obs; final RxList<StationModel> stationOptions = <StationModel>[].obs;
final Rxn<String> selectedStationId = Rxn<String>(); final Rxn<String> selectedStationId = Rxn<String>(); // 用 ID 来选择
String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value); String get formattedDate => DateFormat('yyyy-MM-dd').format(selectedDate.value);
/// 时间段 String get formattedStartTime => _formatTimeOfDay(startTime.value);
String get formattedTimeSlot =>
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}'; String get formattedEndTime => _formatTimeOfDay(endTime.value);
void pickDate(BuildContext context) { void pickDate(BuildContext context) {
DateTime tempDate = selectedDate.value; DateTime tempDate = selectedDate.value;
@@ -124,7 +83,22 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
Expanded( Expanded(
child: CupertinoDatePicker( child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date, mode: CupertinoDatePickerMode.date,
initialDateTime: selectedDate.value, initialDateTime:
selectedDate.value.isBefore(
DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
),
)
? DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
)
: selectedDate.value,
// 设置最小可选日期为“今天凌晨0点”
minimumDate: DateTime( minimumDate: DateTime(
DateTime.now().year, DateTime.now().year,
DateTime.now().month, DateTime.now().month,
@@ -143,76 +117,69 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
); );
} }
///30 分钟为间隔 时间选择器 void pickTime(BuildContext context, bool isStartTime) {
void pickTime(BuildContext context) { // 确定当前操作的时间和初始值
final now = DateTime.now(); TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value;
final isToday = DateTime now = DateTime.now();
selectedDate.value.year == now.year &&
selectedDate.value.month == now.month &&
selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = []; // 计算最小可选时间
for (int i = 0; i < 48; i++) { DateTime? minimumDateTime; // 默认为 null即可选任意时间
final startMinutes = i * 30;
final endMinutes = startMinutes + 30;
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60); // 获取当前选择的日期(年月日)
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60); final selectedDay = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
);
final slotStartDateTime = DateTime( // 获取今天的日期(年月日)
final today = DateTime(now.year, now.month, now.day);
if (isStartTime) {
// 如果是选择开始时间并且日期是 今天
if (selectedDay.isAtSameMomentAs(today)) {
minimumDateTime = now; // 最小可选时间就是现在
}
} else {
// 如果是选择结束时间
// 将开始时间转换为 DateTime 对象
final startDateTime = DateTime(
selectedDate.value.year, selectedDate.value.year,
selectedDate.value.month, selectedDate.value.month,
selectedDate.value.day, selectedDate.value.day,
startTime.hour, startTime.value.hour,
startTime.minute, startTime.value.minute,
); );
// 如果不是今天,所有时间段都有效 // 结束时间的最小值必须晚于开始时间
if (!isToday) { minimumDateTime = startDateTime;
availableSlots.add(TimeSlot(startTime, endTime));
} else {
// 如果是今天,需要判断该时间段是否可选
// 创建时间段的结束时间对象
final slotEndDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
endTime.hour,
endTime.minute,
);
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的 // 如果日期是今天,并且开始时间早于现在,那么结束时间的最小值也应该是现在
if (slotEndDateTime.isAfter(now)) { if (selectedDay.isAtSameMomentAs(today) && startDateTime.isBefore(now)) {
availableSlots.add(TimeSlot(startTime, endTime)); minimumDateTime = now;
}
} }
} }
if (availableSlots.isEmpty) { // 初始显示时间
showToast('今天已没有可预约的时间段'); DateTime initialDateTime = DateTime(
return; selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
initialTime.hour,
initialTime.minute,
);
// 确保初始时间不早于最小时间
if (minimumDateTime != null && initialDateTime.isBefore(minimumDateTime)) {
initialDateTime = minimumDateTime;
} }
int initialItem = availableSlots.indexWhere( DateTime tempTime = initialDateTime;
(slot) =>
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
);
if (initialItem == -1) {
initialItem = 0;
}
TimeSlot tempSlot = availableSlots[initialItem];
final FixedExtentScrollController scrollController = FixedExtentScrollController(
initialItem: initialItem,
);
Get.bottomSheet( Get.bottomSheet(
Container( Container(
height: 300, height: 300,
padding: const EdgeInsets.only(top: 6.0),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -236,8 +203,60 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
), ),
CupertinoButton( CupertinoButton(
onPressed: () { onPressed: () {
startTime.value = tempSlot.start; final pickedTempTime = TimeOfDay.fromDateTime(tempTime);
endTime.value = tempSlot.end;
// 验证条件1不能选择过去的时间
// 这个验证只在选择【今天】的【开始时间】或【结束时间】时有意义
final selectedDay = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
);
final today = DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
if (selectedDay.isAtSameMomentAs(today) &&
tempTime.isBefore(DateTime.now())) {
showToast('不能选择过去的时间');
return; // 中断执行,不关闭弹窗
}
//结束时间必须晚于开始时间
if (isStartTime) {
// 已有的结束时间 比较
final pickedStartInMinutes =
pickedTempTime.hour * 60 + pickedTempTime.minute;
final endInMinutes =
endTime.value.hour * 60 + endTime.value.minute;
if (pickedStartInMinutes >= endInMinutes) {
// 整结束时间
startTime.value = pickedTempTime;
final newEndDateTime = tempTime.add(
const Duration(minutes: 30),
);
endTime.value = TimeOfDay.fromDateTime(newEndDateTime);
} else {
//设置开始时间
startTime.value = pickedTempTime;
}
} else {
// 如果当前正在设置结束时间,我们来和已有的开始时间比较
final pickedEndInMinutes =
pickedTempTime.hour * 60 + pickedTempTime.minute;
final startInMinutes =
startTime.value.hour * 60 + startTime.value.minute;
if (pickedEndInMinutes <= startInMinutes) {
showToast('结束时间必须晚于开始时间');
return; // 中断执行,不关闭弹窗
} else {
endTime.value = pickedTempTime;
}
}
Get.back(); Get.back();
}, },
child: const Text( child: const Text(
@@ -253,15 +272,14 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
), ),
const Divider(height: 1, color: Color(0xFFE5E5E5)), const Divider(height: 1, color: Color(0xFFE5E5E5)),
Expanded( Expanded(
child: CupertinoPicker( child: CupertinoDatePicker(
scrollController: scrollController, mode: CupertinoDatePickerMode.time,
itemExtent: 40.0, use24hFormat: true,
onSelectedItemChanged: (index) { initialDateTime: initialDateTime,
tempSlot = availableSlots[index]; minimumDate: minimumDateTime,
onDateTimeChanged: (DateTime newTime) {
tempTime = newTime;
}, },
children: availableSlots
.map((slot) => Center(child: Text(slot.display)))
.toList(),
), ),
), ),
], ],
@@ -271,9 +289,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
); );
} }
// 用于存储上一次成功预约的信息
ReservationModel? lastSuccessfulReservation;
/// 提交预约 /// 提交预约
void submitReservation() async { void submitReservation() async {
if (plateNumber.isEmpty) { if (plateNumber.isEmpty) {
@@ -285,62 +300,33 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
showToast("请输入需要预约的氢量"); showToast("请输入需要预约的氢量");
return; return;
} }
double ampuntDouble = (double.tryParse(ampuntStr) ?? 0.0);
if (ampuntDouble == 0) {
showToast("请输入需要预约的氢量");
return;
}
if (ampuntDouble > (double.tryParse(difference) ?? 0.0)) {
showToast('当前最大可预约氢量为${difference}(KG)');
return;
}
if (selectedStationId.value == null || selectedStationId.value!.isEmpty) { if (selectedStationId.value == null || selectedStationId.value!.isEmpty) {
showToast("请先选择加氢站"); showToast("请先选择加氢站");
return; return;
} }
// 将选择的日期和时间组合成一个完整的 DateTime 对象
final dateStr = formattedDate; final reservationStartDateTime = DateTime(
final startTimeStr =
'$dateStr ${_formatTimeOfDay(startTime.value)}:00'; // Use helper directly
if (lastSuccessfulReservation != null &&
lastSuccessfulReservation!.id == selectedStationId.value &&
lastSuccessfulReservation!.startTime == startTimeStr) {
showToast("请勿重复提交相同的预约");
return;
}
final reservationEndDateTime = DateTime(
selectedDate.value.year, selectedDate.value.year,
selectedDate.value.month, selectedDate.value.month,
selectedDate.value.day, selectedDate.value.day,
endTime.value.hour, startTime.value.hour,
endTime.value.minute, startTime.value.minute,
); );
//判断预约区间的结束时间是否早于当前时间留出1分钟缓冲 // 检查预约时间是否当前时间之前
if (reservationEndDateTime.isBefore( if (reservationStartDateTime.isBefore(DateTime.now())) {
DateTime.now().subtract(const Duration(minutes: 1)), showToast("不可预约过去的时间");
)) {
showToast("无法预约已过去的时间段");
return; return;
} }
try { try {
showLoading("提交中");
final selectedStation = stationOptions.firstWhere( final selectedStation = stationOptions.firstWhere(
(s) => s.hydrogenId == selectedStationId.value, (s) => s.hydrogenId == selectedStationId.value,
); );
if (selectedStation.siteStatusName != "营运中") { final dateStr = formattedDate; // "yyyy-MM-dd"
showToast("该站点${selectedStation.siteStatusName},暂无法预约"); final startTimeStr = '$dateStr ${formattedStartTime}:00'; // "yyyy-MM-dd HH:mm:ss"
return; final endTimeStr = '$dateStr ${formattedEndTime}:00';
}
showLoading("提交中");
final endTimeStr =
'$dateStr ${_formatTimeOfDay(endTime.value)}:00'; // Use helper directly
var responseData = await HttpService.to.post( var responseData = await HttpService.to.post(
'appointment/orderAddHyd/saveOrUpdate', 'appointment/orderAddHyd/saveOrUpdate',
@@ -367,31 +353,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
var result = BaseModel.fromJson(responseData.data); var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) { if (result.code == 0) {
showSuccessToast("预约成功"); showSuccessToast("预约成功");
lastSuccessfulReservation = ReservationModel(
id: selectedStationId.value!,
hydAmount: ampuntStr,
startTime: startTimeStr,
endTime: endTimeStr,
stationName: selectedStation.name,
plateNumber: '',
amount: '',
time: '',
contactPerson: '',
contactPhone: '',
contacts: '',
phone: '',
date: '',
state: '',
stateName: '',
addStatus: '',
addStatusName: '',
);
//打开预约列表
Future.delayed(const Duration(milliseconds: 800), () {
getReservationList();
});
} else { } else {
showErrorToast(result.message); showErrorToast(result.message);
} }
@@ -535,57 +496,33 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
), // Blue border ), // Blue border
), ),
child: Text( child: Text(
reservation.stateName + reservation.stateName,
"-" +
reservation.addStatusName,
style: const TextStyle( style: const TextStyle(
color: Color(0xFF1890FF), color: Color(0xFF1890FF),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
plateNumber.isEmpty Container(
? SizedBox() padding: const EdgeInsets.symmetric(
: GestureDetector( horizontal: 12,
onTap: () async { vertical: 4,
var result = await Get.to( ),
() => ReservationEditPage(), decoration: BoxDecoration(
arguments: { color: const Color(
'reservation': reservation, 0xFFFFF7E6,
'difference': difference, ), // Light orange background
}, borderRadius: BorderRadius.circular(12),
binding: BindingsBuilder(() { ),
Get.put(ReservationEditController()); child: Text(
}), reservation.addStatusName,
preventDuplicates: false, style: const TextStyle(
); color: Color(0xFFFA8C16),
showToast("555$result"); fontWeight: FontWeight.bold,
if (result == true) { fontSize: 12,
Get.back(); ),
getReservationList(); ),
} ),
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(
0xFFFFF7E6,
), // Light orange background
borderRadius: BorderRadius.circular(12),
),
child: Text(
"修改",
style: const TextStyle(
color: Color(0xFFFA8C16),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -638,14 +575,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
); );
} }
String leftHydrogen = "0";
String workEfficiency = "0"; String workEfficiency = "0";
String fillingWeight = "0"; String fillingWeight = "0";
String fillingTimes = "0"; String fillingTimes = "0";
String plateNumber = ""; String plateNumber = "";
String vin = ""; String vin = "";
String leftHydrogen = "0";
num maxHydrogen = 0;
String difference = "";
@override @override
bool get listenLifecycleEvent => true; bool get listenLifecycleEvent => true;
@@ -666,20 +601,11 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
plateNumber = bean.plateNumber; plateNumber = bean.plateNumber;
vin = bean.vin; vin = bean.vin;
plateNumberController = TextEditingController(text: plateNumber); plateNumberController = TextEditingController(text: plateNumber);
maxHydrogen = bean.maxHydrogen;
getCatinfo(); getCatinfo();
getJqinfo(); getJqinfo();
} }
} }
void doQrCode() async {
var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) {
getUserBindCarInfo();
refreshAppui();
}
}
void getJqinfo() async { void getJqinfo() async {
try { try {
HttpService.to.setBaseUrl(AppTheme.test_service_url); HttpService.to.setBaseUrl(AppTheme.test_service_url);
@@ -725,9 +651,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
leftHydrogen = result.data["leftHydrogen"].toString(); leftHydrogen = result.data["leftHydrogen"].toString();
workEfficiency = result.data["workEfficiency"].toString(); workEfficiency = result.data["workEfficiency"].toString();
final leftHydrogenNum = double.tryParse(leftHydrogen) ?? 0.0;
difference = (maxHydrogen - leftHydrogenNum).toStringAsFixed(2);
updateUi(); updateUi();
} catch (e) { } catch (e) {
} finally { } finally {
@@ -784,22 +707,6 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
dismissLoading(); dismissLoading();
HttpService.to.setBaseUrl(AppTheme.test_service_url); HttpService.to.setBaseUrl(AppTheme.test_service_url);
HttpService.to.dio.options.headers = originalHeaders; HttpService.to.dio.options.headers = originalHeaders;
// 如果未绑定车辆,且本次会话尚未提示过,则弹出提示
if (!StorageService.to.hasShownBindVehicleDialog) {
Future.delayed(const Duration(milliseconds: 500), () {
DialogX.to.showConfirmDialog(
title: '当前尚未绑定车辆',
confirmText: "去绑定",
cancelText: "稍后",
onConfirm: () {
doQrCode();
},
);
// 标记为已显示,本次会话不再提示
StorageService.to.markBindVehicleDialogAsShown();
});
}
} }
} }

View File

@@ -201,7 +201,12 @@ class ReservationPage extends GetView<C_ReservationController> {
isButton isButton
? GestureDetector( ? GestureDetector(
onTap: () async { onTap: () async {
controller.doQrCode(); //判断是否绑定成功
var scanResult = await Get.to(() => const QrCodePage());
if (scanResult == true) {
controller.getUserBindCarInfo();
refreshAppui();
}
}, },
child: Container( child: Container(
margin: EdgeInsetsGeometry.only(left: 10.w), margin: EdgeInsetsGeometry.only(left: 10.w),
@@ -258,15 +263,21 @@ class ReservationPage extends GetView<C_ReservationController> {
onTap: () => controller.pickDate(context), onTap: () => controller.pickDate(context),
), ),
_buildPickerRow( _buildPickerRow(
label: '预约时间', label: '开始时间',
value: controller.formattedTimeSlot, value: controller.formattedStartTime,
icon: Icons.access_time_outlined, icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context), onTap: () => controller.pickTime(context, true),
),
_buildPickerRow(
label: '结束时间',
value: controller.formattedEndTime,
icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context, false),
), ),
_buildTextField( _buildTextField(
label: '预约氢量(KG)', label: '预约氢量(kg)',
controller: controller.amountController, controller: controller.amountController,
hint: '当前最大可预约氢量${controller.difference}(KG)', hint: '请输入氢量(kg)',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
_buildTextField( _buildTextField(
@@ -432,7 +443,6 @@ class ReservationPage extends GetView<C_ReservationController> {
.map( .map(
(station) => DropdownMenuItem<String>( (station) => DropdownMenuItem<String>(
value: station.hydrogenId, // value 是站点的唯一ID value: station.hydrogenId, // value 是站点的唯一ID
enabled: station.isSelect == 1,
child: _buildDropdownItem(station), // child 是自定义的 Widget child: _buildDropdownItem(station), // child 是自定义的 Widget
), ),
) )
@@ -479,10 +489,7 @@ class ReservationPage extends GetView<C_ReservationController> {
/// 构建下拉菜单中的每一项 /// 构建下拉菜单中的每一项
Widget _buildDropdownItem(StationModel station) { Widget _buildDropdownItem(StationModel station) {
bool isSelectable = (station.isSelect == 1); bool isMaintenance = (station.siteStatusName != '营运中');
//营运并且可用
bool isMaintenance = ((station.siteStatusName != '营运中') && isSelectable);
final textColor = isSelectable ? Colors.black : Colors.grey;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row( child: Row(
@@ -499,22 +506,14 @@ class ReservationPage extends GetView<C_ReservationController> {
Flexible( Flexible(
child: Text( child: Text(
station.name, station.name,
style: TextStyle( style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
color: textColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
), ),
), ),
Text( Text(
' | ¥${station.price}/kg', ' | ¥${station.price}/kg',
style: TextStyle( style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
color: textColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
), ),
if (isMaintenance) const SizedBox(width: 8), if (isMaintenance) const SizedBox(width: 8),
if (isMaintenance) if (isMaintenance)

View File

@@ -1,253 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:intl/intl.dart';
import 'package:ln_jq_app/common/model/base_model.dart';
import 'package:ln_jq_app/pages/b_page/site/controller.dart'; // For ReservationModel
import 'package:ln_jq_app/common/styles/theme.dart';
class TimeSlot {
final TimeOfDay start;
final TimeOfDay end;
TimeSlot(this.start, this.end);
String get display {
final startStr =
'${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}';
final endStr =
'${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}';
return '$startStr - $endStr';
}
}
class ReservationEditController extends GetxController with BaseControllerMixin {
late final ReservationModel reservation;
late final String difference;
// --- State Variables ---
final Rx<DateTime> selectedDate = DateTime.now().obs;
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
final Rx<TimeOfDay> endTime = TimeOfDay.now().obs;
final TextEditingController amountController = TextEditingController();
// --- Getters for UI display ---
String get formattedTimeSlot =>
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
@override
void onInit() {
super.onInit();
// Expect a Map containing both the reservation and the difference
final args = Get.arguments as Map<String, dynamic>;
reservation = args['reservation'] as ReservationModel;
difference = args['difference'] as String? ?? '0';
try {
// Initialize the UI with the data from the passed reservation object
selectedDate.value = DateFormat('yyyy-MM-dd').parse(reservation.date);
final startDateTime = DateFormat(
'yyyy-MM-dd HH:mm:ss',
).parse(reservation.startTime);
startTime.value = TimeOfDay.fromDateTime(startDateTime);
final endDateTime = DateFormat('yyyy-MM-dd HH:mm:ss').parse(reservation.endTime);
endTime.value = TimeOfDay.fromDateTime(endDateTime);
amountController.text = reservation.hydAmount.replaceAll('kg', '');
} catch (e) {
showErrorToast("加载预约数据时出错");
Get.back(); // Go back if data is invalid
}
}
@override
void onClose() {
amountController.dispose();
super.onClose();
}
/// Reusable time slot picker logic, copied from the reservation creation page.
void pickTime(BuildContext context) {
final now = DateTime.now();
final isToday =
selectedDate.value.year == now.year &&
selectedDate.value.month == now.month &&
selectedDate.value.day == now.day;
final List<TimeSlot> availableSlots = [];
for (int i = 0; i < 48; i++) {
final startMinutes = i * 30;
final endMinutes = startMinutes + 30;
final slotStartTime = TimeOfDay(
hour: startMinutes ~/ 60,
minute: startMinutes % 60,
);
final slotEndTime = TimeOfDay(
hour: (endMinutes ~/ 60) % 24,
minute: endMinutes % 60,
);
final slotStartDateTime = DateTime(
selectedDate.value.year,
selectedDate.value.month,
selectedDate.value.day,
slotStartTime.hour,
slotStartTime.minute,
);
if (!isToday || slotStartDateTime.isAfter(now)) {
availableSlots.add(TimeSlot(slotStartTime, slotEndTime));
}
}
if (availableSlots.isEmpty) {
showToast('今天已没有可预约的时间段');
return;
}
int initialItem = availableSlots.indexWhere(
(slot) =>
slot.start.hour == startTime.value.hour &&
(startTime.value.minute < 30
? slot.start.minute == 0
: slot.start.minute == 30),
);
if (initialItem == -1) initialItem = 0;
TimeSlot tempSlot = availableSlots[initialItem];
Get.bottomSheet(
Container(
height: 300,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
onPressed: () {
if (Get.isBottomSheetOpen ?? false) {
Navigator.of(Get.overlayContext!).pop(); // 更加安全的关闭当前弹窗的方式
}
},
child: const Text(
'取消',
style: TextStyle(color: CupertinoColors.systemGrey),
),
),
CupertinoButton(
onPressed: () {
startTime.value = tempSlot.start;
endTime.value = tempSlot.end;
if (Get.isBottomSheetOpen ?? false) {
Navigator.of(Get.overlayContext!).pop(); // 更加安全的关闭当前弹窗的方式
}
},
child: const Text(
'确认',
style: TextStyle(
color: AppTheme.themeColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFE5E5E5)),
Expanded(
child: CupertinoPicker(
scrollController: FixedExtentScrollController(initialItem: initialItem),
itemExtent: 40.0,
onSelectedItemChanged: (index) {
tempSlot = availableSlots[index];
},
children: availableSlots
.map((slot) => Center(child: Text(slot.display)))
.toList(),
),
),
],
),
),
backgroundColor: Colors.transparent,
);
}
/// This function will be called when the 'Save Changes' button is pressed.
void updateReservation() async {
String amountStr = amountController.text.toString();
if (amountStr.isEmpty) {
showToast("请输入需要预约的氢量");
return;
}
double amountDouble = (double.tryParse(amountStr) ?? 0.0);
if (amountDouble <= 0) {
showToast("预约氢量必须大于0");
return;
}
if (amountDouble > (double.tryParse(difference) ?? 0.0)) {
showToast('当前最大可预约氢量为$difference(KG)');
return;
}
showLoading("正在保存修改...");
final dateStr = DateFormat('yyyy-MM-dd').format(selectedDate.value);
final startTimeStr = '$dateStr ${_formatTimeOfDay(startTime.value)}:00';
final endTimeStr = '$dateStr ${_formatTimeOfDay(endTime.value)}:00';
try {
var responseData = await HttpService.to.post(
'appointment/orderAddHyd/saveOrUpdate',
data: {
'id': reservation.id,
'startTime': startTimeStr,
'endTime': endTimeStr,
'hydAmount': amountStr,
},
);
if (responseData == null || responseData.data == null) {
showToast('服务暂不可用,请稍后');
dismissLoading();
return;
}
var result = BaseModel.fromJson(responseData.data);
if (result.code == 0) {
showSuccessToast("修改成功");
//弹窗刷新数据
Get.back(result: true);
} else {
showErrorToast(result.message);
}
} catch (e) {
showErrorToast("保存失败,请稍后再试");
} finally {
dismissLoading();
}
}
String _formatTimeOfDay(TimeOfDay time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
@override
String get builderId => "reservationeditpage";
}

View File

@@ -1,127 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class ReservationEditPage extends GetView<ReservationEditController> {
const ReservationEditPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetBuilder(
init: ReservationEditController(),
id: 'reservationeditpage',
builder: (_) {
return Scaffold(
appBar: AppBar(title: const Text('修改预约'), centerTitle: true),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPickerRow(
label: '预约时间',
value: controller.formattedTimeSlot,
icon: Icons.access_time_outlined,
onTap: () => controller.pickTime(context),
),
_buildTextField(
label: '预约氢量(KG)',
controller: controller.amountController,
// Use Obx to make the hint text responsive if needed, though here it's static.
hint: '当前最大可预约氢量${controller.difference}(KG)',
keyboardType: TextInputType.number,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: controller.updateReservation,
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: const Text(
'保存修改',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
),
),
),
);
},
);
}
Widget _buildPickerRow({
required String label,
required String value,
required IconData icon,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
const SizedBox(height: 8),
InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[400]!),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(value, style: const TextStyle(fontSize: 16)),
Icon(icon, color: Colors.grey, size: 20),
],
),
),
),
],
),
);
}
Widget _buildTextField({
required String label,
required TextEditingController controller,
required String hint,
TextInputType? keyboardType,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
),
),
],
),
);
}
}

View File

@@ -20,10 +20,9 @@ class StorageService extends GetxService {
static const String _phoneKey = 'user_phone'; static const String _phoneKey = 'user_phone';
static const String _idCardKey = 'user_id_card'; static const String _idCardKey = 'user_id_card';
static const String _vehicleInfoKey = 'vehicle_info'; static const String _vehicleInfoKey = 'vehicle_info';
// 加氢站登录凭证的键名
static const String _stationAccountKey = 'station_account'; static const String _stationAccountKey = 'station_account';
static const String _stationPasswordKey = 'station_password'; static const String _stationPasswordKey = 'station_password';
// 新增:用于标记“绑定车辆”弹窗是否已在本会话中显示过
static const String _bindDialogShownKey = 'bind_vehicle_dialog_shown';
static StorageService get to => Get.find(); static StorageService get to => Get.find();
@@ -42,13 +41,10 @@ class StorageService extends GetxService {
String? get idCard => _box.read<String?>(_idCardKey); String? get idCard => _box.read<String?>(_idCardKey);
bool get hasVehicleInfo => _box.hasData(_vehicleInfoKey); bool get hasVehicleInfo => _box.hasData(_vehicleInfoKey);
// 记住密码:获取加氢站账号和密码
String? get stationAccount => _box.read<String?>(_stationAccountKey); String? get stationAccount => _box.read<String?>(_stationAccountKey);
String? get stationPassword => _box.read<String?>(_stationPasswordKey); String? get stationPassword => _box.read<String?>(_stationPasswordKey);
// 新增:获取“绑定车辆”弹窗是否已显示的标志
bool get hasShownBindVehicleDialog => _box.read<bool>(_bindDialogShownKey) ?? false;
VehicleInfo? get vehicleInfo { VehicleInfo? get vehicleInfo {
final vehicleJson = _box.read<String?>(_vehicleInfoKey); final vehicleJson = _box.read<String?>(_vehicleInfoKey);
if (vehicleJson != null) { if (vehicleJson != null) {
@@ -88,21 +84,17 @@ class StorageService extends GetxService {
await _box.write(_vehicleInfoKey, vehicleInfoToJson(data)); await _box.write(_vehicleInfoKey, vehicleInfoToJson(data));
} }
// 保存加氢站登录凭证
Future<void> saveStationCredentials(String account, String password) async { Future<void> saveStationCredentials(String account, String password) async {
await _box.write(_stationAccountKey, account); await _box.write(_stationAccountKey, account);
await _box.write(_stationPasswordKey, password); await _box.write(_stationPasswordKey, password);
} }
// 新增:标记“绑定车辆”弹窗已显示
Future<void> markBindVehicleDialogAsShown() async {
await _box.write(_bindDialogShownKey, true);
}
Future<void> clearVehicleInfo() async { Future<void> clearVehicleInfo() async {
await _box.remove(_vehicleInfoKey); await _box.remove(_vehicleInfoKey);
} }
// 清除加氢站登录凭证
Future<void> clearStationCredentials() async { Future<void> clearStationCredentials() async {
await _box.remove(_stationAccountKey); await _box.remove(_stationAccountKey);
await _box.remove(_stationPasswordKey); await _box.remove(_stationPasswordKey);
@@ -116,7 +108,6 @@ class StorageService extends GetxService {
await _box.remove(_phoneKey); await _box.remove(_phoneKey);
await _box.remove(_idCardKey); await _box.remove(_idCardKey);
await clearVehicleInfo(); await clearVehicleInfo();
// 登出时,清除“绑定车辆”弹窗的显示记录,以便下次登录时可以再次弹出 // 注意:登出时我们不清除“记住的密码”,以便下次用户登录时仍然可以回填
await _box.remove(_bindDialogShownKey);
} }
} }