Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88b16ca69e | |||
| ba3467810c | |||
| 0bfedd54cb | |||
| f25d7e4567 | |||
| 4cdedff654 | |||
| 4f99ab4164 | |||
| cdc5af7f45 | |||
| 20e5e58ded | |||
| d3b7ab8fbe | |||
| 5609594439 | |||
| da4149ec60 | |||
| aa30d13b91 | |||
| b854d295f9 | |||
| 26a24efeb8 | |||
| 496d77482d |
@@ -19,7 +19,7 @@
|
|||||||
<application
|
<application
|
||||||
android:label="小羚羚"
|
android:label="小羚羚"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/logo">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
BIN
ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.png
Normal file
BIN
ln_jq_app/android/app/src/main/res/mipmap-xhdpi/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -1,13 +1,35 @@
|
|||||||
<!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>
|
|
||||||
html, body, #container { width: 100%; height: 100%; margin: 0; }
|
|
||||||
|
|
||||||
/* 搜索栏样式 */
|
<style>
|
||||||
|
/* --- 全局样式 --- */
|
||||||
|
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;
|
||||||
@@ -16,47 +38,114 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
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: 30%; /* 占据底部30% */
|
height: 35%;
|
||||||
|
/* 面板高度 */
|
||||||
background-color: white;
|
background-color: white;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-top: 1px solid #ccc;
|
border-top: 1px solid #eee;
|
||||||
|
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. 加载地图和插件 -->
|
<!-- 2. 加载地图和插件 (去掉了 Geolocation 插件,避免弹窗) -->
|
||||||
<!-- 注意:如果你要货车规划,需要在plugin里加上 AMap.TruckDriving -->
|
<script
|
||||||
<script src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete"></script>
|
src="https://webapi.amap.com/maps?v=2.0&key=2cc1d822e313307fe311c3127a1deeb5&plugin=AMap.MoveAnimation,AMap.Driving,AMap.TruckDriving,AMap.AutoComplete,AMap.ToolBar,AMap.Scale">
|
||||||
|
</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>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input id="endInput" placeholder="终点: 请输入目的地" />
|
<input id="endInput" placeholder="终点: 请输入目的地" />
|
||||||
@@ -65,13 +154,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
|
|
||||||
<div id="panel"></div>
|
<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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var map, marker, driving, truckDriving;
|
var map, marker, driving, truckDriving;
|
||||||
var currentLat, currentLng;
|
var currentLat, currentLng;
|
||||||
|
|
||||||
// 标记是否使用货车模式 (true: 货车, false: 轿车)
|
|
||||||
var isTruckMode = false;
|
var isTruckMode = false;
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
@@ -81,21 +178,38 @@
|
|||||||
viewMode: '3D'
|
viewMode: '3D'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 通知 Flutter 地图加载完毕
|
||||||
|
map.on('complete', function () {
|
||||||
|
console.log("JS->: Map is ready.");
|
||||||
|
if (window.flutter_inappwebview) {
|
||||||
|
window.flutter_inappwebview.callHandler('mapReady');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加基础控件
|
||||||
|
map.addControl(new AMap.Scale());
|
||||||
|
map.addControl(new AMap.ToolBar({
|
||||||
|
visible: true,
|
||||||
|
position: {
|
||||||
|
top: '200px',
|
||||||
|
right: '10px'
|
||||||
|
} // 稍微避开定位按钮
|
||||||
|
}));
|
||||||
|
|
||||||
// --- 初始化轿车规划 ---
|
// --- 初始化轿车规划 ---
|
||||||
driving = new AMap.Driving({
|
driving = new AMap.Driving({
|
||||||
map: map,
|
map: map,
|
||||||
panel: "panel", // 结果显示在 panel div 中
|
panel: "panel",
|
||||||
policy: AMap.DrivingPolicy.LEAST_TIME
|
policy: AMap.DrivingPolicy.LEAST_TIME
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 初始化货车规划 (按需开启) ---
|
// --- 初始化货车规划 ---
|
||||||
// 货车需要设置 size (1:微型, 2:轻型, 3:中型, 4:重型)
|
|
||||||
if (AMap.TruckDriving) {
|
if (AMap.TruckDriving) {
|
||||||
truckDriving = new AMap.TruckDriving({
|
truckDriving = new AMap.TruckDriving({
|
||||||
map: map,
|
map: map,
|
||||||
panel: "panel",
|
panel: "panel",
|
||||||
size: 2,
|
size: 2,
|
||||||
policy: 0, // 0: 躲避拥堵
|
policy: 0,
|
||||||
width: 2.5,
|
width: 2.5,
|
||||||
height: 2,
|
height: 2,
|
||||||
load: 1,
|
load: 1,
|
||||||
@@ -104,16 +218,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 输入提示 (自动补全) ---
|
// --- 输入提示 ---
|
||||||
new AMap.AutoComplete({ input: "startInput" });
|
new AMap.AutoComplete({
|
||||||
new AMap.AutoComplete({ input: "endInput" });
|
input: "startInput"
|
||||||
|
});
|
||||||
|
new AMap.AutoComplete({
|
||||||
|
input: "endInput"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 被动接收 Flutter 传来的定位 ---
|
/**
|
||||||
|
* 核心功能 1: 接收 Flutter 传来的定位数据
|
||||||
|
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
|
||||||
|
*/
|
||||||
function updateMyLocation(lat, lng, angle) {
|
function updateMyLocation(lat, lng, angle) {
|
||||||
currentLat = lat;
|
var rawLat = parseFloat(lat);
|
||||||
currentLng = lng;
|
var rawLng = parseFloat(lng);
|
||||||
var position = [lng, lat];
|
var rawAngle = parseFloat(angle);
|
||||||
|
var gps = [rawLng, rawLat];
|
||||||
|
|
||||||
|
AMap.convertFrom(gps, 'gps', function (status, result) {
|
||||||
|
if (result.info === 'ok') {
|
||||||
|
var mPoint = result.locations[0];
|
||||||
|
currentLng = mPoint.lng;
|
||||||
|
currentLat = mPoint.lat;
|
||||||
|
var position = [currentLng, currentLat];
|
||||||
|
|
||||||
if (!marker) {
|
if (!marker) {
|
||||||
marker = new AMap.Marker({
|
marker = new AMap.Marker({
|
||||||
@@ -122,89 +251,114 @@
|
|||||||
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: angle || 0,
|
angle: isNaN(rawAngle) ? 0 : rawAngle,
|
||||||
});
|
});
|
||||||
// 第一次定位移动中心
|
|
||||||
map.setCenter(position);
|
map.setCenter(position);
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
marker.setPosition(position);
|
marker.moveTo(position, {
|
||||||
if (angle) marker.setAngle(angle);
|
duration: 1000,
|
||||||
|
autoRotation: true
|
||||||
|
});
|
||||||
|
if (!isNaN(rawAngle)) marker.setAngle(rawAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心功能 2: 点击按钮回到当前位置
|
||||||
|
*/
|
||||||
|
function backToLocation() {
|
||||||
|
if (currentLng && currentLat) {
|
||||||
|
map.panTo([currentLng, currentLat]);
|
||||||
|
map.setZoom(17);
|
||||||
|
console.log("JS: 已回到当前位置");
|
||||||
|
} else {
|
||||||
|
console.log("JS: 暂无定位数据,向 Flutter 请求...");
|
||||||
|
if (window.flutter_inappwebview) {
|
||||||
|
window.flutter_inappwebview.callHandler('requestLocation');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 核心:规划路线 ---
|
/**
|
||||||
|
* 核心功能 3: 路径规划
|
||||||
|
*/
|
||||||
function startRouteSearch() {
|
function startRouteSearch() {
|
||||||
|
// 获取输入框的文字
|
||||||
var startKw = document.getElementById('startInput').value;
|
var startKw = document.getElementById('startInput').value;
|
||||||
var endKw = document.getElementById('endInput').value;
|
var endKw = document.getElementById('endInput').value;
|
||||||
|
|
||||||
if(!endKw) {
|
if (!startKw) {
|
||||||
console.log("FlutterLog: 终点不能为空");
|
alert("请输入起点");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("FlutterLog: 开始规划..." + (isTruckMode ? "货车" : "轿车"));
|
if (!endKw) {
|
||||||
|
alert("请输入终点");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 清除之前的路线
|
// 清除旧路线
|
||||||
if (driving) driving.clear();
|
if (driving) driving.clear();
|
||||||
if(truckDriving) truckDriving.clear();
|
|
||||||
|
|
||||||
// 构造起点和终点参数
|
// 收起键盘
|
||||||
// 高德API Search方法支持点对象数组:[{keyword: '名字', city: '城市'}, {keyword: '...'}]
|
document.getElementById('startInput').blur();
|
||||||
|
document.getElementById('endInput').blur();
|
||||||
|
|
||||||
|
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
|
||||||
var points = [];
|
var points = [];
|
||||||
|
|
||||||
// 1. 处理起点
|
// 1. 处理起点逻辑
|
||||||
if (startKw) {
|
// 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
|
||||||
// 如果用户输入了文字
|
if (!startKw || startKw === '我的位置') {
|
||||||
points.push({ keyword: startKw });
|
if (!currentLng || !currentLat) {
|
||||||
} else {
|
// 如果还没获取到定位
|
||||||
// 如果用户没输入,使用当前定位
|
if (window.flutter_inappwebview) {
|
||||||
if (currentLng && currentLat) {
|
window.flutter_inappwebview.callHandler('requestLocation');
|
||||||
// 关键点:当混合使用坐标和关键字时,必须构建 LngLat 对象
|
}
|
||||||
points.push({
|
alert("正在获取定位,请稍后...");
|
||||||
keyword: '我的位置',
|
|
||||||
location: new AMap.LngLat(currentLng, currentLat)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("FlutterLog: 无定位数据且无输入");
|
|
||||||
alert("未获取到定位,请输入起点");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
|
||||||
|
points.push({
|
||||||
|
keyword: '我的位置', // 用于显示的名字
|
||||||
|
location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果用户手动输入了地点 (例如 "北京南站")
|
||||||
|
// 直接存入关键词,让高德自己去搜
|
||||||
|
points.push({
|
||||||
|
keyword: startKw
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 处理终点
|
// 2. 处理终点逻辑 (通常是关键词)
|
||||||
points.push({ keyword: endKw });
|
points.push({
|
||||||
|
keyword: endKw
|
||||||
|
});
|
||||||
|
|
||||||
// 显示底部面板
|
// 3. 发起搜索
|
||||||
document.getElementById('panel').style.display = 'block';
|
// points 数组里现在是一个起点对象和一个终点对象
|
||||||
|
|
||||||
// 3. 发起请求
|
|
||||||
if (isTruckMode && truckDriving) {
|
|
||||||
// 货车接口略有不同,需要传入 path 数组
|
|
||||||
// truckDriving.search(path, callback)
|
|
||||||
// 这里为了简化,我们先演示轿车。货车通常需要具体的经纬度,建议先通过 Geocoder 把 endKw 转成经纬度再传给货车接口
|
|
||||||
console.log("FlutterLog: 货车API需要更严格的经纬度参数,建议先使用轿车演示");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认使用轿车规划 (支持 keyword + location 混合)
|
|
||||||
driving.search(points, function (status, result) {
|
driving.search(points, function (status, result) {
|
||||||
if (status === 'complete') {
|
if (status === 'complete') {
|
||||||
console.log('FlutterLog: 规划成功');
|
console.log('JS: 规划成功');
|
||||||
|
var panel = document.getElementById('panel');
|
||||||
|
panel.style.display = 'block';
|
||||||
|
document.body.classList.add('panel-active');
|
||||||
} else {
|
} else {
|
||||||
console.log('FlutterLog: 规划失败: ' + result);
|
console.log('JS: 规划失败', result);
|
||||||
// 常见错误: "USER_DAILY_QUERY_OVER_LIMIT" (Key额度超限)
|
alert("规划失败,请检查起终点名称");
|
||||||
// "INVALID_USER_KEY" (Key错误)
|
|
||||||
// "no_data" (地点没找到)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 绘制自定义路径 (保持原有功能) ---
|
// 辅助功能:手动绘制路线 (Flutter 可能用到)
|
||||||
function drawCustomRoute(pointsJsonString) {
|
function drawCustomRoute(pointsJsonString) {
|
||||||
// 如果是从Flutter传来的JSON字符串,需要Parse,如果是对象则直接用
|
|
||||||
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
|
var path = typeof pointsJsonString === 'string' ? JSON.parse(pointsJsonString) : pointsJsonString;
|
||||||
|
|
||||||
if (driving) driving.clear();
|
if (driving) driving.clear();
|
||||||
|
|
||||||
var polyline = new AMap.Polyline({
|
var polyline = new AMap.Polyline({
|
||||||
path: path,
|
path: path,
|
||||||
isOutline: true,
|
isOutline: true,
|
||||||
@@ -221,4 +375,5 @@
|
|||||||
window.onload = initMap;
|
window.onload = initMap;
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,122 +1,122 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
"scale" : "2x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@3x.png",
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
"scale" : "3x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
"scale" : "1x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
"scale" : "2x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@3x.png",
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
"scale" : "3x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
"scale" : "2x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
"scale" : "3x",
|
||||||
"scale" : "2x"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"filename" : "icon 1.png",
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
"scale" : "2x",
|
||||||
"scale" : "3x"
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@1x.png",
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
"scale" : "1x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
"scale" : "2x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
"scale" : "1x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
"scale" : "2x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@1x.png",
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
"scale" : "1x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
"scale" : "2x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
"scale" : "1x"
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "76x76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@2x.png",
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
"scale" : "2x",
|
||||||
"scale" : "2x"
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "Icon-App-1024x1024@1x.png",
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
"scale" : "1x"
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 862 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
ln_jq_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon.png
Normal file
BIN
ln_jq_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -4,6 +4,7 @@ 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,
|
||||||
@@ -11,6 +12,7 @@ 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 创建对象的工厂构造函数
|
||||||
@@ -21,6 +23,7 @@ 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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ 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 = "http://47.100.49.118:8090/api/";
|
static const String test_service_url = "https://beta-esg.api.lnh2e.com/";
|
||||||
static const String release_service_url = "";
|
static const String release_service_url = "";
|
||||||
|
|
||||||
//加氢站相关查询
|
//加氢站相关查询
|
||||||
static const String jiaqing_service_url = "https://lnh2e.com/api/lingniu-manager-v1/v1/";
|
static const String jiaqing_service_url =
|
||||||
|
"https://beta.lnh2e.com/api/lingniu-manager-v1/v1/";
|
||||||
|
|
||||||
//车辆信息
|
//车辆信息
|
||||||
static const String car_service_url = "http://47.99.166.38:20000/";
|
static const String car_service_url = "http://47.99.166.38:20000/";
|
||||||
|
|
||||||
@@ -56,10 +59,7 @@ class AppTheme {
|
|||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: Color.fromARGB(255, 34, 34, 34),
|
backgroundColor: Color.fromARGB(255, 34, 34, 34),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
bottomAppBarTheme: BottomAppBarThemeData(
|
bottomAppBarTheme: BottomAppBarThemeData(
|
||||||
color: Color.fromARGB(255, 34, 34, 34),
|
color: Color.fromARGB(255, 34, 34, 34),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ void main() async {
|
|||||||
// 设计稿尺寸 单位:dp
|
// 设计稿尺寸 单位:dp
|
||||||
designSize: const Size(390, 844),
|
designSize: const Size(390, 844),
|
||||||
// Getx Log
|
// Getx Log
|
||||||
enableLog: false,
|
enableLog: true,
|
||||||
// 默认的跳转动画
|
// 默认的跳转动画
|
||||||
defaultTransition: Transition.rightToLeft,
|
defaultTransition: Transition.rightToLeft,
|
||||||
// 主题模式
|
// 主题模式
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ 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: '站点信息',
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class ReservationPage extends GetView<ReservationController> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_buildHeaderStat(controller.costPrice, '氢气价格'),
|
_buildHeaderStat(controller.customerPrice, '氢气价格'),
|
||||||
_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),
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ 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 = (startTimeStr.isNotEmpty && endTimeStr.isNotEmpty && dateStr.isNotEmpty)
|
String timeRange =
|
||||||
|
(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
|
||||||
: '时间未定';
|
: '时间未定';
|
||||||
|
|
||||||
@@ -119,6 +120,8 @@ 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();
|
||||||
@@ -130,6 +133,7 @@ class SiteController extends GetxController with BaseControllerMixin {
|
|||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
|
searchController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +159,17 @@ 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': 30, // 暂时不考虑分页,一次获取30条
|
'pageSize': 50, // 暂时不考虑分页,一次获取30条
|
||||||
|
'plateNumber': searchText, // 加氢站名称
|
||||||
|
'phone': searchText, //手机号
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -267,6 +275,8 @@ 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 {
|
||||||
@@ -286,8 +296,14 @@ 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();
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ 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, '未加氢总量'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -150,6 +161,7 @@ class SitePage extends GetView<SiteController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_buildSearchView(),
|
||||||
controller.hasReservationData
|
controller.hasReservationData
|
||||||
? _buildReservationListView()
|
? _buildReservationListView()
|
||||||
: _buildEmptyReservationView(),
|
: _buildEmptyReservationView(),
|
||||||
@@ -179,6 +191,66 @@ 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(
|
||||||
|
|||||||
@@ -51,14 +51,11 @@ 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: '车辆信息',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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';
|
||||||
@@ -27,6 +28,33 @@ 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) {
|
||||||
|
|||||||
@@ -173,12 +173,7 @@ 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),
|
||||||
|
|||||||
@@ -4,124 +4,133 @@ 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 {
|
class MapController extends GetxController with BaseControllerMixin ,WidgetsBindingObserver{
|
||||||
@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;
|
|
||||||
|
|
||||||
// 缓存最后一次的位置,防止 WebView 加载慢于定位
|
// --- 状态标志 ---
|
||||||
|
final RxBool isLocationPermissionGranted = false.obs;
|
||||||
|
final RxBool isMapReady = false.obs; // 跟踪地图是否加载完成
|
||||||
|
|
||||||
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 requestLocationPermission() async {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
var status = await Permission.locationWhenInUse.request();
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
// 回到前台,如果之前有权限且开启过,恢复定位
|
||||||
|
if(isLocationPermissionGranted.value) _startLocationTracking();
|
||||||
|
} else if (state == AppLifecycleState.paused) {
|
||||||
|
// 切到后台,取消定位流
|
||||||
|
positionStream?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
print("权限状态: $status"); // 在控制台看这个输出
|
/// WebView 创建完成时调用
|
||||||
|
void onWebViewCreated(InAppWebViewController controller) {
|
||||||
if (status.isGranted) {
|
webViewController = controller;
|
||||||
isLocationPermissionGranted.value = true;
|
// 添加 JS Handler 来监听来自网页的 'mapReady' 事件
|
||||||
showToast('定位权限已获取');
|
controller.addJavaScriptHandler(
|
||||||
|
handlerName: 'mapReady',
|
||||||
|
callback: (args) {
|
||||||
|
print("Flutter<-JS: Map is ready, starting location tracking.");
|
||||||
|
isMapReady.value = true;
|
||||||
|
// 地图就绪后,如果已有权限和缓存的定位,则开始定位
|
||||||
|
if (isLocationPermissionGranted.value) {
|
||||||
_startLocationTracking();
|
_startLocationTracking();
|
||||||
}
|
}
|
||||||
else if (status.isPermanentlyDenied) {
|
},
|
||||||
// iOS 特性:如果你之前点过“不允许”,之后再请求都会直接进这里
|
);
|
||||||
isLocationPermissionGranted.value = false;
|
|
||||||
// 引导用户去设置页面开启
|
|
||||||
openAppSettings();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 第一次被拒绝,或者其他情况
|
|
||||||
isLocationPermissionGranted.value = false;
|
|
||||||
showErrorToast('需要定位权限才能显示您的位置');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/// 优化后的权限请求函数
|
||||||
|
void requestLocationPermission() async {
|
||||||
// 开启定位监听
|
|
||||||
void _startLocationTracking() async {
|
|
||||||
// 再次检查服务是否开启
|
|
||||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
|
showErrorToast('请开启手机的定位服务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = await Permission.locationWhenInUse.request();
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case PermissionStatus.granted:
|
||||||
|
isLocationPermissionGranted.value = true;
|
||||||
|
// 权限已获取,如果地图也已就绪,则开始定位
|
||||||
|
if (isMapReady.value) {
|
||||||
|
_startLocationTracking();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PermissionStatus.denied:
|
||||||
|
showErrorToast('需要定位权限才能显示您的位置');
|
||||||
|
break;
|
||||||
|
case PermissionStatus.permanentlyDenied:
|
||||||
|
showErrorToast('定位权限已被永久拒绝,请到设置中手动开启');
|
||||||
|
openAppSettings();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showErrorToast('获取定位权限时发生未知错误');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开始定位监听 (不再需要 delay)
|
||||||
|
void _startLocationTracking() async {
|
||||||
|
if (!isLocationPermissionGranted.value ||
|
||||||
|
!await Geolocator.isLocationServiceEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationSettings locationSettings = LocationSettings(
|
const LocationSettings locationSettings = LocationSettings(
|
||||||
accuracy: LocationAccuracy.high,
|
accuracy: LocationAccuracy.high,
|
||||||
distanceFilter: 50, // 移动更新范围
|
distanceFilter: 10,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 先获取一次当前位置,用于快速初始化中心
|
// 立即获取一次当前位置,用于快速初始化
|
||||||
Geolocator.getCurrentPosition().then((position) {
|
try {
|
||||||
|
Position position = await Geolocator.getCurrentPosition();
|
||||||
_lastKnownPosition = position;
|
_lastKnownPosition = position;
|
||||||
_syncLocationToMap(position, isInit: true);
|
_syncLocationToMap(position);
|
||||||
});
|
} 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, isInit: false);
|
_syncLocationToMap(position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将位置同步给 JS
|
// 将 Flutter 的定位数据同步给 JS
|
||||||
void _syncLocationToMap(Position position, {required bool isInit}) {
|
void _syncLocationToMap(Position position) {
|
||||||
if (webViewController == null) return;
|
if (webViewController == null || !isMapReady.value) return;
|
||||||
|
// 只有当 heading 有效且大于 0 时才传给 JS,否则传 null,让 JS 保持上一次的角度
|
||||||
|
String headingVal = (position.heading > 0) ? "${position.heading}" : "null";
|
||||||
|
|
||||||
// 如果是第一次定位,或者 WebView 刚加载完且从未设置过中心
|
|
||||||
if (isInit || !_hasInitializedMapCenter) {
|
|
||||||
webViewController!.evaluateJavascript(
|
webViewController!.evaluateJavascript(
|
||||||
source: "initLocation(${position.latitude}, ${position.longitude})"
|
source:
|
||||||
);
|
"updateMyLocation(${position.latitude}, ${position.longitude}, ${position.heading})",
|
||||||
_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)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
@@ -10,23 +11,21 @@ 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,
|
||||||
geolocationEnabled: true,
|
// 既然完全依赖 Flutter 定位,建议把 WebView 的定位彻底关掉,防止 JS 意外触发
|
||||||
// 允许 JS 弹窗 (Alert) 用于调试
|
geolocationEnabled: false,
|
||||||
javaScriptCanOpenWindowsAutomatically: true,
|
javaScriptEnabled: true,
|
||||||
|
// Android 混合开发推荐配置
|
||||||
|
useHybridComposition: true, // 提升地图渲染性能(重要)
|
||||||
|
allowFileAccessFromFileURLs: true, // 允许本地 html 访问本地资源
|
||||||
|
allowUniversalAccessFromFileURLs: true,
|
||||||
),
|
),
|
||||||
onWebViewCreated: (c) {
|
// 当 WebView 创建完成后,将 controller 实例传递给我们的 MapController
|
||||||
controller.webViewController = c;
|
onWebViewCreated: controller.onWebViewCreated,
|
||||||
},
|
|
||||||
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}");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,31 +9,72 @@ 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 Rx<DateTime> selectedDate = DateTime.now().obs;
|
final DateTime _now = DateTime.now();
|
||||||
final Rx<TimeOfDay> startTime = TimeOfDay.now().obs;
|
|
||||||
final Rx<TimeOfDay> endTime = TimeOfDay.fromDateTime(
|
// 计算当前时间属于哪个半小时区间
|
||||||
DateTime.now().add(const Duration(minutes: 30)),
|
late final TimeOfDay _initialStartTime = _calculateInitialStartTime(_now);
|
||||||
).obs;
|
late final TimeOfDay _initialEndTime = TimeOfDay.fromDateTime(
|
||||||
|
_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>(); // 用 ID 来选择
|
final Rxn<String> selectedStationId = Rxn<String>();
|
||||||
|
|
||||||
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 =>
|
||||||
String get formattedEndTime => _formatTimeOfDay(endTime.value);
|
'${_formatTimeOfDay(startTime.value)} - ${_formatTimeOfDay(endTime.value)}';
|
||||||
|
|
||||||
void pickDate(BuildContext context) {
|
void pickDate(BuildContext context) {
|
||||||
DateTime tempDate = selectedDate.value;
|
DateTime tempDate = selectedDate.value;
|
||||||
@@ -83,22 +124,7 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: CupertinoDatePicker(
|
child: CupertinoDatePicker(
|
||||||
mode: CupertinoDatePickerMode.date,
|
mode: CupertinoDatePickerMode.date,
|
||||||
initialDateTime:
|
initialDateTime: selectedDate.value,
|
||||||
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,
|
||||||
@@ -117,69 +143,76 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void pickTime(BuildContext context, bool isStartTime) {
|
///30 分钟为间隔 时间选择器
|
||||||
// 确定当前操作的时间和初始值
|
void pickTime(BuildContext context) {
|
||||||
TimeOfDay initialTime = isStartTime ? startTime.value : endTime.value;
|
final now = DateTime.now();
|
||||||
DateTime now = DateTime.now();
|
final isToday =
|
||||||
|
selectedDate.value.year == now.year &&
|
||||||
|
selectedDate.value.month == now.month &&
|
||||||
|
selectedDate.value.day == now.day;
|
||||||
|
|
||||||
// 计算最小可选时间
|
final List<TimeSlot> availableSlots = [];
|
||||||
DateTime? minimumDateTime; // 默认为 null,即可选任意时间
|
for (int i = 0; i < 48; i++) {
|
||||||
|
final startMinutes = i * 30;
|
||||||
|
final endMinutes = startMinutes + 30;
|
||||||
|
|
||||||
// 获取当前选择的日期(年月日)
|
final startTime = TimeOfDay(hour: startMinutes ~/ 60, minute: startMinutes % 60);
|
||||||
final selectedDay = DateTime(
|
final endTime = TimeOfDay(hour: (endMinutes ~/ 60) % 24, minute: endMinutes % 60);
|
||||||
|
|
||||||
|
final slotStartDateTime = DateTime(
|
||||||
selectedDate.value.year,
|
selectedDate.value.year,
|
||||||
selectedDate.value.month,
|
selectedDate.value.month,
|
||||||
selectedDate.value.day,
|
selectedDate.value.day,
|
||||||
|
startTime.hour,
|
||||||
|
startTime.minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取今天的日期(年月日)
|
// 如果不是今天,所有时间段都有效
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
if (!isToday) {
|
||||||
|
availableSlots.add(TimeSlot(startTime, endTime));
|
||||||
if (isStartTime) {
|
|
||||||
// 如果是选择开始时间并且日期是 今天
|
|
||||||
if (selectedDay.isAtSameMomentAs(today)) {
|
|
||||||
minimumDateTime = now; // 最小可选时间就是现在
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 如果是选择结束时间
|
// 如果是今天,需要判断该时间段是否可选
|
||||||
// 将开始时间转换为 DateTime 对象
|
// 创建时间段的结束时间对象
|
||||||
final startDateTime = DateTime(
|
final slotEndDateTime = DateTime(
|
||||||
selectedDate.value.year,
|
selectedDate.value.year,
|
||||||
selectedDate.value.month,
|
selectedDate.value.month,
|
||||||
selectedDate.value.day,
|
selectedDate.value.day,
|
||||||
startTime.value.hour,
|
endTime.hour,
|
||||||
startTime.value.minute,
|
endTime.minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 结束时间的最小值必须晚于开始时间
|
// 只要时间段的结束时间晚于当前时间,这个时间段就是可预约的
|
||||||
minimumDateTime = startDateTime;
|
if (slotEndDateTime.isAfter(now)) {
|
||||||
|
availableSlots.add(TimeSlot(startTime, endTime));
|
||||||
// 如果日期是今天,并且开始时间早于现在,那么结束时间的最小值也应该是现在
|
}
|
||||||
if (selectedDay.isAtSameMomentAs(today) && startDateTime.isBefore(now)) {
|
|
||||||
minimumDateTime = now;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始显示时间
|
if (availableSlots.isEmpty) {
|
||||||
DateTime initialDateTime = DateTime(
|
showToast('今天已没有可预约的时间段');
|
||||||
selectedDate.value.year,
|
return;
|
||||||
selectedDate.value.month,
|
}
|
||||||
selectedDate.value.day,
|
|
||||||
initialTime.hour,
|
int initialItem = availableSlots.indexWhere(
|
||||||
initialTime.minute,
|
(slot) =>
|
||||||
|
slot.start.hour == startTime.value.hour &&
|
||||||
|
(startTime.value.minute < 30
|
||||||
|
? slot.start.minute == 0
|
||||||
|
: slot.start.minute == 30),
|
||||||
);
|
);
|
||||||
|
if (initialItem == -1) {
|
||||||
// 确保初始时间不早于最小时间
|
initialItem = 0;
|
||||||
if (minimumDateTime != null && initialDateTime.isBefore(minimumDateTime)) {
|
|
||||||
initialDateTime = minimumDateTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime tempTime = initialDateTime;
|
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(
|
||||||
@@ -203,60 +236,8 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
),
|
),
|
||||||
CupertinoButton(
|
CupertinoButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final pickedTempTime = TimeOfDay.fromDateTime(tempTime);
|
startTime.value = tempSlot.start;
|
||||||
|
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(
|
||||||
@@ -272,14 +253,15 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1, color: Color(0xFFE5E5E5)),
|
const Divider(height: 1, color: Color(0xFFE5E5E5)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CupertinoDatePicker(
|
child: CupertinoPicker(
|
||||||
mode: CupertinoDatePickerMode.time,
|
scrollController: scrollController,
|
||||||
use24hFormat: true,
|
itemExtent: 40.0,
|
||||||
initialDateTime: initialDateTime,
|
onSelectedItemChanged: (index) {
|
||||||
minimumDate: minimumDateTime,
|
tempSlot = availableSlots[index];
|
||||||
onDateTimeChanged: (DateTime newTime) {
|
|
||||||
tempTime = newTime;
|
|
||||||
},
|
},
|
||||||
|
children: availableSlots
|
||||||
|
.map((slot) => Center(child: Text(slot.display)))
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -289,6 +271,9 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用于存储上一次成功预约的信息
|
||||||
|
ReservationModel? lastSuccessfulReservation;
|
||||||
|
|
||||||
/// 提交预约
|
/// 提交预约
|
||||||
void submitReservation() async {
|
void submitReservation() async {
|
||||||
if (plateNumber.isEmpty) {
|
if (plateNumber.isEmpty) {
|
||||||
@@ -300,33 +285,62 @@ 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 reservationStartDateTime = DateTime(
|
final dateStr = formattedDate;
|
||||||
|
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,
|
||||||
startTime.value.hour,
|
endTime.value.hour,
|
||||||
startTime.value.minute,
|
endTime.value.minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 检查预约时间是否在当前时间之前
|
//判断预约区间的结束时间是否早于当前时间(留出1分钟缓冲)
|
||||||
if (reservationStartDateTime.isBefore(DateTime.now())) {
|
if (reservationEndDateTime.isBefore(
|
||||||
showToast("不可预约过去的时间");
|
DateTime.now().subtract(const Duration(minutes: 1)),
|
||||||
|
)) {
|
||||||
|
showToast("无法预约已过去的时间段");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoading("提交中");
|
|
||||||
final selectedStation = stationOptions.firstWhere(
|
final selectedStation = stationOptions.firstWhere(
|
||||||
(s) => s.hydrogenId == selectedStationId.value,
|
(s) => s.hydrogenId == selectedStationId.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
final dateStr = formattedDate; // "yyyy-MM-dd"
|
if (selectedStation.siteStatusName != "营运中") {
|
||||||
final startTimeStr = '$dateStr ${formattedStartTime}:00'; // "yyyy-MM-dd HH:mm:ss"
|
showToast("该站点${selectedStation.siteStatusName},暂无法预约");
|
||||||
final endTimeStr = '$dateStr ${formattedEndTime}:00';
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
@@ -353,6 +367,31 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -496,14 +535,37 @@ 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
plateNumber.isEmpty
|
||||||
|
? SizedBox()
|
||||||
|
: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
var result = await Get.to(
|
||||||
|
() => ReservationEditPage(),
|
||||||
|
arguments: {
|
||||||
|
'reservation': reservation,
|
||||||
|
'difference': difference,
|
||||||
|
},
|
||||||
|
binding: BindingsBuilder(() {
|
||||||
|
Get.put(ReservationEditController());
|
||||||
|
}),
|
||||||
|
preventDuplicates: false,
|
||||||
|
);
|
||||||
|
showToast("555$result");
|
||||||
|
if (result == true) {
|
||||||
|
Get.back();
|
||||||
|
getReservationList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
@@ -515,11 +577,12 @@ class C_ReservationController extends GetxController with BaseControllerMixin {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
reservation.addStatusName,
|
"修改",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFFFA8C16),
|
color: Color(0xFFFA8C16),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -575,12 +638,14 @@ 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;
|
||||||
@@ -601,11 +666,20 @@ 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);
|
||||||
@@ -651,6 +725,9 @@ 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 {
|
||||||
@@ -707,6 +784,22 @@ 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,12 +201,7 @@ 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),
|
||||||
@@ -263,21 +258,15 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
onTap: () => controller.pickDate(context),
|
onTap: () => controller.pickDate(context),
|
||||||
),
|
),
|
||||||
_buildPickerRow(
|
_buildPickerRow(
|
||||||
label: '开始时间',
|
label: '预约时间',
|
||||||
value: controller.formattedStartTime,
|
value: controller.formattedTimeSlot,
|
||||||
icon: Icons.access_time_outlined,
|
icon: Icons.access_time_outlined,
|
||||||
onTap: () => controller.pickTime(context, true),
|
onTap: () => controller.pickTime(context),
|
||||||
),
|
|
||||||
_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: '请输入氢量(kg)',
|
hint: '当前最大可预约氢量${controller.difference}(KG)',
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
@@ -443,6 +432,7 @@ 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
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -489,7 +479,10 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
|
|
||||||
/// 构建下拉菜单中的每一项
|
/// 构建下拉菜单中的每一项
|
||||||
Widget _buildDropdownItem(StationModel station) {
|
Widget _buildDropdownItem(StationModel station) {
|
||||||
bool isMaintenance = (station.siteStatusName != '营运中');
|
bool isSelectable = (station.isSelect == 1);
|
||||||
|
//营运并且可用
|
||||||
|
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(
|
||||||
@@ -506,14 +499,22 @@ class ReservationPage extends GetView<C_ReservationController> {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
station.name,
|
station.name,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
style: TextStyle(
|
||||||
|
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: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isMaintenance) const SizedBox(width: 8),
|
if (isMaintenance) const SizedBox(width: 8),
|
||||||
if (isMaintenance)
|
if (isMaintenance)
|
||||||
|
|||||||
253
ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart
Normal file
253
ln_jq_app/lib/pages/c_page/reservation_edit/controller.dart
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
127
ln_jq_app/lib/pages/c_page/reservation_edit/view.dart
Normal file
127
ln_jq_app/lib/pages/c_page/reservation_edit/view.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,10 @@ 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();
|
||||||
@@ -41,10 +42,13 @@ 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) {
|
||||||
@@ -84,17 +88,21 @@ 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);
|
||||||
@@ -108,6 +116,7 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user